diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 44408e7..ab731d2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,10 +1,58 @@ -on: [push, pull_request] -name: Run tests +name: Run Geojson tests + +on: + push: + branches: + - main + - staging + - trying + - release/** + pull_request: + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + jobs: build_and_test: + name: Build and test all Geojson features runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - run: cargo install cargo-all-features - run: cargo build-all-features --verbose - run: cargo test-all-features --verbose + check: + name: Geojson Rustfmt and Clippy check + runs-on: ubuntu-latest + if: "!contains(github.event.head_commit.message, '[skip ci]')" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy, rustfmt + - name: Check formatting using Rustfmt + run: cargo fmt --check + - name: Lint using Clippy + run: cargo clippy --tests + all_checks_complete: + needs: + - build_and_test + - check + if: always() + runs-on: ubuntu-latest + steps: + - name: Result + run: | + jq -C <<< "${needs}" + # Check if all needs were successful or skipped. + "$(jq -r 'all(.result as $result | (["success", "skipped"] | contains([$result])))' <<< "${needs}")" + env: + needs: ${{ toJson(needs) }} diff --git a/CHANGES.md b/CHANGES.md index 4ab0e5f..c6ad9c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,17 @@ ## Unreleased +* Add support of serializing optional `geo-types` with `serialize_optional_geometry`. +* Add support of deserializing optional `geo-types` with `deserialize_optional_geometry`. +* Add support for foreign members to `FeatureWriter`. * Added conversion from `Vec` to `GeoJson`. * Added `GeoJson::to_string_pretty` as convenience wrappers around the same `serde_json` methods. +* Changed `Serialize` impls to avoid creating intermediate `JsonObject`s. +* Better CI: lint, all features +* Implement `Default` on `FeatureCollection`. +* Added `GeometryCollection::try_from(&GeoJson)` and deprecated + `quick_collection` for conventional naming and simpler docs. + * ## 0.24.1 @@ -12,7 +21,7 @@ ## 0.24.0 -* Added `geojson::{ser, de}` helpers to convert your custom struct to and from GeoJSON. +* Added `geojson::{ser, de}` helpers to convert your custom struct to and from GeoJSON. * For external geometry types like geo-types, use the `serialize_geometry`/`deserialize_geometry` helpers. * Example: ``` diff --git a/Cargo.toml b/Cargo.toml index eaa8c16..4634dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default = ["geo-types"] [dependencies] serde = { version="~1.0", features = ["derive"] } serde_json = "~1.0" -geo-types = { version = "0.7", features = ["serde"], optional = true } +geo-types = { version = "0.7.13", features = ["serde"], optional = true } thiserror = "1.0.20" log = "0.4.17" diff --git a/benches/to_geo_types.rs b/benches/to_geo_types.rs index efa0dac..dc8bdeb 100644 --- a/benches/to_geo_types.rs +++ b/benches/to_geo_types.rs @@ -1,15 +1,13 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use std::convert::TryFrom; fn benchmark_group(c: &mut Criterion) { let geojson_str = include_str!("../tests/fixtures/countries.geojson"); let geojson = geojson_str.parse::().unwrap(); #[cfg(feature = "geo-types")] - c.bench_function("quick_collection", move |b| { - b.iter(|| { - let _: Result, _> = - black_box(geojson::quick_collection(&geojson)); - }); + c.bench_function("Convert to geo-types", move |b| { + b.iter(|| black_box(geo_types::GeometryCollection::::try_from(&geojson).unwrap())); }); } diff --git a/src/conversion/from_geo_types.rs b/src/conversion/from_geo_types.rs index ec05d87..8e96d7c 100644 --- a/src/conversion/from_geo_types.rs +++ b/src/conversion/from_geo_types.rs @@ -194,7 +194,7 @@ where T: CoordFloat, { line_string - .points_iter() + .points() .map(|point| create_point_type(&point)) .collect() } @@ -242,7 +242,7 @@ where { let mut coords = vec![polygon .exterior() - .points_iter() + .points() .map(|point| create_point_type(&point)) .collect()]; @@ -271,8 +271,8 @@ where mod tests { use crate::{GeoJson, Geometry, Value}; use geo_types::{ - Coordinate, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, - MultiPolygon, Point, Polygon, Rect, Triangle, + Coord, GeometryCollection, Line, LineString, MultiLineString, MultiPoint, MultiPolygon, + Point, Polygon, Rect, Triangle, }; #[test] @@ -356,9 +356,9 @@ mod tests { #[test] fn geo_triangle_conversion_test() { - let c1 = Coordinate { x: 0., y: 0. }; - let c2 = Coordinate { x: 10., y: 20. }; - let c3 = Coordinate { x: 20., y: -10. }; + let c1: Coord = Coord { x: 0., y: 0. }; + let c2: Coord = Coord { x: 10., y: 20. }; + let c3: Coord = Coord { x: 20., y: -10. }; let triangle = Triangle(c1, c2, c3); @@ -366,14 +366,14 @@ mod tests { // Geo-types Polygon construction introduces an extra vertex: let's check it! if let Value::Polygon(c) = geojson_polygon { - assert_almost_eq!(c1.x as f64, c[0][0][0], 1e-6); - assert_almost_eq!(c1.y as f64, c[0][0][1], 1e-6); - assert_almost_eq!(c2.x as f64, c[0][1][0], 1e-6); - assert_almost_eq!(c2.y as f64, c[0][1][1], 1e-6); - assert_almost_eq!(c3.x as f64, c[0][2][0], 1e-6); - assert_almost_eq!(c3.y as f64, c[0][2][1], 1e-6); - assert_almost_eq!(c1.x as f64, c[0][3][0], 1e-6); - assert_almost_eq!(c1.y as f64, c[0][3][1], 1e-6); + assert_almost_eq!(c1.x, c[0][0][0], 1e-6); + assert_almost_eq!(c1.y, c[0][0][1], 1e-6); + assert_almost_eq!(c2.x, c[0][1][0], 1e-6); + assert_almost_eq!(c2.y, c[0][1][1], 1e-6); + assert_almost_eq!(c3.x, c[0][2][0], 1e-6); + assert_almost_eq!(c3.y, c[0][2][1], 1e-6); + assert_almost_eq!(c1.x, c[0][3][0], 1e-6); + assert_almost_eq!(c1.y, c[0][3][1], 1e-6); } else { panic!("Not valid geometry {:?}", geojson_polygon); } @@ -381,8 +381,8 @@ mod tests { #[test] fn geo_rect_conversion_test() { - let c1 = Coordinate { x: 0., y: 0. }; - let c2 = Coordinate { x: 10., y: 20. }; + let c1: Coord = Coord { x: 0., y: 0. }; + let c2: Coord = Coord { x: 10., y: 20. }; let rect = Rect::new(c1, c2); @@ -390,16 +390,16 @@ mod tests { // Geo-types Polygon construction introduces an extra vertex: let's check it! if let Value::Polygon(c) = geojson_polygon { - assert_almost_eq!(c1.x as f64, c[0][0][0], 1e-6); - assert_almost_eq!(c1.y as f64, c[0][0][1], 1e-6); - assert_almost_eq!(c1.x as f64, c[0][1][0], 1e-6); - assert_almost_eq!(c2.y as f64, c[0][1][1], 1e-6); - assert_almost_eq!(c2.x as f64, c[0][2][0], 1e-6); - assert_almost_eq!(c2.y as f64, c[0][2][1], 1e-6); - assert_almost_eq!(c2.x as f64, c[0][3][0], 1e-6); - assert_almost_eq!(c1.y as f64, c[0][3][1], 1e-6); - assert_almost_eq!(c1.x as f64, c[0][4][0], 1e-6); - assert_almost_eq!(c1.y as f64, c[0][4][1], 1e-6); + assert_almost_eq!(c1.x, c[0][0][0], 1e-6); + assert_almost_eq!(c1.y, c[0][0][1], 1e-6); + assert_almost_eq!(c1.x, c[0][1][0], 1e-6); + assert_almost_eq!(c2.y, c[0][1][1], 1e-6); + assert_almost_eq!(c2.x, c[0][2][0], 1e-6); + assert_almost_eq!(c2.y, c[0][2][1], 1e-6); + assert_almost_eq!(c2.x, c[0][3][0], 1e-6); + assert_almost_eq!(c1.y, c[0][3][1], 1e-6); + assert_almost_eq!(c1.x, c[0][4][0], 1e-6); + assert_almost_eq!(c1.y, c[0][4][1], 1e-6); } else { panic!("Not valid geometry {:?}", geojson_polygon); } diff --git a/src/conversion/mod.rs b/src/conversion/mod.rs index 9c3f4cf..e4bfad3 100644 --- a/src/conversion/mod.rs +++ b/src/conversion/mod.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use geo_types::{self, CoordFloat, GeometryCollection}; +use geo_types::CoordFloat; use crate::geojson::GeoJson; -use crate::geojson::GeoJson::{Feature, FeatureCollection, Geometry}; use crate::Result; -use std::convert::TryInto; +use std::convert::TryFrom; #[cfg(test)] macro_rules! assert_almost_eq { @@ -79,32 +78,6 @@ macro_rules! try_from_owned_value { pub(crate) mod from_geo_types; pub(crate) mod to_geo_types; -// Process top-level `GeoJSON` items, returning a geo_types::GeometryCollection or an Error -fn process_geojson(gj: &GeoJson) -> Result> -where - T: CoordFloat, -{ - match gj { - FeatureCollection(collection) => Ok(GeometryCollection( - collection - .features - .iter() - // Only pass on non-empty geometries - .filter_map(|feature| feature.geometry.as_ref()) - .map(|geometry| geometry.clone().try_into()) - .collect::>()?, - )), - Feature(feature) => { - if let Some(geometry) = &feature.geometry { - Ok(GeometryCollection(vec![geometry.clone().try_into()?])) - } else { - Ok(GeometryCollection(vec![])) - } - } - Geometry(geometry) => Ok(GeometryCollection(vec![geometry.clone().try_into()?])), - } -} - /// A shortcut for producing `geo_types` [GeometryCollection](../geo_types/struct.GeometryCollection.html) objects /// from arbitrary valid GeoJSON input. /// @@ -113,7 +86,8 @@ where /// # Example /// /// ``` -/// use geo_types::GeometryCollection; +/// use geo_types::{Geometry, GeometryCollection, Point}; +/// #[allow(deprecated)] /// use geojson::{quick_collection, GeoJson}; /// /// let geojson_str = r#" @@ -125,10 +99,7 @@ where /// "properties": {}, /// "geometry": { /// "type": "Point", -/// "coordinates": [ -/// -0.13583511114120483, -/// 51.5218870403801 -/// ] +/// "coordinates": [-1.0, 2.0] /// } /// } /// ] @@ -136,12 +107,18 @@ where /// "#; /// let geojson = geojson_str.parse::().unwrap(); /// // Turn the GeoJSON string into a geo_types GeometryCollection +/// #[allow(deprecated)] /// let mut collection: GeometryCollection = quick_collection(&geojson).unwrap(); +/// assert_eq!(collection[0], Geometry::Point(Point::new(-1.0, 2.0))) /// ``` +#[deprecated( + since = "0.24.1", + note = "use `geo_types::GeometryCollection::try_from(&geojson)` instead" +)] #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] pub fn quick_collection(gj: &GeoJson) -> Result> where T: CoordFloat, { - process_geojson(gj) + geo_types::GeometryCollection::try_from(gj) } diff --git a/src/conversion/to_geo_types.rs b/src/conversion/to_geo_types.rs index 996a730..b57907e 100644 --- a/src/conversion/to_geo_types.rs +++ b/src/conversion/to_geo_types.rs @@ -2,10 +2,8 @@ use geo_types::{self, CoordFloat}; use crate::geometry; -use crate::{ - quick_collection, Feature, FeatureCollection, GeoJson, LineStringType, PointType, PolygonType, -}; use crate::{Error, Result}; +use crate::{Feature, FeatureCollection, GeoJson, LineStringType, PointType, PolygonType}; use std::convert::{TryFrom, TryInto}; #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] @@ -243,6 +241,40 @@ impl_try_from_geom_value![ GeometryCollection ]; +impl TryFrom<&GeoJson> for geo_types::GeometryCollection { + type Error = Error; + + /// Process top-level `GeoJSON` items, returning a geo_types::GeometryCollection or an Error + fn try_from(gj: &GeoJson) -> Result> + where + T: CoordFloat, + { + match gj { + GeoJson::FeatureCollection(collection) => Ok(geo_types::GeometryCollection( + collection + .features + .iter() + // Only pass on non-empty geometries + .filter_map(|feature| feature.geometry.as_ref()) + .map(|geometry| geometry.clone().try_into()) + .collect::>()?, + )), + GeoJson::Feature(feature) => { + if let Some(geometry) = &feature.geometry { + Ok(geo_types::GeometryCollection(vec![geometry + .clone() + .try_into()?])) + } else { + Ok(geo_types::GeometryCollection(vec![])) + } + } + GeoJson::Geometry(geometry) => Ok(geo_types::GeometryCollection(vec![geometry + .clone() + .try_into()?])), + } + } +} + #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] impl TryFrom for geo_types::Geometry where @@ -251,9 +283,9 @@ where type Error = Error; fn try_from(val: FeatureCollection) -> Result> { - Ok(geo_types::Geometry::GeometryCollection(quick_collection( - &GeoJson::FeatureCollection(val), - )?)) + Ok(geo_types::Geometry::GeometryCollection( + geo_types::GeometryCollection::try_from(&GeoJson::FeatureCollection(val))?, + )) } } @@ -273,11 +305,11 @@ where } } -fn create_geo_coordinate(point_type: &PointType) -> geo_types::Coordinate +fn create_geo_coordinate(point_type: &PointType) -> geo_types::Coord where T: CoordFloat, { - geo_types::Coordinate { + geo_types::Coord { x: T::from(point_type[0]).unwrap(), y: T::from(point_type[1]).unwrap(), } @@ -324,7 +356,7 @@ where T: CoordFloat, { let exterior = polygon_type - .get(0) + .first() .map(|e| create_geo_line_string(e)) .unwrap_or_else(|| create_geo_line_string(&vec![])); diff --git a/src/de.rs b/src/de.rs index feb318e..f0ed06a 100644 --- a/src/de.rs +++ b/src/de.rs @@ -9,7 +9,7 @@ //! } //! ``` //! -//! Your type *must* have a field called `geometry` and it must be `deserialized_with` [`deserialize_geometry`](crate::de::deserialize_geometry): +//! Your type *must* have a field called `geometry` and it must be `deserialize_with` [`deserialize_geometry`](crate::de::deserialize_geometry): //! ```rust, ignore //! #[derive(serde::Deserialize)] //! struct MyStruct { @@ -30,7 +30,7 @@ //! //! #[derive(Deserialize)] //! struct MyStruct { -//! // Deserialize from geojson, rather than expecting the type's default serialization +//! // Deserialize from GeoJSON, rather than expecting the type's default serialization //! #[serde(deserialize_with = "deserialize_geometry")] //! geometry: geo_types::Point, //! name: String, @@ -76,7 +76,7 @@ //! ```ignore //! #[derive(serde::Serialize, serde::Deserialize)] //! struct MyStruct { -//! // Serialize as geojson, rather than using the type's default serialization +//! // Serialize as GeoJSON, rather than using the type's default serialization //! #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] //! geometry: geo_types::Point, //! ... @@ -234,7 +234,64 @@ where let geojson_geometry = crate::Geometry::deserialize(deserializer)?; geojson_geometry .try_into() - .map_err(|err| Error::custom(format!("unable to convert from geojson Geometry: {}", err))) + .map_err(deserialize_error_msg::) +} + +/// [`serde::deserialize_with`](https://serde.rs/field-attrs.html#deserialize_with) helper to deserialize an optional GeoJSON Geometry into another type, +/// like an optional [`geo_types`] Geometry. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use geojson::de::deserialize_optional_geometry; +/// use serde::Deserialize; +/// use serde_json::{json, from_value}; +/// +/// #[derive(Deserialize)] +/// struct MyStruct { +/// #[serde(rename = "count")] +/// _count: usize, +/// #[serde(default, deserialize_with = "deserialize_optional_geometry")] +/// geometry: Option>, +/// } +/// +/// let json = json! {{ +/// "count": 0, +/// "geometry": { +/// "type": "Point", +/// "coordinates": [125.6, 10.1] +/// }, +/// }}; +/// let feature: MyStruct = from_value(json).unwrap(); +/// assert!(feature.geometry.is_some()); +/// +/// let json = json! {{ +/// "count": 1, +/// }}; +/// let feature: MyStruct = from_value(json).unwrap(); +/// assert!(feature.geometry.is_none()) +/// ``` +pub fn deserialize_optional_geometry<'de, D, G>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, + G: TryFrom, + G::Error: std::fmt::Display, +{ + Option::::deserialize(deserializer)? + .map(TryInto::try_into) + .transpose() + .map_err(deserialize_error_msg::) +} + +fn deserialize_error_msg<'de, D: Deserializer<'de>>( + error: impl std::fmt::Display, +) -> >::Error { + Error::custom(format!( + "unable to convert from geojson Geometry: {}", + error + )) } /// Deserialize a GeoJSON FeatureCollection into [`Feature`] structs. @@ -414,7 +471,7 @@ pub(crate) mod tests { assert_eq!(records.len(), 2); let first_age = { - let props = records.get(0).unwrap().properties.as_ref().unwrap(); + let props = records.first().unwrap().properties.as_ref().unwrap(); props.get("age").unwrap().as_i64().unwrap() }; assert_eq!(first_age, 123); @@ -527,6 +584,33 @@ pub(crate) mod tests { let expected_err_text = r#"Error while deserializing JSON: unable to convert from geojson Geometry: Expected type: `LineString`, but found `Point`"#; assert_eq!(err.to_string(), expected_err_text); } + + #[test] + fn deserializes_optional_point() { + #[derive(serde::Deserialize)] + struct MyStruct { + #[serde(rename = "count")] + _count: usize, + #[serde(default, deserialize_with = "deserialize_optional_geometry")] + geometry: Option>, + } + + let json = json! {{ + "count": 0, + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + }}; + let feature: MyStruct = serde_json::from_value(json).unwrap(); + assert!(feature.geometry.is_some()); + + let json = json! {{ + "count": 1, + }}; + let feature: MyStruct = serde_json::from_value(json).unwrap(); + assert!(feature.geometry.is_none()) + } } #[cfg(feature = "geo-types")] diff --git a/src/feature.rs b/src/feature.rs index 9e82e32..dc7aaf1 100644 --- a/src/feature.rs +++ b/src/feature.rs @@ -18,8 +18,7 @@ use std::str::FromStr; use crate::errors::{Error, Result}; use crate::{util, Feature, Geometry, Value}; use crate::{JsonObject, JsonValue}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::json; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; impl From for Feature { fn from(geom: Geometry) -> Feature { @@ -55,35 +54,18 @@ impl FromStr for Feature { impl<'a> From<&'a Feature> for JsonObject { fn from(feature: &'a Feature) -> JsonObject { - let mut map = JsonObject::new(); - map.insert(String::from("type"), json!("Feature")); - map.insert( - String::from("geometry"), - serde_json::to_value(&feature.geometry).unwrap(), - ); - if let Some(ref properties) = feature.properties { - map.insert( - String::from("properties"), - serde_json::to_value(properties).unwrap(), - ); - } else { - map.insert( - String::from("properties"), - serde_json::to_value(Some(serde_json::Map::new())).unwrap(), - ); - } - if let Some(ref bbox) = feature.bbox { - map.insert(String::from("bbox"), serde_json::to_value(bbox).unwrap()); - } - if let Some(ref id) = feature.id { - map.insert(String::from("id"), serde_json::to_value(id).unwrap()); - } - if let Some(ref foreign_members) = feature.foreign_members { - for (key, value) in foreign_members { - map.insert(key.to_owned(), value.to_owned()); + // The unwrap() should never panic, because Feature contains only JSON-serializable types + match serde_json::to_value(feature).unwrap() { + serde_json::Value::Object(obj) => obj, + value => { + // Panic should never happen, because `impl Serialize for Feature` always produces an + // Object + panic!( + "serializing Feature should result in an Object, but got something {:?}", + value + ) } } - map } } @@ -182,7 +164,22 @@ impl Serialize for Feature { where S: Serializer, { - JsonObject::from(self).serialize(serializer) + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", "Feature")?; + map.serialize_entry("geometry", &self.geometry)?; + map.serialize_entry("properties", &self.properties)?; + if let Some(ref bbox) = self.bbox { + map.serialize_entry("bbox", bbox)?; + } + if let Some(ref id) = self.id { + map.serialize_entry("id", id)?; + } + if let Some(ref foreign_members) = self.foreign_members { + for (key, value) in foreign_members { + map.serialize_entry(key, value)?; + } + } + map.end() } } @@ -229,8 +226,7 @@ mod tests { use std::str::FromStr; fn feature_json_str() -> &'static str { - "{\"geometry\":{\"coordinates\":[1.1,2.1],\"type\":\"Point\"},\"properties\":{},\"type\":\ - \"Feature\"}" + "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[1.1,2.1]},\"properties\":{}}" } fn properties() -> Option { @@ -314,8 +310,7 @@ mod tests { #[test] fn test_display_feature() { let f = feature().to_string(); - assert_eq!(f, "{\"geometry\":{\"coordinates\":[1.1,2.1],\"type\":\"Point\"},\"properties\":{},\"type\":\ - \"Feature\"}"); + assert_eq!(f, "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[1.1,2.1]},\"properties\":{}}"); } #[test] @@ -344,7 +339,7 @@ mod tests { #[test] fn encode_decode_feature_with_id_number() { - let feature_json_str = "{\"geometry\":{\"coordinates\":[1.1,2.1],\"type\":\"Point\"},\"id\":0,\"properties\":{},\"type\":\"Feature\"}"; + let feature_json_str = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[1.1,2.1]},\"properties\":{},\"id\":0}"; let feature = crate::Feature { geometry: Some(Geometry { value: Value::Point(vec![1.1, 2.1]), @@ -370,7 +365,7 @@ mod tests { #[test] fn encode_decode_feature_with_id_string() { - let feature_json_str = "{\"geometry\":{\"coordinates\":[1.1,2.1],\"type\":\"Point\"},\"id\":\"foo\",\"properties\":{},\"type\":\"Feature\"}"; + let feature_json_str = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[1.1,2.1]},\"properties\":{},\"id\":\"foo\"}"; let feature = crate::Feature { geometry: Some(Geometry { value: Value::Point(vec![1.1, 2.1]), @@ -416,7 +411,8 @@ mod tests { fn encode_decode_feature_with_foreign_member() { use crate::JsonObject; use serde_json; - let feature_json_str = "{\"geometry\":{\"coordinates\":[1.1,2.1],\"type\":\"Point\"},\"other_member\":\"some_value\",\"properties\":{},\"type\":\"Feature\"}"; + let feature_json_str = "{\"type\":\"Feature\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[1.1,2.1]},\"properties\":{},\"other_member\":\"some_value\"}"; + let mut foreign_members = JsonObject::new(); foreign_members.insert( String::from("other_member"), @@ -445,6 +441,29 @@ mod tests { assert_eq!(decoded_feature, feature); } + #[test] + fn encode_decode_feature_with_null_properties() { + let feature_json_str = r#"{"type":"Feature","geometry":{"type":"Point","coordinates":[1.1,2.1]},"properties":null}"#; + + let feature = crate::Feature { + geometry: Some(Value::Point(vec![1.1, 2.1]).into()), + properties: None, + bbox: None, + id: None, + foreign_members: None, + }; + // Test encode + let json_string = encode(&feature); + assert_eq!(json_string, feature_json_str); + + // Test decode + let decoded_feature = match decode(feature_json_str.into()) { + GeoJson::Feature(f) => f, + _ => unreachable!(), + }; + assert_eq!(decoded_feature, feature); + } + #[test] fn feature_ergonomic_property_access() { use serde_json::json; diff --git a/src/feature_collection.rs b/src/feature_collection.rs index ba2c83f..f43da49 100644 --- a/src/feature_collection.rs +++ b/src/feature_collection.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use serde::ser::SerializeMap; use std::convert::TryFrom; use std::iter::FromIterator; use std::str::FromStr; @@ -20,7 +21,6 @@ use crate::errors::{Error, Result}; use crate::{util, Bbox, Feature}; use crate::{JsonObject, JsonValue}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::json; /// Feature Collection Objects /// @@ -44,7 +44,7 @@ use serde_json::json; /// /// assert_eq!( /// serialized, -/// "{\"features\":[],\"type\":\"FeatureCollection\"}" +/// "{\"type\":\"FeatureCollection\",\"features\":[]}" /// ); /// ``` /// @@ -61,7 +61,7 @@ use serde_json::json; /// .collect(); /// assert_eq!(fc.features.len(), 10); /// ``` -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct FeatureCollection { /// Bounding Box /// @@ -94,24 +94,15 @@ impl<'a> IntoIterator for &'a FeatureCollection { impl<'a> From<&'a FeatureCollection> for JsonObject { fn from(fc: &'a FeatureCollection) -> JsonObject { - let mut map = JsonObject::new(); - map.insert(String::from("type"), json!("FeatureCollection")); - map.insert( - String::from("features"), - serde_json::to_value(&fc.features).unwrap(), - ); - - if let Some(ref bbox) = fc.bbox { - map.insert(String::from("bbox"), serde_json::to_value(bbox).unwrap()); - } - - if let Some(ref foreign_members) = fc.foreign_members { - for (key, value) in foreign_members { - map.insert(key.to_owned(), value.to_owned()); + // The unwrap() should never panic, because FeatureCollection contains only JSON-serializable types + match serde_json::to_value(fc).unwrap() { + serde_json::Value::Object(obj) => obj, + value => { + // Panic should never happen, because `impl Serialize for FeatureCollection` always produces an + // Object + panic!("serializing FeatureCollection should result in an Object, but got something {:?}", value) } } - - map } } @@ -168,7 +159,21 @@ impl Serialize for FeatureCollection { where S: Serializer, { - JsonObject::from(self).serialize(serializer) + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("type", "FeatureCollection")?; + map.serialize_entry("features", &self.features)?; + + if let Some(ref bbox) = self.bbox { + map.serialize_entry("bbox", bbox)?; + } + + if let Some(ref foreign_members) = self.foreign_members { + for (key, value) in foreign_members { + map.serialize_entry(key, value)?; + } + } + + map.end() } } @@ -222,7 +227,7 @@ impl FromIterator for FeatureCollection { } Some(fbox) if curr_len == 0 => { // First iteration: just copy values from fbox - *curr_bbox = fbox.clone(); + curr_bbox.clone_from(fbox); } Some(fbox) if curr_len != fbox.len() => { bbox = None; diff --git a/src/feature_iterator.rs b/src/feature_iterator.rs index 9155067..27729ee 100644 --- a/src/feature_iterator.rs +++ b/src/feature_iterator.rs @@ -40,6 +40,7 @@ pub struct FeatureIterator<'de, R, D = Feature> { lifetime: PhantomData<&'de ()>, } +#[allow(clippy::enum_variant_names)] #[derive(Debug, Copy, Clone)] enum State { BeforeFeatures, diff --git a/src/feature_writer.rs b/src/feature_writer.rs index a48c578..9ed00d0 100644 --- a/src/feature_writer.rs +++ b/src/feature_writer.rs @@ -7,7 +7,8 @@ use std::io::Write; #[derive(PartialEq)] enum State { New, - Started, + WritingFeatures, + WritingForeignMembers, Finished, } @@ -23,6 +24,9 @@ impl FeatureWriter { /// To append features from your custom structs, use [`FeatureWriter::serialize`]. /// /// To append features from [`Feature`] use [`FeatureWriter::write_feature`]. + /// + /// To write a foreign member, use [`FeatureWriter::write_foreign_member`] before appending any + /// features. pub fn from_writer(writer: W) -> Self { Self { writer, @@ -41,11 +45,15 @@ impl FeatureWriter { } State::New => { self.write_prefix()?; - self.state = State::Started; + self.state = State::WritingFeatures; } - State::Started => { + State::WritingFeatures => { self.write_str(",")?; } + State::WritingForeignMembers => { + self.write_str(r#" "features": ["#)?; + self.state = State::WritingFeatures; + } } serde_json::to_writer(&mut self.writer, feature)?; Ok(()) @@ -158,15 +166,51 @@ impl FeatureWriter { } State::New => { self.write_prefix()?; - self.state = State::Started; + self.state = State::WritingFeatures; } - State::Started => { + State::WritingFeatures => { self.write_str(",")?; } + State::WritingForeignMembers => { + self.write_str(r#" "features": ["#)?; + self.state = State::WritingFeatures; + } } to_feature_writer(&mut self.writer, value) } + /// Write a [foreign member](https://datatracker.ietf.org/doc/html/rfc7946#section-6) to the + /// output stream. This must be done before appending any features. + pub fn write_foreign_member( + &mut self, + key: &str, + value: &T, + ) -> Result<()> { + match self.state { + State::Finished => Err(Error::InvalidWriterState( + "cannot write foreign member when writer has already finished", + )), + State::New => { + self.write_str(r#"{ "type": "FeatureCollection", "#)?; + write!(self.writer, "\"{key}\": ")?; + serde_json::to_writer(&mut self.writer, value)?; + self.write_str(",")?; + + self.state = State::WritingForeignMembers; + Ok(()) + } + State::WritingFeatures => Err(Error::InvalidWriterState( + "must write foreign members before any features", + )), + State::WritingForeignMembers => { + write!(self.writer, "\"{key}\": ")?; + serde_json::to_writer(&mut self.writer, value)?; + self.write_str(",")?; + Ok(()) + } + } + } + /// Writes the closing syntax for the FeatureCollection. /// /// You shouldn't normally need to call this manually, as the writer will close itself upon @@ -183,7 +227,7 @@ impl FeatureWriter { self.write_prefix()?; self.write_suffix()?; } - State::Started => { + State::WritingFeatures | State::WritingForeignMembers => { self.state = State::Finished; self.write_suffix()?; } @@ -378,6 +422,58 @@ mod tests { assert_eq!(actual_json, expected) } + #[test] + fn write_foreign_members() { + let mut buffer: Vec = vec![]; + { + let mut writer = FeatureWriter::from_writer(&mut buffer); + + writer.write_foreign_member("extra", "string").unwrap(); + writer.write_foreign_member("list", &[1, 2, 3]).unwrap(); + writer + .write_foreign_member("nested", &json!({"foo": "bar"})) + .unwrap(); + + let record_1 = { + let mut props = serde_json::Map::new(); + props.insert("name".to_string(), "Mishka".into()); + props.insert("age".to_string(), 12.into()); + + Feature { + bbox: None, + geometry: Some(crate::Geometry::from(crate::Value::Point(vec![1.1, 1.2]))), + id: None, + properties: Some(props), + foreign_members: None, + } + }; + + writer.write_feature(&record_1).unwrap(); + writer.flush().unwrap(); + } + + let expected = json!({ + "type": "FeatureCollection", + "extra": "string", + "list": [1, 2, 3], + "nested": { + "foo": "bar", + }, + "features": [ + { + "type": "Feature", + "geometry": { "type": "Point", "coordinates": [1.1, 1.2] }, + "properties": { "name": "Mishka", "age": 12 + } + }, + ] + }); + + println!("{}", String::from_utf8(buffer.clone()).unwrap()); + let actual_json: JsonValue = serde_json::from_slice(&buffer).expect("valid json"); + assert_eq!(actual_json, expected) + } + #[cfg(feature = "geo-types")] mod test_geo_types { use super::*; diff --git a/src/geojson.rs b/src/geojson.rs index 75fae4c..7213a29 100644 --- a/src/geojson.rs +++ b/src/geojson.rs @@ -308,7 +308,11 @@ impl Serialize for GeoJson { where S: Serializer, { - JsonObject::from(self).serialize(serializer) + match self { + GeoJson::Geometry(ref geometry) => geometry.serialize(serializer), + GeoJson::Feature(ref feature) => feature.serialize(serializer), + GeoJson::FeatureCollection(ref fc) => fc.serialize(serializer), + } } } diff --git a/src/geometry.rs b/src/geometry.rs index a150654..a4a129d 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -18,7 +18,7 @@ use std::{convert::TryFrom, fmt}; use crate::errors::{Error, Result}; use crate::{util, Bbox, LineStringType, PointType, PolygonType}; use crate::{JsonObject, JsonValue}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; /// The underlying value for a `Geometry`. /// @@ -102,6 +102,7 @@ impl<'a> From<&'a Value> for JsonObject { let mut map = JsonObject::new(); map.insert( String::from("type"), + // The unwrap() should never panic, because &str always serializes to JSON ::serde_json::to_value(value.type_name()).unwrap(), ); map.insert( @@ -109,6 +110,7 @@ impl<'a> From<&'a Value> for JsonObject { Value::GeometryCollection(..) => "geometries", _ => "coordinates", }), + // The unwrap() should never panic, because Value contains only JSON-serializable types ::serde_json::to_value(value).unwrap(), ); map @@ -123,6 +125,21 @@ impl Value { pub fn from_json_value(value: JsonValue) -> Result { Self::try_from(value) } + + fn serialize_to_map( + &self, + map: &mut SM, + ) -> std::result::Result<(), SM::Error> { + map.serialize_entry("type", self.type_name())?; + map.serialize_entry( + match self { + Value::GeometryCollection(..) => "geometries", + _ => "coordinates", + }, + self, + )?; + Ok(()) + } } impl TryFrom for Value { @@ -155,16 +172,7 @@ impl fmt::Display for Value { impl<'a> From<&'a Value> for JsonValue { fn from(value: &'a Value) -> JsonValue { - match *value { - Value::Point(ref x) => ::serde_json::to_value(x), - Value::MultiPoint(ref x) => ::serde_json::to_value(x), - Value::LineString(ref x) => ::serde_json::to_value(x), - Value::MultiLineString(ref x) => ::serde_json::to_value(x), - Value::Polygon(ref x) => ::serde_json::to_value(x), - Value::MultiPolygon(ref x) => ::serde_json::to_value(x), - Value::GeometryCollection(ref x) => ::serde_json::to_value(x), - } - .unwrap() + ::serde_json::to_value(value).unwrap() } } @@ -173,7 +181,15 @@ impl Serialize for Value { where S: Serializer, { - JsonValue::from(self).serialize(serializer) + match self { + Value::Point(x) => x.serialize(serializer), + Value::MultiPoint(x) => x.serialize(serializer), + Value::LineString(x) => x.serialize(serializer), + Value::MultiLineString(x) => x.serialize(serializer), + Value::Polygon(x) => x.serialize(serializer), + Value::MultiPolygon(x) => x.serialize(serializer), + Value::GeometryCollection(x) => x.serialize(serializer), + } } } @@ -208,7 +224,7 @@ impl Serialize for Value { /// let geojson_string = geometry.to_string(); /// /// assert_eq!( -/// "{\"coordinates\":[7.428959,1.513394],\"type\":\"Point\"}", +/// "{\"type\":\"Point\",\"coordinates\":[7.428959,1.513394]}", /// geojson_string, /// ); /// ``` @@ -291,6 +307,23 @@ impl Geometry { pub fn from_json_value(value: JsonValue) -> Result { Self::try_from(value) } + + fn serialize_to_map( + &self, + map: &mut SM, + ) -> std::result::Result<(), SM::Error> { + self.value.serialize_to_map(map)?; + if let Some(ref bbox) = self.bbox { + map.serialize_entry("bbox", bbox)?; + } + + if let Some(ref foreign_members) = self.foreign_members { + for (key, value) in foreign_members { + map.serialize_entry(key, value)? + } + } + Ok(()) + } } impl TryFrom for Geometry { @@ -333,7 +366,9 @@ impl Serialize for Geometry { where S: Serializer, { - JsonObject::from(self).serialize(serializer) + let mut map = serializer.serialize_map(None)?; + self.serialize_to_map(&mut map)?; + map.end() } } @@ -374,7 +409,7 @@ mod tests { #[test] fn encode_decode_geometry() { - let geometry_json_str = "{\"coordinates\":[1.1,2.1],\"type\":\"Point\"}"; + let geometry_json_str = "{\"type\":\"Point\",\"coordinates\":[1.1,2.1]}"; let geometry = Geometry { value: Value::Point(vec![1.1, 2.1]), bbox: None, @@ -422,8 +457,8 @@ mod tests { let v = Value::LineString(vec![vec![0.0, 0.1], vec![0.1, 0.2], vec![0.2, 0.3]]); let geometry = Geometry::new(v); assert_eq!( - "{\"coordinates\":[[0.0,0.1],[0.1,0.2],[0.2,0.3]],\"type\":\"LineString\"}", - geometry.to_string() + geometry.to_string(), + "{\"type\":\"LineString\",\"coordinates\":[[0.0,0.1],[0.1,0.2],[0.2,0.3]]}" ); } @@ -439,7 +474,7 @@ mod tests { #[test] fn encode_decode_geometry_with_foreign_member() { let geometry_json_str = - "{\"coordinates\":[1.1,2.1],\"other_member\":true,\"type\":\"Point\"}"; + "{\"type\":\"Point\",\"coordinates\":[1.1,2.1],\"other_member\":true}"; let mut foreign_members = JsonObject::new(); foreign_members.insert( String::from("other_member"), @@ -482,7 +517,7 @@ mod tests { foreign_members: None, }; - let geometry_collection_string = "{\"geometries\":[{\"coordinates\":[100.0,0.0],\"type\":\"Point\"},{\"coordinates\":[[101.0,0.0],[102.0,1.0]],\"type\":\"LineString\"}],\"type\":\"GeometryCollection\"}"; + let geometry_collection_string = "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"Point\",\"coordinates\":[100.0,0.0]},{\"type\":\"LineString\",\"coordinates\":[[101.0,0.0],[102.0,1.0]]}]}"; // Test encode let json_string = encode(&geometry_collection); assert_eq!(json_string, geometry_collection_string); diff --git a/src/lib.rs b/src/lib.rs index 045faa0..f7da524 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ //! 1. A [`Geometry`] represents points, curves, and surfaces in coordinate space. //! 2. A [`Feature`] usually contains a `Geometry` and some associated data, for example a "name" //! field or any other properties you'd like associated with the `Geometry`. -//! 3. A [`FeatureCollection`] is a list of one or more `Feature`s. +//! 3. A [`FeatureCollection`] is a list of `Feature`s. //! //! Because [`Feature`] and [`FeatureCollection`] are more flexible, bare [`Geometry`] GeoJSON //! documents are rarely encountered in the wild. As such, conversions from [`Geometry`] @@ -172,21 +172,21 @@ //! GeoJson::FeatureCollection(ref ctn) => { //! for feature in &ctn.features { //! if let Some(ref geom) = feature.geometry { -//! match_geometry(geom) +//! process_geometry(geom) //! } //! } //! } //! GeoJson::Feature(ref feature) => { //! if let Some(ref geom) = feature.geometry { -//! match_geometry(geom) +//! process_geometry(geom) //! } //! } -//! GeoJson::Geometry(ref geometry) => match_geometry(geometry), +//! GeoJson::Geometry(ref geometry) => process_geometry(geometry), //! } //! } //! //! /// Process GeoJSON geometries -//! fn match_geometry(geom: &Geometry) { +//! fn process_geometry(geom: &Geometry) { //! match geom.value { //! Value::Polygon(_) => println!("Matched a Polygon"), //! Value::MultiPolygon(_) => println!("Matched a MultiPolygon"), @@ -195,7 +195,7 @@ //! // !!! GeometryCollections contain other Geometry types, and can //! // nest — we deal with this by recursively processing each geometry //! for geometry in gc { -//! match_geometry(geometry) +//! process_geometry(geometry) //! } //! } //! // Point, LineString, and their Multi– counterparts @@ -245,7 +245,7 @@ //! [`geo-types`](../geo_types/index.html#structs) are a common geometry format used across many //! geospatial processing crates. The `geo-types` feature is enabled by default. //! -//! ### From geo-types to geojson +//! ### Convert `geo-types` to `geojson` //! //! [`From`] is implemented on the [`Value`] enum variants to allow conversion _from_ [`geo-types` //! Geometries](../geo_types/index.html#structs). @@ -295,67 +295,21 @@ //! # } //! ``` //! -//! ### From geojson to geo-types +//! ### Convert `geojson` to `geo-types` //! -//! The optional `geo-types` feature implements the [`TryFrom`](../std/convert/trait.TryFrom.html) -//! trait, providing **fallible** conversions _to_ [geo-types Geometries](../geo_types/index.html#structs) -//! from [GeoJSON `Value`](enum.Value.html) enums. +//! The `geo-types` feature implements the [`TryFrom`](../std/convert/trait.TryFrom.html) trait, +//! providing **fallible** conversions _to_ [geo-types Geometries](../geo_types/index.html#structs) +//! from [`GeoJson`], [`Value`], [`Feature`], [`FeatureCollection`] or [`Geometry`] types. //! -//! **In most cases it is assumed that you want to convert GeoJSON into `geo` primitive types in -//! order to process, transform, or measure them:** -//! - `match` on `geojson`, iterating over its `features` field, yielding `Option`. -//! - process each `Feature`, accessing its `Value` field, yielding `Option`. -//! -//! Each [`Value`](enum.Value.html) represents a primitive type, such as a coordinate, point, -//! linestring, polygon, or its multi- equivalent, **and each of these has an equivalent `geo` -//! primitive type**, which you can convert to using the `std::convert::TryFrom` trait. -//! -//! #### GeoJSON to geo_types::GeometryCollection -//! -//! Unifying these features, the [`quick_collection`](fn.quick_collection.html) function accepts a [`GeoJson`](enum.GeoJson.html) enum -//! and processes it, producing a [`GeometryCollection`](../geo_types/struct.GeometryCollection.html) -//! whose members can be transformed, measured, rotated, etc using the algorithms and functions in -//! the [`geo`](https://docs.rs/geo) crate: -//! -//! ``` -//! # #[cfg(feature = "geo-types")] -//! # { -//! // requires enabling the `geo-types` feature -//! use geo_types::GeometryCollection; -//! use geojson::{quick_collection, GeoJson}; -//! let geojson_str = r#" -//! { -//! "type": "FeatureCollection", -//! "features": [ -//! { -//! "type": "Feature", -//! "properties": {}, -//! "geometry": { -//! "type": "Point", -//! "coordinates": [ -//! -0.13583511114120483, -//! 51.5218870403801 -//! ] -//! } -//! } -//! ] -//! } -//! "#; -//! let geojson = geojson_str.parse::().unwrap(); -//! // Turn the GeoJSON string into a geo_types GeometryCollection -//! let mut collection: GeometryCollection = quick_collection(&geojson).unwrap(); -//! # } -//! ``` -//! -//! #### Convert `GeoJson` to `geo_types::Geometry` +//! #### Convert `geojson` to `geo_types::Geometry` //! //! ``` //! # #[cfg(feature = "geo-types")] //! # { -//! // requires enabling the `geo-types` feature +//! // This example requires the `geo-types` feature //! use geo_types::Geometry; //! use geojson::GeoJson; -//! use std::convert::TryInto; +//! use std::convert::TryFrom; //! use std::str::FromStr; //! //! let geojson_str = r#" @@ -373,7 +327,7 @@ //! "#; //! let geojson = GeoJson::from_str(geojson_str).unwrap(); //! // Turn the GeoJSON string into a geo_types Geometry -//! let geom: geo_types::Geometry = geojson.try_into().unwrap(); +//! let geom = geo_types::Geometry::::try_from(geojson).unwrap(); //! # } //! ``` //! @@ -466,6 +420,7 @@ pub use feature_reader::FeatureReader; mod feature_writer; pub use feature_writer::FeatureWriter; +#[allow(deprecated)] #[cfg(feature = "geo-types")] pub use conversion::quick_collection; diff --git a/src/ser.rs b/src/ser.rs index 1a17e5f..6a52638 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -9,7 +9,7 @@ //! } //! ``` //! -//! Your type *must* have a field called `geometry` and it must be `serialized_with` [`serialize_geometry`](crate::ser::serialize_geometry): +//! Your type *must* have a field called `geometry` and it must be `serialize_with` [`serialize_geometry`](crate::ser::serialize_geometry): //! ```rust, ignore //! #[derive(serde::Serialize)] //! struct MyStruct { @@ -85,12 +85,12 @@ //! //! # Reading *and* Writing GeoJSON //! -//! This module is only concerned with Writing out GeoJSON. If you'd also like to reading GeoJSON, +//! This module is only concerned with Writing out GeoJSON. If you'd also like to read GeoJSON, //! you'll want to combine this with the functionality from the [`crate::de`] module: //! ```ignore //! #[derive(serde::Serialize, serde::Deserialize)] //! struct MyStruct { -//! // Serialize as geojson, rather than using the type's default serialization +//! // Serialize as GeoJSON, rather than using the type's default serialization //! #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] //! geometry: geo_types::Point, //! ... @@ -100,7 +100,7 @@ use crate::{JsonObject, JsonValue, Result}; use serde::{ser::Error, Serialize, Serializer}; -use std::io; +use std::{convert::TryInto, io}; /// Serialize a single data structure to a GeoJSON Feature string. /// @@ -255,13 +255,77 @@ where /// ``` pub fn serialize_geometry(geometry: IG, ser: S) -> std::result::Result where - IG: std::convert::TryInto, + IG: TryInto, S: serde::Serializer, + >::Error: std::fmt::Display, { geometry .try_into() - .map_err(|_e| Error::custom("failed to convert geometry to geojson")) - .and_then(|geojson_geometry| geojson_geometry.serialize(ser)) + .map_err(serialize_error_msg::)? + .serialize(ser) +} + +/// [`serde::serialize_with`](https://serde.rs/field-attrs.html#serialize_with) helper to serialize an optional type like a +/// [`geo_types`], as an optional GeoJSON Geometry. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use geojson::ser::serialize_optional_geometry; +/// use serde::Serialize; +/// use serde_json::{json, to_value}; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// count: usize, +/// #[serde( +/// skip_serializing_if = "Option::is_none", +/// serialize_with = "serialize_optional_geometry" +/// )] +/// geometry: Option>, +/// } +/// +/// let my_struct = MyStruct { +/// count: 0, +/// geometry: Some(geo_types::Point::new(1.2, 0.5)), +/// }; +/// let json = json! {{ +/// "count": 0, +/// "geometry": { +/// "type": "Point", +/// "coordinates": [1.2, 0.5] +/// }, +/// }}; +/// assert_eq!(json, to_value(my_struct).unwrap()); +/// +/// let my_struct = MyStruct { +/// count: 1, +/// geometry: None, +/// }; +/// let json = json! {{ +/// "count": 1, +/// }}; +/// assert_eq!(json, to_value(my_struct).unwrap()); +/// ``` +pub fn serialize_optional_geometry<'a, IG, S>( + geometry: &'a Option, + ser: S, +) -> std::result::Result +where + &'a IG: std::convert::TryInto, + S: serde::Serializer, + <&'a IG as TryInto>::Error: std::fmt::Display, +{ + geometry + .as_ref() + .map(TryInto::try_into) + .transpose() + .map_err(serialize_error_msg::)? + .serialize(ser) +} + +fn serialize_error_msg(error: impl std::fmt::Display) -> S::Error { + Error::custom(format!("failed to convert geometry to GeoJSON: {}", error)) } struct Features<'a, T> @@ -506,6 +570,41 @@ mod tests { use super::*; use crate::de::tests::feature_collection; + #[test] + fn serializes_optional_point() { + #[derive(serde::Serialize)] + struct MyStruct { + count: usize, + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_optional_geometry" + )] + geometry: Option>, + } + + let my_struct = MyStruct { + count: 0, + geometry: Some(geo_types::Point::new(1.2, 0.5)), + }; + let json = json! {{ + "count": 0, + "geometry": { + "type": "Point", + "coordinates": [1.2, 0.5] + }, + }}; + assert_eq!(json, serde_json::to_value(my_struct).unwrap()); + + let my_struct = MyStruct { + count: 1, + geometry: None, + }; + let json = json! {{ + "count": 1, + }}; + assert_eq!(json, serde_json::to_value(my_struct).unwrap()); + } + #[test] fn geometry_field_without_helper() { #[derive(Serialize)] diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs index 623d36d..db23d4b 100644 --- a/tests/roundtrip.rs +++ b/tests/roundtrip.rs @@ -47,7 +47,7 @@ mod roundtrip_tests { /// Verifies that we can parse and then re-encode geojson back to the same representation /// without losing any data. fn test_round_trip(file_path: &str) { - let mut file = File::open(&file_path).unwrap(); + let mut file = File::open(file_path).unwrap(); let mut file_contents = String::new(); let _ = file.read_to_string(&mut file_contents);