Skip to content

Commit

Permalink
Merge pull request #9 from planetlabs/more-specs
Browse files Browse the repository at this point in the history
Foundation for additional spec support
  • Loading branch information
tschaub authored Jun 28, 2023
2 parents b57985e + 509ed6f commit 14b42b0
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 0 deletions.
5 changes: 5 additions & 0 deletions api/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,8 @@ type Root struct {
Description string `json:"description,omitempty"`
Attribution string `json:"attribution,omitempty"`
}

type Extension interface {
URI() string
Encode(map[string]any) error
}
70 changes: 70 additions & 0 deletions api/features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Copyright 2023 Planet Labs PBC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package api

import (
"encoding/json"
"fmt"

"github.com/mitchellh/mapstructure"
)

type Feature struct {
Id string `json:"id,omitempty"`
Geometry any `json:"geometry"`
Properties map[string]any `json:"properties"`
Links []*Link `json:"links,omitempty"`
Extensions []Extension `json:"-"`
}

var _ json.Marshaler = (*Feature)(nil)

func (feature Feature) MarshalJSON() ([]byte, error) {
featureMap := map[string]any{"type": "Feature"}
decoder, decoderErr := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: &featureMap,
})
if decoderErr != nil {
return nil, decoderErr
}

decodeErr := decoder.Decode(feature)
if decodeErr != nil {
return nil, decodeErr
}

extensionUris := []string{}
lookup := map[string]bool{}

for _, extension := range feature.Extensions {
if err := extension.Encode(featureMap); err != nil {
return nil, fmt.Errorf("trouble encoding feature JSON with the %q extension: %w", extension.URI(), err)
}
uri := extension.URI()
if !lookup[uri] {
extensionUris = append(extensionUris, uri)
lookup[uri] = true
}
}

if len(extensionUris) > 0 {
featureMap["conformsTo"] = extensionUris
}

return json.Marshal(featureMap)
}
149 changes: 149 additions & 0 deletions api/features_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Copyright 2023 Planet Labs PBC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package api_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/planetlabs/go-ogc/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestFeatureMarshal(t *testing.T) {
cases := []struct {
name string
feature *api.Feature
expected string
}{
{
name: "basic",
feature: &api.Feature{
Id: "foo",
Geometry: nil,
Properties: map[string]interface{}{
"one": "foo",
"two": "bar",
},
Links: []*api.Link{
{
Href: "http://example.com/resource.json",
Rel: "self",
Type: "application/geo+json",
},
},
},
expected: `{
"type": "Feature",
"id": "foo",
"geometry": null,
"properties": {
"one": "foo",
"two": "bar"
},
"links": [
{
"href": "http://example.com/resource.json",
"rel": "self",
"type": "application/geo+json"
}
]
}`,
},
{
name: "minimal",
feature: &api.Feature{
Geometry: nil,
Properties: map[string]interface{}{
"one": "foo",
"two": "bar",
},
},
expected: `{
"type": "Feature",
"geometry": null,
"properties": {
"one": "foo",
"two": "bar"
}
}`,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual, err := json.Marshal(tc.feature)
require.NoError(t, err)
assert.JSONEq(t, tc.expected, string(actual))
})
}
}

type TestExtension struct {
RootFoo string
NestedBar string
}

var _ api.Extension = (*TestExtension)(nil)

func (e *TestExtension) Encode(featureMap map[string]any) error {
featureMap["test:foo"] = e.RootFoo
propertiesMap, ok := featureMap["properties"].(map[string]any)
if !ok {
return fmt.Errorf("expected properties on a feature ")
}
propertiesMap["test:bar"] = e.NestedBar
return nil
}

func (e *TestExtension) URI() string {
return "https://example.com/test-extension"
}

func TestFeatureMarshalExtension(t *testing.T) {
feature := &api.Feature{
Geometry: nil,
Properties: map[string]interface{}{
"one": "core-property",
},
Extensions: []api.Extension{
&TestExtension{
RootFoo: "foo-value",
NestedBar: "bar-value",
},
},
}

expected := `{
"type": "Feature",
"geometry": null,
"properties": {
"one": "core-property",
"test:bar": "bar-value"
},
"test:foo": "foo-value",
"conformsTo": [
"https://example.com/test-extension"
]
}`

actual, err := json.Marshal(feature)
require.NoError(t, err)
assert.JSONEq(t, expected, string(actual))
}
93 changes: 93 additions & 0 deletions api/records.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright 2023 Planet Labs PBC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package api

import (
"errors"
"time"
)

type RecordCore struct {
Id string
Type string
Title string
Description string
Time time.Time
Created time.Time
Updated time.Time
}

var (
_ Extension = (*RecordCore)(nil)
)

func (r *RecordCore) URI() string {
return "http://www.opengis.net/spec/ogcapi-records-1/1.0/req/record-core"
}

func (r *RecordCore) Encode(featureMap map[string]any) error {
propertiesMap, ok := featureMap["properties"].(map[string]any)
if !ok {
return errors.New("missing properties")
}

// required id
id, _ := featureMap["id"].(string)
if id == "" {
id = r.Id
if id == "" {
return errors.New("missing id")
}
}
featureMap["id"] = id

// required time
if r.Time.IsZero() {
featureMap["time"] = nil
} else {
featureMap["time"] = r.Time.Format(time.RFC3339Nano)
}

// required type
if r.Type == "" {
return errors.New("missing type")
}
propertiesMap["type"] = r.Type

// required title
if r.Title == "" {
return errors.New("missing title")
}
propertiesMap["title"] = r.Title

// optional description
if r.Description != "" {
propertiesMap["description"] = r.Description
}

// optional created
if !r.Created.IsZero() {
propertiesMap["created"] = r.Created.Format(time.RFC3339Nano)
}

// optional updated
if !r.Updated.IsZero() {
propertiesMap["updated"] = r.Updated.Format(time.RFC3339Nano)
}

return nil
}
Loading

0 comments on commit 14b42b0

Please sign in to comment.