From 6ed84b5dcfe36a53ea7e104651727073af83f889 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 19 Feb 2025 14:14:25 -0800 Subject: [PATCH] Convert your custom struct to/from a single Feature analogous to serde_json::{to_value,from_value} Your custom struct must have a `geometry` field, all other fields will be mapped to feature.properties. --- CHANGES.md | 2 ++ src/de.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++ src/ser.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 188 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6bd81ca3..599ed3e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ## Unreleased +* Add `to_feature` to convert a single S: Serialize to a Feature +* Add `from_feature` to convert a single Feature to a D: Deserialize * Upgrade from thiserror v1 to v2 * Add support of serializing optional `geo-types` with `serialize_optional_geometry`. * Add support of deserializing optional `geo-types` with `deserialize_optional_geometry`. diff --git a/src/de.rs b/src/de.rs index 5e3ed507..4344de83 100644 --- a/src/de.rs +++ b/src/de.rs @@ -348,6 +348,57 @@ where Ok(deserializer.deserialize_map(visitor)?) } +/// Interpret a [`Feature`] as an instance of type `T`. +/// +/// This is analogous to [`serde_json::from_value`](https://docs.rs/serde_json/latest/serde_json/fn.from_value.html) +/// +/// `T`'s `geometry` field will be deserialized from `feature.geometry`. +/// All other fields will be deserialized from `feature.properties`. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Deserialize; +/// use geojson::Feature; +/// use geojson::de::{from_feature, deserialize_geometry, deserialize_single_feature}; +/// use std::str::FromStr; +/// +/// #[derive(Deserialize)] +/// struct MyStruct { +/// // Deserialize `geometry` as GeoJSON, rather than using the type's default deserialization +/// #[serde(deserialize_with = "deserialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let geojson_str = r#"{ +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [1.0, 2.0] }, +/// "properties": { +/// "name": "My Name" +/// } +/// }"#; +/// let feature = Feature::from_str(geojson_str).unwrap(); +/// +/// let my_struct: MyStruct = from_feature(feature).unwrap(); +/// +/// assert_eq!("My Name", my_struct.name); +/// assert_eq!(geo_types::Point::new(1.0, 2.0), my_struct.geometry); +/// ``` +/// +/// # Errors +/// +/// Deserialization can fail if `T`'s implementation of `Deserialize` decides to fail. +pub fn from_feature<'de, T>(feature: Feature) -> Result +where + T: Deserialize<'de>, +{ + let feature_value: JsonValue = serde_json::to_value(feature)?; + let deserializer = feature_value.into_deserializer(); + let visitor = FeatureVisitor::new(); + Ok(deserializer.deserialize_map(visitor)?) +} + struct FeatureVisitor { _marker: PhantomData, } @@ -611,6 +662,41 @@ pub(crate) mod tests { let feature: MyStruct = serde_json::from_value(json).unwrap(); assert!(feature.geometry.is_none()) } + + #[test] + fn test_from_feature() { + #[derive(Debug, PartialEq, Deserialize)] + struct MyStruct { + #[serde(deserialize_with = "deserialize_geometry")] + geometry: geo_types::Point, + name: String, + age: u64, + } + + let feature = Feature { + bbox: None, + geometry: Some(crate::Geometry::new(crate::Value::Point(vec![125.6, 10.1]))), + id: None, + properties: Some( + json!({ + "name": "Dinagat Islands", + "age": 123, + }) + .as_object() + .unwrap() + .clone(), + ), + foreign_members: None, + }; + + let actual: MyStruct = from_feature(feature).unwrap(); + let expected = MyStruct { + geometry: geo_types::Point::new(125.6, 10.1), + name: "Dinagat Islands".to_string(), + age: 123, + }; + assert_eq!(actual, expected); + } } #[cfg(feature = "geo-types")] diff --git a/src/ser.rs b/src/ser.rs index 78e57121..1491fddb 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -96,10 +96,12 @@ //! ... //! } //! ``` -use crate::{JsonObject, JsonValue, Result}; +use crate::{Feature, JsonObject, JsonValue, Result}; use serde::{ser::Error, Serialize, Serializer}; +use crate::util::expect_owned_object; +use std::convert::TryFrom; use std::{convert::TryInto, io}; /// Serialize a single data structure to a GeoJSON Feature string. @@ -198,6 +200,66 @@ where Ok(()) } +/// Convert a `T` into a [`Feature`]. +/// +/// This is analogous to [`serde_json::to_value`](https://docs.rs/serde_json/latest/serde_json/fn.to_value.html) +/// +/// Note that if (and only if) `T` has a field named `geometry`, it will be serialized to +/// `feature.geometry`. +/// +/// All other fields will be serialized to `feature.properties`. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Serialize; +/// use geojson::{Feature, Value, Geometry}; +/// use geojson::ser::{to_feature, serialize_geometry}; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// // Serialize `geometry` as geojson, rather than using the type's default serialization +/// #[serde(serialize_with = "serialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let my_struct = MyStruct { +/// geometry: geo_types::Point::new(1.0, 2.0), +/// name: "My Name".to_string() +/// }; +/// +/// let feature: Feature = to_feature(my_struct).unwrap(); +/// assert_eq!("My Name", feature.properties.unwrap()["name"]); +/// assert_eq!(feature.geometry.unwrap(), Geometry::new(Value::Point(vec![1.0, 2.0]))); +/// ``` +/// +/// # Errors +/// +/// Serialization can fail if `T`'s implementation of `Serialize` decides to +/// fail, or if `T` contains a map with non-string keys. +pub fn to_feature(value: T) -> Result +where + T: Serialize, +{ + let js_value = serde_json::to_value(value)?; + let mut js_object = expect_owned_object(js_value)?; + + let geometry = if let Some(geometry_value) = js_object.remove("geometry") { + Some(crate::Geometry::try_from(geometry_value)?) + } else { + None + }; + + Ok(Feature { + bbox: None, + geometry, + id: None, + properties: Some(js_object), + foreign_members: None, + }) +} + /// Serialize elements as a GeoJSON FeatureCollection into the IO stream. /// /// Note that `T` must have a column called `geometry`. @@ -569,6 +631,7 @@ mod tests { mod geo_types_tests { use super::*; use crate::de::tests::feature_collection; + use crate::Geometry; #[test] fn serializes_optional_point() { @@ -677,6 +740,42 @@ mod tests { assert_eq!(actual_output, expected_output); } + #[test] + fn test_to_feature() { + #[derive(Serialize)] + struct MyStruct { + #[serde(serialize_with = "serialize_geometry")] + geometry: geo_types::Point, + name: String, + age: u64, + } + + let my_struct = MyStruct { + geometry: geo_types::point!(x: 125.6, y: 10.1), + name: "Dinagat Islands".to_string(), + age: 123, + }; + + let actual = to_feature(&my_struct).unwrap(); + let expected = Feature { + bbox: None, + geometry: Some(Geometry::new(crate::Value::Point(vec![125.6, 10.1]))), + id: None, + properties: Some( + json!({ + "name": "Dinagat Islands", + "age": 123 + }) + .as_object() + .unwrap() + .clone(), + ), + foreign_members: None, + }; + + assert_eq!(actual, expected) + } + #[test] fn serialize_feature_collection() { #[derive(Serialize)]