feat(openapi): Add Geometry type support and fix array schema validation (#1120)
Some checks failed
CI / check (map[name:poem path:poem]) (push) Has been cancelled
CI / check (map[name:poem-derive path:poem-derive]) (push) Has been cancelled
CI / check (map[name:poem-grpc path:poem-grpc]) (push) Has been cancelled
CI / check (map[name:poem-grpc-build path:poem-grpc-build]) (push) Has been cancelled
CI / check (map[name:poem-lambda path:poem-lambda]) (push) Has been cancelled
CI / check (map[name:poem-mcpserver path:poem-mcpserver]) (push) Has been cancelled
CI / check (map[name:poem-mcpserver-macros path:poem-mcpserver-macros]) (push) Has been cancelled
CI / check (map[name:poem-openapi path:poem-openapi]) (push) Has been cancelled
CI / check (map[name:poem-openapi-derive path:poem-openapi-derive]) (push) Has been cancelled
CI / check-examples (push) Has been cancelled
Code Coverage / cover (push) Has been cancelled

This commit is contained in:
gauteoh
2025-12-14 03:59:00 +01:00
committed by GitHub
parent f361a7177f
commit 030d62e4df
3 changed files with 264 additions and 12 deletions

View File

@@ -21,8 +21,8 @@ impl<T: Type, const LEN: usize> Type for [T; LEN] {
fn schema_ref() -> MetaSchemaRef {
MetaSchemaRef::Inline(Box::new(MetaSchema {
items: Some(Box::new(T::schema_ref())),
max_length: Some(LEN),
min_length: Some(LEN),
max_items: Some(LEN),
min_items: Some(LEN),
..MetaSchema::new("array")
}))
}

View File

@@ -29,14 +29,36 @@ macro_rules! impl_geojson_types {
registry.create_schema::<Self, _>(Self::name().into_owned(), |registry| {
String::register(registry);
<<Self as GeoJson>::Coordinates>::register(registry);
// Create enum schema for the type field
let type_schema = crate::registry::MetaSchemaRef::Inline(Box::new(
crate::registry::MetaSchema {
enum_items: vec![::serde_json::Value::String($name.to_string())],
..crate::registry::MetaSchema::new("string")
},
));
// Create bbox schema (optional array of numbers with minItems: 4)
let bbox_schema = crate::registry::MetaSchemaRef::Inline(Box::new(
crate::registry::MetaSchema {
items: Some(Box::new(crate::registry::MetaSchemaRef::Inline(
Box::new(crate::registry::MetaSchema::new("number")),
))),
min_items: Some(4),
..crate::registry::MetaSchema::new("array")
},
));
crate::registry::MetaSchema {
title: Some(concat!("GeoJSON ", $name).to_string()),
required: vec!["type", "coordinates"],
properties: vec![
("type", String::schema_ref()),
("type", type_schema),
(
"coordinates",
<<Self as GeoJson>::Coordinates>::schema_ref(),
),
("bbox", bbox_schema),
],
..crate::registry::MetaSchema::new("object")
}
@@ -83,11 +105,87 @@ 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]>>>);
// Implementation for Geometry enum (union of all geometry types)
impl crate::types::Type for Geometry {
const IS_REQUIRED: bool = true;
type RawValueType = Self;
type RawElementValueType = Self;
fn name() -> ::std::borrow::Cow<'static, str> {
"GeoJSON_Geometry".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| {
// Register all the individual geometry types
Point::register(registry);
LineString::register(registry);
Polygon::register(registry);
MultiPoint::register(registry);
MultiLineString::register(registry);
MultiPolygon::register(registry);
crate::registry::MetaSchema {
title: Some("GeoJSON Geometry".to_string()),
one_of: vec![
Point::schema_ref(),
LineString::schema_ref(),
Polygon::schema_ref(),
MultiPoint::schema_ref(),
MultiLineString::schema_ref(),
MultiPolygon::schema_ref(),
],
..crate::registry::MetaSchema::ANY
}
})
}
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())?;
// Try to parse as a geojson::Geometry and convert to geo_types::Geometry
let geojson_geom = geojson::Geometry::try_from(value).map_err(|e| {
crate::types::ParseError::custom(format!("Invalid GeoJSON geometry: {}", e))
})?;
Self::try_from(&geojson_geom).map_err(Into::into)
}
}
impl crate::types::ToJSON for Geometry {
fn to_json(&self) -> Option<::serde_json::Value> {
// Convert to geojson::Geometry and then to JSON
let geojson_geom = geojson::Geometry::from(self);
Some(::serde_json::Map::<String, ::serde_json::Value>::from(&geojson_geom).into())
}
}
#[cfg(test)]
mod tests {
use geo_types::Point;
use geo_types::{Geometry, LineString, Point};
use crate::types::{ParseFromJSON, ToJSON};
use crate::{
registry::{MetaSchemaRef, Registry},
types::{ParseFromJSON, ToJSON, Type},
};
fn point_geo() -> Point {
Point::new(1.0, 2.0)
@@ -112,4 +210,163 @@ mod tests {
point_geo()
)
}
#[test]
fn schema_has_correct_structure() {
let mut registry = Registry::new();
Point::<f64>::register(&mut registry);
let schema_name = Point::<f64>::name();
let schema = registry.schemas.get(schema_name.as_ref()).unwrap();
// Check title
assert_eq!(schema.title, Some("GeoJSON Point".to_string()));
// Check type
assert_eq!(schema.ty, "object");
// Check required fields
assert_eq!(schema.required, vec!["type", "coordinates"]);
// Check properties
assert_eq!(schema.properties.len(), 3);
assert_eq!(schema.properties[0].0, "type");
assert_eq!(schema.properties[1].0, "coordinates");
assert_eq!(schema.properties[2].0, "bbox");
// Check type field has enum constraint
if let MetaSchemaRef::Inline(type_schema) = &schema.properties[0].1 {
assert_eq!(type_schema.ty, "string");
assert_eq!(
type_schema.enum_items,
vec![serde_json::Value::String("Point".to_string())]
);
} else {
panic!("type field should be inline schema");
}
// Check coordinates field uses minItems instead of minLength
if let MetaSchemaRef::Inline(coords_schema) = &schema.properties[1].1 {
assert_eq!(coords_schema.ty, "array");
assert_eq!(coords_schema.min_items, Some(2));
assert_eq!(coords_schema.max_items, Some(2));
assert_eq!(coords_schema.min_length, None);
assert_eq!(coords_schema.max_length, None);
} else {
panic!("coordinates field should be inline schema");
}
// Check bbox field structure
if let MetaSchemaRef::Inline(bbox_schema) = &schema.properties[2].1 {
assert_eq!(bbox_schema.ty, "array");
assert_eq!(bbox_schema.min_items, Some(4));
if let Some(items) = &bbox_schema.items {
if let MetaSchemaRef::Inline(item_schema) = items.as_ref() {
assert_eq!(item_schema.ty, "number");
} else {
panic!("bbox items should be inline schema");
}
} else {
panic!("bbox should have items");
}
} else {
panic!("bbox field should be inline schema");
}
}
#[test]
fn geometry_enum_serializes_point() {
let point: Geometry = Geometry::Point(Point::new(1.0, 2.0));
let json = point.to_json().unwrap();
assert_eq!(
json,
serde_json::json!({
"type": "Point",
"coordinates": [1.0, 2.0]
})
);
}
#[test]
fn geometry_enum_serializes_linestring() {
let linestring: Geometry =
Geometry::LineString(LineString::from(vec![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]));
let json = linestring.to_json().unwrap();
assert_eq!(
json,
serde_json::json!({
"type": "LineString",
"coordinates": [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]
})
);
}
#[test]
fn geometry_enum_deserializes_point() {
let json = serde_json::json!({
"type": "Point",
"coordinates": [1.0, 2.0]
});
let geometry = Geometry::parse_from_json(Some(json)).unwrap();
match geometry {
Geometry::Point(p) => {
assert_eq!(p.x(), 1.0);
assert_eq!(p.y(), 2.0);
}
_ => panic!("Expected Point variant"),
}
}
#[test]
fn geometry_enum_deserializes_linestring() {
let json = serde_json::json!({
"type": "LineString",
"coordinates": [[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]]
});
let geometry = Geometry::parse_from_json(Some(json)).unwrap();
match geometry {
Geometry::LineString(ls) => {
assert_eq!(ls.0.len(), 3);
}
_ => panic!("Expected LineString variant"),
}
}
#[test]
fn geometry_enum_schema_uses_oneof() {
let mut registry = Registry::new();
Geometry::register(&mut registry);
let schema_name = Geometry::name();
let schema = registry.schemas.get(schema_name.as_ref()).unwrap();
// Check title
assert_eq!(schema.title, Some("GeoJSON Geometry".to_string()));
// Check that it uses oneOf with all geometry types
assert_eq!(schema.one_of.len(), 6);
// Verify that the oneOf includes references to each geometry type
let one_of_refs: Vec<String> = schema
.one_of
.iter()
.filter_map(|ref_| match ref_ {
MetaSchemaRef::Reference(name) => Some(name.clone()),
_ => None,
})
.collect();
assert!(one_of_refs.contains(&"GeoJSON_Point".to_string()));
assert!(one_of_refs.contains(&"GeoJSON_LineString".to_string()));
assert!(one_of_refs.contains(&"GeoJSON_Polygon".to_string()));
assert!(one_of_refs.contains(&"GeoJSON_MultiPoint".to_string()));
assert!(one_of_refs.contains(&"GeoJSON_MultiLineString".to_string()));
assert!(one_of_refs.contains(&"GeoJSON_MultiPolygon".to_string()));
}
}

View File

@@ -83,9 +83,10 @@ use crate::{
/// cli.get("/get").send().await.assert_json(json!({"attr1": null, "attr2": "abc"}));
/// # });
/// ```
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash, Default)]
pub enum MaybeUndefined<T> {
/// Undefined
#[default]
Undefined,
/// Null
Null,
@@ -93,12 +94,6 @@ pub enum MaybeUndefined<T> {
Value(T),
}
impl<T> Default for MaybeUndefined<T> {
fn default() -> Self {
Self::Undefined
}
}
impl<T> From<T> for MaybeUndefined<T> {
fn from(value: T) -> Self {
MaybeUndefined::Value(value)