From 35b0ab91c3a9758a14438d0b898f856128d6ae31 Mon Sep 17 00:00:00 2001 From: Aleksei Gurianov Date: Fri, 27 Sep 2024 01:10:40 +0300 Subject: [PATCH] feat: initial implementation Credits goes to OpenAI o1-preview + Cursor with Claude 3.5 Sonet `o1-preview` generated bunch of syntax that doesn't exist. I had to only use cursor.dev pretrained on Gleam to fix syntax errors and provide docs for stdlib on further iterations. You can find creation session at https://chatgpt.com/share/66f5db20-c7fc-8004-b7d6-db55d720f6c8 --- .github/workflows/test.yml | 23 ++ .gitignore | 4 + README.md | 55 ++++ gleam.toml | 14 + manifest.toml | 13 + src/gleojson.gleam | 605 +++++++++++++++++++++++++++++++++++++ test/gleojson_test.gleam | 360 ++++++++++++++++++++++ 7 files changed, 1074 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 gleam.toml create mode 100644 manifest.toml create mode 100644 src/gleojson.gleam create mode 100644 test/gleojson_test.gleam diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b591a5c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.0.1" + gleam-version: "1.5.1" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..a68df11 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# gleojson + +[![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. + +## Installation + +Add **gleojson** to your Gleam project: + +```sh +gleam add gleojson +``` + +## Usage + +```gleam +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 + let encoded = gleojson.encode_geojson(geojson) + // encoded is now a Dynamic representation of the GeoJSON object + // You can use it for further processing or encoding back to JSON + } + Error(errors) -> { + todo + // Handle decoding errors + // errors contains information about what went wrong during decoding + } + } +} +``` + +Further documentation can be found at https://hexdocs.pm/gleojson. + +## Development + +``` +gleam build +gleam test +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..5a4e40e --- /dev/null +++ b/gleam.toml @@ -0,0 +1,14 @@ +name = "gleojson" +version = "0.1.0" + +description = "A Gleam library for encoding and decoding GeoJSON" +licences = ["MIT"] +repository = { type = "github", user = "guria", repo = "gleojson" } +links = [{ title = "RFC7946", href = "https://datatracker.ietf.org/doc/html/rfc7946" }] + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +gleam_json = ">= 2.0.0 and < 3.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..b026a54 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,13 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { 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" }, +] + +[requirements] +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 new file mode 100644 index 0000000..8201dfb --- /dev/null +++ b/src/gleojson.gleam @@ -0,0 +1,605 @@ +//// Functions for working with GeoJSON data. +//// +//// This module provides types and functions for encoding and decoding GeoJSON data. +//// It supports all GeoJSON object types including Point, MultiPoint, LineString, +//// MultiLineString, Polygon, MultiPolygon, GeometryCollection, Feature, and FeatureCollection. +//// +//// ## Usage +//// +//// To use this module, you can import it in your Gleam code: +//// +//// ```gleam +//// import gleojson +//// ``` +//// +//// Then you can use the provided functions to encode and decode GeoJSON data. +//// Types + +import gleam/dict +import gleam/dynamic +import gleam/list +import gleam/option +import gleam/result + +/// A position in a GeoJSON object. +pub type Position = + List(Float) + +/// A point in a GeoJSON object. +pub type Point { + Point(coordinates: Position) +} + +/// A multi-point in a GeoJSON object. +pub type MultiPoint { + MultiPoint(coordinates: List(Position)) +} + +/// A line string in a GeoJSON object. +pub type LineString { + LineString(coordinates: List(Position)) +} + +/// A multi-line string in a GeoJSON object. +pub type MultiLineString { + MultiLineString(coordinates: List(List(Position))) +} + +/// A polygon in a GeoJSON object. +pub type Polygon { + Polygon(coordinates: List(List(Position))) +} + +/// A multi-polygon in a GeoJSON object. +pub type MultiPolygon { + MultiPolygon(coordinates: List(List(List(Position)))) +} + +/// A collection of geometries in a GeoJSON object. +pub type GeometryCollection { + GeometryCollection(geometries: List(Geometry)) +} + +/// A geometry in a GeoJSON object. +pub type Geometry { + GeometryPoint(Point) + GeometryMultiPoint(MultiPoint) + GeometryLineString(LineString) + GeometryMultiLineString(MultiLineString) + GeometryPolygon(Polygon) + GeometryMultiPolygon(MultiPolygon) + GeometryCollectionType(GeometryCollection) +} + +/// A feature in a GeoJSON object. +pub type Feature { + Feature( + geometry: option.Option(Geometry), + properties: option.Option(dict.Dict(String, dynamic.Dynamic)), + id: option.Option(dynamic.Dynamic), + ) +} + +/// A collection of features in a GeoJSON object. +pub type FeatureCollection { + FeatureCollection(features: List(Feature)) +} + +/// A GeoJSON object. +pub type GeoJSON { + GeoJSONGeometry(Geometry) + GeoJSONFeature(Feature) + GeoJSONFeatureCollection(FeatureCollection) +} + +// Encoding Functions + +/// Encodes a position into a dynamic value. +pub fn encode_position(position: Position) -> dynamic.Dynamic { + dynamic.from(position) +} + +/// Encodes a list of positions into a dynamic value. +pub fn encode_positions(positions: List(Position)) -> dynamic.Dynamic { + dynamic.from(positions) +} + +/// Encodes a list of positions into a dynamic value. +pub fn encode_positions_list( + positions_list: List(List(Position)), +) -> dynamic.Dynamic { + dynamic.from(positions_list) +} + +/// Encodes a list of lists of positions into a dynamic value. +pub fn encode_positions_list_list( + positions_list_list: List(List(List(Position))), +) -> dynamic.Dynamic { + dynamic.from(positions_list_list) +} + +/// Encodes a point into a dynamic value. +pub fn encode_point(point: Point) -> dynamic.Dynamic { + let Point(coordinates) = point + let obj = + dict.from_list([ + #("type", dynamic.from("Point")), + #("coordinates", encode_position(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a multi-point into a dynamic value. +pub fn encode_multipoint(multipoint: MultiPoint) -> dynamic.Dynamic { + let MultiPoint(coordinates) = multipoint + let obj = + dict.from_list([ + #("type", dynamic.from("MultiPoint")), + #("coordinates", encode_positions(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a line string into a dynamic value. +pub fn encode_linestring(linestring: LineString) -> dynamic.Dynamic { + let LineString(coordinates) = linestring + let obj = + dict.from_list([ + #("type", dynamic.from("LineString")), + #("coordinates", encode_positions(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a multi-line string into a dynamic value. +pub fn encode_multilinestring( + multilinestring: MultiLineString, +) -> dynamic.Dynamic { + let MultiLineString(coordinates) = multilinestring + let obj = + dict.from_list([ + #("type", dynamic.from("MultiLineString")), + #("coordinates", encode_positions_list(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a polygon into a dynamic value. +pub fn encode_polygon(polygon: Polygon) -> dynamic.Dynamic { + let Polygon(coordinates) = polygon + let obj = + dict.from_list([ + #("type", dynamic.from("Polygon")), + #("coordinates", encode_positions_list(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a multi-polygon into a dynamic value. +pub fn encode_multipolygon(multipolygon: MultiPolygon) -> dynamic.Dynamic { + let MultiPolygon(coordinates) = multipolygon + let obj = + dict.from_list([ + #("type", dynamic.from("MultiPolygon")), + #("coordinates", encode_positions_list_list(coordinates)), + ]) + dynamic.from(obj) +} + +/// Encodes a geometry collection into a dynamic value. +pub fn encode_geometrycollection( + collection: GeometryCollection, +) -> dynamic.Dynamic { + let GeometryCollection(geometries) = collection + let geometries_dyn_list = list.map(geometries, encode_geometry) + let obj = + dict.from_list([ + #("type", dynamic.from("GeometryCollection")), + #("geometries", dynamic.from(geometries_dyn_list)), + ]) + dynamic.from(obj) +} + +/// Encodes a geometry into a dynamic value. +pub fn encode_geometry(geometry: Geometry) -> dynamic.Dynamic { + case geometry { + GeometryPoint(point) -> encode_point(point) + GeometryMultiPoint(multipoint) -> encode_multipoint(multipoint) + GeometryLineString(linestring) -> encode_linestring(linestring) + GeometryMultiLineString(multilinestring) -> + encode_multilinestring(multilinestring) + GeometryPolygon(polygon) -> encode_polygon(polygon) + GeometryMultiPolygon(multipolygon) -> encode_multipolygon(multipolygon) + GeometryCollectionType(collection) -> encode_geometrycollection(collection) + } +} + +/// Encodes a feature into a dynamic value. +pub fn encode_feature(feature: Feature) -> dynamic.Dynamic { + let Feature(geometry_opt, properties_opt, id_opt) = feature + let geometry_dyn = case geometry_opt { + option.Some(geometry) -> encode_geometry(geometry) + option.None -> dynamic.from(Nil) + } + let properties_dyn = case properties_opt { + option.Some(props) -> dynamic.from(props) + option.None -> dynamic.from(Nil) + } + let base_obj = + dict.from_list([ + #("type", dynamic.from("Feature")), + #("geometry", geometry_dyn), + #("properties", properties_dyn), + ]) + let obj = case id_opt { + option.Some(id_dyn) -> dict.insert(base_obj, "id", id_dyn) + option.None -> base_obj + } + dynamic.from(obj) +} + +/// Encodes a feature collection into a dynamic value. +pub fn encode_featurecollection( + collection: FeatureCollection, +) -> dynamic.Dynamic { + let FeatureCollection(features) = collection + let features_dyn_list = list.map(features, encode_feature) + let obj = + dict.from_list([ + #("type", dynamic.from("FeatureCollection")), + #("features", dynamic.from(features_dyn_list)), + ]) + dynamic.from(obj) +} + +/// Encodes a GeoJSON object into a dynamic value. +pub fn encode_geojson(geojson: GeoJSON) -> dynamic.Dynamic { + case geojson { + GeoJSONGeometry(geometry) -> encode_geometry(geometry) + GeoJSONFeature(feature) -> encode_feature(feature) + GeoJSONFeatureCollection(collection) -> encode_featurecollection(collection) + } +} + +// Decoding Functions + +/// Decodes a position from a dynamic value. +pub fn position_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(Position, List(dynamic.DecodeError)) { + dynamic.list(of: dynamic.float)(dynamic_value) +} + +/// Decodes a list of positions from a dynamic value. +pub fn positions_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(List(Position), List(dynamic.DecodeError)) { + dynamic.list(of: position_decoder)(dynamic_value) +} + +/// Decodes a list of lists of positions from a dynamic value. +pub fn positions_list_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(List(List(Position)), List(dynamic.DecodeError)) { + dynamic.list(of: positions_decoder)(dynamic_value) +} + +/// Decodes a list of lists of lists of positions from a dynamic value. +pub fn positions_list_list_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(List(List(List(Position))), List(dynamic.DecodeError)) { + dynamic.list(of: positions_list_decoder)(dynamic_value) +} + +/// Decodes a point from a dynamic value. +pub fn point_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(Point, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "Point" -> + dynamic.field(named: "coordinates", of: position_decoder)(dynamic_value) + |> result.map(Point) + _ -> + Error([ + dynamic.DecodeError(expected: "Point", found: type_str, path: ["type"]), + ]) + } + }) +} + +/// Decodes a multi-point from a dynamic value. +pub fn multipoint_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(MultiPoint, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "MultiPoint" -> + dynamic.field(named: "coordinates", of: positions_decoder)( + dynamic_value, + ) + |> result.map(MultiPoint) + _ -> + Error([ + dynamic.DecodeError(expected: "MultiPoint", found: type_str, path: [ + "type", + ]), + ]) + } + }) +} + +/// Decodes a line string from a dynamic value. +pub fn linestring_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(LineString, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "LineString" -> + dynamic.field(named: "coordinates", of: positions_decoder)( + dynamic_value, + ) + |> result.map(LineString) + _ -> + Error([ + dynamic.DecodeError(expected: "LineString", found: type_str, path: [ + "type", + ]), + ]) + } + }) +} + +/// Decodes a multi-line string from a dynamic value. +pub fn multilinestring_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(MultiLineString, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "MultiLineString" -> + dynamic.field(named: "coordinates", of: positions_list_decoder)( + dynamic_value, + ) + |> result.map(MultiLineString) + _ -> + Error([ + dynamic.DecodeError( + expected: "MultiLineString", + found: type_str, + path: ["type"], + ), + ]) + } + }) +} + +/// Decodes a polygon from a dynamic value. +pub fn polygon_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(Polygon, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "Polygon" -> + dynamic.field(named: "coordinates", of: positions_list_decoder)( + dynamic_value, + ) + |> result.map(Polygon) + _ -> + Error([ + dynamic.DecodeError(expected: "Polygon", found: type_str, path: [ + "type", + ]), + ]) + } + }) +} + +/// Decodes a multi-polygon from a dynamic value. +pub fn multipolygon_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(MultiPolygon, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "MultiPolygon" -> + dynamic.field(named: "coordinates", of: positions_list_list_decoder)( + dynamic_value, + ) + |> result.map(MultiPolygon) + _ -> + Error([ + dynamic.DecodeError(expected: "MultiPolygon", found: type_str, path: [ + "type", + ]), + ]) + } + }) +} + +/// Decodes a geometry collection from a dynamic value. +pub fn geometrycollection_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(GeometryCollection, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "GeometryCollection" -> + dynamic.field( + named: "geometries", + of: dynamic.list(of: geometry_decoder), + )(dynamic_value) + |> result.map(GeometryCollection) + _ -> + Error([ + dynamic.DecodeError( + expected: "GeometryCollection", + found: type_str, + path: ["type"], + ), + ]) + } + }) +} + +/// Decodes a geometry from a dynamic value. +pub fn geometry_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(Geometry, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "Point" -> + point_decoder(dynamic_value) + |> result.map(GeometryPoint) + "MultiPoint" -> + multipoint_decoder(dynamic_value) + |> result.map(GeometryMultiPoint) + "LineString" -> + linestring_decoder(dynamic_value) + |> result.map(GeometryLineString) + "MultiLineString" -> + multilinestring_decoder(dynamic_value) + |> result.map(GeometryMultiLineString) + "Polygon" -> + polygon_decoder(dynamic_value) + |> result.map(GeometryPolygon) + "MultiPolygon" -> + multipolygon_decoder(dynamic_value) + |> result.map(GeometryMultiPolygon) + "GeometryCollection" -> + geometrycollection_decoder(dynamic_value) + |> result.map(GeometryCollectionType) + _ -> + Error([ + dynamic.DecodeError( + expected: "Known Geometry Type", + found: type_str, + path: ["type"], + ), + ]) + } + }) +} + +/// Decodes a feature from a dynamic value. +pub fn feature_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(Feature, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "Feature" -> { + let geometry_result = + dynamic.field( + named: "geometry", + of: dynamic.optional(geometry_decoder), + )(dynamic_value) + + let properties_result = + dynamic.field( + named: "properties", + of: dynamic.optional(dynamic.dict(dynamic.string, dynamic.dynamic)), + )(dynamic_value) + + let id_result = + dynamic.optional_field(named: "id", of: dynamic.dynamic)( + dynamic_value, + ) + + let geometry_result = + geometry_result + |> result.map_error(fn(_errs) { + [ + dynamic.DecodeError(expected: "Geometry", found: "Invalid", path: [ + "geometry", + ]), + ] + }) + + let properties_result = + properties_result + |> result.map_error(fn(_errs) { + [ + dynamic.DecodeError( + expected: "Properties", + found: "Invalid", + path: ["properties"], + ), + ] + }) + + let id_result = + id_result + |> result.map_error(fn(_errs) { + [ + dynamic.DecodeError(expected: "ID", found: "Invalid", path: ["id"]), + ] + }) + + result.try(geometry_result, fn(geometry_opt) { + result.try(properties_result, fn(properties_opt) { + result.map(id_result, fn(id_opt) { + Feature(geometry_opt, properties_opt, id_opt) + }) + }) + }) + } + + _ -> + Error([ + dynamic.DecodeError(expected: "Feature", found: type_str, path: [ + "type", + ]), + ]) + } + }) +} + +/// Decodes a feature collection from a dynamic value. +pub fn featurecollection_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(FeatureCollection, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "FeatureCollection" -> + dynamic.field(named: "features", of: dynamic.list(of: feature_decoder))( + dynamic_value, + ) + |> result.map(FeatureCollection) + _ -> + Error([ + dynamic.DecodeError( + expected: "FeatureCollection", + found: type_str, + path: ["type"], + ), + ]) + } + }) +} + +/// Decodes a GeoJSON object from a dynamic value. +pub fn geojson_decoder( + dynamic_value: dynamic.Dynamic, +) -> Result(GeoJSON, List(dynamic.DecodeError)) { + dynamic.field(named: "type", of: dynamic.string)(dynamic_value) + |> result.then(fn(type_str) { + case type_str { + "Feature" -> + feature_decoder(dynamic_value) + |> result.map(GeoJSONFeature) + "FeatureCollection" -> + featurecollection_decoder(dynamic_value) + |> result.map(GeoJSONFeatureCollection) + _ -> + geometry_decoder(dynamic_value) + |> result.map(GeoJSONGeometry) + } + }) +} diff --git a/test/gleojson_test.gleam b/test/gleojson_test.gleam new file mode 100644 index 0000000..c2958af --- /dev/null +++ b/test/gleojson_test.gleam @@ -0,0 +1,360 @@ +import gleam/dict +import gleam/dynamic +import gleam/json +import gleam/option +import gleeunit +import gleeunit/should + +import gleojson + +pub fn main() { + gleeunit.main() +} + +pub fn point_encode_decode_test() { + let original_point = gleojson.Point([1.0, 2.0]) + + let encoded_dynamic = gleojson.encode_point(original_point) + + let decoded_result = gleojson.point_decoder(encoded_dynamic) + + let decoded_point = + decoded_result + |> should.be_ok + + decoded_point + |> should.equal(original_point) +} + +pub fn multipoint_encode_decode_test() { + let original_multipoint = gleojson.MultiPoint([[1.0, 2.0], [3.0, 4.0]]) + + let encoded_dynamic = gleojson.encode_multipoint(original_multipoint) + + let decoded_result = gleojson.multipoint_decoder(encoded_dynamic) + + let decoded_multipoint = + decoded_result + |> should.be_ok + + decoded_multipoint + |> should.equal(original_multipoint) +} + +pub fn linestring_encode_decode_test() { + let original_linestring = gleojson.LineString([[1.0, 2.0], [3.0, 4.0]]) + + let encoded_dynamic = gleojson.encode_linestring(original_linestring) + + let decoded_result = gleojson.linestring_decoder(encoded_dynamic) + + let decoded_linestring = + decoded_result + |> should.be_ok + + decoded_linestring + |> should.equal(original_linestring) +} + +pub fn polygon_encode_decode_test() { + let original_polygon = + gleojson.Polygon([[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]]) + + let encoded_dynamic = gleojson.encode_polygon(original_polygon) + + let decoded_result = gleojson.polygon_decoder(encoded_dynamic) + + let decoded_polygon = + decoded_result + |> should.be_ok + + decoded_polygon + |> should.equal(original_polygon) +} + +pub fn multipolygon_encode_decode_test() { + let original_multipolygon = + gleojson.MultiPolygon([ + [[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [1.0, 2.0]]], + [[[7.0, 8.0], [9.0, 10.0], [11.0, 12.0], [7.0, 8.0]]], + ]) + + let encoded_dynamic = gleojson.encode_multipolygon(original_multipolygon) + + let decoded_result = gleojson.multipolygon_decoder(encoded_dynamic) + + let decoded_multipolygon = + decoded_result + |> should.be_ok + + decoded_multipolygon + |> should.equal(original_multipolygon) +} + +pub fn geometrycollection_encode_decode_test() { + let point = gleojson.Point([1.0, 2.0]) + let linestring = gleojson.LineString([[3.0, 4.0], [5.0, 6.0]]) + + let original_geometrycollection = + gleojson.GeometryCollection([ + gleojson.GeometryPoint(point), + gleojson.GeometryLineString(linestring), + ]) + + let encoded_dynamic = + gleojson.encode_geometrycollection(original_geometrycollection) + + let decoded_result = gleojson.geometrycollection_decoder(encoded_dynamic) + + let decoded_geometrycollection = + decoded_result + |> should.be_ok + + decoded_geometrycollection + |> should.equal(original_geometrycollection) +} + +pub fn feature_encode_decode_test() { + let point = gleojson.Point([1.0, 2.0]) + + let properties = + dict.from_list([ + #("name", dynamic.from("Test Point")), + #("value", dynamic.from(42)), + ]) + + let original_feature = + gleojson.Feature( + geometry: option.Some(gleojson.GeometryPoint(point)), + properties: option.Some(properties), + id: option.Some(dynamic.from("feature-id")), + ) + + let encoded_dynamic = gleojson.encode_feature(original_feature) + + let decoded_result = gleojson.feature_decoder(encoded_dynamic) + + let decoded_feature = + decoded_result + |> should.be_ok + + decoded_feature + |> should.equal(original_feature) +} + +pub fn featurecollection_encode_decode_test() { + let point = gleojson.Point([1.0, 2.0]) + + let properties = + dict.from_list([ + #("name", dynamic.from("Test Point")), + #("value", dynamic.from(42)), + ]) + + let feature = + gleojson.Feature( + geometry: option.Some(gleojson.GeometryPoint(point)), + properties: option.Some(properties), + id: option.Some(dynamic.from("feature-id")), + ) + + let original_featurecollection = gleojson.FeatureCollection([feature]) + + let encoded_dynamic = + gleojson.encode_featurecollection(original_featurecollection) + + let decoded_result = gleojson.featurecollection_decoder(encoded_dynamic) + + let decoded_featurecollection = + decoded_result + |> should.be_ok + + decoded_featurecollection + |> should.equal(original_featurecollection) +} + +pub fn gleojson_encode_decode_test() { + let point = gleojson.Point([1.0, 2.0]) + let geometry = gleojson.GeometryPoint(point) + + let original_geojson = gleojson.GeoJSONGeometry(geometry) + + let encoded_dynamic = gleojson.encode_geojson(original_geojson) + + let decoded_result = gleojson.geojson_decoder(encoded_dynamic) + + let decoded_geojson = + decoded_result + |> should.be_ok + + decoded_geojson + |> should.equal(original_geojson) +} + +pub fn invalid_type_decode_test() { + let invalid_dynamic = + dynamic.from( + dict.from_list([ + #("type", dynamic.from("InvalidType")), + #("coordinates", dynamic.from([1.0, 2.0])), + ]), + ) + + let decoded_result = gleojson.geometry_decoder(invalid_dynamic) + + decoded_result + |> should.be_error +} + +pub fn invalid_coordinates_decode_test() { + let invalid_dynamic = + dynamic.from( + dict.from_list([ + #("type", dynamic.from("Point")), + #("coordinates", dynamic.from("invalid coordinates")), + ]), + ) + + let decoded_result = gleojson.point_decoder(invalid_dynamic) + + decoded_result + |> should.be_error +} + +pub fn featurecollection_decode_test() { + let json_string = + " + { + \"type\": \"FeatureCollection\", + \"features\": [{ + \"type\": \"Feature\", + \"geometry\": { + \"type\": \"Point\", + \"coordinates\": [102.0, 0.5] + }, + \"properties\": { + \"prop0\": \"value0\" + } + }, { + \"type\": \"Feature\", + \"geometry\": { + \"type\": \"LineString\", + \"coordinates\": [ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0] + ] + }, + \"properties\": { + \"prop0\": \"value0\", + \"prop1\": 0.0 + } + }, { + \"type\": \"Feature\", + \"geometry\": { + \"type\": \"Polygon\", + \"coordinates\": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0] + ] + ] + }, + \"properties\": { + \"prop0\": \"value0\", + \"prop1\": { + \"this\": \"that\" + } + } + }] + } + " + + // Decode the JSON string into a Dynamic value + let decode_result = + json.decode(from: json_string, using: gleojson.geojson_decoder) + + // Ensure decoding was successful + let decoded_geojson = + decode_result + |> should.be_ok + + // Construct the expected GeoJSON data structure + let point_feature = + gleojson.Feature( + geometry: option.Some( + gleojson.GeometryPoint(gleojson.Point([102.0, 0.5])), + ), + properties: option.Some( + dict.from_list([#("prop0", dynamic.from("value0"))]), + ), + id: option.None, + ) + + let linestring_feature = + gleojson.Feature( + geometry: option.Some( + gleojson.GeometryLineString( + gleojson.LineString([ + [102.0, 0.0], + [103.0, 1.0], + [104.0, 0.0], + [105.0, 1.0], + ]), + ), + ), + properties: option.Some( + dict.from_list([ + #("prop0", dynamic.from("value0")), + #("prop1", dynamic.from(0.0)), + ]), + ), + id: option.None, + ) + + let polygon_feature = + gleojson.Feature( + geometry: option.Some( + gleojson.GeometryPolygon( + gleojson.Polygon([ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ], + ]), + ), + ), + properties: option.Some( + dict.from_list([ + #("prop0", dynamic.from("value0")), + #( + "prop1", + dynamic.from( + dynamic.from(dict.from_list([#("this", dynamic.from("that"))])), + ), + ), + ]), + ), + id: option.None, + ) + + let expected_geojson = + gleojson.GeoJSONFeatureCollection( + gleojson.FeatureCollection([ + point_feature, + linestring_feature, + polygon_feature, + ]), + ) + + // Compare the decoded GeoJSON with the expected structure + decoded_geojson + |> should.equal(expected_geojson) +}