diff --git a/poem-openapi/Cargo.toml b/poem-openapi/Cargo.toml index d1f8b7d417..3355668aef 100644 --- a/poem-openapi/Cargo.toml +++ b/poem-openapi/Cargo.toml @@ -22,6 +22,7 @@ email = ["email_address"] hostname = ["hostname-validator"] static-files = ["poem/static-files"] websocket = ["poem/websocket"] +geo = ["dep:geo-types", "dep:geojson"] [dependencies] poem-openapi-derive.workspace = true @@ -68,6 +69,8 @@ bson = { version = "2.0.0", optional = true } rust_decimal = { version = "1.22.0", optional = true } humantime = { version = "2.1.0", optional = true } ipnet = { version = "2.7.1", optional = true } +geo-types = { version = "0.7.12", optional = true } +geojson = { version = "0.24.1", features = ["geo-types"], optional = true } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/poem-openapi/README.md b/poem-openapi/README.md index 21ae40b74d..f3fb6bcc51 100644 --- a/poem-openapi/README.md +++ b/poem-openapi/README.md @@ -63,6 +63,7 @@ To avoid compiling unused dependencies, Poem gates certain features, some of whi | hostname | Support for hostname string | | uuid | Integrate with the [`uuid` crate](https://crates.io/crates/uuid) | | url | Integrate with the [`url` crate](https://crates.io/crates/url) | +| geo | Integrate with the [`geo-types` crate](https://crates.io/crates/geo-types) | | bson | Integrate with the [`bson` crate](https://crates.io/crates/bson) | | rust_decimal | Integrate with the [`rust_decimal` crate](https://crates.io/crates/rust_decimal) | | static-files | Support for static file response | diff --git a/poem-openapi/src/types/external/geo.rs b/poem-openapi/src/types/external/geo.rs new file mode 100644 index 0000000000..cbab0afc41 --- /dev/null +++ b/poem-openapi/src/types/external/geo.rs @@ -0,0 +1,115 @@ +use geo_types::*; + +use crate::types::Type; + +trait GeoJson { + type Coordinates: Type; +} + +macro_rules! impl_geojson_types { + ($geometry:tt, $name:literal, $coordinates:ty) => { + impl GeoJson for $geometry { + type Coordinates = $coordinates; + } + + impl crate::types::Type for $geometry { + const IS_REQUIRED: bool = true; + type RawValueType = Self; + type RawElementValueType = Self; + + fn name() -> ::std::borrow::Cow<'static, str> { + concat!("GeoJSON<", $name, ">").into() + } + + fn schema_ref() -> crate::registry::MetaSchemaRef { + crate::registry::MetaSchemaRef::Reference(Self::name().into_owned()) + } + + fn register(registry: &mut crate::registry::Registry) { + registry.create_schema::(Self::name().into_owned(), |registry| { + String::register(registry); + <::Coordinates>::register(registry); + crate::registry::MetaSchema { + required: vec!["type", "coordinates"], + properties: vec![ + ("type", String::schema_ref()), + ( + "coordinates", + <::Coordinates>::schema_ref(), + ), + ], + ..crate::registry::MetaSchema::new("object") + } + }) + } + + fn as_raw_value(&self) -> Option<&Self::RawValueType> { + Some(self) + } + + fn raw_element_iter<'a>( + &'a self, + ) -> Box + 'a> { + Box::new(IntoIterator::into_iter(self.as_raw_value())) + } + } + + impl crate::types::ParseFromJSON for $geometry { + fn parse_from_json( + value: Option<::serde_json::Value>, + ) -> Result> { + let value = value.ok_or(crate::types::ParseError::expected_input())?; + Self::try_from(geojson::Geometry::try_from(value)?).map_err(Into::into) + } + } + + impl crate::types::ToJSON for $geometry { + fn to_json(&self) -> Option<::serde_json::Value> { + Some( + ::serde_json::Map::::from( + &geojson::Geometry::from(self), + ) + .into(), + ) + } + } + }; +} + +impl_geojson_types!(Point, "Point", [T; 2]); +impl_geojson_types!(MultiPoint, "MultiPoint", Vec<[T; 2]>); +impl_geojson_types!(LineString, "LineString", Vec<[T; 2]>); +impl_geojson_types!(MultiLineString, "MultiLineString", Vec>); +impl_geojson_types!(Polygon, "Polygon", Vec>); +impl_geojson_types!(MultiPolygon, "MultiPolygon", Vec>>); + +#[cfg(test)] +mod tests { + use geo_types::Point; + + use crate::types::{ParseFromJSON, ToJSON}; + + fn point_geo() -> Point { + Point::new(1.0, 2.0) + } + + fn point_json() -> serde_json::Value { + serde_json::json!({ + "type": "Point", + "coordinates": [1.0, 2.0] + }) + } + + #[test] + fn serializes_geo_to_json() { + assert_eq!(point_json(), point_geo().to_json().unwrap()) + } + + #[test] + fn deserializes_json_to_geo() { + assert_eq!( + Point::parse_from_json(Some(point_json())).unwrap(), + point_geo() + ) + } +} diff --git a/poem-openapi/src/types/external/mod.rs b/poem-openapi/src/types/external/mod.rs index 101ee448c2..18dfd5cd07 100644 --- a/poem-openapi/src/types/external/mod.rs +++ b/poem-openapi/src/types/external/mod.rs @@ -10,6 +10,8 @@ mod chrono; #[cfg(feature = "rust_decimal")] mod decimal; mod floats; +#[cfg(feature = "geo")] +mod geo; mod hashmap; mod hashset; #[cfg(feature = "humantime")]