Skip to content

Commit 653b23f

Browse files
committed
feat: encoding decoding arbitrary Feature properties
AI session: https://chatgpt.com/share/66fc3275-e248-8004-b619-34c3a8fe04ef Signed-off-by: Aleksei Gurianov <[email protected]>
1 parent b5c19b4 commit 653b23f

6 files changed

+371
-501
lines changed

birdie_snapshots/feature_encode_decode.accepted

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
version: 1.2.3
33
title: feature_encode_decode
44
---
5-
{"id":"feature-id","type":"Feature","geometry":{"type":"Point","coordinates":[1.0,2.0]},"properties":null}
5+
{"id":"feature-id","type":"Feature","geometry":{"type":"Point","coordinates":[1.0,2.0]},"properties":{"name":"Test Point","value":42.0}}

birdie_snapshots/featurecollection_encode_decode.accepted

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
version: 1.2.3
3+
title: real_life_feature
4+
---
5+
{"id":"yosemite","type":"Feature","geometry":{"type":"Polygon","coordinates":[[[-119.5383,37.8651],[-119.5127,37.8777],[-119.4939,37.8685],[-119.5383,37.8651]]]},"properties":{"name":"Yosemite National Park","area_sq_km":3029.87,"year_established":1890,"is_protected":true}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
version: 1.2.3
3+
title: real_life_featurecollection
4+
---
5+
{"type":"FeatureCollection","features":[{"id":"tokyo","type":"Feature","geometry":{"type":"Point","coordinates":[139.6917,35.6895]},"properties":{"name":"Tokyo","population":37435191,"timezone":"Asia/Tokyo","elevation":40.0}},{"id":"colorado-river","type":"Feature","geometry":{"type":"LineString","coordinates":[[-115.1728,36.1147],[-116.2139,36.5674],[-117.1522,36.6567]]},"properties":{"name":"Colorado River","length_km":2330.0,"countries":["USA","Mexico"]}}]}

src/gleojson.gleam

+78-54
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
////
1515
//// Then you can use the provided functions to encode and decode GeoJSON data.
1616

17-
import gleam/dict
1817
import gleam/dynamic
1918
import gleam/json
19+
20+
// import gleam/list
2021
import gleam/option
2122
import gleam/result
2223

@@ -42,56 +43,53 @@ pub type FeatureId {
4243
}
4344

4445
/// A feature in a GeoJSON object, consisting of a geometry, properties, and an optional id.
45-
pub type Feature {
46+
pub type Feature(properties) {
4647
Feature(
4748
geometry: option.Option(Geometry),
48-
properties: option.Option(dict.Dict(String, dynamic.Dynamic)),
49+
properties: option.Option(properties),
4950
id: option.Option(FeatureId),
5051
)
5152
}
5253

5354
/// A collection of features in a GeoJSON object.
54-
pub type FeatureCollection {
55-
FeatureCollection(features: List(Feature))
55+
pub type FeatureCollection(properties) {
56+
FeatureCollection(features: List(Feature(properties)))
5657
}
5758

5859
/// A GeoJSON object.
59-
pub type GeoJSON {
60+
pub type GeoJSON(properties) {
6061
GeoJSONGeometry(Geometry)
61-
GeoJSONFeature(Feature)
62-
GeoJSONFeatureCollection(FeatureCollection)
62+
GeoJSONFeature(Feature(properties))
63+
GeoJSONFeatureCollection(FeatureCollection(properties))
6364
}
6465

6566
// Encoding Functions
6667

6768
/// Encodes a geometry into a JSON object.
6869
fn encode_geometry(geometry: Geometry) -> json.Json {
6970
case geometry {
70-
Point(coordinates) -> {
71+
Point(coordinates) ->
7172
json.object([
7273
#("type", json.string("Point")),
7374
#("coordinates", json.array(coordinates, of: json.float)),
7475
])
75-
}
76-
MultiPoint(multipoint) -> {
76+
MultiPoint(multipoint) ->
7777
json.object([
7878
#("type", json.string("MultiPoint")),
7979
#(
8080
"coordinates",
8181
json.array(multipoint, of: json.array(_, of: json.float)),
8282
),
8383
])
84-
}
85-
LineString(linestring) -> {
84+
LineString(linestring) ->
8685
json.object([
8786
#("type", json.string("LineString")),
8887
#(
8988
"coordinates",
9089
json.array(linestring, of: json.array(_, of: json.float)),
9190
),
9291
])
93-
}
94-
MultiLineString(multilinestring) -> {
92+
MultiLineString(multilinestring) ->
9593
json.object([
9694
#("type", json.string("MultiLineString")),
9795
#(
@@ -102,8 +100,7 @@ fn encode_geometry(geometry: Geometry) -> json.Json {
102100
))),
103101
),
104102
])
105-
}
106-
Polygon(polygon) -> {
103+
Polygon(polygon) ->
107104
json.object([
108105
#("type", json.string("Polygon")),
109106
#(
@@ -114,8 +111,7 @@ fn encode_geometry(geometry: Geometry) -> json.Json {
114111
))),
115112
),
116113
])
117-
}
118-
MultiPolygon(multipolygon) -> {
114+
MultiPolygon(multipolygon) ->
119115
json.object([
120116
#("type", json.string("MultiPolygon")),
121117
#(
@@ -129,63 +125,77 @@ fn encode_geometry(geometry: Geometry) -> json.Json {
129125
),
130126
),
131127
])
132-
}
133-
GeometryCollection(collection) -> {
128+
GeometryCollection(collection) ->
134129
json.object([
135130
#("type", json.string("GeometryCollection")),
136131
#("geometries", json.array(collection, of: encode_geometry)),
137132
])
138-
}
139133
}
140134
}
141135

142136
/// Encodes a feature into a JSON object.
143-
fn encode_feature(feature: Feature) -> json.Json {
144-
let Feature(geometry_opt, _properties_opt, id_opt) = feature
137+
fn encode_feature(
138+
properties_encoder: fn(properties) -> json.Json,
139+
feature: Feature(properties),
140+
) -> json.Json {
141+
let Feature(geometry_opt, properties_opt, id_opt) = feature
145142
let geometry_json = case geometry_opt {
146143
option.Some(geometry) -> encode_geometry(geometry)
147144
option.None -> json.null()
148145
}
149-
// let properties_json = case properties_opt {
150-
// option.Some(props) -> json.object(props)
151-
// option.None -> json.object([])
152-
// }
146+
let properties_json = case properties_opt {
147+
option.Some(props) -> properties_encoder(props)
148+
option.None -> json.null()
149+
}
150+
153151
let base_obj = [
154152
#("type", json.string("Feature")),
155153
#("geometry", geometry_json),
156-
#("properties", json.null()),
154+
#("properties", properties_json),
157155
]
158-
case id_opt {
156+
let full_obj = case id_opt {
159157
option.Some(StringId(id)) -> [#("id", json.string(id)), ..base_obj]
160158
option.Some(NumberId(id)) -> [#("id", json.float(id)), ..base_obj]
161159
option.None -> base_obj
162160
}
163-
|> json.object
161+
json.object(full_obj)
164162
}
165163

166164
/// Encodes a feature collection into a JSON object.
167-
fn encode_featurecollection(collection: FeatureCollection) -> json.Json {
165+
fn encode_featurecollection(
166+
properties_encoder: fn(properties) -> json.Json,
167+
collection: FeatureCollection(properties),
168+
) -> json.Json {
168169
let FeatureCollection(features) = collection
169170
json.object([
170171
#("type", json.string("FeatureCollection")),
171-
#("features", json.array(features, of: encode_feature)),
172+
#(
173+
"features",
174+
json.array(features, of: fn(feature) {
175+
encode_feature(properties_encoder, feature)
176+
}),
177+
),
172178
])
173179
}
174180

175-
/// Encodes a GeoJSON object into a dynamic value.
181+
/// Encodes a GeoJSON object into a JSON value.
176182
///
177183
/// ## Example
178184
///
179185
/// ```gleam
180186
/// let point = GeoJSONGeometry(Point([0.0, 0.0]))
181-
/// let encoded = encode_geojson(point)
182-
/// // encoded will be a dynamic representation of the GeoJSON object
187+
/// let encoded = encode_geojson(point, properties_encoder)
188+
/// // encoded will be a JSON representation of the GeoJSON object
183189
/// ```
184-
pub fn encode_geojson(geojson: GeoJSON) -> json.Json {
190+
pub fn encode_geojson(
191+
geojson: GeoJSON(properties),
192+
properties_encoder: fn(properties) -> json.Json,
193+
) -> json.Json {
185194
case geojson {
186195
GeoJSONGeometry(geometry) -> encode_geometry(geometry)
187-
GeoJSONFeature(feature) -> encode_feature(feature)
188-
GeoJSONFeatureCollection(collection) -> encode_featurecollection(collection)
196+
GeoJSONFeature(feature) -> encode_feature(properties_encoder, feature)
197+
GeoJSONFeatureCollection(collection) ->
198+
encode_featurecollection(properties_encoder, collection)
189199
}
190200
}
191201

@@ -219,6 +229,12 @@ fn positions_list_list_decoder(
219229
dynamic.list(of: positions_list_decoder)(dyn_value)
220230
}
221231

232+
fn decode_type_field(
233+
dyn_value: dynamic.Dynamic,
234+
) -> Result(String, List(dynamic.DecodeError)) {
235+
dynamic.field(named: "type", of: dynamic.string)(dyn_value)
236+
}
237+
222238
/// Decodes a geometry from a dynamic value.
223239
fn geometry_decoder(
224240
dyn_value: dynamic.Dynamic,
@@ -272,8 +288,9 @@ fn feature_id_decoder(
272288

273289
/// Decodes a feature from a dynamic value.
274290
fn feature_decoder(
291+
properties_decoder: dynamic.Decoder(properties),
275292
dyn_value: dynamic.Dynamic,
276-
) -> Result(Feature, List(dynamic.DecodeError)) {
293+
) -> Result(Feature(properties), List(dynamic.DecodeError)) {
277294
use type_str <- result.try(decode_type_field(dyn_value))
278295
case type_str {
279296
"Feature" -> {
@@ -285,7 +302,7 @@ fn feature_decoder(
285302
let properties_result =
286303
dynamic.field(
287304
named: "properties",
288-
of: dynamic.optional(dynamic.dict(dynamic.string, dynamic.dynamic)),
305+
of: dynamic.optional(properties_decoder),
289306
)(dyn_value)
290307
|> result.map_error(fn(_errs) {
291308
[
@@ -326,14 +343,18 @@ fn feature_decoder(
326343

327344
/// Decodes a feature collection from a dynamic value.
328345
fn featurecollection_decoder(
329-
dyn_value,
330-
) -> Result(FeatureCollection, List(dynamic.DecodeError)) {
346+
properties_decoder: dynamic.Decoder(properties),
347+
dyn_value: dynamic.Dynamic,
348+
) -> Result(FeatureCollection(properties), List(dynamic.DecodeError)) {
331349
use type_str <- result.try(decode_type_field(dyn_value))
332350
case type_str {
333351
"FeatureCollection" ->
334-
dynamic.field(named: "features", of: dynamic.list(of: feature_decoder))(
335-
dyn_value,
336-
)
352+
dynamic.field(
353+
named: "features",
354+
of: dynamic.list(of: fn(dyn_value) {
355+
feature_decoder(properties_decoder, dyn_value)
356+
}),
357+
)(dyn_value)
337358
|> result.map(FeatureCollection)
338359
_ ->
339360
Error([
@@ -353,22 +374,25 @@ fn featurecollection_decoder(
353374
/// ```gleam
354375
/// let json_string = "{\"type\":\"Point\",\"coordinates\":[0.0,0.0]}"
355376
/// let decoded = json.decode(json_string)
356-
/// |> result.then(geojson_decoder)
377+
/// |> result.then(fn dyn_value { geojson_decoder(properties_decoder, dyn_value) })
357378
/// // decoded will be Ok(GeoJSONGeometry(Point([0.0, 0.0]))) if successful
358379
/// ```
359380
///
360381
/// Note: This function expects a valid GeoJSON structure. Invalid or incomplete
361382
/// GeoJSON data will result in a decode error.
362-
pub fn geojson_decoder(dyn_value) -> Result(GeoJSON, List(dynamic.DecodeError)) {
383+
pub fn geojson_decoder(
384+
properties_decoder: dynamic.Decoder(properties),
385+
dyn_value: dynamic.Dynamic,
386+
) -> Result(GeoJSON(properties), List(dynamic.DecodeError)) {
363387
use type_str <- result.try(decode_type_field(dyn_value))
364388
case type_str {
365-
"Feature" -> result.map(feature_decoder(dyn_value), GeoJSONFeature)
389+
"Feature" ->
390+
result.map(feature_decoder(properties_decoder, dyn_value), GeoJSONFeature)
366391
"FeatureCollection" ->
367-
result.map(featurecollection_decoder(dyn_value), GeoJSONFeatureCollection)
392+
result.map(
393+
featurecollection_decoder(properties_decoder, dyn_value),
394+
GeoJSONFeatureCollection,
395+
)
368396
_ -> result.map(geometry_decoder(dyn_value), GeoJSONGeometry)
369397
}
370398
}
371-
372-
fn decode_type_field(dyn_value) -> Result(String, List(dynamic.DecodeError)) {
373-
dynamic.field(named: "type", of: dynamic.string)(dyn_value)
374-
}

0 commit comments

Comments
 (0)