From d8bfaf826c6295d6715aad316fb87ed818354e08 Mon Sep 17 00:00:00 2001 From: Aleksei Gurianov Date: Tue, 1 Oct 2024 23:40:34 +0300 Subject: [PATCH] refactor: manually refactor code after generation BREAKING CHANGE: API adjusted to follow Gleam conventions and to be more idiomatic. Signed-off-by: Aleksei Gurianov --- NOTICE | 2 +- README.md | 91 +++++---- src/gleojson.gleam | 405 ++++++++++++++++++++++++--------------- test/gleojson_test.gleam | 168 +++++----------- 4 files changed, 354 insertions(+), 312 deletions(-) diff --git a/NOTICE b/NOTICE index 355a1ff..3148777 100644 --- a/NOTICE +++ b/NOTICE @@ -8,7 +8,7 @@ This project is licensed under the MIT License. See the LICENSE file for the ful This project was bootstrapped with assistance from the OpenAI `o1-preview` model. It was used to scaffold basic types and encoder/decoder functions according to [RFC 7946](https://datatracker.ietf.org/doc/html/rfc7946). Manual edits were made to adjust the generated code to the actual Gleam language and to fix compilation errors. -Captured assistance session could be found in initial commit message. +Captured assistance session could be found in commit messages. Other AI models from different providers were used to provide suggestions, generate initial code structures, and assist in problem-solving during the early stages of development. diff --git a/README.md b/README.md index c70396d..e7f3048 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,30 @@ [![Package Version](https://img.shields.io/hexpm/v/gleojson)](https://hex.pm/packages/gleojson) [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleojson/) -**gleojson** is a GeoJSON parsing and encoding library for Gleam, following the [RFC 7946](https://tools.ietf.org/html/rfc7946) specification. It provides types and utility functions to encode and decode GeoJSON objects such as Points, LineStrings, Polygons, and more. +**gleojson** is a comprehensive GeoJSON parsing and encoding library for Gleam, following the [RFC 7946](https://tools.ietf.org/html/rfc7946) specification. It provides robust types and utility functions to seamlessly encode and decode GeoJSON objects such as Points, LineStrings, Polygons, and more. **Note:** This package is currently in development and has not reached version 1.0.0 yet. The API is considered unstable and may undergo breaking changes in future releases. Please use with caution in production environments and expect potential updates that might require code changes. +## Features + +- Full support for all GeoJSON object types: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, Feature, and FeatureCollection +- Flexible encoding and decoding of GeoJSON objects +- Custom property support for Feature and FeatureCollection objects +- Type-safe representation of GeoJSON structures + +## Current Limitations + +While **gleojson** aims to fully implement the GeoJSON specification (RFC 7946), some features are still under development. Key areas for future improvement include: + +1. Coordinate validation +1. Antimeridian and pole handling +1. Bounding box support +1. Right-hand rule enforcement for polygon orientation +1. Position array length limitation +1. GeometryCollection usage recommendations + +Despite these limitations, **gleojson** is fully functional for most common GeoJSON use cases. + ## Installation Add **gleojson** to your Gleam project: @@ -17,56 +37,51 @@ gleam add gleojson ## Usage +Here's a basic example of how to use gleojson: + ```gleam +import gleojson +import gleam/json +import gleam/option +import gleam/io + pub fn main() { - let json_string = "{ - \"type\": \"Feature\", - \"geometry\": { - \"type\": \"Point\", - \"coordinates\": [125.6, 10.1] - }, - \"properties\": { - \"name\": \"Dinagat Islands\" - } - }" - // Decode the JSON string into a GeoJSON object - let result = json.decode(from: json_string, using: gleojson.geojson_decoder) - case result { - Ok(geojson) -> { - // Successfully decoded GeoJSON - } - Error(errors) -> { - todo - // Handle decoding errors - // errors contains information about what went wrong during decoding - } - } - - // Construct GeoJSON from types - let geojson = gleojson.GeoJSONFeatureCollection( - gleojson.FeatureCollection([ - gleojson.Feature( - geometry: option.Some(gleojson.Point([1.0, 2.0])), - properties: option.None, - id: option.Some(gleojson.StringId("feature-id")), - ), - ]), + // Create a Point geometry + let point = gleojson.Point([125.6, 10.1]) + + // Create a Feature with the Point geometry + let feature = gleojson.Feature( + geometry: option.Some(point), + properties: option.None, + id: option.Some(gleojson.StringId("example-point")) ) - // Encode to JSON string - gleojson.encode_geojson(geojson) |> json.to_string + // Encode the Feature to GeoJSON + let geojson = gleojson.GeoJSONFeature(feature) + let encoded = gleojson.encode_geojson(geojson, gleojson.properties_null_encoder) + + // Print the encoded GeoJSON + io.println(json.to_string(encoded)) } ``` -Further documentation can be found at https://hexdocs.pm/gleojson. +For more advanced usage, including custom properties and decoding, see the [documentation](https://hexdocs.pm/gleojson). ## Development -``` +To build and test the project: + +```sh gleam build gleam test ``` +## Contributing + +Contributions to gleojson are welcome! Please feel free to submit a Pull Request. Before contributing, please review our [contribution guidelines](CONTRIBUTING.md). + ## License -This project is licensed under the MIT License. See the [LICENSE](LICENSE) and [NOTICE](NOTICE) files for more details. +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +Please see the [NOTICE](NOTICE) file for information about third party components and the use of AI assistance in this project. diff --git a/src/gleojson.gleam b/src/gleojson.gleam index dd1637a..b09a154 100644 --- a/src/gleojson.gleam +++ b/src/gleojson.gleam @@ -16,16 +16,12 @@ import gleam/dynamic import gleam/json - -// import gleam/list import gleam/option import gleam/result -/// A position in a GeoJSON object. pub type Position = List(Float) -/// A Geometry in a GeoJSON object. pub type Geometry { Point(coordinates: Position) MultiPoint(coordinates: List(Position)) @@ -36,13 +32,11 @@ pub type Geometry { GeometryCollection(geometries: List(Geometry)) } -/// Represents either a String or a Number, used for the Feature id. pub type FeatureId { StringId(String) NumberId(Float) } -/// A feature in a GeoJSON object, consisting of a geometry, properties, and an optional id. pub type Feature(properties) { Feature( geometry: option.Option(Geometry), @@ -51,53 +45,39 @@ pub type Feature(properties) { ) } -/// A collection of features in a GeoJSON object. pub type FeatureCollection(properties) { FeatureCollection(features: List(Feature(properties))) } -/// A GeoJSON object. pub type GeoJSON(properties) { GeoJSONGeometry(Geometry) GeoJSONFeature(Feature(properties)) GeoJSONFeatureCollection(FeatureCollection(properties)) } -// Encoding Functions - -/// Encodes a geometry into a JSON object. fn encode_geometry(geometry: Geometry) -> json.Json { case geometry { Point(coordinates) -> json.object([ #("type", json.string("Point")), - #("coordinates", json.array(coordinates, of: json.float)), + #("coordinates", json.array(coordinates, json.float)), ]) MultiPoint(multipoint) -> json.object([ #("type", json.string("MultiPoint")), - #( - "coordinates", - json.array(multipoint, of: json.array(_, of: json.float)), - ), + #("coordinates", json.array(multipoint, json.array(_, json.float))), ]) LineString(linestring) -> json.object([ #("type", json.string("LineString")), - #( - "coordinates", - json.array(linestring, of: json.array(_, of: json.float)), - ), + #("coordinates", json.array(linestring, json.array(_, json.float))), ]) MultiLineString(multilinestring) -> json.object([ #("type", json.string("MultiLineString")), #( "coordinates", - json.array(multilinestring, of: json.array(_, of: json.array( - _, - of: json.float, - ))), + json.array(multilinestring, json.array(_, json.array(_, json.float))), ), ]) Polygon(polygon) -> @@ -105,10 +85,7 @@ fn encode_geometry(geometry: Geometry) -> json.Json { #("type", json.string("Polygon")), #( "coordinates", - json.array(polygon, of: json.array(_, of: json.array( - _, - of: json.float, - ))), + json.array(polygon, json.array(_, json.array(_, json.float))), ), ]) MultiPolygon(multipolygon) -> @@ -116,24 +93,20 @@ fn encode_geometry(geometry: Geometry) -> json.Json { #("type", json.string("MultiPolygon")), #( "coordinates", - json.array( - multipolygon, - of: json.array(_, of: json.array(_, of: json.array( - _, - of: json.float, - ))), - ), + json.array(multipolygon, json.array(_, json.array(_, json.array( + _, + json.float, + )))), ), ]) GeometryCollection(collection) -> json.object([ #("type", json.string("GeometryCollection")), - #("geometries", json.array(collection, of: encode_geometry)), + #("geometries", json.array(collection, encode_geometry)), ]) } } -/// Encodes a feature into a JSON object. fn encode_feature( properties_encoder: fn(properties) -> json.Json, feature: Feature(properties), @@ -145,7 +118,7 @@ fn encode_feature( } let properties_json = case properties_opt { option.Some(props) -> properties_encoder(props) - option.None -> json.null() + _ -> json.null() } let base_obj = [ @@ -161,7 +134,6 @@ fn encode_feature( json.object(full_obj) } -/// Encodes a feature collection into a JSON object. fn encode_featurecollection( properties_encoder: fn(properties) -> json.Json, collection: FeatureCollection(properties), @@ -171,7 +143,7 @@ fn encode_featurecollection( #("type", json.string("FeatureCollection")), #( "features", - json.array(features, of: fn(feature) { + json.array(features, fn(feature) { encode_feature(properties_encoder, feature) }), ), @@ -180,12 +152,50 @@ fn encode_featurecollection( /// Encodes a GeoJSON object into a JSON value. /// +/// This function takes a GeoJSON object and a properties encoder function, +/// and returns a JSON representation of the GeoJSON object. +/// +/// ## Arguments +/// +/// - `geojson`: The GeoJSON object to encode. +/// - `properties_encoder`: A function that encodes the properties of Features and FeatureCollections. +/// +/// ## Returns +/// +/// A JSON representation of the GeoJSON object. +/// /// ## Example /// /// ```gleam -/// let point = GeoJSONGeometry(Point([0.0, 0.0])) -/// let encoded = encode_geojson(point, properties_encoder) -/// // encoded will be a JSON representation of the GeoJSON object +/// import gleojson +/// import gleam/json +/// import gleam/option +/// import gleam/io +/// +/// pub type CustomProperties { +/// CustomProperties(name: String, value: Float) +/// } +/// +/// pub fn custom_properties_encoder(props: CustomProperties) -> json.Json { +/// json.object([ +/// #("name", json.string(props.name)), +/// #("value", json.float(props.value)), +/// ]) +/// } +/// +/// pub fn main() { +/// let point = gleojson.Point([0.0, 0.0]) +/// let properties = CustomProperties("Example", 42.0) +/// let feature = gleojson.Feature( +/// geometry: option.Some(point), +/// properties: option.Some(properties), +/// id: option.Some(gleojson.StringId("example-point")) +/// ) +/// let geojson = gleojson.GeoJSONFeature(feature) +/// +/// let encoded = gleojson.encode_geojson(geojson, custom_properties_encoder) +/// io.println(json.to_string(encoded)) +/// } /// ``` pub fn encode_geojson( geojson: GeoJSON(properties), @@ -199,72 +209,61 @@ pub fn encode_geojson( } } -// Decoding Functions - -/// Decodes a position from a dynamic value. fn position_decoder( dyn_value: dynamic.Dynamic, ) -> Result(Position, List(dynamic.DecodeError)) { - dynamic.list(of: dynamic.float)(dyn_value) + dynamic.list(dynamic.float)(dyn_value) } -/// Decodes a list of positions from a dynamic value. fn positions_decoder( dyn_value: dynamic.Dynamic, ) -> Result(List(Position), List(dynamic.DecodeError)) { - dynamic.list(of: position_decoder)(dyn_value) + dynamic.list(position_decoder)(dyn_value) } -/// Decodes a list of lists of positions from a dynamic value. fn positions_list_decoder( dyn_value: dynamic.Dynamic, ) -> Result(List(List(Position)), List(dynamic.DecodeError)) { - dynamic.list(of: positions_decoder)(dyn_value) + dynamic.list(positions_decoder)(dyn_value) } -/// Decodes a list of lists of lists of positions from a dynamic value. fn positions_list_list_decoder( dyn_value: dynamic.Dynamic, ) -> Result(List(List(List(Position))), List(dynamic.DecodeError)) { - dynamic.list(of: positions_list_decoder)(dyn_value) + dynamic.list(positions_list_decoder)(dyn_value) } fn decode_type_field( dyn_value: dynamic.Dynamic, ) -> Result(String, List(dynamic.DecodeError)) { - dynamic.field(named: "type", of: dynamic.string)(dyn_value) + dynamic.field("type", dynamic.string)(dyn_value) } -/// Decodes a geometry from a dynamic value. fn geometry_decoder( dyn_value: dynamic.Dynamic, ) -> Result(Geometry, List(dynamic.DecodeError)) { use type_str <- result.try(decode_type_field(dyn_value)) case type_str { "Point" -> - dynamic.field(named: "coordinates", of: position_decoder)(dyn_value) + dynamic.field("coordinates", position_decoder)(dyn_value) |> result.map(Point) "MultiPoint" -> - dynamic.field(named: "coordinates", of: positions_decoder)(dyn_value) + dynamic.field("coordinates", positions_decoder)(dyn_value) |> result.map(MultiPoint) "LineString" -> - dynamic.field(named: "coordinates", of: positions_decoder)(dyn_value) + dynamic.field("coordinates", positions_decoder)(dyn_value) |> result.map(LineString) "MultiLineString" -> - dynamic.field(named: "coordinates", of: positions_list_decoder)(dyn_value) + dynamic.field("coordinates", positions_list_decoder)(dyn_value) |> result.map(MultiLineString) "Polygon" -> - dynamic.field(named: "coordinates", of: positions_list_decoder)(dyn_value) + dynamic.field("coordinates", positions_list_decoder)(dyn_value) |> result.map(Polygon) "MultiPolygon" -> - dynamic.field(named: "coordinates", of: positions_list_list_decoder)( - dyn_value, - ) + dynamic.field("coordinates", positions_list_list_decoder)(dyn_value) |> result.map(MultiPolygon) "GeometryCollection" -> - dynamic.field(named: "geometries", of: dynamic.list(of: geometry_decoder))( - dyn_value, - ) + dynamic.field("geometries", dynamic.list(geometry_decoder))(dyn_value) |> result.map(GeometryCollection) _ -> Error([ @@ -277,7 +276,6 @@ fn geometry_decoder( } } -/// Decodes a FeatureId from a dynamic value. fn feature_id_decoder( dyn_value: dynamic.Dynamic, ) -> Result(FeatureId, List(dynamic.DecodeError)) { @@ -286,113 +284,206 @@ fn feature_id_decoder( |> result.lazy_or(fn() { dynamic.float(dyn_value) |> result.map(NumberId) }) } -/// Decodes a feature from a dynamic value. -fn feature_decoder( - properties_decoder: dynamic.Decoder(properties), - dyn_value: dynamic.Dynamic, -) -> Result(Feature(properties), List(dynamic.DecodeError)) { - use type_str <- result.try(decode_type_field(dyn_value)) - case type_str { - "Feature" -> { - let geometry_result = - dynamic.field(named: "geometry", of: dynamic.optional(geometry_decoder))( - dyn_value, - ) - - let properties_result = - dynamic.field( - named: "properties", - of: dynamic.optional(properties_decoder), +fn feature_decoder(properties_decoder: dynamic.Decoder(properties)) { + fn(dyn_value: dynamic.Dynamic) -> Result( + Feature(properties), + List(dynamic.DecodeError), + ) { + use type_str <- result.try(decode_type_field(dyn_value)) + case type_str { + "Feature" -> { + dynamic.decode3( + Feature, + dynamic.field("geometry", dynamic.optional(geometry_decoder)), + dynamic.field("properties", dynamic.optional(properties_decoder)), + dynamic.optional_field("id", feature_id_decoder), )(dyn_value) - |> result.map_error(fn(_errs) { - [ - dynamic.DecodeError(expected: "Properties", found: "Invalid", path: [ - "properties", - ]), - ] - }) - - let id_result = - dynamic.optional_field(named: "id", of: feature_id_decoder)(dyn_value) - |> result.map_error(fn(_errs) { - [dynamic.DecodeError(expected: "ID", found: "Invalid", path: ["id"])] - }) - - let geometry_result = - geometry_result - |> result.map_error(fn(_errs) { - [ - dynamic.DecodeError(expected: "Geometry", found: "Invalid", path: [ - "geometry", - ]), - ] - }) + } - use geometry_opt <- result.try(geometry_result) - use properties_opt <- result.try(properties_result) - use id_opt <- result.try(id_result) - Ok(Feature(geometry_opt, properties_opt, id_opt)) + _ -> + Error([ + dynamic.DecodeError(expected: "Feature", found: type_str, path: [ + "type", + ]), + ]) } - - _ -> - Error([ - dynamic.DecodeError(expected: "Feature", found: type_str, path: ["type"]), - ]) } } -/// Decodes a feature collection from a dynamic value. -fn featurecollection_decoder( - properties_decoder: dynamic.Decoder(properties), - dyn_value: dynamic.Dynamic, -) -> Result(FeatureCollection(properties), List(dynamic.DecodeError)) { - use type_str <- result.try(decode_type_field(dyn_value)) - case type_str { - "FeatureCollection" -> - dynamic.field( - named: "features", - of: dynamic.list(of: fn(dyn_value) { - feature_decoder(properties_decoder, dyn_value) - }), - )(dyn_value) - |> result.map(FeatureCollection) - _ -> - Error([ - dynamic.DecodeError( - expected: "FeatureCollection", - found: type_str, - path: ["type"], - ), - ]) +fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { + fn(dyn_value: dynamic.Dynamic) -> Result( + FeatureCollection(properties), + List(dynamic.DecodeError), + ) { + use type_str <- result.try(decode_type_field(dyn_value)) + case type_str { + "FeatureCollection" -> + dynamic.decode1( + FeatureCollection, + dynamic.field( + "features", + dynamic.list(feature_decoder(properties_decoder)), + ), + )(dyn_value) + _ -> + Error([ + dynamic.DecodeError( + expected: "FeatureCollection", + found: type_str, + path: ["type"], + ), + ]) + } } } /// Decodes a GeoJSON object from a dynamic value. /// +/// This function takes a dynamic value (typically parsed from JSON) and a properties decoder, +/// and attempts to decode it into a GeoJSON object. +/// +/// ## Arguments +/// +/// - `properties_decoder`: A function that decodes the properties of Features and FeatureCollections. +/// +/// ## Returns +/// +/// A function that takes a dynamic value and returns a Result containing either +/// the decoded GeoJSON object or a list of decode errors. +/// /// ## Example /// /// ```gleam -/// let json_string = "{\"type\":\"Point\",\"coordinates\":[0.0,0.0]}" -/// let decoded = json.decode(json_string) -/// |> result.then(fn dyn_value { geojson_decoder(properties_decoder, dyn_value) }) -/// // decoded will be Ok(GeoJSONGeometry(Point([0.0, 0.0]))) if successful +/// import gleojson +/// import gleam/json +/// import gleam/result +/// import gleam/dynamic +/// import gleam/io +/// import gleam/string +/// +/// pub type CustomProperties { +/// CustomProperties(name: String, value: Float) +/// } +/// +/// pub fn custom_properties_decoder( +/// dyn: dynamic.Dynamic, +/// ) -> Result(CustomProperties, List(dynamic.DecodeError)) { +/// dynamic.decode2( +/// CustomProperties, +/// dynamic.field("name", dynamic.string), +/// dynamic.field("value", dynamic.float), +/// )(dyn) +/// } +/// +/// pub fn main() { +/// let json_string = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0.0,0.0]},\"properties\":{\"name\":\"Example\",\"value\":42.0}}" +/// +/// let decoded = +/// json.decode( +/// from: json_string, +/// using: gleojson.geojson_decoder(custom_properties_decoder) +/// ) +/// +/// case decoded { +/// Ok(geojson) -> { +/// // Work with the decoded GeoJSON object +/// case geojson { +/// gleojson.GeoJSONFeature(feature) -> { +/// io.println("Decoded a feature") +/// } +/// _ -> io.println("Decoded a different type of GeoJSON object") +/// } +/// } +/// Error(errors) -> { +/// // Handle decoding errors +/// io.println("Failed to decode: " <> string.join(errors, ", ")) +/// } +/// } +/// } /// ``` /// /// Note: This function expects a valid GeoJSON structure. Invalid or incomplete /// GeoJSON data will result in a decode error. -pub fn geojson_decoder( - properties_decoder: dynamic.Decoder(properties), - dyn_value: dynamic.Dynamic, -) -> Result(GeoJSON(properties), List(dynamic.DecodeError)) { - use type_str <- result.try(decode_type_field(dyn_value)) - case type_str { - "Feature" -> - result.map(feature_decoder(properties_decoder, dyn_value), GeoJSONFeature) - "FeatureCollection" -> - result.map( - featurecollection_decoder(properties_decoder, dyn_value), - GeoJSONFeatureCollection, - ) - _ -> result.map(geometry_decoder(dyn_value), GeoJSONGeometry) +pub fn geojson_decoder(properties_decoder: dynamic.Decoder(properties)) { + fn(dyn_value: dynamic.Dynamic) -> Result( + GeoJSON(properties), + List(dynamic.DecodeError), + ) { + use type_str <- result.try(decode_type_field(dyn_value)) + case type_str { + "Feature" -> + dynamic.decode1(GeoJSONFeature, feature_decoder(properties_decoder)) + "FeatureCollection" -> + dynamic.decode1( + GeoJSONFeatureCollection, + featurecollection_decoder(properties_decoder), + ) + _ -> dynamic.decode1(GeoJSONGeometry, geometry_decoder) + }(dyn_value) } } + +/// Encodes null properties for Features and FeatureCollections. +/// +/// This is a utility function that can be used as the `properties_encoder` +/// argument for `encode_geojson` when you don't need to encode any properties. +/// +/// ## Returns +/// +/// A JSON null value. +/// +/// ## Example +/// +/// ```gleam +/// import gleojson +/// import gleam/json +/// import gleam/option +/// +/// pub fn main() { +/// let point = gleojson.Point([0.0, 0.0]) +/// let feature = gleojson.Feature( +/// geometry: option.Some(point), +/// properties: option.None, +/// id: option.None +/// ) +/// let geojson = gleojson.GeoJSONFeature(feature) +/// +/// let encoded = gleojson.encode_geojson(geojson, gleojson.properties_null_encoder) +/// // The "properties" field in the resulting JSON will be null +/// } +/// ``` +pub fn properties_null_encoder(_props) { + json.null() +} + +/// Decodes null properties for Features and FeatureCollections. +/// +/// This is a utility function that can be used as the `properties_decoder` +/// argument for `geojson_decoder` when you don't need to decode any properties. +/// +/// ## Returns +/// +/// Always returns `Ok(Nil)`. +/// +/// ## Example +/// +/// ```gleam +/// import gleojson +/// import gleam/json +/// import gleam/result +/// +/// pub fn main() { +/// let json_string = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[0.0,0.0]},\"properties\":null}" +/// +/// let decoded = +/// json.decode( +/// from: json_string, +/// using: gleojson.geojson_decoder(gleojson.properties_null_decoder) +/// ) +/// +/// // The "properties" field in the decoded Feature will be None +/// } +/// ``` +pub fn properties_null_decoder(_dyn) -> Result(Nil, List(dynamic.DecodeError)) { + Ok(Nil) +} diff --git a/test/gleojson_test.gleam b/test/gleojson_test.gleam index cffd9d7..74a9b54 100644 --- a/test/gleojson_test.gleam +++ b/test/gleojson_test.gleam @@ -2,7 +2,6 @@ import birdie import gleam/dynamic import gleam/json import gleam/option -import gleam/result import gleeunit import gleeunit/should @@ -12,34 +11,25 @@ pub fn main() { gleeunit.main() } -// Define custom property types for tests - -// TestProperties for feature_encode_decode_test pub type TestProperties { TestProperties(name: String, value: Float) } -// Encoder for TestProperties fn test_properties_encoder(props: TestProperties) -> json.Json { let TestProperties(name, value) = props json.object([#("name", json.string(name)), #("value", json.float(value))]) } -/// Decoder for TestProperties fn test_properties_decoder( dyn_value: dynamic.Dynamic, ) -> Result(TestProperties, List(dynamic.DecodeError)) { - use name <- result.try(dynamic.field(named: "name", of: dynamic.string)( - dyn_value, - )) - use value <- result.try(dynamic.field(named: "value", of: dynamic.float)( - dyn_value, - )) - - Ok(TestProperties(name, value)) + dynamic.decode2( + TestProperties, + dynamic.field("name", dynamic.string), + dynamic.field("value", dynamic.float), + )(dyn_value) } -// ParkProperties for real_life_feature_test pub type ParkProperties { ParkProperties( name: String, @@ -49,7 +39,6 @@ pub type ParkProperties { ) } -// Encoder for ParkProperties fn park_properties_encoder(props: ParkProperties) -> json.Json { let ParkProperties(name, area_sq_km, year_established, is_protected) = props json.object([ @@ -60,30 +49,19 @@ fn park_properties_encoder(props: ParkProperties) -> json.Json { ]) } -// Decoder for ParkProperties fn park_properties_decoder( dyn_value: dynamic.Dynamic, ) -> Result(ParkProperties, List(dynamic.DecodeError)) { - use name <- result.try(dynamic.field(named: "name", of: dynamic.string)( - dyn_value, - )) - use area_sq_km <- result.try(dynamic.field( - named: "area_sq_km", - of: dynamic.float, - )(dyn_value)) - use year_established <- result.try(dynamic.field( - named: "year_established", - of: dynamic.int, - )(dyn_value)) - use is_protected <- result.try(dynamic.field( - named: "is_protected", - of: dynamic.bool, - )(dyn_value)) - Ok(ParkProperties(name, area_sq_km, year_established, is_protected)) + dynamic.decode4( + ParkProperties, + dynamic.field("name", dynamic.string), + dynamic.field("area_sq_km", dynamic.float), + dynamic.field("year_established", dynamic.int), + dynamic.field("is_protected", dynamic.bool), + )(dyn_value) } -// Properties type for real_life_featurecollection_test -pub type Properties { +pub type MixedFeaturesProperties { CityProperties( name: String, population: Int, @@ -93,8 +71,9 @@ pub type Properties { RiverProperties(name: String, length_km: Float, countries: List(String)) } -// Encoder for Properties -fn properties_encoder(props: Properties) -> json.Json { +fn mixed_features_properties_encoder( + props: MixedFeaturesProperties, +) -> json.Json { case props { CityProperties(name, population, timezone, elevation) -> json.object([ @@ -107,53 +86,31 @@ fn properties_encoder(props: Properties) -> json.Json { json.object([ #("name", json.string(name)), #("length_km", json.float(length_km)), - #("countries", json.array(countries, of: json.string)), + #("countries", json.array(countries, json.string)), ]) } } -// Decoder for Properties -fn properties_decoder( +fn mixed_features_properties_decoder( dyn_value: dynamic.Dynamic, -) -> Result(Properties, List(dynamic.DecodeError)) { - use name <- result.try(dynamic.field(named: "name", of: dynamic.string)( - dyn_value, - )) - // Try decoding as CityProperties - let population_result = - dynamic.field(named: "population", of: dynamic.int)(dyn_value) - let timezone_result = - dynamic.field(named: "timezone", of: dynamic.string)(dyn_value) - let elevation_result = - dynamic.field(named: "elevation", of: dynamic.float)(dyn_value) - case population_result, timezone_result, elevation_result { - Ok(population), Ok(timezone), Ok(elevation) -> - Ok(CityProperties(name, population, timezone, elevation)) - _, _, _ -> { - // Try decoding as RiverProperties - let length_km_result = - dynamic.field(named: "length_km", of: dynamic.float)(dyn_value) - let countries_result = - dynamic.field(named: "countries", of: dynamic.list(of: dynamic.string))( - dyn_value, - ) - case length_km_result, countries_result { - Ok(length_km), Ok(countries) -> - Ok(RiverProperties(name, length_km, countries)) - _, _ -> - Error([ - dynamic.DecodeError( - expected: "Properties", - found: "Invalid", - path: [], - ), - ]) - } - } - } +) -> Result(MixedFeaturesProperties, List(dynamic.DecodeError)) { + dynamic.any([ + dynamic.decode4( + CityProperties, + dynamic.field("name", dynamic.string), + dynamic.field("population", dynamic.int), + dynamic.field("timezone", dynamic.string), + dynamic.field("elevation", dynamic.float), + ), + dynamic.decode3( + RiverProperties, + dynamic.field("name", dynamic.string), + dynamic.field("length_km", dynamic.float), + dynamic.field("countries", dynamic.list(dynamic.string)), + ), + ])(dyn_value) } -// General assertion function for encoding and decoding fn assert_encode_decode( geojson: gleojson.GeoJSON(properties), properties_encoder: fn(properties) -> json.Json, @@ -166,10 +123,10 @@ fn assert_encode_decode( birdie.snap(encoded, name) - json.decode(from: encoded, using: gleojson.geojson_decoder( - properties_decoder, - _, - )) + json.decode( + from: encoded, + using: gleojson.geojson_decoder(properties_decoder), + ) |> should.be_ok |> should.equal(geojson) } @@ -179,14 +136,10 @@ fn assert_encode_decode( pub fn point_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry(gleojson.Point([1.0, 2.0])) - // Since there are no properties, use the unit type `Nil` - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "point_encode_decode", ) } @@ -195,13 +148,10 @@ pub fn multipoint_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry(gleojson.MultiPoint([[1.0, 2.0], [3.0, 4.0]])) - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "multipoint_encode_decode", ) } @@ -210,13 +160,10 @@ pub fn linestring_encode_decode_test() { let geojson = gleojson.GeoJSONGeometry(gleojson.LineString([[1.0, 2.0], [3.0, 4.0]])) - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "linestring_encode_decode", ) } @@ -227,13 +174,10 @@ pub fn polygon_encode_decode_test() { gleojson.Polygon([[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]]), ) - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "polygon_encode_decode", ) } @@ -247,13 +191,10 @@ pub fn multipolygon_encode_decode_test() { ]), ) - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "multipolygon_encode_decode", ) } @@ -267,19 +208,14 @@ pub fn geometrycollection_encode_decode_test() { ]), ) - let properties_encoder = fn(_props) { json.null() } - let properties_decoder = fn(_dyn_value) { Ok(Nil) } - assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + gleojson.properties_null_encoder, + gleojson.properties_null_decoder, "geometrycollection_encode_decode", ) } -// Existing test functions... - pub fn feature_encode_decode_test() { let properties = TestProperties("Test Point", 42.0) @@ -362,8 +298,8 @@ pub fn real_life_featurecollection_test() { assert_encode_decode( geojson, - properties_encoder, - properties_decoder, + mixed_features_properties_encoder, + mixed_features_properties_decoder, "real_life_featurecollection", ) }