From 8cc243327386e6c22a032c49458b6d8b93346eb0 Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Fri, 7 Jul 2023 17:47:04 -0600 Subject: [PATCH] Support extensions on collections --- api/common.go | 1 + api/features.go | 86 ++++++++++++++++++++++++++++++++++++++++++++ api/features_test.go | 76 +++++++++++++++++++++++++++++++++++---- api/records.go | 5 +++ 4 files changed, 162 insertions(+), 6 deletions(-) diff --git a/api/common.go b/api/common.go index 7a1c64f..311ee54 100644 --- a/api/common.go +++ b/api/common.go @@ -84,4 +84,5 @@ type Root struct { type Extension interface { URI() string Encode(map[string]any) error + Decode([]byte) error } diff --git a/api/features.go b/api/features.go index 66e2f27..349f3db 100644 --- a/api/features.go +++ b/api/features.go @@ -23,6 +23,83 @@ import ( "github.com/mitchellh/mapstructure" ) +type Collection struct { + Id string `json:"id"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Links []*Link `json:"links"` + Extent *Extent `json:"extent,omitempty"` + ItemType string `json:"itemType,omitempty"` + Crs []string `json:"crs,omitempty"` + Extensions []Extension `json:"-"` +} + +var ( + _ json.Marshaler = (*Collection)(nil) + _ json.Unmarshaler = (*Collection)(nil) +) + +func (collection Collection) MarshalJSON() ([]byte, error) { + collectionMap := map[string]any{} + decoder, decoderErr := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + TagName: "json", + Result: &collectionMap, + }) + if decoderErr != nil { + return nil, decoderErr + } + + decodeErr := decoder.Decode(collection) + if decodeErr != nil { + return nil, decodeErr + } + + for _, extension := range collection.Extensions { + if err := extension.Encode(collectionMap); err != nil { + return nil, fmt.Errorf("trouble encoding feature JSON with the %q extension: %w", extension.URI(), err) + } + } + + return json.Marshal(collectionMap) +} + +type decodedCollection Collection + +func (collection *Collection) UnmarshalJSON(data []byte) error { + d := &decodedCollection{Extensions: collection.Extensions} + if err := json.Unmarshal(data, d); err != nil { + return err + } + *collection = Collection(*d) + + for _, e := range collection.Extensions { + if err := e.Decode(data); err != nil { + return err + } + } + return nil +} + +type Extent struct { + Spatial *SpatialExtent `json:"spatial,omitempty"` + Temporal *TemporalExtent `json:"temporal,omitempty"` +} + +type SpatialExtent struct { + Bbox [][]float64 `json:"bbox"` + Crs string `json:"crs"` +} + +type TemporalExtent struct { + Interval [][]any `json:"interval"` + Trs string `json:"trs"` +} + +type CollectionsList struct { + Collections []*Collection `json:"collections"` + Links []*Link `json:"links"` +} + type Feature struct { Id string `json:"id,omitempty"` Geometry any `json:"geometry"` @@ -68,3 +145,12 @@ func (feature Feature) MarshalJSON() ([]byte, error) { return json.Marshal(featureMap) } + +type FeatureCollection struct { + Type string `json:"type"` + Features []*Feature `json:"features"` + Links []*Link `json:"links,omitempty"` + TimeStamp string `json:"timeStamp,omitempty"` + NumberMatched int `json:"numberMatched,omitempty"` + NumberReturned int `json:"numberReturned,omitempty"` +} diff --git a/api/features_test.go b/api/features_test.go index 0036033..031c95b 100644 --- a/api/features_test.go +++ b/api/features_test.go @@ -18,6 +18,7 @@ package api_test import ( "encoding/json" + "errors" "fmt" "testing" @@ -95,14 +96,17 @@ func TestFeatureMarshal(t *testing.T) { } } -type TestExtension struct { +var ( + _ api.Extension = (*FeatureExtension)(nil) + _ api.Extension = (*CollectionExtension)(nil) +) + +type FeatureExtension struct { RootFoo string NestedBar string } -var _ api.Extension = (*TestExtension)(nil) - -func (e *TestExtension) Encode(featureMap map[string]any) error { +func (e *FeatureExtension) Encode(featureMap map[string]any) error { featureMap["test:foo"] = e.RootFoo propertiesMap, ok := featureMap["properties"].(map[string]any) if !ok { @@ -112,7 +116,28 @@ func (e *TestExtension) Encode(featureMap map[string]any) error { return nil } -func (e *TestExtension) URI() string { +func (e *FeatureExtension) Decode(data []byte) error { + return errors.New("not implemented") +} + +func (e *FeatureExtension) URI() string { + return "https://example.com/test-extension" +} + +type CollectionExtension struct { + Classes []string +} + +func (e *CollectionExtension) Encode(collectionMap map[string]any) error { + collectionMap["classes"] = e.Classes + return nil +} + +func (e *CollectionExtension) Decode(data []byte) error { + return json.Unmarshal(data, e) +} + +func (e *CollectionExtension) URI() string { return "https://example.com/test-extension" } @@ -123,7 +148,7 @@ func TestFeatureMarshalExtension(t *testing.T) { "one": "core-property", }, Extensions: []api.Extension{ - &TestExtension{ + &FeatureExtension{ RootFoo: "foo-value", NestedBar: "bar-value", }, @@ -147,3 +172,42 @@ func TestFeatureMarshalExtension(t *testing.T) { require.NoError(t, err) assert.JSONEq(t, expected, string(actual)) } + +func TestCollectionMarshalExtension(t *testing.T) { + collection := &api.Collection{ + Id: "test-collection", + Extensions: []api.Extension{ + &CollectionExtension{ + Classes: []string{"foo", "bar"}, + }, + }, + Links: []*api.Link{}, + } + + expected := `{ + "id": "test-collection", + "classes": ["foo", "bar"], + "links": [] + }` + + actual, err := json.Marshal(collection) + require.NoError(t, err) + assert.JSONEq(t, expected, string(actual)) +} + +func TestCollectionUnmarshalExtension(t *testing.T) { + data := `{ + "id": "test-collection", + "classes": ["foo", "bar"], + "links": [] + }` + + extension := &CollectionExtension{} + collection := &api.Collection{ + Extensions: []api.Extension{extension}, + } + + err := json.Unmarshal([]byte(data), collection) + require.NoError(t, err) + assert.Equal(t, []string{"foo", "bar"}, extension.Classes) +} diff --git a/api/records.go b/api/records.go index ee15225..08e07db 100644 --- a/api/records.go +++ b/api/records.go @@ -91,3 +91,8 @@ func (r *RecordCore) Encode(featureMap map[string]any) error { return nil } + +func (r *RecordCore) Decode(data []byte) error { + // TODO: implement this + return nil +}