From b932ad032f6d041987bbb096d5ebd2b84da7a02a Mon Sep 17 00:00:00 2001 From: Aleksei Gurianov Date: Mon, 28 Oct 2024 21:42:57 +0300 Subject: [PATCH] feat: use decode/zero as decoding library Signed-off-by: Aleksei Gurianov --- gleam.toml | 1 + manifest.toml | 4 +- src/gleojson.gleam | 224 ++++++++++++++++----------------------- test/gleojson_test.gleam | 77 +++++++------- 4 files changed, 136 insertions(+), 170 deletions(-) diff --git a/gleam.toml b/gleam.toml index c1f8597..e9a4f3e 100644 --- a/gleam.toml +++ b/gleam.toml @@ -11,6 +11,7 @@ links = [ [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" gleam_json = ">= 2.0.0 and < 3.0.0" +decode = ">= 0.3.0 and < 1.0.0" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml index af12f11..2fc771f 100644 --- a/manifest.toml +++ b/manifest.toml @@ -4,12 +4,13 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "birdie", version = "1.2.3", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "justin", "rank", "simplifile", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "AE1207210E9CC8F4170BCE3FB3C23932F314C352C3FD1BCEA44CF4BF8CF60F93" }, + { name = "decode", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "EE9B979C0D8A5E058E2519EC0EE9CA4C7CEE15B12997BFF50492636CDC53D0C7" }, { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" }, { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, { name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" }, - { name = "gleam_erlang", version = "0.27.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "DE468F676D71B313C6C8C5334425CFCF827837333F8AB47B64D8A6D7AA40185D" }, + { name = "gleam_erlang", version = "0.28.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "BE551521F708DCE5CB954AFBBDF08519C1C44986521FD40753608825F48FFA9E" }, { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, @@ -23,6 +24,7 @@ packages = [ [requirements] argv = { version = ">= 1.0.2 and < 2.0.0" } birdie = { version = ">= 1.2.3 and < 2.0.0" } +decode = { version = ">= 0.3.0 and < 1.0.0" } gleam_json = { version = ">= 2.0.0 and < 3.0.0" } gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/gleojson.gleam b/src/gleojson.gleam index 3042467..4b0f0e3 100644 --- a/src/gleojson.gleam +++ b/src/gleojson.gleam @@ -1,7 +1,6 @@ -import gleam/dynamic +import decode/zero import gleam/json import gleam/option -import gleam/result pub type Lon { Lon(Float) @@ -196,148 +195,120 @@ pub fn encode_geojson( } fn position_decoder() { - fn(dyn_value) { - use list <- result.try(dynamic.list(dynamic.float)(dyn_value)) - case list { - [lon, lat, alt] -> Ok(new_position_3d(lon, lat, alt)) - [lon, lat] -> Ok(new_position_2d(lon, lat)) - _ -> - Error([ - dynamic.DecodeError( - expected: "list at least 2 coordinates", - found: dynamic.classify(dyn_value), - path: [], - ), - ]) - } + use decoded_list <- zero.then(zero.list(zero.float)) + case decoded_list { + [lon, lat, alt] -> zero.success(new_position_3d(lon, lat, alt)) + [lon, lat] -> zero.success(new_position_2d(lon, lat)) + _ -> zero.failure(new_position_2d(0.0, 0.0), "list at least 2 coordinates") } } fn positions_decoder() { - dynamic.list(position_decoder()) + zero.list(position_decoder()) } fn positions_list_decoder() { - dynamic.list(positions_decoder()) + zero.list(positions_decoder()) } fn positions_list_list_decoder() { - dynamic.list(positions_list_decoder()) + zero.list(positions_list_decoder()) } fn type_decoder() { - dynamic.field("type", dynamic.string) + zero.field("type", zero.string, zero.success) } -fn coords_decoder(decoder) { - dynamic.field("coordinates", decoder) +fn coords_decoder(decoder, next) { + zero.field("coordinates", decoder, next) } -fn geometry_decoder(dyn_value: dynamic.Dynamic) { - use type_str <- result.try(type_decoder()(dyn_value)) +fn geometry_decoder() { + use type_str <- zero.then(type_decoder()) case type_str { - "Point" -> dynamic.decode1(Point, coords_decoder(position_decoder())) - "MultiPoint" -> - dynamic.decode1(MultiPoint, coords_decoder(positions_decoder())) - "LineString" -> - dynamic.decode1(LineString, coords_decoder(positions_decoder())) - "MultiLineString" -> - dynamic.decode1(MultiLineString, coords_decoder(positions_list_decoder())) - "Polygon" -> - dynamic.decode1(Polygon, coords_decoder(positions_list_decoder())) - "MultiPolygon" -> - dynamic.decode1( - MultiPolygon, - coords_decoder(positions_list_list_decoder()), - ) - "GeometryCollection" -> - dynamic.decode1( - GeometryCollection, - dynamic.field("geometries", dynamic.list(geometry_decoder)), - ) - _ -> fn(_) { - Error([ - dynamic.DecodeError( - expected: "one of [Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]", - found: type_str, - path: ["type"], - ), - ]) + "Point" -> { + use position <- coords_decoder(position_decoder()) + zero.success(Point(position)) + } + "MultiPoint" -> { + use positions <- coords_decoder(positions_decoder()) + zero.success(MultiPoint(positions)) + } + "LineString" -> { + use positions <- coords_decoder(positions_decoder()) + zero.success(LineString(positions)) + } + "MultiLineString" -> { + use positions_list <- coords_decoder(positions_list_decoder()) + zero.success(MultiLineString(positions_list)) + } + "Polygon" -> { + use positions_list <- coords_decoder(positions_list_decoder()) + zero.success(Polygon(positions_list)) + } + "MultiPolygon" -> { + use positions_list_list <- coords_decoder(positions_list_list_decoder()) + zero.success(MultiPolygon(positions_list_list)) } - }(dyn_value) + "GeometryCollection" -> { + use geometries <- zero.field("geometries", zero.list(geometry_decoder())) + zero.success(GeometryCollection(geometries)) + } + _ -> zero.failure(Point(new_position_2d(0.0, 0.0)), "unknown geometry type") + } } fn feature_id_decoder() { - dynamic.any([ - dynamic.decode1(StringId, dynamic.string), - dynamic.decode1(NumberId, dynamic.float), + zero.one_of(zero.string |> zero.map(StringId), [ + zero.float |> zero.map(NumberId), ]) } -fn feature_decoder(properties_decoder: dynamic.Decoder(properties)) { - fn(dyn_value: dynamic.Dynamic) -> Result( - Feature(properties), - List(dynamic.DecodeError), - ) { - use type_str <- result.try(type_decoder()(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) - } - _ -> - Error([ - dynamic.DecodeError(expected: "Feature", found: type_str, path: [ - "type", - ]), - ]) +fn feature_decoder(properties_decoder: zero.Decoder(properties)) { + use type_str <- zero.then(type_decoder()) + case type_str { + "Feature" -> { + use geometry <- zero.field("geometry", zero.optional(geometry_decoder())) + use properties <- zero.field( + "properties", + zero.optional(properties_decoder), + ) + use id <- zero.field("id", zero.optional(feature_id_decoder())) + zero.success(Feature(geometry, properties, id)) } + _ -> + zero.failure( + Feature(option.None, option.None, option.None), + "expected Feature", + ) } } -fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { - fn(dyn_value: dynamic.Dynamic) -> Result( - FeatureCollection(properties), - List(dynamic.DecodeError), - ) { - use type_str <- result.try(type_decoder()(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"], - ), - ]) +fn featurecollection_decoder(properties_decoder: zero.Decoder(properties)) { + use type_str <- zero.then(type_decoder()) + case type_str { + "FeatureCollection" -> { + use features <- zero.field( + "features", + zero.list(feature_decoder(properties_decoder)), + ) + zero.success(FeatureCollection(features)) } + _ -> zero.failure(FeatureCollection([]), "expected FeatureCollection") } } /// 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. +/// This function takes a properties decoder for Feature and FeatureCollection properties, +/// and returns a decoder for GeoJSON objects. /// /// ## Example /// /// ```gleam /// import gleojson /// import gleam/json -/// import gleam/result -/// import gleam/dynamic +/// import decode/zero /// import gleam/io /// import gleam/string /// @@ -346,11 +317,10 @@ fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { /// } /// /// pub fn custom_properties_decoder() { -/// dynamic.decode2( -/// CustomProperties, -/// dynamic.field("name", dynamic.string), -/// dynamic.field("value", dynamic.float), -/// ) +/// use name <- zero.field("name", zero.string) +/// use value <- zero.field("value", zero.float) +/// CustomProperties(name: name, value: value) +/// |> zero.success /// } /// /// pub fn main() { @@ -359,12 +329,13 @@ fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { /// let decoded = /// json.decode( /// from: json_string, -/// using: gleojson.geojson_decoder(custom_properties_decoder()) +/// using: fn(dynamic_value) { +/// zero.run(dynamic_value, gleojson.geojson_decoder(custom_properties_decoder())) +/// } /// ) /// /// case decoded { /// Ok(geojson) -> { -/// // Work with the decoded GeoJSON object /// case geojson { /// gleojson.GeoFeature(feature) -> { /// io.println("Decoded a feature") @@ -372,9 +343,8 @@ fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { /// _ -> io.println("Decoded a different type of GeoJSON object") /// } /// } -/// Error(errors) -> { -/// // Handle decoding errors -/// io.println("Failed to decode: " <> string.join(errors, ", ")) +/// Error(error) -> { +/// io.println("Failed to decode: " <> error) /// } /// } /// } @@ -382,22 +352,14 @@ fn featurecollection_decoder(properties_decoder: dynamic.Decoder(properties)) { /// /// 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)) { - fn(dyn_value: dynamic.Dynamic) -> Result( - GeoJSON(properties), - List(dynamic.DecodeError), - ) { - use type_str <- result.try(type_decoder()(dyn_value)) - case type_str { - "Feature" -> - dynamic.decode1(GeoFeature, feature_decoder(properties_decoder)) - "FeatureCollection" -> - dynamic.decode1( - GeoFeatureCollection, - featurecollection_decoder(properties_decoder), - ) - _ -> dynamic.decode1(GeoGeometry, geometry_decoder) - }(dyn_value) +pub fn geojson_decoder(properties_decoder: zero.Decoder(properties)) { + use type_str <- zero.then(type_decoder()) + case type_str { + "Feature" -> feature_decoder(properties_decoder) |> zero.map(GeoFeature) + "FeatureCollection" -> + featurecollection_decoder(properties_decoder) + |> zero.map(GeoFeatureCollection) + _ -> geometry_decoder() |> zero.map(GeoGeometry) } } @@ -413,8 +375,8 @@ pub fn properties_null_encoder(_props) { /// /// 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. -pub fn properties_null_decoder(_dyn) -> Result(Nil, List(dynamic.DecodeError)) { - Ok(Nil) +pub fn properties_null_decoder() { + zero.success(Ok(Nil)) } /// Creates a 2D Position object from longitude and latitude values. diff --git a/test/gleojson_test.gleam b/test/gleojson_test.gleam index 4d62ce3..b284ff6 100644 --- a/test/gleojson_test.gleam +++ b/test/gleojson_test.gleam @@ -1,4 +1,5 @@ import birdie +import decode/zero import examples/encode import gleam/dynamic import gleam/json @@ -21,11 +22,10 @@ fn test_properties_encoder(props: TestProperties) -> json.Json { } fn test_properties_decoder() { - dynamic.decode2( - TestProperties, - dynamic.field("name", dynamic.string), - dynamic.field("value", dynamic.float), - ) + use name <- zero.field("name", zero.string) + use value <- zero.field("value", zero.float) + TestProperties(name:, value:) + |> zero.success } pub type ParkProperties { @@ -48,13 +48,12 @@ fn park_properties_encoder(props: ParkProperties) -> json.Json { } fn park_properties_decoder() { - 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), - ) + use name <- zero.field("name", zero.string) + use area_sq_km <- zero.field("area_sq_km", zero.float) + use year_established <- zero.field("year_established", zero.int) + use is_protected <- zero.field("is_protected", zero.bool) + ParkProperties(name:, area_sq_km:, year_established:, is_protected:) + |> zero.success } pub type MixedFeaturesProperties { @@ -88,27 +87,30 @@ fn mixed_features_properties_encoder( } fn mixed_features_properties_decoder() { - 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)), - ), - ]) + use name <- zero.field("name", zero.string) + zero.one_of( + { + use population <- zero.field("population", zero.int) + use timezone <- zero.field("timezone", zero.string) + use elevation <- zero.field("elevation", zero.float) + CityProperties(name:, population:, timezone:, elevation:) + |> zero.success + }, + [ + { + use length_km <- zero.field("length_km", zero.float) + use countries <- zero.field("countries", zero.list(zero.string)) + RiverProperties(name:, length_km:, countries:) + |> zero.success + }, + ], + ) } fn assert_encode_decode( geojson: gleojson.GeoJSON(properties), properties_encoder: fn(properties) -> json.Json, - properties_decoder: dynamic.Decoder(properties), + properties_decoder: zero.Decoder(properties), name: String, ) { let encoded = @@ -117,10 +119,9 @@ fn assert_encode_decode( birdie.snap(encoded, name) - json.decode( - from: encoded, - using: gleojson.geojson_decoder(properties_decoder), - ) + json.decode(from: encoded, using: fn(dynamic_value) { + zero.run(dynamic_value, gleojson.geojson_decoder(properties_decoder)) + }) |> should.be_ok |> should.equal(geojson) } @@ -134,7 +135,7 @@ pub fn point_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "point_encode_decode", ) } @@ -151,7 +152,7 @@ pub fn multipoint_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "multipoint_encode_decode", ) } @@ -168,7 +169,7 @@ pub fn linestring_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "linestring_encode_decode", ) } @@ -189,7 +190,7 @@ pub fn polygon_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "polygon_encode_decode", ) } @@ -220,7 +221,7 @@ pub fn multipolygon_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "multipolygon_encode_decode", ) } @@ -240,7 +241,7 @@ pub fn geometrycollection_encode_decode_test() { assert_encode_decode( geojson, gleojson.properties_null_encoder, - gleojson.properties_null_decoder, + gleojson.properties_null_decoder(), "geometrycollection_encode_decode", ) }