Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
199: GeoJSON to/from custom struct using serde r=urschrei,rmanoka a=michaelkirk - [x] I agree to follow the project's [code of conduct](https://github.com/georust/geo/blob/master/CODE_OF_CONDUCT.md). - [x] I added an entry to `CHANGES.md` if knowledge of this change could be valuable to users. --- This is an attempt to improve the ergonomics of parsing GeoJson using serde (FIXES #184) . ~~This PR is a draft because there are a lot of error paths related to invalid parsing that I'd like to add tests for, but I first wanted to check in on overall direction of the API. What do people think?~~ I think this is ready for review! # Examples Given some geojson like this: ```rust 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); ``` ## Before ### Deserialization You used to parse it like this: ``` use geojson:: FeatureIterator; let feature_reader = FeatureIterator::new(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(); let geometry = feature.geometry.value.try_into().unwrap(); if name == "Dinagat Islands" { assert_eq!(123, age); assert_matches!(geometry, geo_types::Point::new(125.6, 10.1).into()); } else if name == "Neverland" { assert_eq!(456, age); assert_matches!(geometry, geo_types::Point::new(2.3, 4.5).into()); } else { panic!("unexpected name: {}", name); } } ``` ### Serialization Then, to write it back to geojson, you'd have to either do all your processing strictly with the geojson types, or somehow convert your entities from and back to one of the GeoJson entities: Something like: ``` // The implementation of this method is potentially a little messy / boilerplatey let feature_collection: geojson::FeatureCollection = some_custom_method_to_convert_to_geojson(&my_structs); // This part is easy enough though serde_json::to_string(&geojson); ``` ## After But now you also have the option of parsing it into your own declarative struct using serde like this: ### Declaration ``` use geojson::{ser::serialize_geometry, de::deserialize_geometry}; use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] struct MyStruct { // You can parse directly to geo_types via these helpers, otherwise this field will need to be a `geojson::Geometry` #[serde(serialize_with = "serialize_geometry", deserialize_with = "deserialize_geometry")] geometry: geo_types::Point<f64>, name: String, age: u64, } ``` ### Deserialization ``` for feature in geojson::de::deserialize_feature_collection::<MyStruct>(io_reader).unwrap() { let my_struct = feature.expect("valid geojson feature"); if my_struct.name == "Dinagat Islands" { assert_eq!(my_struct.age, 123); assert_eq!(my_struct.geometry, geo_types::Point::new(125.6, 10.1)); } else if my_struct.name == "Neverland" { assert_eq!(my_struct.age, 456); assert_eq!(my_struct.geometry, geo_types::Point::new(2.3, 4.5)); } else { panic!("unexpected name: {}", my_struct.name); } } ``` ### Serialization ``` let my_structs: Vec<MyStruct> = get_my_structs(); geojson::ser::to_feature_collection_writer(writer, &my_structs).expect("valid serialization"); ``` ## Caveats ### Performance Performance currently isn't great. There's a couple of things which seem ridiculous in the code that I've marked with `PERF:` that I don't have an immediate solution for. This is my first time really diving into the internals of serde and it's kind of a lot! My hope is that performance improvements would be possible with no or little changes to the API. Some specific numbers (from some admittedly crude benchmarks): **Old Deserialization:** ``` FeatureReader::features (countries.geojson) time: [5.8497 ms 5.8607 ms 5.8728 ms] ``` **New Deserialization:** ``` FeatureReader::deserialize (countries.geojson) time: [7.1702 ms 7.1865 ms 7.2035 ms] ``` **Old serialization:** ``` serialize geojson::FeatureCollection struct (countries.geojson) time: [3.1471 ms 3.1552 ms 3.1637 ms] ``` **New serialization:** ``` serialize custom struct (countries.geojson) time: [3.8076 ms 3.8144 ms 3.8219 ms] ``` So the new "ergonomic" serialization/deserialization takes about 1.2x the time as the old way. Though it's actually probably a bit better than that because with the new form you have all your data ready to use. With the old way, you still need to go through this dance before you can start your analysis: ``` let name = feature.property("name").unwrap().as_str().unwrap(); let age = feature.property("age").unwrap().as_u64().unwrap(); let geometry = feature.geometry.value.try_into().unwrap(); ``` Anyway, I think this kind of speed difference is well worth the improved usability for my use cases. ### Foreign Members This doesn't support anything besides `geometry` and `properties` - e.g. foreign members are dropped. I'm hopeful that this is useful even with that limitation, but if not, maybe we can think of a way to accommodate most people's needs. Co-authored-by: Michael Kirk <[email protected]>
- Loading branch information