From daddbd4154a64ea8256baf961528ac480947259f Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 24 Aug 2022 20:49:19 -0700 Subject: [PATCH 1/9] to/from custom structs --- CHANGES.md | 28 +- Cargo.toml | 10 +- benches/parse.rs | 47 ++- benches/serialize.rs | 45 +++ src/conversion/mod.rs | 5 +- src/conversion/to_geo_types.rs | 84 +++-- src/de.rs | 653 +++++++++++++++++++++++++++++++++ src/errors.rs | 4 +- src/feature_reader.rs | 271 ++++++++++++++ src/lib.rs | 10 + src/ser.rs | 599 ++++++++++++++++++++++++++++++ src/util.rs | 2 +- 12 files changed, 1706 insertions(+), 52 deletions(-) create mode 100644 benches/serialize.rs create mode 100644 src/de.rs create mode 100644 src/feature_reader.rs create mode 100644 src/ser.rs diff --git a/CHANGES.md b/CHANGES.md index 57b53d5..b0407e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,12 +4,36 @@ * Fix: FeatureIterator errors when reading "features" field before "type" field. * +* 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: + ``` + #[derive(Serialize, Deserialize)] + struct MyStruct { + #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] + geometry: geo_types::Point, + name: String, + age: u64, + } + + // read your input + let my_structs: Vec = geojson::de::deserialize_feature_collection(geojson_reader).unwrap(); + + // do some processing + process(&mut my_structs); + + // write back your results + geojson::ser::to_feature_collection_string(&my_structs).unwrap(); + ``` + * PR: + * Added IntoIter implementation for FeatureCollection. * -* Add `geojson::Result` +* Added `geojson::Result`. * +* BREAKING: Change the Result type of FeatureIterator from io::Result to crate::Result + * * Add `TryFrom<&geometry::Value>` for geo_type variants. - * ## 0.23.0 diff --git a/Cargo.toml b/Cargo.toml index 73841c3..447a1b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,11 @@ edition = "2018" default = ["geo-types"] [dependencies] -serde = "~1.0" +serde = { version="~1.0", features = ["derive"] } serde_json = "~1.0" -serde_derive = "~1.0" -geo-types = { version = "0.7", optional = true } +geo-types = { version = "0.7", features = ["serde"], optional = true } thiserror = "1.0.20" +log = "0.4.17" [dev-dependencies] num-traits = "0.2" @@ -28,6 +28,10 @@ criterion = "0.3" name = "parse" harness = false +[[bench]] +name = "serialize" +harness = false + [[bench]] name = "to_geo_types" harness = false diff --git a/benches/parse.rs b/benches/parse.rs index 5486574..35770dc 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -3,9 +3,9 @@ use geojson::GeoJson; use std::io::BufReader; fn parse_feature_collection_benchmark(c: &mut Criterion) { - c.bench_function("parse (countries.geojson)", |b| { - let geojson_str = include_str!("../tests/fixtures/countries.geojson"); + let geojson_str = include_str!("../tests/fixtures/countries.geojson"); + c.bench_function("parse (countries.geojson)", |b| { b.iter(|| { let _ = black_box({ match geojson_str.parse::() { @@ -19,17 +19,52 @@ fn parse_feature_collection_benchmark(c: &mut Criterion) { }); c.bench_function("FeatureIter (countries.geojson)", |b| { - let geojson_str = include_str!("../tests/fixtures/countries.geojson"); - b.iter(|| { let feature_iter = geojson::FeatureIterator::new(BufReader::new(geojson_str.as_bytes())); let _ = black_box({ let mut count = 0; - for _ in feature_iter { + for feature in feature_iter { + let _ = feature.unwrap(); + count += 1; + } + assert_eq!(count, 180); + }); + }); + }); + + c.bench_function("FeatureReader::features (countries.geojson)", |b| { + b.iter(|| { + let feature_reader = + geojson::FeatureReader::from_reader(BufReader::new(geojson_str.as_bytes())); + let _ = black_box({ + let mut count = 0; + for feature in feature_reader.features() { + let _ = feature.unwrap(); + count += 1; + } + assert_eq!(count, 180); + }); + }); + }); + + c.bench_function("FeatureReader::deserialize (countries.geojson)", |b| { + b.iter(|| { + #[derive(serde::Deserialize)] + struct Country { + geometry: geojson::Geometry, + name: String, + } + let feature_reader = + geojson::FeatureReader::from_reader(BufReader::new(geojson_str.as_bytes())); + + let _ = black_box({ + let mut count = 0; + for feature in feature_reader.deserialize::().unwrap() { + let _ = feature.unwrap(); count += 1; } - assert_eq!(count, 184); + assert_eq!(count, 180); }); }); }); diff --git a/benches/serialize.rs b/benches/serialize.rs new file mode 100644 index 0000000..3a15481 --- /dev/null +++ b/benches/serialize.rs @@ -0,0 +1,45 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +fn serialize_feature_collection_benchmark(c: &mut Criterion) { + let geojson_str = include_str!("../tests/fixtures/countries.geojson"); + + c.bench_function( + "serialize geojson::FeatureCollection struct (countries.geojson)", + |b| { + let geojson = geojson_str.parse::().unwrap(); + + b.iter(|| { + black_box({ + let geojson_string = serde_json::to_string(&geojson).unwrap(); + // Sanity check that we serialized a long string of some kind. + assert_eq!(geojson_string.len(), 256890); + }); + }); + }, + ); + + c.bench_function("serialize custom struct (countries.geojson)", |b| { + #[derive(serde::Serialize, serde::Deserialize)] + struct Country { + geometry: geojson::Geometry, + name: String, + } + let features = + geojson::de::deserialize_feature_collection_str_to_vec::(geojson_str).unwrap(); + assert_eq!(features.len(), 180); + + b.iter(|| { + black_box({ + let geojson_string = geojson::ser::to_feature_collection_string(&features).unwrap(); + // Sanity check that we serialized a long string of some kind. + // + // Note this is slightly shorter than the GeoJson round-trip above because we drop + // some fields, like foreign members + assert_eq!(geojson_string.len(), 254908); + }); + }); + }); +} + +criterion_group!(benches, serialize_feature_collection_benchmark); +criterion_main!(benches); diff --git a/src/conversion/mod.rs b/src/conversion/mod.rs index cf853d3..3cd583d 100644 --- a/src/conversion/mod.rs +++ b/src/conversion/mod.rs @@ -63,12 +63,10 @@ macro_rules! assert_almost_eq { }}; } - macro_rules! try_from_owned_value { ($to:ty) => { #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] - impl TryFrom for $to - { + impl TryFrom for $to { type Error = Error; fn try_from(value: geometry::Value) -> Result { @@ -78,7 +76,6 @@ macro_rules! try_from_owned_value { }; } - pub(crate) mod from_geo_types; pub(crate) mod to_geo_types; diff --git a/src/conversion/to_geo_types.rs b/src/conversion/to_geo_types.rs index 35d8727..9a26dea 100644 --- a/src/conversion/to_geo_types.rs +++ b/src/conversion/to_geo_types.rs @@ -3,8 +3,7 @@ use crate::geo_types::{self, CoordFloat}; use crate::geometry; use crate::{ - quick_collection, Feature, FeatureCollection, GeoJson, Geometry, LineStringType, PointType, - PolygonType, + quick_collection, Feature, FeatureCollection, GeoJson, LineStringType, PointType, PolygonType, }; use crate::{Error, Result}; use std::convert::{TryFrom, TryInto}; @@ -138,10 +137,8 @@ where } } } - try_from_owned_value!(geo_types::GeometryCollection); - #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] impl TryFrom<&geometry::Value> for geo_types::Geometry where @@ -191,45 +188,62 @@ where } try_from_owned_value!(geo_types::Geometry); -#[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] -impl TryFrom for geo_types::Geometry -where - T: CoordFloat, -{ - type Error = Error; +macro_rules! impl_try_from_geom_value { + ($($kind:ident),*) => { + $( + #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] + impl TryFrom<&$crate::Geometry> for geo_types::$kind + where + T: CoordFloat, + { + type Error = Error; - fn try_from(val: Geometry) -> Result> { - (&val).try_into() - } -} + fn try_from(geometry: &crate::Geometry) -> Result { + Self::try_from(&geometry.value) + } + } -#[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] -impl TryFrom<&Geometry> for geo_types::Geometry -where - T: CoordFloat, -{ - type Error = Error; + #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] + impl TryFrom<$crate::Geometry> for geo_types::$kind + where + T: CoordFloat, + { + type Error = Error; - fn try_from(val: &Geometry) -> Result> { - (&val.value).try_into() - } -} + fn try_from(geometry: crate::Geometry) -> Result { + Self::try_from(geometry.value) + } + } -#[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] -impl TryFrom for geo_types::Geometry -where - T: CoordFloat, -{ - type Error = Error; + #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] + impl TryFrom<$crate::Feature> for geo_types::$kind + where + T: CoordFloat, + { + type Error = Error; - fn try_from(val: Feature) -> Result> { - match val.geometry { - None => Err(Error::FeatureHasNoGeometry(val)), - Some(geom) => geom.try_into(), - } + fn try_from(val: Feature) -> Result { + match val.geometry { + None => Err(Error::FeatureHasNoGeometry(val)), + Some(geom) => geom.try_into(), + } + } + } + )* } } +impl_try_from_geom_value![ + Point, + LineString, + Polygon, + MultiPoint, + MultiLineString, + MultiPolygon, + Geometry, + GeometryCollection +]; + #[cfg_attr(docsrs, doc(cfg(feature = "geo-types")))] impl TryFrom for geo_types::Geometry where diff --git a/src/de.rs b/src/de.rs new file mode 100644 index 0000000..d8edd48 --- /dev/null +++ b/src/de.rs @@ -0,0 +1,653 @@ +//! +//! To build instances of your struct from a GeoJSON String or reader, your type *must* +//! implement or derive [`serde::Deserialize`]: +//! +//! ```rust, ignore +//! #[derive(serde::Deserialize)] +//! struct MyStruct { +//! ... +//! } +//! ``` +//! +//! Your type *must* have a field called `geometry` and it must be `deserialized_with` [`deserialize_geometry`](crate::de::deserialize_geometry): +//! ```rust, ignore +//! #[derive(serde::Deserialize)] +//! struct MyStruct { +//! #[serde(serialize_with = "geojson::de::deserialize_geometry")] +//! geometry: geo_types::Point, +//! ... +//! } +//! ``` +//! +//! All fields in your struct other than `geometry` will be deserialized from the `properties` of the +//! GeoJSON Feature. +//! +//! # Examples +#![cfg_attr(feature = "geo-types", doc = "```")] +#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +//! use serde::Deserialize; +//! use geojson::de::deserialize_geometry; +//! +//! #[derive(Deserialize)] +//! struct MyStruct { +//! // Deserialize from geojson, rather than expecting the type's default serialization +//! #[serde(deserialize_with = "deserialize_geometry")] +//! geometry: geo_types::Point, +//! name: String, +//! population: u64 +//! } +//! +//! let input_geojson = serde_json::json!( +//! { +//! "type":"FeatureCollection", +//! "features": [ +//! { +//! "type": "Feature", +//! "geometry": { "coordinates": [11.1,22.2], "type": "Point" }, +//! "properties": { +//! "name": "Downtown", +//! "population": 123 +//! } +//! }, +//! { +//! "type": "Feature", +//! "geometry": { "coordinates": [33.3, 44.4], "type": "Point" }, +//! "properties": { +//! "name": "Uptown", +//! "population": 456 +//! } +//! } +//! ] +//! } +//! ).to_string(); +//! +//! let my_structs: Vec = geojson::de::deserialize_feature_collection_str_to_vec(&input_geojson).unwrap(); +//! assert_eq!("Downtown", my_structs[0].name); +//! assert_eq!(11.1, my_structs[0].geometry.x()); +//! +//! assert_eq!("Uptown", my_structs[1].name); +//! assert_eq!(33.3, my_structs[1].geometry.x()); +//! ``` +//! +//! # Reading *and* Writing GeoJSON +//! +//! This module is only concerned with _reading in_ GeoJSON. If you'd also like to write GeoJSON +//! output, you'll want to combine this with the functionality from the [`crate::ser`] module: +//! ```ignore +//! #[derive(serde::Serialize, serde::Deserialize)] +//! struct MyStruct { +//! // Serialize as geojson, rather than using the type's default serialization +//! #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] +//! geometry: geo_types::Point, +//! ... +//! } +//! ``` +use crate::{Feature, FeatureReader, JsonValue, Result}; + +use std::convert::{TryFrom, TryInto}; +use std::fmt::Formatter; +use std::io::Read; +use std::marker::PhantomData; + +use serde::de::{Deserialize, Deserializer, Error, IntoDeserializer}; + +/// Deserialize a GeoJSON FeatureCollection into your custom structs. +/// +/// Your struct must implement or derive `serde::Deserialize`. +/// +/// You must use the [`deserialize_geometry`] helper if you are using geo_types or some other geometry +/// representation other than geojson::Geometry. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Deserialize; +/// use geojson::de::deserialize_geometry; +/// +/// #[derive(Deserialize)] +/// struct MyStruct { +/// // You must use the `deserialize_geometry` helper if you are using geo_types or some other +/// // geometry representation other than geojson::Geometry +/// #[serde(deserialize_with = "deserialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let feature_collection_str = r#"{ +/// "type": "FeatureCollection", +/// "features": [ +/// { +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [11.1, 22.2] }, +/// "properties": { "name": "Downtown" } +/// }, +/// { +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [33.3, 44.4] }, +/// "properties": { "name": "Uptown" } +/// } +/// ] +/// }"#; +/// let reader = feature_collection_str.as_bytes(); +/// +/// // enumerate over the features in the feature collection +/// for (idx, feature_result) in geojson::de::deserialize_feature_collection::(reader).unwrap().enumerate() { +/// let my_struct = feature_result.expect("valid geojson for MyStruct"); +/// if idx == 0 { +/// assert_eq!(my_struct.name, "Downtown"); +/// assert_eq!(my_struct.geometry.x(), 11.1); +/// } else if idx == 1 { +/// assert_eq!(my_struct.name, "Uptown"); +/// assert_eq!(my_struct.geometry.x(), 33.3); +/// } else { +/// unreachable!("there are only two features in this collection"); +/// } +/// } +/// ``` +pub fn deserialize_feature_collection<'de, T>( + feature_collection_reader: impl Read, +) -> Result>> +where + T: Deserialize<'de>, +{ + let mut deserializer = serde_json::Deserializer::from_reader(feature_collection_reader); + + // PERF: rather than deserializing the entirety of the `features:` array into memory here, it'd + // be nice to stream the features. However, I ran into difficulty while trying to return any + // borrowed reference from the visitor methods (e.g. MapAccess) + let visitor = FeatureCollectionVisitor::new(); + let objects = deserializer.deserialize_map(visitor)?; + + Ok(objects.into_iter().map(|feature_value| { + let deserializer = feature_value.into_deserializer(); + let visitor = FeatureVisitor::new(); + let record: T = deserializer.deserialize_map(visitor)?; + + Ok(record) + })) +} + +/// Build a `Vec` of structs from a GeoJson `&str`. +/// +/// See [`deserialize_feature_collection`] for more. +pub fn deserialize_feature_collection_str_to_vec<'de, T>( + feature_collection_str: &str, +) -> Result> +where + T: Deserialize<'de>, +{ + let feature_collection_reader = feature_collection_str.as_bytes(); + deserialize_feature_collection(feature_collection_reader)?.collect() +} + +/// Build a `Vec` of structs from a GeoJson reader. +/// +/// See [`deserialize_feature_collection`] for more. +pub fn deserialize_feature_collection_to_vec<'de, T>( + feature_collection_reader: impl Read, +) -> Result> +where + T: Deserialize<'de>, +{ + deserialize_feature_collection(feature_collection_reader)?.collect() +} + +/// [`serde::deserialize_with`](https://serde.rs/field-attrs.html#deserialize_with) helper to deserialize a GeoJSON Geometry into another type, like a +/// [`geo_types`] Geometry. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Deserialize; +/// use geojson::de::deserialize_geometry; +/// +/// #[derive(Deserialize)] +/// struct MyStruct { +/// #[serde(deserialize_with = "deserialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let feature_collection_str = r#"{ +/// "type": "FeatureCollection", +/// "features": [ +/// { +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [11.1, 22.2] }, +/// "properties": { "name": "Downtown" } +/// }, +/// { +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [33.3, 44.4] }, +/// "properties": { "name": "Uptown" } +/// } +/// ] +/// }"#; +/// +/// let features: Vec = geojson::de::deserialize_feature_collection_str_to_vec(feature_collection_str).unwrap(); +/// +/// assert_eq!(features[0].name, "Downtown"); +/// assert_eq!(features[0].geometry.x(), 11.1); +/// ``` +pub fn deserialize_geometry<'de, D, G>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, + G: TryFrom, + G::Error: std::fmt::Display, +{ + let geojson_geometry = crate::Geometry::deserialize(deserializer)?; + geojson_geometry + .try_into() + .map_err(|err| Error::custom(format!("unable to convert from geojson Geometry: {}", err))) +} + +/// Deserialize a GeoJSON FeatureCollection into [`Feature`] structs. +/// +/// If instead you'd like to deserialize your own structs from GeoJSON, see [`deserialize_feature_collection`]. +pub fn deserialize_features_from_feature_collection( + feature_collection_reader: impl Read, +) -> impl Iterator> { + FeatureReader::from_reader(feature_collection_reader).features() +} + +/// Deserialize a single GeoJSON Feature into your custom struct. +/// +/// It's more common to deserialize a FeatureCollection than a single feature. If you're looking to +/// do that, see [`deserialize_feature_collection`] instead. +/// +/// Your struct must implement or derive `serde::Deserialize`. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Deserialize; +/// use geojson::de::deserialize_geometry; +/// +/// #[derive(Deserialize)] +/// struct MyStruct { +/// // You must use the `deserialize_geometry` helper if you are using geo_types or some other +/// // geometry representation other than geojson::Geometry +/// #[serde(deserialize_with = "deserialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let feature_str = r#"{ +/// "type": "Feature", +/// "geometry": { "type": "Point", "coordinates": [11.1, 22.2] }, +/// "properties": { "name": "Downtown" } +/// }"#; +/// let reader = feature_str.as_bytes(); +/// +/// // build your struct from GeoJSON +/// let my_struct = geojson::de::deserialize_single_feature::(reader).expect("valid geojson for MyStruct"); +/// +/// assert_eq!(my_struct.name, "Downtown"); +/// assert_eq!(my_struct.geometry.x(), 11.1); +/// ``` +pub fn deserialize_single_feature<'de, T>(feature_reader: impl Read) -> Result +where + T: Deserialize<'de>, +{ + let feature_value: JsonValue = serde_json::from_reader(feature_reader)?; + let deserializer = feature_value.into_deserializer(); + let visitor = FeatureVisitor::new(); + Ok(deserializer.deserialize_map(visitor)?) +} + +struct FeatureCollectionVisitor; + +impl FeatureCollectionVisitor { + fn new() -> Self { + Self + } +} + +impl<'de> serde::de::Visitor<'de> for FeatureCollectionVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "a valid GeoJSON Feature object") + } + + fn visit_map(self, mut map_access: A) -> std::result::Result + where + A: serde::de::MapAccess<'de>, + { + let mut has_feature_collection_type = false; + let mut features = None; + while let Some((key, value)) = map_access.next_entry::()? { + if key == "type" { + if value == JsonValue::String("FeatureCollection".to_string()) { + has_feature_collection_type = true; + } else { + return Err(Error::custom("invalid type for feature collection")); + } + } else if key == "features" { + if let JsonValue::Array(value) = value { + if features.is_some() { + return Err(Error::custom( + "Encountered more than one list of `features`", + )); + } + features = Some(value); + } else { + return Err(Error::custom("`features` had unexpected value")); + } + } else { + log::warn!("foreign members are not handled by FeatureCollection deserializer") + } + } + + if let Some(features) = features { + if has_feature_collection_type { + Ok(features) + } else { + Err(Error::custom("No `type` field was found")) + } + } else { + Err(Error::custom("No `features` field was found")) + } + } +} + +struct FeatureVisitor { + _marker: PhantomData, +} + +impl FeatureVisitor { + fn new() -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl<'de, D> serde::de::Visitor<'de> for FeatureVisitor +where + D: Deserialize<'de>, +{ + type Value = D; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + write!(formatter, "a valid GeoJSON Feature object") + } + + fn visit_map(self, mut map_access: A) -> std::result::Result + where + A: serde::de::MapAccess<'de>, + { + let mut has_feature_type = false; + use std::collections::HashMap; + let mut hash_map: HashMap = HashMap::new(); + + while let Some((key, value)) = map_access.next_entry::()? { + if key == "type" { + if value.as_str() == Some("Feature") { + has_feature_type = true; + } else { + return Err(Error::custom( + "GeoJSON Feature had a `type` other than \"Feature\"", + )); + } + } else if key == "geometry" { + if let JsonValue::Object(_) = value { + hash_map.insert("geometry".to_string(), value); + } else { + return Err(Error::custom("GeoJSON Feature had a unexpected geometry")); + } + } else if key == "properties" { + if let JsonValue::Object(properties) = value { + // flatten properties onto struct + for (prop_key, prop_value) in properties { + hash_map.insert(prop_key, prop_value); + } + } else { + return Err(Error::custom("GeoJSON Feature had a unexpected geometry")); + } + } else { + log::debug!("foreign members are not handled by Feature deserializer") + } + } + + if has_feature_type { + // What do I actually do here? serde-transcode? or create a new MapAccess or Struct that + // has the fields needed by a child visitor - perhaps using serde::de::value::MapAccessDeserializer? + // use serde::de::value::MapAccessDeserializer; + let d2 = hash_map.into_deserializer(); + let result = + Deserialize::deserialize(d2).map_err(|e| Error::custom(format!("{}", e)))?; + Ok(result) + } else { + Err(Error::custom( + "A GeoJSON Feature must have a `type: \"Feature\"` field, but found none.", + )) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + use crate::JsonValue; + + use serde::Deserialize; + use serde_json::json; + + pub(crate) fn feature_collection() -> JsonValue { + json!({ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands", + "age": 123 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [2.3, 4.5] + }, + "properties": { + "name": "Neverland", + "age": 456 + } + } + ] + }) + } + + #[test] + fn test_deserialize_feature_collection() { + use crate::Feature; + + let feature_collection_string = feature_collection().to_string(); + let bytes_reader = feature_collection_string.as_bytes(); + + let records: Vec = deserialize_features_from_feature_collection(bytes_reader) + .map(|feature_result: Result| feature_result.unwrap()) + .collect(); + + assert_eq!(records.len(), 2); + let first_age = { + let props = records.get(0).unwrap().properties.as_ref().unwrap(); + props.get("age").unwrap().as_i64().unwrap() + }; + assert_eq!(first_age, 123); + + let second_age = { + let props = records.get(1).unwrap().properties.as_ref().unwrap(); + props.get("age").unwrap().as_i64().unwrap() + }; + assert_eq!(second_age, 456); + } + + #[cfg(feature = "geo-types")] + mod geo_types_tests { + use super::*; + + #[test] + fn geometry_field() { + #[derive(Deserialize)] + struct MyStruct { + #[serde(deserialize_with = "deserialize_geometry")] + geometry: geo_types::Geometry, + name: String, + age: u64, + } + + let feature_collection_string = feature_collection().to_string(); + let bytes_reader = feature_collection_string.as_bytes(); + + let records: Vec = deserialize_feature_collection(bytes_reader) + .expect("a valid feature collection") + .collect::>>() + .expect("valid features"); + + assert_eq!(records.len(), 2); + + assert_eq!( + records[0].geometry, + geo_types::point!(x: 125.6, y: 10.1).into() + ); + assert_eq!(records[0].name, "Dinagat Islands"); + assert_eq!(records[0].age, 123); + + assert_eq!( + records[1].geometry, + geo_types::point!(x: 2.3, y: 4.5).into() + ); + assert_eq!(records[1].name, "Neverland"); + assert_eq!(records[1].age, 456); + } + + #[test] + fn specific_geometry_variant_field() { + #[derive(Deserialize)] + struct MyStruct { + #[serde(deserialize_with = "deserialize_geometry")] + geometry: geo_types::Point, + name: String, + age: u64, + } + + let feature_collection_string = feature_collection().to_string(); + let bytes_reader = feature_collection_string.as_bytes(); + + let records: Vec = deserialize_feature_collection(bytes_reader) + .expect("a valid feature collection") + .collect::>>() + .expect("valid features"); + + assert_eq!(records.len(), 2); + + assert_eq!(records[0].geometry, geo_types::point!(x: 125.6, y: 10.1)); + assert_eq!(records[0].name, "Dinagat Islands"); + assert_eq!(records[0].age, 123); + + assert_eq!(records[1].geometry, geo_types::point!(x: 2.3, y: 4.5)); + assert_eq!(records[1].name, "Neverland"); + assert_eq!(records[1].age, 456); + } + + #[test] + fn wrong_geometry_variant_field() { + #[allow(unused)] + #[derive(Deserialize)] + struct MyStruct { + #[serde(deserialize_with = "deserialize_geometry")] + geometry: geo_types::LineString, + name: String, + age: u64, + } + + let feature_collection_string = feature_collection().to_string(); + let bytes_reader = feature_collection_string.as_bytes(); + + let records: Vec> = deserialize_feature_collection(bytes_reader) + .unwrap() + .collect(); + assert_eq!(records.len(), 2); + assert!(records[0].is_err()); + assert!(records[1].is_err()); + + let err = match records[0].as_ref() { + Ok(_ok) => panic!("expected Err, but found OK"), + Err(e) => e, + }; + + // This will fail if we update our error text, but I wanted to show that the error text + // is reasonably discernible. + let expected_err_text = r#"Error while deserializing JSON: unable to convert from geojson Geometry: Encountered a mismatch when converting to a Geo type: `{"coordinates":[125.6,10.1],"type":"Point"}`"#; + assert_eq!(err.to_string(), expected_err_text); + } + } + + #[cfg(feature = "geo-types")] + #[test] + fn roundtrip() { + use crate::ser::serialize_geometry; + use serde::Serialize; + + #[derive(Serialize, Deserialize)] + struct MyStruct { + #[serde( + serialize_with = "serialize_geometry", + deserialize_with = "deserialize_geometry" + )] + geometry: geo_types::Point, + name: String, + age: u64, + } + + let feature_collection_string = feature_collection().to_string(); + let bytes_reader = feature_collection_string.as_bytes(); + + let mut elements = deserialize_feature_collection_to_vec::(bytes_reader).unwrap(); + for element in &mut elements { + element.age += 1; + element.geometry.set_x(element.geometry.x() + 1.0); + } + let actual_output = crate::ser::to_feature_collection_string(&elements).unwrap(); + + use std::str::FromStr; + let actual_output_json = JsonValue::from_str(&actual_output).unwrap(); + let expected_output_json = json!({ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [126.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands", + "age": 124 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [3.3, 4.5] + }, + "properties": { + "name": "Neverland", + "age": 457 + } + } + ] + }); + + assert_eq!(actual_output_json, expected_output_json); + } +} diff --git a/src/errors.rs b/src/errors.rs index 52fc50e..27660e9 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -21,6 +21,8 @@ pub enum Error { /// This was previously `GeoJsonUnknownType`, but has been split for clarity #[error("Expected a Feature mapping, but got a `{0}`")] NotAFeature(String), + // TODO: Expect vs. Found (and maybe it doesn't need to be "geo-type" specific, but anything + // that can be converted)? #[error("Encountered a mismatch when converting to a Geo type: `{0}`")] InvalidGeometryConversion(GValue), #[error( @@ -29,7 +31,7 @@ pub enum Error { FeatureHasNoGeometry(Feature), #[error("Encountered an unknown 'geometry' object type: `{0}`")] GeometryUnknownType(String), - #[error("Encountered malformed JSON: {0}")] + #[error("Error while deserializing JSON: {0}")] MalformedJson(serde_json::Error), #[error("Encountered neither object type nor null type for 'properties' object: `{0}`")] PropertiesExpectedObjectOrNull(Value), diff --git a/src/feature_reader.rs b/src/feature_reader.rs new file mode 100644 index 0000000..2f8b6be --- /dev/null +++ b/src/feature_reader.rs @@ -0,0 +1,271 @@ +use crate::de::deserialize_feature_collection; +use crate::{Feature, Result}; + +use serde::de::DeserializeOwned; + +use std::io::Read; + +pub struct FeatureReader { + reader: R, +} + +impl FeatureReader { + pub fn from_reader(reader: R) -> Self { + Self { reader } + } + + /// Iterate over the individual [`Feature`s](Feature) of a FeatureCollection + /// + /// # Examples + /// + /// ``` + /// let feature_collection_string = r#"{ + /// "type": "FeatureCollection", + /// "features": [ + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [125.6, 10.1] + /// }, + /// "properties": { + /// "name": "Dinagat Islands", + /// "age": 123 + /// } + /// }, + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [2.3, 4.5] + /// }, + /// "properties": { + /// "name": "Neverland", + /// "age": 456 + /// } + /// } + /// ] + /// }"# + /// .as_bytes(); + /// let io_reader = std::io::BufReader::new(feature_collection_string); + /// + /// use geojson::FeatureReader; + /// let feature_reader = FeatureReader::from_reader(io_reader); + /// for feature in feature_reader.features() { + /// let feature = feature.expect("valid geojson feature"); + /// + /// let name = feature.property("name").unwrap().as_str().unwrap(); + /// let age = feature.property("age").unwrap().as_u64().unwrap(); + /// + /// if name == "Dinagat Islands" { + /// assert_eq!(123, age); + /// } else if name == "Neverland" { + /// assert_eq!(456, age); + /// } else { + /// panic!("unexpected name: {}", name); + /// } + /// } + /// ``` + pub fn features(self) -> impl Iterator> { + crate::FeatureIterator::new(self.reader) + } + + /// Deserialize the features of FeatureCollection into your own custom + /// struct using the [`serde`](../../serde) crate. + /// + /// # Examples + /// + /// ``` + /// let feature_collection_string = r#"{ + /// "type": "FeatureCollection", + /// "features": [ + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [125.6, 10.1] + /// }, + /// "properties": { + /// "name": "Dinagat Islands", + /// "age": 123 + /// } + /// }, + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [2.3, 4.5] + /// }, + /// "properties": { + /// "name": "Neverland", + /// "age": 456 + /// } + /// } + /// ] + /// }"# + /// .as_bytes(); + /// let io_reader = std::io::BufReader::new(feature_collection_string); + /// + /// use serde::Deserialize; + /// #[derive(Debug, Deserialize)] + /// struct MyStruct { + /// geometry: geojson::Geometry, + /// name: String, + /// age: u64, + /// } + /// + /// use geojson::FeatureReader; + /// use geojson::GeoJson::Geometry; + /// let feature_reader = FeatureReader::from_reader(io_reader); + /// for feature in feature_reader.deserialize::().unwrap() { + /// let my_struct = feature.expect("valid geojson feature"); + /// + /// if my_struct.name == "Dinagat Islands" { + /// assert_eq!(123, my_struct.age); + /// } else if my_struct.name == "Neverland" { + /// assert_eq!(456, my_struct.age); + /// } else { + /// panic!("unexpected name: {}", my_struct.name); + /// } + /// } + /// ``` + /// + /// ## With geo-types Geometry + #[cfg_attr(feature = "geo-types", doc = "```")] + #[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] + /// let feature_collection_string = r#"{ + /// "type": "FeatureCollection", + /// "features": [ + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [125.6, 10.1] + /// }, + /// "properties": { + /// "name": "Dinagat Islands", + /// "age": 123 + /// } + /// }, + /// { + /// "type": "Feature", + /// "geometry": { + /// "type": "Point", + /// "coordinates": [2.3, 4.5] + /// }, + /// "properties": { + /// "name": "Neverland", + /// "age": 456 + /// } + /// } + /// ] + /// }"# + /// .as_bytes(); + /// + /// let io_reader = std::io::BufReader::new(feature_collection_string); + /// + /// use geojson::de::deserialize_geometry; + /// use geojson::FeatureReader; + /// use serde::Deserialize; + /// + /// #[derive(Debug, Deserialize)] + /// struct MyStruct { + /// #[serde(deserialize_with = "deserialize_geometry")] + /// geometry: geo_types::Geometry, + /// name: String, + /// age: u64, + /// } + /// + /// let feature_reader = FeatureReader::from_reader(io_reader); + /// for feature in feature_reader.deserialize::().unwrap() { + /// let my_struct = feature.expect("valid geojson feature"); + /// + /// if my_struct.name == "Dinagat Islands" { + /// assert_eq!(123, my_struct.age); + /// } else if my_struct.name == "Neverland" { + /// assert_eq!(456, my_struct.age); + /// } else { + /// panic!("unexpected name: {}", my_struct.name); + /// } + /// } + /// ``` + pub fn deserialize(self) -> Result>> { + deserialize_feature_collection(self.reader) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde::Deserialize; + use serde_json::json; + + #[derive(Deserialize)] + struct MyRecord { + geometry: crate::Geometry, + name: String, + age: u64, + } + + fn feature_collection_string() -> String { + json!({ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands", + "age": 123 + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [2.3, 4.5] + }, + "properties": { + "name": "Neverland", + "age": 456 + } + } + ] + }) + .to_string() + } + + #[test] + #[cfg(feature = "geo-types")] + fn deserialize_into_type() { + let feature_collection_string = feature_collection_string(); + let mut bytes_reader = feature_collection_string.as_bytes(); + let feature_reader = FeatureReader::from_reader(&mut bytes_reader); + + let records: Vec = feature_reader + .deserialize() + .expect("a valid feature collection") + .map(|result| result.expect("a valid feature")) + .collect(); + + assert_eq!(records.len(), 2); + + assert_eq!( + records[0].geometry, + (&geo_types::point!(x: 125.6, y: 10.1)).into() + ); + assert_eq!(records[0].name, "Dinagat Islands"); + assert_eq!(records[0].age, 123); + + assert_eq!( + records[1].geometry, + (&geo_types::point!(x: 2.3, y: 4.5)).into() + ); + assert_eq!(records[1].name, "Neverland"); + assert_eq!(records[1].age, 456); + } +} diff --git a/src/lib.rs b/src/lib.rs index 1cec31b..81b8090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -419,6 +419,16 @@ pub use crate::errors::{Error, Result}; #[cfg(feature = "geo-types")] mod conversion; +/// Build your struct from GeoJSON using [`serde`] +pub mod de; + +/// Write your struct to GeoJSON using [`serde`] +pub mod ser; + +mod feature_reader; + +pub use feature_reader::FeatureReader; + #[cfg(feature = "geo-types")] pub use conversion::quick_collection; diff --git a/src/ser.rs b/src/ser.rs new file mode 100644 index 0000000..18a82d1 --- /dev/null +++ b/src/ser.rs @@ -0,0 +1,599 @@ +//! +//! To output your struct to GeoJSON, either as a String, bytes, or to a file, your type *must* +//! implement or derive [`serde::Serialize`]: +//! +//! ```rust, ignore +//! #[derive(serde::Serialize)] +//! struct MyStruct { +//! ... +//! } +//! ``` +//! +//! Your type *must* have a field called `geometry` and it must be `serialized_with` [`serialize_geometry`](crate::ser::serialize_geometry): +//! ```rust, ignore +//! #[derive(serde::Serialize)] +//! struct MyStruct { +//! #[serde(serialize_with = "geojson::ser::serialize_geometry")] +//! geometry: geo_types::Point, +//! ... +//! } +//! ``` +//! +//! All fields in your struct other than `geometry` will be serialized as `properties` of the +//! GeoJSON Feature. +//! +//! # Examples +#![cfg_attr(feature = "geo-types", doc = "```")] +#![cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +//! use serde::Serialize; +//! use geojson::ser::serialize_geometry; +//! +//! #[derive(Serialize)] +//! struct MyStruct { +//! // Serialize as geojson, rather than using the type's default serialization +//! #[serde(serialize_with = "serialize_geometry")] +//! geometry: geo_types::Point, +//! name: String, +//! population: u64 +//! } +//! +//! let my_structs = vec![ +//! MyStruct { +//! geometry: geo_types::Point::new(11.1, 22.2), +//! name: "Downtown".to_string(), +//! population: 123 +//! }, +//! MyStruct { +//! geometry: geo_types::Point::new(33.3, 44.4), +//! name: "Uptown".to_string(), +//! population: 456 +//! } +//! ]; +//! +//! let output_geojson = geojson::ser::to_feature_collection_string(&my_structs).unwrap(); +//! +//! let expected_geojson = serde_json::json!( +//! { +//! "type":"FeatureCollection", +//! "features": [ +//! { +//! "type": "Feature", +//! "geometry": { "coordinates": [11.1,22.2], "type": "Point" }, +//! "properties": { +//! "name": "Downtown", +//! "population": 123 +//! } +//! }, +//! { +//! "type": "Feature", +//! "geometry": { "coordinates": [33.3, 44.4], "type": "Point" }, +//! "properties": { +//! "name": "Uptown", +//! "population": 456 +//! } +//! } +//! ] +//! } +//! ); +//! # +//! # // re-parse the json to do a structural comparison, rather than worry about formatting +//! # // or other meaningless deviations in an exact String comparison. +//! # let output_geojson: serde_json::Value = serde_json::from_str(&output_geojson).unwrap(); +//! # +//! # assert_eq!(output_geojson, expected_geojson); +//! ``` +//! +//! # Reading *and* Writing GeoJSON +//! +//! This module is only concerned with Writing out GeoJSON. If you'd also like to reading 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 +//! #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] +//! geometry: geo_types::Point, +//! ... +//! } +//! ``` +use crate::{JsonObject, Result}; + +use serde::{ser::Error, Serialize, Serializer}; + +use std::io; + +/// Serialize a single data structure to a GeoJSON Feature string. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// See [`to_feature_collection_string`] if instead you'd like to serialize multiple features to a +/// FeatureCollection. +/// +/// # 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_string(value: &T) -> Result +where + T: Serialize, +{ + let vec = to_feature_byte_vec(value)?; + let string = unsafe { + // We do not emit invalid UTF-8. + String::from_utf8_unchecked(vec) + }; + Ok(string) +} + +/// Serialize elements to a GeoJSON FeatureCollection string. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// # 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_collection_string(values: &[T]) -> Result +where + T: Serialize, +{ + let vec = to_feature_collection_byte_vec(values)?; + let string = unsafe { + // We do not emit invalid UTF-8. + String::from_utf8_unchecked(vec) + }; + Ok(string) +} + +/// Serialize a single data structure to a GeoJSON Feature byte vector. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// # 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_byte_vec(value: &T) -> Result> +where + T: Serialize, +{ + let mut writer = Vec::with_capacity(128); + to_feature_writer(&mut writer, value)?; + Ok(writer) +} + +/// Serialize elements to a GeoJSON FeatureCollection byte vector. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// # 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_collection_byte_vec(values: &[T]) -> Result> +where + T: Serialize, +{ + let mut writer = Vec::with_capacity(128); + to_feature_collection_writer(&mut writer, values)?; + Ok(writer) +} + +/// Serialize a single data structure as a GeoJSON Feature into the IO stream. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// # 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_writer(writer: W, value: &T) -> Result<()> +where + W: io::Write, + T: Serialize, +{ + let feature_serializer = FeatureWrapper::new(value); + let mut serializer = serde_json::Serializer::new(writer); + feature_serializer.serialize(&mut serializer)?; + Ok(()) +} + +/// Serialize elements as a GeoJSON FeatureCollection into the IO stream. +/// +/// Note that `T` must have a column called `geometry`. +/// +/// # 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_collection_writer(writer: W, features: &[T]) -> Result<()> +where + W: io::Write, + T: Serialize, +{ + use serde::ser::SerializeMap; + + let mut ser = serde_json::Serializer::new(writer); + let mut map = ser.serialize_map(Some(2))?; + map.serialize_entry("type", "FeatureCollection")?; + map.serialize_entry("features", &Features::new(features))?; + map.end()?; + Ok(()) +} + +/// [`serde::serialize_with`](https://serde.rs/field-attrs.html#serialize_with) helper to serialize a type like a +/// [`geo_types`], as a GeoJSON Geometry. +/// +/// # Examples +#[cfg_attr(feature = "geo-types", doc = "```")] +#[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] +/// use serde::Serialize; +/// use geojson::ser::serialize_geometry; +/// +/// #[derive(Serialize)] +/// struct MyStruct { +/// // Serialize as geojson, rather than using the type's default serialization +/// #[serde(serialize_with = "serialize_geometry")] +/// geometry: geo_types::Point, +/// name: String, +/// } +/// +/// let my_structs = vec![ +/// MyStruct { +/// geometry: geo_types::Point::new(11.1, 22.2), +/// name: "Downtown".to_string() +/// }, +/// MyStruct { +/// geometry: geo_types::Point::new(33.3, 44.4), +/// name: "Uptown".to_string() +/// } +/// ]; +/// +/// let geojson_string = geojson::ser::to_feature_collection_string(&my_structs).unwrap(); +/// +/// assert!(geojson_string.contains(r#""geometry":{"coordinates":[11.1,22.2],"type":"Point"}"#)); +/// ``` +pub fn serialize_geometry(geometry: IG, ser: S) -> std::result::Result +where + IG: std::convert::TryInto, + S: serde::Serializer, +{ + geometry + .try_into() + .map_err(|_e| Error::custom("failed to convert geometry to geojson")) + .and_then(|geojson_geometry| geojson_geometry.serialize(ser)) +} + +struct Features<'a, T> +where + T: Serialize, +{ + features: &'a [T], +} + +impl<'a, T> Features<'a, T> +where + T: Serialize, +{ + fn new(features: &'a [T]) -> Self { + Self { features } + } +} + +impl<'a, T> serde::Serialize for Features<'a, T> +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(None)?; + for feature in self.features.iter() { + seq.serialize_element(&FeatureWrapper::new(feature))?; + } + seq.end() + } +} + +struct FeatureWrapper<'t, T> { + feature: &'t T, +} + +impl<'t, T> FeatureWrapper<'t, T> { + fn new(feature: &'t T) -> Self { + Self { feature } + } +} + +impl Serialize for FeatureWrapper<'_, T> +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let mut json_object: JsonObject = { + // PERF: this is an extra round-trip just to juggle some fields around. + // How can we skip this? + let bytes = serde_json::to_vec(self.feature) + .map_err(|e| S::Error::custom(format!("unable to serialize to json: {}", e)))?; + serde_json::from_slice(&bytes) + .map_err(|e| S::Error::custom(format!("unable to roundtrip from json: {}", e)))? + }; + + if !json_object.contains_key("geometry") { + // Currently it's *required* that the struct's geometry field be named `geometry`. + // + // A likely failure case for users is naming it anything else, e.g. `point: geo::Point`. + // + // We could just silently blunder on and set `geometry` to None in that case, but + // printing a specific error message seems more likely to be helpful. + return Err(S::Error::custom("missing `geometry` field")); + } + let geometry = json_object.remove("geometry"); + + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(3))?; + map.serialize_entry("type", "Feature")?; + map.serialize_entry("geometry", &geometry)?; + map.serialize_entry("properties", &json_object)?; + map.end() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::JsonValue; + + use serde_json::json; + + use std::str::FromStr; + + #[test] + fn happy_path() { + #[derive(Serialize)] + struct MyStruct { + geometry: crate::Geometry, + name: String, + } + + let my_feature = { + let geometry = crate::Geometry::new(crate::Value::Point(vec![0.0, 1.0])); + let name = "burbs".to_string(); + MyStruct { geometry, name } + }; + + let expected_output_json = json!({ + "type": "Feature", + "geometry": { + "coordinates":[0.0,1.0], + "type":"Point" + }, + "properties": { + "name": "burbs" + } + }); + + let actual_output = to_feature_string(&my_feature).unwrap(); + let actual_output_json = JsonValue::from_str(&actual_output).unwrap(); + assert_eq!(actual_output_json, expected_output_json); + } + + mod optional_geometry { + use super::*; + #[derive(Serialize)] + struct MyStruct { + geometry: Option, + name: String, + } + + #[test] + fn with_some_geom() { + let my_feature = { + let geometry = Some(crate::Geometry::new(crate::Value::Point(vec![0.0, 1.0]))); + let name = "burbs".to_string(); + MyStruct { geometry, name } + }; + + let expected_output_json = json!({ + "type": "Feature", + "geometry": { + "coordinates":[0.0,1.0], + "type":"Point" + }, + "properties": { + "name": "burbs" + } + }); + + let actual_output = to_feature_string(&my_feature).unwrap(); + let actual_output_json = JsonValue::from_str(&actual_output).unwrap(); + assert_eq!(actual_output_json, expected_output_json); + } + + #[test] + fn with_none_geom() { + let my_feature = { + let geometry = None; + let name = "burbs".to_string(); + MyStruct { geometry, name } + }; + + let expected_output_json = json!({ + "type": "Feature", + "geometry": null, + "properties": { + "name": "burbs" + } + }); + + let actual_output = to_feature_string(&my_feature).unwrap(); + let actual_output_json = JsonValue::from_str(&actual_output).unwrap(); + assert_eq!(actual_output_json, expected_output_json); + } + + #[test] + fn without_geom_field() { + #[derive(Serialize)] + struct MyStructWithoutGeom { + // geometry: Option, + name: String, + } + let my_feature = { + let name = "burbs".to_string(); + MyStructWithoutGeom { name } + }; + + let actual_output = to_feature_string(&my_feature).unwrap_err(); + let error_message = actual_output.to_string(); + + // BRITTLE: we'll need to update this test if the error message changes. + assert!(error_message.contains("missing")); + assert!(error_message.contains("geometry")); + } + + #[test] + fn serializes_whatever_geometry() { + #[derive(Serialize)] + struct MyStructWithWeirdGeom { + // This isn't a valid geometry representation, but we don't really have a way to "validate" it + // so serde will serialize whatever. This test exists just to document current behavior + // not that it's exactly desirable. + geometry: Vec, + name: String, + } + let my_feature = { + let geometry = vec![1, 2, 3]; + let name = "burbs".to_string(); + MyStructWithWeirdGeom { geometry, name } + }; + + let expected_output_json = json!({ + "type": "Feature", + "geometry": [1, 2, 3], + "properties": { + "name": "burbs" + } + }); + + let actual_output = to_feature_string(&my_feature).unwrap(); + let actual_output_json = JsonValue::from_str(&actual_output).unwrap(); + assert_eq!(actual_output_json, expected_output_json); + } + } + + #[cfg(feature = "geo-types")] + mod geo_types_tests { + use super::*; + use crate::de::tests::feature_collection; + + #[test] + fn geometry_field_without_helper() { + #[derive(Serialize)] + struct MyStruct { + // If we forget the "serialize_with" helper, bad things happen. + // This test documents that: + // + // #[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 expected_invalid_output = json!({ + "type": "Feature", + // This isn't a valid geojson-Geometry. This behavior probably isn't desirable, but this + // test documents the current behavior of what happens if the users forgets "serialize_geometry" + "geometry": { "x": 125.6, "y": 10.1 }, + "properties": { + "name": "Dinagat Islands", + "age": 123 + } + }); + + // Order might vary, so re-parse to do a semantic comparison of the content. + let output_string = to_feature_string(&my_struct).expect("valid serialization"); + let actual_output = JsonValue::from_str(&output_string).unwrap(); + + assert_eq!(actual_output, expected_invalid_output); + } + + #[test] + fn geometry_field() { + #[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 expected_output = json!({ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [125.6, 10.1] + }, + "properties": { + "name": "Dinagat Islands", + "age": 123 + } + }); + + // Order might vary, so re-parse to do a semantic comparison of the content. + let output_string = to_feature_string(&my_struct).expect("valid serialization"); + let actual_output = JsonValue::from_str(&output_string).unwrap(); + + assert_eq!(actual_output, expected_output); + } + + #[test] + fn serialize_feature_collection() { + #[derive(Serialize)] + struct MyStruct { + #[serde(serialize_with = "serialize_geometry")] + geometry: geo_types::Point, + name: String, + age: u64, + } + + let my_structs = vec![ + MyStruct { + geometry: geo_types::point!(x: 125.6, y: 10.1), + name: "Dinagat Islands".to_string(), + age: 123, + }, + MyStruct { + geometry: geo_types::point!(x: 2.3, y: 4.5), + name: "Neverland".to_string(), + age: 456, + }, + ]; + + let output_string = + to_feature_collection_string(&my_structs).expect("valid serialization"); + + // Order might vary, so re-parse to do a semantic comparison of the content. + let expected_output = feature_collection(); + let actual_output = JsonValue::from_str(&output_string).unwrap(); + + assert_eq!(actual_output, expected_output); + } + } +} diff --git a/src/util.rs b/src/util.rs index 0dd228e..d8b5652 100644 --- a/src/util.rs +++ b/src/util.rs @@ -64,7 +64,7 @@ fn expect_owned_array(value: JsonValue) -> Result> { } } -fn expect_owned_object(value: JsonValue) -> Result { +pub(crate) fn expect_owned_object(value: JsonValue) -> Result { match value { JsonValue::Object(o) => Ok(o), _ => Err(Error::ExpectedObjectValue(value)), From ba0fe5755dd1d5c65dcf3f3725fd2b1c321c9546 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 24 Aug 2022 22:08:21 -0700 Subject: [PATCH 2/9] fixup! to/from custom structs --- CHANGES.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b0407e7..7395463 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,18 +15,17 @@ name: String, age: u64, } - + // read your input let my_structs: Vec = geojson::de::deserialize_feature_collection(geojson_reader).unwrap(); - + // do some processing process(&mut my_structs); - - // write back your results + + // write back your results geojson::ser::to_feature_collection_string(&my_structs).unwrap(); ``` * PR: - * Added IntoIter implementation for FeatureCollection. * * Added `geojson::Result`. @@ -34,6 +33,7 @@ * BREAKING: Change the Result type of FeatureIterator from io::Result to crate::Result * * Add `TryFrom<&geometry::Value>` for geo_type variants. + * ## 0.23.0 From 4d036a3f2bf7cbf1d2bc69ef7e4175c4e4903945 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Wed, 24 Aug 2022 22:12:39 -0700 Subject: [PATCH 3/9] fixup! to/from custom structs --- src/de.rs | 3 --- src/errors.rs | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/de.rs b/src/de.rs index d8edd48..ea8c110 100644 --- a/src/de.rs +++ b/src/de.rs @@ -411,9 +411,6 @@ where } if has_feature_type { - // What do I actually do here? serde-transcode? or create a new MapAccess or Struct that - // has the fields needed by a child visitor - perhaps using serde::de::value::MapAccessDeserializer? - // use serde::de::value::MapAccessDeserializer; let d2 = hash_map.into_deserializer(); let result = Deserialize::deserialize(d2).map_err(|e| Error::custom(format!("{}", e)))?; diff --git a/src/errors.rs b/src/errors.rs index 27660e9..f428ea7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -21,8 +21,6 @@ pub enum Error { /// This was previously `GeoJsonUnknownType`, but has been split for clarity #[error("Expected a Feature mapping, but got a `{0}`")] NotAFeature(String), - // TODO: Expect vs. Found (and maybe it doesn't need to be "geo-type" specific, but anything - // that can be converted)? #[error("Encountered a mismatch when converting to a Geo type: `{0}`")] InvalidGeometryConversion(GValue), #[error( From 493463eb9bea7856ae4f04656cdfa06c228956bb Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Aug 2022 13:33:41 -0700 Subject: [PATCH 4/9] improve feature_reader docs --- src/feature_reader.rs | 123 +++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 81 deletions(-) diff --git a/src/feature_reader.rs b/src/feature_reader.rs index 2f8b6be..548409f 100644 --- a/src/feature_reader.rs +++ b/src/feature_reader.rs @@ -5,16 +5,20 @@ use serde::de::DeserializeOwned; use std::io::Read; +/// Enumerates individual Features from a GeoJSON FeatureCollection pub struct FeatureReader { reader: R, } impl FeatureReader { + /// Create a FeatureReader from the given `reader`. pub fn from_reader(reader: R) -> Self { Self { reader } } - /// Iterate over the individual [`Feature`s](Feature) of a FeatureCollection + /// Iterate over the individual [`Feature`s](Feature) of a FeatureCollection. + /// + /// If instead you'd like to deserialize directly to your own struct, see [`FeatureReader::deserialize`]. /// /// # Examples /// @@ -24,10 +28,7 @@ impl FeatureReader { /// "features": [ /// { /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [125.6, 10.1] - /// }, + /// "geometry": { "type": "Point", "coordinates": [125.6, 10.1] }, /// "properties": { /// "name": "Dinagat Islands", /// "age": 123 @@ -35,10 +36,7 @@ impl FeatureReader { /// }, /// { /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [2.3, 4.5] - /// }, + /// "geometry": { "type": "Point", "coordinates": [2.3, 4.5] }, /// "properties": { /// "name": "Neverland", /// "age": 456 @@ -75,62 +73,24 @@ impl FeatureReader { /// /// # Examples /// - /// ``` - /// let feature_collection_string = r#"{ - /// "type": "FeatureCollection", - /// "features": [ - /// { - /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [125.6, 10.1] - /// }, - /// "properties": { - /// "name": "Dinagat Islands", - /// "age": 123 - /// } - /// }, - /// { - /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [2.3, 4.5] - /// }, - /// "properties": { - /// "name": "Neverland", - /// "age": 456 - /// } - /// } - /// ] - /// }"# - /// .as_bytes(); - /// let io_reader = std::io::BufReader::new(feature_collection_string); + /// Your struct must implement or derive [`serde::Deserialize`]. /// - /// use serde::Deserialize; - /// #[derive(Debug, Deserialize)] + /// If you have enabled the `geo-types` feature, which is enabled by default, you can + /// deserialize directly to a useful geometry type. + /// + /// ```rust,ignore + /// use geojson::{FeatureReader, de::deserialize_geometry}; + /// + /// #[derive(serde::Deserialize)] /// struct MyStruct { - /// geometry: geojson::Geometry, + /// #[serde(deserialize_with = "deserialize_geometry")] + /// geometry: geo_types::Point, /// name: String, /// age: u64, /// } - /// - /// use geojson::FeatureReader; - /// use geojson::GeoJson::Geometry; - /// let feature_reader = FeatureReader::from_reader(io_reader); - /// for feature in feature_reader.deserialize::().unwrap() { - /// let my_struct = feature.expect("valid geojson feature"); - /// - /// if my_struct.name == "Dinagat Islands" { - /// assert_eq!(123, my_struct.age); - /// } else if my_struct.name == "Neverland" { - /// assert_eq!(456, my_struct.age); - /// } else { - /// panic!("unexpected name: {}", my_struct.name); - /// } - /// } /// ``` /// - /// ## With geo-types Geometry + /// Then you can deserialize the FeatureCollection directly to your type. #[cfg_attr(feature = "geo-types", doc = "```")] #[cfg_attr(not(feature = "geo-types"), doc = "```ignore")] /// let feature_collection_string = r#"{ @@ -138,10 +98,7 @@ impl FeatureReader { /// "features": [ /// { /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [125.6, 10.1] - /// }, + /// "geometry": { "type": "Point", "coordinates": [125.6, 10.1] }, /// "properties": { /// "name": "Dinagat Islands", /// "age": 123 @@ -149,32 +106,25 @@ impl FeatureReader { /// }, /// { /// "type": "Feature", - /// "geometry": { - /// "type": "Point", - /// "coordinates": [2.3, 4.5] - /// }, + /// "geometry": { "type": "Point", "coordinates": [2.3, 4.5] }, /// "properties": { /// "name": "Neverland", /// "age": 456 /// } /// } /// ] - /// }"# - /// .as_bytes(); - /// + /// }"#.as_bytes(); /// let io_reader = std::io::BufReader::new(feature_collection_string); - /// - /// use geojson::de::deserialize_geometry; - /// use geojson::FeatureReader; - /// use serde::Deserialize; - /// - /// #[derive(Debug, Deserialize)] - /// struct MyStruct { - /// #[serde(deserialize_with = "deserialize_geometry")] - /// geometry: geo_types::Geometry, - /// name: String, - /// age: u64, - /// } + /// # + /// # use geojson::{FeatureReader, de::deserialize_geometry}; + /// # + /// # #[derive(serde::Deserialize)] + /// # struct MyStruct { + /// # #[serde(deserialize_with = "deserialize_geometry")] + /// # geometry: geo_types::Point, + /// # name: String, + /// # age: u64, + /// # } /// /// let feature_reader = FeatureReader::from_reader(io_reader); /// for feature in feature_reader.deserialize::().unwrap() { @@ -189,6 +139,17 @@ impl FeatureReader { /// } /// } /// ``` + /// + /// If you're not using [`geo-types`](geo_types), you can deserialize to a `geojson::Geometry` instead. + /// ```rust,ignore + /// use serde::Deserialize; + /// #[derive(Deserialize)] + /// struct MyStruct { + /// geometry: geojson::Geometry, + /// name: String, + /// age: u64, + /// } + /// ``` pub fn deserialize(self) -> Result>> { deserialize_feature_collection(self.reader) } From 269e29d2bdde946ebf607016ec58d1260dc7e42e Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Aug 2022 13:34:30 -0700 Subject: [PATCH 5/9] Deprecate FeatureIterator in favor of FeatureReader::features Otherwise their description sounds too similar. I think for people who want it, ultimately it'd be best if FeatureIterator were hidden opaquely behind FeatureReader::features(). This is similar to `CSVReader::string_records()` - which deserializes to the libraries lowest-common-denominator built in type (geojson::Feature in our case) And then we have `FeatureReader::deserialize` which, just like `CSVReader::deserialize`, allows the user to deserialize directly to their custom struct. --- benches/parse.rs | 16 +--------------- src/feature_iterator.rs | 6 ++++++ src/feature_reader.rs | 1 + src/lib.rs | 2 ++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/benches/parse.rs b/benches/parse.rs index 35770dc..19ef8e4 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -18,21 +18,6 @@ fn parse_feature_collection_benchmark(c: &mut Criterion) { }) }); - c.bench_function("FeatureIter (countries.geojson)", |b| { - b.iter(|| { - let feature_iter = - geojson::FeatureIterator::new(BufReader::new(geojson_str.as_bytes())); - let _ = black_box({ - let mut count = 0; - for feature in feature_iter { - let _ = feature.unwrap(); - count += 1; - } - assert_eq!(count, 180); - }); - }); - }); - c.bench_function("FeatureReader::features (countries.geojson)", |b| { b.iter(|| { let feature_reader = @@ -50,6 +35,7 @@ fn parse_feature_collection_benchmark(c: &mut Criterion) { c.bench_function("FeatureReader::deserialize (countries.geojson)", |b| { b.iter(|| { + #[allow(unused)] #[derive(serde::Deserialize)] struct Country { geometry: geojson::Geometry, diff --git a/src/feature_iterator.rs b/src/feature_iterator.rs index e0d49a2..70f48a5 100644 --- a/src/feature_iterator.rs +++ b/src/feature_iterator.rs @@ -11,12 +11,18 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +#![allow(deprecated)] use crate::{Feature, Result}; use std::io; use std::marker::PhantomData; +// TODO: Eventually make this private - and expose only FeatureReader. +#[deprecated( + since = "0.24.0", + note = "use FeatureReader::from_reader(io).features() instead" +)] /// Iteratively deserialize individual features from a stream containing a /// GeoJSON [`FeatureCollection`](struct@crate::FeatureCollection) /// diff --git a/src/feature_reader.rs b/src/feature_reader.rs index 548409f..0d0a7e8 100644 --- a/src/feature_reader.rs +++ b/src/feature_reader.rs @@ -65,6 +65,7 @@ impl FeatureReader { /// } /// ``` pub fn features(self) -> impl Iterator> { + #[allow(deprecated)] crate::FeatureIterator::new(self.reader) } diff --git a/src/lib.rs b/src/lib.rs index 81b8090..8776870 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -411,6 +411,8 @@ mod feature_collection; pub use crate::feature_collection::FeatureCollection; mod feature_iterator; +#[allow(deprecated)] +#[doc(hidden)] pub use crate::feature_iterator::FeatureIterator; pub mod errors; From 413b959add564933ef7e1ee76da1a1ed3836834b Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 25 Aug 2022 15:44:12 -0700 Subject: [PATCH 6/9] add frontpage docs --- src/lib.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8776870..155d4b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,14 +25,29 @@ //! geojson = "*" //! ``` //! +//! # Types and crate structure +//! +//! This crate is structured around the GeoJSON spec ([IETF RFC 7946](https://tools.ietf.org/html/rfc7946)), +//! and users are encouraged to familiarise themselves with it. The elements specified in this spec +//! have corresponding struct and type definitions in this crate, e.g. [`FeatureCollection`], [`Feature`], +//! etc. +//! +//! There are two primary ways to use this crate. +//! +//! The first, most general, approach is to write your code to deal in terms of these structs from +//! the GeoJSON spec. This allows you to access the full expressive power of GeoJSON with the speed +//! and safety of Rust. +//! +//! Alternatively, and commonly, if you only need geometry and properties (and not, e.g. +//! [foreign members]()), you can bring your own types, and use this crate's [`serde`] integration +//! to serialize and deserialize your custom types directly to and from a GeoJSON Feature Collection. +//! [See more on using your own types with serde](#using-your-own-types-with-serde). +//! //! If you want to use GeoJSON as input to or output from a geometry processing crate like //! [`geo`](https://docs.rs/geo), see the section on [using geojson with //! geo-types](#use-geojson-with-other-crates-by-converting-to-geo-types). //! -//! # Types and crate structure -//! -//! This crate is structured around the GeoJSON spec ([IETF RFC 7946](https://tools.ietf.org/html/rfc7946)), -//! and users are encouraged to familiarise themselves with it. +//! ## Using structs from the GeoJSON spec //! //! A GeoJSON object can be one of three top-level objects, reflected in this crate as the //! [`GeoJson`] enum members of the same name. @@ -151,7 +166,7 @@ //! ```rust //! use geojson::{GeoJson, Geometry, Value}; //! -//! /// Process top-level GeoJSON items +//! /// Process top-level GeoJSON Object //! fn process_geojson(gj: &GeoJson) { //! match *gj { //! GeoJson::FeatureCollection(ref ctn) => { @@ -376,7 +391,32 @@ //! [`polylabel_cmd`](https://github.com/urschrei/polylabel_cmd/blob/master/src/main.rs) crates contain example //! implementations which may be useful if you wish to perform this kind of processing yourself and require //! more granular control over performance and / or memory allocation. - +//! +//! ## Using your own types with serde +//! +//! If your use case is simple enough, you can read and write GeoJSON directly to and from your own +//! types using serde. +//! +//! Specifically, the requirements are: +//! 1. Your type has a `geometry` field. +//! 1. If your `geometry` field is a [`geo-types` Geometry](geo_types::geometry), you must use +//! the provided `serialize_with`/`deserialize_with` helpers. +//! 2. Otherwise, your `geometry` field must be a [`crate::Geometry`]. +//! 2. Other than `geometry`, you may only use a Feature's `properties` - all other fields, like +//! foreign members, will be lost. +//! +//! ```ignore +//! #[derive(serde::Serialize, serde::Deserialize)] +//! struct MyStruct { +//! // Serialize as geojson, rather than using the type's default serialization +//! #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] +//! geometry: geo_types::Point, +//! name: String, +//! count: u64, +//! } +//! ``` +//! +//! See more in the [serialization](ser) and [deserialization](de) modules. // only enables the `doc_cfg` feature when // the `docsrs` configuration attribute is defined #![cfg_attr(docsrs, feature(doc_cfg))] From 3be76673b0276ba607fd4a9e3898479cf8bbc639 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 26 Aug 2022 10:17:56 -0700 Subject: [PATCH 7/9] avoid extra round-trip when serializing. With the 35% improvement, we're now a bit faster (22%) than the legacy bench. Running benches/serialize.rs (target/release/deps/serialize-0873325e9a7982ee) serialize geojson::FeatureCollection struct (countries.geojson) time: [3.1352 ms 3.1433 ms 3.1520 ms] change: [-0.3321% -0.0199% +0.3402%] (p = 0.91 > 0.05) No change in performance detected. Found 4 outliers among 100 measurements (4.00%) 4 (4.00%) high mild serialize custom struct (countries.geojson) time: [2.4598 ms 2.4647 ms 2.4697 ms] change: [-35.490% -35.313% -35.150%] (p = 0.00 < 0.05) Performance has improved. Found 1 outliers among 100 measurements (1.00%) 1 (1.00%) high mild --- src/ser.rs | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/ser.rs b/src/ser.rs index 18a82d1..c687460 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -96,7 +96,7 @@ //! ... //! } //! ``` -use crate::{JsonObject, Result}; +use crate::{JsonObject, JsonValue, Result}; use serde::{ser::Error, Serialize, Serializer}; @@ -316,12 +316,37 @@ where S: Serializer, { let mut json_object: JsonObject = { - // PERF: this is an extra round-trip just to juggle some fields around. - // How can we skip this? - let bytes = serde_json::to_vec(self.feature) - .map_err(|e| S::Error::custom(format!("unable to serialize to json: {}", e)))?; - serde_json::from_slice(&bytes) - .map_err(|e| S::Error::custom(format!("unable to roundtrip from json: {}", e)))? + let value = serde_json::to_value(self.feature).map_err(|e| { + S::Error::custom(format!("Feature was not serializable as JSON - {}", e)) + })?; + match value { + JsonValue::Object(object) => object, + JsonValue::Null => { + return Err(S::Error::custom(format!( + "expected JSON object but found `null`" + ))) + } + JsonValue::Bool(_) => { + return Err(S::Error::custom(format!( + "expected JSON object but found `bool`" + ))) + } + JsonValue::Number(_) => { + return Err(S::Error::custom(format!( + "expected JSON object but found `number`" + ))) + } + JsonValue::String(_) => { + return Err(S::Error::custom(format!( + "expected JSON object but found `string`" + ))) + } + JsonValue::Array(_) => { + return Err(S::Error::custom(format!( + "expected JSON object but found `array`" + ))) + } + } }; if !json_object.contains_key("geometry") { From a2023fd9e44aec2f6e4a74c9330142fcf0439670 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 26 Aug 2022 10:47:12 -0700 Subject: [PATCH 8/9] add geo-types examples to benches --- benches/parse.rs | 32 +++++++++++++++++++++++++++++++- benches/serialize.rs | 33 +++++++++++++++++++++++++++++++++ benches/to_geo_types.rs | 1 + 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/benches/parse.rs b/benches/parse.rs index 19ef8e4..b6f0370 100644 --- a/benches/parse.rs +++ b/benches/parse.rs @@ -1,5 +1,8 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use geojson::de::deserialize_geometry; use geojson::GeoJson; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + use std::io::BufReader; fn parse_feature_collection_benchmark(c: &mut Criterion) { @@ -54,6 +57,33 @@ fn parse_feature_collection_benchmark(c: &mut Criterion) { }); }); }); + + #[cfg(feature="geo-types")] + c.bench_function( + "FeatureReader::deserialize to geo-types (countries.geojson)", + |b| { + b.iter(|| { + #[allow(unused)] + #[derive(serde::Deserialize)] + struct Country { + #[serde(deserialize_with = "deserialize_geometry")] + geometry: geo_types::Geometry, + name: String, + } + let feature_reader = + geojson::FeatureReader::from_reader(BufReader::new(geojson_str.as_bytes())); + + let _ = black_box({ + let mut count = 0; + for feature in feature_reader.deserialize::().unwrap() { + let _ = feature.unwrap(); + count += 1; + } + assert_eq!(count, 180); + }); + }); + }, + ); } fn parse_geometry_collection_benchmark(c: &mut Criterion) { diff --git a/benches/serialize.rs b/benches/serialize.rs index 3a15481..31821fc 100644 --- a/benches/serialize.rs +++ b/benches/serialize.rs @@ -1,4 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use geojson::{de::deserialize_geometry, ser::serialize_geometry}; fn serialize_feature_collection_benchmark(c: &mut Criterion) { let geojson_str = include_str!("../tests/fixtures/countries.geojson"); @@ -39,6 +40,38 @@ fn serialize_feature_collection_benchmark(c: &mut Criterion) { }); }); }); + + #[cfg(feature="geo-types")] + c.bench_function( + "serialize custom struct to geo-types (countries.geojson)", + |b| { + #[derive(serde::Serialize, serde::Deserialize)] + struct Country { + #[serde( + serialize_with = "serialize_geometry", + deserialize_with = "deserialize_geometry" + )] + geometry: geo_types::Geometry, + name: String, + } + let features = + geojson::de::deserialize_feature_collection_str_to_vec::(geojson_str) + .unwrap(); + assert_eq!(features.len(), 180); + + b.iter(|| { + black_box({ + let geojson_string = + geojson::ser::to_feature_collection_string(&features).unwrap(); + // Sanity check that we serialized a long string of some kind. + // + // Note this is slightly shorter than the GeoJson round-trip above because we drop + // some fields, like foreign members + assert_eq!(geojson_string.len(), 254908); + }); + }); + }, + ); } criterion_group!(benches, serialize_feature_collection_benchmark); diff --git a/benches/to_geo_types.rs b/benches/to_geo_types.rs index 8dc312f..4b29211 100644 --- a/benches/to_geo_types.rs +++ b/benches/to_geo_types.rs @@ -4,6 +4,7 @@ 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, _> = From b936683e8a685c3c2fd1775c597c3de4a0a6cd26 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 26 Aug 2022 11:32:13 -0700 Subject: [PATCH 9/9] add stand-alone code examples --- examples/deserialize.rs | 28 ++++++++++++++++++ examples/deserialize_to_geo_types.rs | 37 ++++++++++++++++++++++++ examples/deserialize_to_geojson_types.rs | 18 ++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 examples/deserialize.rs create mode 100644 examples/deserialize_to_geo_types.rs create mode 100644 examples/deserialize_to_geojson_types.rs diff --git a/examples/deserialize.rs b/examples/deserialize.rs new file mode 100644 index 0000000..ea08d72 --- /dev/null +++ b/examples/deserialize.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct Country { + // see the geo_types example if you want to store + // geotypes in your struct + geometry: geojson::Geometry, + name: String, +} + +use std::error::Error; +use std::fs::File; +use std::io::{BufReader, BufWriter}; + +fn main() -> Result<(), Box> { + let file_reader = BufReader::new(File::open("tests/fixtures/countries.geojson")?); + + // Create a Vec of Country structs from the GeoJSON + let countries: Vec = + geojson::de::deserialize_feature_collection_to_vec::(file_reader)?; + assert_eq!(countries.len(), 180); + + // Write the structs back to GeoJSON + let file_writer = BufWriter::new(File::create("example-output-countries.geojson")?); + geojson::ser::to_feature_collection_writer(file_writer, &countries)?; + + Ok(()) +} diff --git a/examples/deserialize_to_geo_types.rs b/examples/deserialize_to_geo_types.rs new file mode 100644 index 0000000..b926620 --- /dev/null +++ b/examples/deserialize_to_geo_types.rs @@ -0,0 +1,37 @@ +use geojson::{de::deserialize_geometry, ser::serialize_geometry}; + +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fs::File; +use std::io::{BufReader, BufWriter}; + +#[cfg(not(feature="geo-types"))] +fn main() -> Result<(), Box> { + panic!("this example requires geo-types") +} + +#[cfg(feature="geo-types")] +fn main() -> Result<(), Box> { + #[derive(Serialize, Deserialize)] + struct Country { + #[serde( + serialize_with = "serialize_geometry", + deserialize_with = "deserialize_geometry" + )] + geometry: geo_types::Geometry, + name: String, + } + + let file_reader = BufReader::new(File::open("tests/fixtures/countries.geojson")?); + + // Create a Vec of Country structs from the GeoJSON + let countries: Vec = + geojson::de::deserialize_feature_collection_to_vec::(file_reader)?; + assert_eq!(countries.len(), 180); + + // Write the structs back to GeoJSON + let file_writer = BufWriter::new(File::create("example-output-countries.geojson")?); + geojson::ser::to_feature_collection_writer(file_writer, &countries)?; + + Ok(()) +} diff --git a/examples/deserialize_to_geojson_types.rs b/examples/deserialize_to_geojson_types.rs new file mode 100644 index 0000000..82a664d --- /dev/null +++ b/examples/deserialize_to_geojson_types.rs @@ -0,0 +1,18 @@ +use std::error::Error; +use std::fs::File; +use std::io::{BufReader, BufWriter}; + +use geojson::FeatureCollection; + +fn main() -> Result<(), Box> { + let file_reader = BufReader::new(File::open("tests/fixtures/countries.geojson")?); + + let countries: FeatureCollection = serde_json::from_reader(file_reader)?; + assert_eq!(countries.features.len(), 180); + + // Write the structs back to GeoJSON + let file_writer = BufWriter::new(File::create("example-output-countries.geojson")?); + serde_json::to_writer(file_writer, &countries)?; + + Ok(()) +}