Skip to content

Commit

Permalink
feat: use decode/zero as decoding library
Browse files Browse the repository at this point in the history
Signed-off-by: Aleksei Gurianov <[email protected]>
  • Loading branch information
Guria committed Oct 28, 2024
1 parent 534abf7 commit b932ad0
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 170 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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" }
Expand Down
224 changes: 93 additions & 131 deletions src/gleojson.gleam
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import gleam/dynamic
import decode/zero
import gleam/json
import gleam/option
import gleam/result

pub type Lon {
Lon(Float)
Expand Down Expand Up @@ -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
///
Expand All @@ -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() {
Expand All @@ -359,45 +329,37 @@ 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")
/// }
/// _ -> 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)
/// }
/// }
/// }
/// ```
///
/// 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)
}
}

Expand All @@ -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.
Expand Down
Loading

0 comments on commit b932ad0

Please sign in to comment.