Skip to content

Commit 452df87

Browse files
authored
geojson: handle extra/foreign members in feature (#164)
1 parent 9839fd9 commit 452df87

File tree

3 files changed

+207
-35
lines changed

3 files changed

+207
-35
lines changed

geojson/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ rawJSON, _ := fc.MarshalJSON()
4747
blob, _ := json.Marshal(fc)
4848
```
4949

50-
## Foreign/extra members in a feature collection
50+
## Foreign/extra members in feature and feature collection
5151

5252
```go
5353
rawJSON := []byte(`

geojson/feature.go

Lines changed: 151 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ type Feature struct {
1515
BBox BBox `json:"bbox,omitempty"`
1616
Geometry orb.Geometry `json:"geometry"`
1717
Properties Properties `json:"properties"`
18+
19+
// ExtraMembers can be used to encoded/decode extra key/members in
20+
// the base of the feature object. Note that keys of "id", "type", "bbox"
21+
// "geometry" and "properties" will not work as those are reserved by the
22+
// GeoJSON spec.
23+
ExtraMembers Properties `json:"-"`
1824
}
1925

2026
// NewFeature creates and initializes a GeoJSON feature given the required attributes.
@@ -38,31 +44,65 @@ var _ orb.Pointer = &Feature{}
3844
// MarshalJSON converts the feature object into the proper JSON.
3945
// It will handle the encoding of all the child geometries.
4046
// Alternately one can call json.Marshal(f) directly for the same result.
47+
// Items in the ExtraMembers map will be included in the base of the
48+
// feature object.
4149
func (f Feature) MarshalJSON() ([]byte, error) {
4250
return marshalJSON(newFeatureDoc(&f))
4351
}
4452

4553
// MarshalBSON converts the feature object into the proper JSON.
4654
// It will handle the encoding of all the child geometries.
4755
// Alternately one can call json.Marshal(f) directly for the same result.
56+
// Items in the ExtraMembers map will be included in the base of the
57+
// feature object.
4858
func (f Feature) MarshalBSON() ([]byte, error) {
4959
return bson.Marshal(newFeatureDoc(&f))
5060
}
5161

52-
func newFeatureDoc(f *Feature) *featureDoc {
53-
doc := &featureDoc{
54-
ID: f.ID,
55-
Type: "Feature",
56-
Properties: f.Properties,
57-
BBox: f.BBox,
58-
Geometry: NewGeometry(f.Geometry),
62+
func newFeatureDoc(f *Feature) interface{} {
63+
if len(f.ExtraMembers) == 0 {
64+
doc := &featureDoc{
65+
ID: f.ID,
66+
Type: "Feature",
67+
Properties: f.Properties,
68+
BBox: f.BBox,
69+
Geometry: NewGeometry(f.Geometry),
70+
}
71+
72+
if len(doc.Properties) == 0 {
73+
doc.Properties = nil
74+
}
75+
76+
return doc
5977
}
6078

61-
if len(doc.Properties) == 0 {
62-
doc.Properties = nil
79+
var tmp map[string]interface{}
80+
if f.ExtraMembers != nil {
81+
tmp = f.ExtraMembers.Clone()
82+
} else {
83+
tmp = make(map[string]interface{}, 3)
84+
}
85+
86+
delete(tmp, "id")
87+
if f.ID != nil {
88+
tmp["id"] = f.ID
89+
}
90+
tmp["type"] = "Feature"
91+
92+
delete(tmp, "bbox")
93+
if f.BBox != nil {
94+
tmp["bbox"] = f.BBox
95+
}
96+
97+
tmp["geometry"] = NewGeometry(f.Geometry)
98+
99+
if len(f.Properties) == 0 {
100+
tmp["properties"] = nil
101+
} else {
102+
tmp["properties"] = f.Properties
63103
}
64104

65-
return doc
105+
return tmp
66106
}
67107

68108
// UnmarshalFeature decodes the data into a GeoJSON feature.
@@ -85,45 +125,122 @@ func (f *Feature) UnmarshalJSON(data []byte) error {
85125
return nil
86126
}
87127

88-
doc := &featureDoc{}
89-
err := unmarshalJSON(data, &doc)
128+
tmp := make(map[string]nocopyRawMessage, 4)
129+
130+
err := unmarshalJSON(data, &tmp)
90131
if err != nil {
91132
return err
92133
}
93134

94-
return featureUnmarshalFinish(doc, f)
135+
*f = Feature{}
136+
for key, value := range tmp {
137+
switch key {
138+
case "id":
139+
err := unmarshalJSON(value, &f.ID)
140+
if err != nil {
141+
return err
142+
}
143+
case "type":
144+
err := unmarshalJSON(value, &f.Type)
145+
if err != nil {
146+
return err
147+
}
148+
case "bbox":
149+
err := unmarshalJSON(value, &f.BBox)
150+
if err != nil {
151+
return err
152+
}
153+
case "geometry":
154+
g := &Geometry{}
155+
err := unmarshalJSON(value, &g)
156+
if err != nil {
157+
return err
158+
}
159+
160+
if g != nil {
161+
f.Geometry = g.Geometry()
162+
}
163+
case "properties":
164+
err := unmarshalJSON(value, &f.Properties)
165+
if err != nil {
166+
return err
167+
}
168+
default:
169+
if f.ExtraMembers == nil {
170+
f.ExtraMembers = Properties{}
171+
}
172+
173+
var val interface{}
174+
err := unmarshalJSON(value, &val)
175+
if err != nil {
176+
return err
177+
}
178+
f.ExtraMembers[key] = val
179+
}
180+
}
181+
182+
if f.Type != "Feature" {
183+
return fmt.Errorf("geojson: not a feature: type=%s", f.Type)
184+
}
185+
186+
return nil
95187
}
96188

97189
// UnmarshalBSON will unmarshal a BSON document created with bson.Marshal.
98190
func (f *Feature) UnmarshalBSON(data []byte) error {
99-
doc := &featureDoc{}
100-
err := bson.Unmarshal(data, &doc)
191+
tmp := make(map[string]bson.RawValue, 4)
192+
193+
err := bson.Unmarshal(data, &tmp)
101194
if err != nil {
102195
return err
103196
}
104197

105-
return featureUnmarshalFinish(doc, f)
106-
}
107-
108-
func featureUnmarshalFinish(doc *featureDoc, f *Feature) error {
109-
if doc.Type != "Feature" {
110-
return fmt.Errorf("geojson: not a feature: type=%s", doc.Type)
111-
}
112-
113-
var g orb.Geometry
114-
if doc.Geometry != nil {
115-
if doc.Geometry.Coordinates == nil && doc.Geometry.Geometries == nil {
116-
return ErrInvalidGeometry
198+
*f = Feature{}
199+
for key, value := range tmp {
200+
switch key {
201+
case "id":
202+
err := value.Unmarshal(&f.ID)
203+
if err != nil {
204+
return err
205+
}
206+
case "type":
207+
f.Type, _ = bson.RawValue(value).StringValueOK()
208+
case "bbox":
209+
err := value.Unmarshal(&f.BBox)
210+
if err != nil {
211+
return err
212+
}
213+
case "geometry":
214+
g := &Geometry{}
215+
err := value.Unmarshal(&g)
216+
if err != nil {
217+
return err
218+
}
219+
220+
if g != nil {
221+
f.Geometry = g.Geometry()
222+
}
223+
case "properties":
224+
err := value.Unmarshal(&f.Properties)
225+
if err != nil {
226+
return err
227+
}
228+
default:
229+
if f.ExtraMembers == nil {
230+
f.ExtraMembers = Properties{}
231+
}
232+
233+
var val interface{}
234+
err := value.Unmarshal(&val)
235+
if err != nil {
236+
return err
237+
}
238+
f.ExtraMembers[key] = val
117239
}
118-
g = doc.Geometry.Geometry()
119240
}
120241

121-
*f = Feature{
122-
ID: doc.ID,
123-
Type: doc.Type,
124-
Properties: doc.Properties,
125-
BBox: doc.BBox,
126-
Geometry: g,
242+
if f.Type != "Feature" {
243+
return fmt.Errorf("geojson: not a feature: type=%s", f.Type)
127244
}
128245

129246
return nil

geojson/feature_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,33 @@ func TestFeatureMarshalJSON_null(t *testing.T) {
236236
})
237237
}
238238

239+
func TestFeatureMarshalJSON_extraMembers(t *testing.T) {
240+
rawJSON := `
241+
{ "type": "Feature",
242+
"geometry": {"type": "Point", "coordinates": [102.0, 0.5]},
243+
"properties": {"prop0": "value0"},
244+
"foo": "bar"
245+
}`
246+
247+
f, err := UnmarshalFeature([]byte(rawJSON))
248+
if err != nil {
249+
t.Fatalf("should unmarshal feature collection without issue, err %v", err)
250+
}
251+
252+
if v := f.ExtraMembers.MustString("foo", ""); v != "bar" {
253+
t.Errorf("missing extra: foo: %v", v)
254+
}
255+
256+
data, err := f.MarshalJSON()
257+
if err != nil {
258+
t.Fatalf("unable to marshal: %v", err)
259+
}
260+
261+
if !bytes.Contains(data, []byte(`"foo":"bar"`)) {
262+
t.Fatalf("extras not in marshalled data")
263+
}
264+
}
265+
239266
func TestUnmarshalBSON_missingGeometry(t *testing.T) {
240267
t.Run("missing geometry", func(t *testing.T) {
241268
f := NewFeature(nil)
@@ -355,6 +382,34 @@ func TestMarshalRing(t *testing.T) {
355382
}
356383
}
357384

385+
func TestFeature_MarshalBSON_extraMembers(t *testing.T) {
386+
f := NewFeature(orb.Point{1, 2})
387+
388+
f.ExtraMembers = map[string]interface{}{
389+
"a": 1.0,
390+
"b": 2.0,
391+
}
392+
393+
data, err := bson.Marshal(f)
394+
if err != nil {
395+
t.Fatalf("unable to marshal feature collection: %v", err)
396+
}
397+
398+
nf := &Feature{}
399+
err = bson.Unmarshal(data, &nf)
400+
if err != nil {
401+
t.Fatalf("unable to unmarshal feature collection: %v", err)
402+
}
403+
404+
if v := nf.ExtraMembers["a"]; v != 1.0 {
405+
t.Errorf("incorrect extra member: %v != %v", v, 1.0)
406+
}
407+
408+
if v := nf.ExtraMembers["b"]; v != 2.0 {
409+
t.Errorf("incorrect extra member: %v != %v", v, 2.0)
410+
}
411+
}
412+
358413
// uncomment to test/benchmark custom json marshalling
359414
// func init() {
360415
// var c = jsoniter.Config{

0 commit comments

Comments
 (0)