Skip to content

Commit

Permalink
Initial GeoJSON support (#693)
Browse files Browse the repository at this point in the history
* Initial GeoJSON support

Implements `Type` on GeoJSON geometries by integrating with `geo-types`
and `geojson`. The only missing geometry is [`GeometryCollection`]
(https://datatracker.ietf.org/doc/html/rfc7946#appendix-A.7).

* Add geo `ToJSON` and `ParseFromJSON` tests.

---------

Co-authored-by: gibbz00 <[email protected]>
  • Loading branch information
gibbz00 and gibbz00 authored Nov 18, 2023
1 parent 2413236 commit c555d37
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 0 deletions.
3 changes: 3 additions & 0 deletions poem-openapi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"] }
Expand Down
1 change: 1 addition & 0 deletions poem-openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
115 changes: 115 additions & 0 deletions poem-openapi/src/types/external/geo.rs
Original file line number Diff line number Diff line change
@@ -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<T: CoordNum + crate::types::Type> GeoJson for $geometry<T> {
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, _>(Self::name().into_owned(), |registry| {
String::register(registry);
<<Self as GeoJson>::Coordinates>::register(registry);
crate::registry::MetaSchema {
required: vec!["type", "coordinates"],
properties: vec![
("type", String::schema_ref()),
(
"coordinates",
<<Self as GeoJson>::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<dyn Iterator<Item = &'a Self::RawElementValueType> + '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<Self, crate::types::ParseError<Self>> {
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::<String, ::serde_json::Value>::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<Vec<[T; 2]>>);
impl_geojson_types!(Polygon, "Polygon", Vec<Vec<[T; 2]>>);
impl_geojson_types!(MultiPolygon, "MultiPolygon", Vec<Vec<Vec<[T; 2]>>>);

#[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()
)
}
}
2 changes: 2 additions & 0 deletions poem-openapi/src/types/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit c555d37

Please sign in to comment.