From daca1fd7b53e4ebb6ed84abcbd6a43e937ba3ed7 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 12 May 2025 22:30:24 +0200 Subject: [PATCH 01/31] Implement Temporal Primitive Geometry for MovingFeatures --- ogcapi-types/Cargo.toml | 1 + ogcapi-types/src/lib.rs | 4 + ogcapi-types/src/movingfeatures/json_utils.rs | 60 +++ ogcapi-types/src/movingfeatures/mod.rs | 5 + .../temporal_primitive_geometry.rs | 493 ++++++++++++++++++ ogcapi-types/src/movingfeatures/trs.rs | 35 ++ 6 files changed, 598 insertions(+) create mode 100644 ogcapi-types/src/movingfeatures/json_utils.rs create mode 100644 ogcapi-types/src/movingfeatures/mod.rs create mode 100644 ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs create mode 100644 ogcapi-types/src/movingfeatures/trs.rs diff --git a/ogcapi-types/Cargo.toml b/ogcapi-types/Cargo.toml index a2d003b..19bf3b9 100644 --- a/ogcapi-types/Cargo.toml +++ b/ogcapi-types/Cargo.toml @@ -22,6 +22,7 @@ stac = ["features"] styles = [] tiles = ["common"] coverages = [] +movingfeatures = ["common"] [dependencies] chrono = { version = "0.4.41", features = ["serde"] } diff --git a/ogcapi-types/src/lib.rs b/ogcapi-types/src/lib.rs index fd1a8da..35576fe 100644 --- a/ogcapi-types/src/lib.rs +++ b/ogcapi-types/src/lib.rs @@ -22,5 +22,9 @@ pub mod styles; #[cfg(feature = "tiles")] pub mod tiles; +/// Types specified in the `OGC API - Moving Features` standard. +#[cfg(feature = "movingfeatures")] +pub mod movingfeatures; + #[cfg(feature = "coverages")] mod coverages; diff --git a/ogcapi-types/src/movingfeatures/json_utils.rs b/ogcapi-types/src/movingfeatures/json_utils.rs new file mode 100644 index 0000000..843ee18 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/json_utils.rs @@ -0,0 +1,60 @@ +use serde_json; + +use serde::de::{self, DeserializeOwned, Error}; + +use serde::Serialize; + +use geojson::JsonValue; + +use geojson::JsonObject; + +pub(crate) fn expect_type(value: &mut JsonObject) -> Result { + let prop = expect_property(value, "type")?; + expect_string(prop) +} + +pub(crate) fn expect_vec(value: JsonValue) -> Result, serde_json::Error> { + match value { + JsonValue::Array(s) => Ok(s), + _ => Err(Error::invalid_type( + de::Unexpected::Other("type"), + &"an array", + )), + } +} + +pub(crate) fn expect_string(value: JsonValue) -> Result { + match value { + JsonValue::String(s) => Ok(s), + _ => Err(Error::invalid_type( + de::Unexpected::Other("type"), + &"a string", + )), + } +} + +pub(crate) fn expect_property( + obj: &mut JsonObject, + name: &'static str, +) -> Result { + match obj.remove(name) { + Some(v) => Ok(v), + None => Err(Error::missing_field(name)), + } +} + +pub(crate) fn expect_named_vec( + value: &mut JsonObject, + name: &'static str, +) -> Result, serde_json::Error> { + let prop = expect_property(value, name)?; + expect_vec(prop) +} + +pub(crate) fn deserialize_iter( + json_vec: Vec, +) -> impl Iterator { + json_vec + .into_iter() + .map(|v: JsonValue| serde_json::from_value(v).unwrap()) +} diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs new file mode 100644 index 0000000..d544bd8 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -0,0 +1,5 @@ +pub mod temporal_primitive_geometry; +pub mod trs; + +mod json_utils; + diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs new file mode 100644 index 0000000..9ee5cf2 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -0,0 +1,493 @@ +use chrono::{DateTime, Utc}; +use geojson::{JsonObject, JsonValue, LineStringType, PointType, PolygonType}; +use serde::{ + Deserialize, Deserializer, Serialize, Serializer, + de::{self, Error}, +}; + +use crate::common::Crs; + +use super::{json_utils, trs::Trs}; + +/// TemporalPrimitiveGeometry Object +/// +/// A [TemporalPrimitiveGeometry](https://docs.ogc.org/is/19-045r3/19-045r3.html#tprimitive) object describes the +/// movement of a geographic feature whose leaf geometry at a time instant is drawn by a primitive geometry such as a +/// point, linestring, and polygon in the two- or three-dimensional spatial coordinate system, or a point cloud in the +/// three-dimensional spatial coordinate system. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct TemporalPrimitiveGeometry { + #[serde(flatten)] + pub value: Value, + #[serde(default)] + pub interpolation: Interpolation, + // FIXME apparently specification of moving features CRS and TRS is different to common::CRS ?! + pub crs: Option, + pub trs: Option, + pub foreign_members: Option, +} + +impl TemporalPrimitiveGeometry { + pub fn new(value: Value) -> Self { + Self { + value, + interpolation: Interpolation::default(), + crs: Default::default(), + trs: Default::default(), + foreign_members: Default::default(), + } + } +} + +impl From for TemporalPrimitiveGeometry +where + V: Into, +{ + fn from(v: V) -> TemporalPrimitiveGeometry { + TemporalPrimitiveGeometry::new(v.into()) + } +} + +///The value specifies the variants of a TemporalPrimitiveGeometry object with constraints on the interpretation of the +///array value of the "coordinates" member, the same-length "datetimes" array member and the optional members "base" and +///"orientations". +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + ///The type represents a trajectory of a time-parametered 0-dimensional (0D) geometric primitive (Point), + ///representing a single position at a time position (instant) within its temporal domain. Intuitively a temporal + ///geometry of a continuous movement of point depicts a set of curves in a spatiotemporal domain. + ///It supports more complex movements of moving features, as well as linear movement like MF-JSON Trajectory. + ///For example, the non-linear movement information of people, vehicles, or hurricanes can be shared as a + ///TemporalPrimitiveGeometry object with the "MovingPoint" type. + MovingPoint(Vec<(DateTime, PointType)>), + ///The type represents the prism of a time-parametered 1-dimensional (1D) geometric primitive (LineString), whose + ///leaf geometry at a time position is a 1D linear object in a particular period. Intuitively a temporal geometry + ///of a continuous movement of curve depicts a set of surfaces in a spatiotemporal domain. For example, the + ///movement information of weather fronts or traffic congestion on roads can be shared as a + ///TemporalPrimitiveGeometry object with the "MovingLineString" type. + MovingLineString(Vec<(DateTime, LineStringType)>), + ///The type represents the prism of a time-parameterized 2-dimensional (2D) geometric primitive (Polygon), whose + ///leaf geometry at a time position is a 2D polygonal object in a particular period. The list of points are in + ///counterclockwise order. Intuitively a temporal geometry of a continuous movement of polygon depicts a set of + ///volumes in a spatiotemporal domain. For example, the changes of flooding areas or the movement information of + ///air pollution can be shared as a TemporalPrimitiveGeometry object with the "MovingPolygon" type. + MovingPolygon(Vec<(DateTime, PolygonType)>), + ///The type represents the prism of a time-parameterized point cloud whose leaf geometry at a time position is a + ///set of points in a particular period. Intuitively a temporal geometry of a continuous movement of point set + ///depicts a set of curves in a spatiotemporal domain. For example, the tacking information by using Light + ///Detection and Ranging (LiDAR) can be shared as a TemporalPrimitiveGeometry object with the "MovingPointCloud" + ///type. + MovingPointCloud(Vec<(DateTime, Vec)>), + ///The constraints on the "base" and "orientation" members are represented in the additional variant "BaseRepresentation" + ///where a 3D Model given as "Base" is moved along a trajectory of "MovingPoint" and rotated and scaled according to the + ///"orientations" member. + BaseRepresentation(Base, Vec<(DateTime, PointType, Orientations)>), +} + +///MF-JSON Prism separates out translational motion and rotational motion. The "interpolation" member is default and +///represents the translational motion of the geometry described by the "coordinates" value. Its value is a MotionCurve +///object described by one of predefined five motion curves (i.e., "Discrete", "Step", "Linear", "Quadratic", and +///"Cubic") or a URL (e.g., "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/motioncurve") +/// +///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub enum Interpolation { + ///The positions are NOT connected. The position is valid only at the time instant in datetimes + Discrete, + ///This method is realized as a jump from one position to the next at the end of a subinterval. The curve is not + ///continuous but would be useful for representing an accident or event. This interpolation requires at least two + ///positions. + Step, + ///This method is the default value of the "interpolation" member. It connects straight lines between positions. + ///The position with respect to time is constructed from linear splines that are two–positions interpolating + ///polynomials. Therefore, this interpolation also requires at least two positions. + #[default] + Linear, + ///This method interpolates the position at time t by using a piecewise quadratic spline on each interval [t_{-1},t] + ///with first-order parametric continuity. Between consecutive positions, piecewise quadratic splines are constructed + ///from the following parametric equations in terms of the time variable. This method results in a curve of a + ///temporal trajectory that is continuous and has a continuous first derivative at the positions in coordinates + ///except the two end positions. For this interpolation, at least three leaves at particular times are required. + Quadratic, + ///This method interpolates the position at time t by using a Catmull–Rom (cubic) spline on each interval [t_{-1},t]. + /// + ///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) + Cubic, + ///If applications need to define their own interpolation methods, the "interpolation" member in the + ///TemporalPrimitiveGeometry object has a URL to a JSON array of parametric equations defined on a set of intervals of parameter t-value. + /// + ///See [7.2.10.2 URLs for user-defined parametric curve](https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_10_2_urls_for_user_defined_parametric_curve) + Url(String), +} + +///The 3D model represents a base geometry of a 3D shape, and the combination of the "base" and "orientations" members +///represents a 3D temporal geometry of the MF_RigidTemporalGeometry type in ISO 19141. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Base { + ///The "type" member has a JSON string to represent a 3D File format such as STL, OBJ, PLY, and glTF. + r#type: String, + ///The "href" member has a URL to address 3D model data. + href: String, +} + +///Orientations represents rotational motion of the base representation of a member named "base" +///as a transform matrix of the base representation at each time of the elements in "datetimes". +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Orientations { + ///The "scales" member has a array value of numbers along the x, y and z axis in order as three scale factors. + scales: ScaleType, + ///the "angles" member has a JSON array value of numbers along the x, y and z axis in order as Euler angles in degree. + ///Angles are defined according to the right-hand rule; a positive value represents a rotation that appears clockwise + ///when looking in the positive direction of the axis and a negative value represents a counter-clockwise rotation. + angles: AngleType, +} +type ScaleType = Vec; +type AngleType = Vec; + +impl Value { + fn type_name(&self) -> &'static str { + match self { + Value::MovingPoint(_) => "MovingPoint", + Value::MovingLineString(_) => "MovingLineString", + Value::MovingPolygon(_) => "MovingPolygon", + Value::MovingPointCloud(_) => "MovingPointCloud", + Value::BaseRepresentation(_, _) => "MovingPoint", + } + } + + fn unzip( + &self, + ) -> ( + Vec<&DateTime>, + Vec, + Option<(&Base, Vec<&Orientations>)>, + ) { + match self { + Value::MovingPoint(x) => { + let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x + .iter() + .map(|(a, b)| (a, geojson::Value::Point(b.to_vec()))) + .unzip(); + (datetimes, coordinates, None) + } + Value::MovingLineString(x) => { + let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x + .iter() + .map(|(a, b)| (a, geojson::Value::LineString(b.to_vec()))) + .unzip(); + (datetimes, coordinates, None) + } + Value::MovingPolygon(x) => { + let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x + .iter() + .map(|(a, b)| (a, geojson::Value::Polygon(b.to_vec()))) + .unzip(); + (datetimes, coordinates, None) + } + Value::MovingPointCloud(x) => { + let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x + .iter() + .map(|(a, b)| (a, geojson::Value::MultiPoint(b.to_vec()))) + .unzip(); + (datetimes, coordinates, None) + } + Value::BaseRepresentation(base, x) => { + let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x + .iter() + .map(|(a, b, _)| (a, geojson::Value::Point(b.to_vec()))) + .unzip(); + let orientations: Vec<&Orientations> = + x.iter().map(|(_, _, orientations)| orientations).collect(); + (datetimes, coordinates, Some((base, orientations))) + } + } + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + use serde::de::Error as SerdeError; + + let mut val = JsonObject::deserialize(deserializer)?; + + Value::try_from(&mut val).map_err(|e| D::Error::custom(e.to_string())) + } +} + +impl TryFrom<&mut JsonObject> for Value { + type Error = serde_json::Error; + + fn try_from(object: &mut JsonObject) -> Result { + let res = &*json_utils::expect_type(object)?; + let coordinates = json_utils::expect_named_vec(object, "coordinates")?; + let dt = json_utils::expect_named_vec(object, "datetimes")?; + let base: Option = object + .remove("base") + .map(serde_json::from_value) + .transpose()?; + if coordinates.len() != dt.len() { + Err(serde_json::Error::invalid_length( + dt.len(), + &"coordinates and datetimes must be of same length!", + ))?; + } + let datetimes = json_utils::deserialize_iter::>(dt); + match res { + "MovingPoint" if base.is_some() => { + let orientations = json_utils::expect_named_vec(object, "orientations")?; + if coordinates.len() != orientations.len() { + Err(serde_json::Error::invalid_length( + coordinates.len(), + &"orientations, coordinates and datetimes must be of same length!", + ))?; + } + Ok(Value::BaseRepresentation( + base.unwrap(), + datetimes + .zip(json_utils::deserialize_iter::(coordinates)) + .zip(json_utils::deserialize_iter::(orientations)) + .map(|((dt, coord), orientations)| (dt, coord, orientations)) + .collect(), + )) + } + "MovingPoint" => Ok(Value::MovingPoint( + datetimes + .zip(json_utils::deserialize_iter::(coordinates)) + .collect(), + )), + "MovingLineString" => Ok(Value::MovingLineString( + datetimes + .into_iter() + .zip(json_utils::deserialize_iter::(coordinates)) + .collect(), + )), + "MovingPolygon" => Ok(Value::MovingPolygon( + datetimes + .into_iter() + .zip(json_utils::deserialize_iter::(coordinates)) + .collect(), + )), + "MovingPointCloud" => Ok(Value::MovingPointCloud( + datetimes + .into_iter() + .zip(json_utils::deserialize_iter::>(coordinates)) + .collect(), + )), + unknown_variant => Err(serde_json::Error::unknown_variant( + unknown_variant, + &[ + "MovingPoint", + "MovingLineString", + "MovingPolygon", + "MovingPointCloud", + ], + )), + } + } +} + +impl TryFrom for Value { + type Error = serde_json::Error; + + fn try_from(value: JsonValue) -> Result { + if let JsonValue::Object(mut obj) = value { + Self::try_from(&mut obj) + } else { + Err(serde_json::Error::invalid_type( + de::Unexpected::Other("type"), + &"object", + )) + } + } +} + +impl<'a> From<&'a Value> for JsonValue { + fn from(value: &'a Value) -> JsonValue { + serde_json::to_value(value).unwrap() + } +} + +impl<'a> From<&'a Value> for JsonObject { + fn from(value: &'a Value) -> JsonObject { + let (datetimes, coordinates, base_rep) = value.unzip(); + let mut map = JsonObject::new(); + map.insert( + String::from("type"), + // The unwrap() should never panic, because &str always serializes to JSON + serde_json::to_value(value.type_name()).unwrap(), + ); + map.insert( + String::from("coordinates"), + // The unwrap() should never panic, because coordinates contains only JSON-serializable types + serde_json::to_value(coordinates).unwrap(), + ); + map.insert( + String::from("datetimes"), + // The unwrap() should never panic, because Value contains only JSON-serializable types + serde_json::to_value(datetimes).unwrap(), + ); + if let Some(base_rep) = base_rep { + map.insert( + String::from("base"), + // The unwrap() should never panic, because Base contains only JSON-serializable types + serde_json::to_value(base_rep.0).unwrap(), + ); + map.insert( + String::from("orientations"), + // The unwrap() should never panic, because Orientations contains only JSON-serializable types + serde_json::to_value(base_rep.1).unwrap(), + ); + } + map + } +} + +impl Serialize for Value { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + let json_object: JsonObject = self.into(); + json_object.serialize(serializer) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn from_json_object() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let moving_point = Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()); + let mut jo: JsonObject = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(moving_point, Value::try_from(&mut jo).unwrap()); + } + + #[test] + fn from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let moving_point = Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()); + let jv: JsonValue = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(moving_point, Value::try_from(jv).unwrap()); + } + + #[test] + fn moving_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalPrimitiveGeometry = + Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()).into(); + let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } + + #[test] + fn invalid_moving_geometry_from_json_value() { + let geometry_too_few_datetimes: Result = + serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z"] + }"#, + ); + assert!(geometry_too_few_datetimes.is_err()); + + let geometry_too_few_coordinates: Result = + serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ); + assert!(geometry_too_few_coordinates.is_err()) + } + + #[test] + fn moving_base_rep_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + let orientations = vec![ + Orientations { + scales: vec![1.0, 1.0, 1.0], + angles: vec![0.0, 0.0, 0.0], + }, + Orientations { + scales: vec![1.0, 1.0, 1.0], + angles: vec![0.0, 355.0, 0.0], + }, + Orientations { + scales: vec![1.0, 1.0, 1.0], + angles: vec![0.0, 0.0, 330.0], + }, + ]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalPrimitiveGeometry = + Value::BaseRepresentation( + Base{r#type: "glTF".to_string(), href: "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf".to_string()}, + datetimes.into_iter().zip(coordinates).zip(orientations).map(|((a,b), c)| (a,b,c)).collect()).into(); + let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"], + "interpolation": "Linear", + "base": { + "type": "glTF", + "href": "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf" + }, + "orientations": [ + {"scales":[1,1,1], "angles":[0,0,0]}, + {"scales":[1,1,1], "angles":[0,355,0]}, + {"scales":[1,1,1], "angles":[0,0,330]} + ] + }"# + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } +} diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs new file mode 100644 index 0000000..0732b50 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; + +// TODO enforce variants linkedTRS vs namedCRS + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct Trs { + r#type: String, + properties: TrsProperties, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TrsProperties { + r#type: Option, + name: Option, + href: Option, +} + +impl Default for Trs { + fn default() -> Self { + Self { + r#type: "Name".to_string(), + properties: Default::default(), + } + } +} + +impl Default for TrsProperties { + fn default() -> Self { + Self { + r#type: None, + name: Some("urn:ogc:data:time:iso8601".to_string()), + href: None, + } + } +} From 4f7d540271dcd6b7604a191aaa024089d1d272ca Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sat, 17 May 2025 22:36:54 +0200 Subject: [PATCH 02/31] Implement CRS and TRS for Moving Features --- ogcapi-types/src/movingfeatures/crs.rs | 110 ++++++++++++++++++ ogcapi-types/src/movingfeatures/mod.rs | 1 + .../temporal_primitive_geometry.rs | 5 +- ogcapi-types/src/movingfeatures/trs.rs | 55 ++++++--- 4 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 ogcapi-types/src/movingfeatures/crs.rs diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs new file mode 100644 index 0000000..afe446a --- /dev/null +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +use crate::common; + +/// MF-JSON uses a CRS as described in in (GeoJSON:2008)[https://geojson.org/geojson-spec#coordinate-reference-system-objects] +/// See (7.2.3 CoordinateReferenceSystem Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#crs] +/// See (6. Overview of Moving features JSON Encodings)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_overview_of_moving_features_json_encodings_informative] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(tag = "type")] +pub enum Crs { + Name { properties: NamedCrs }, + Link { properties: LinkedCrs }, +} + +/// A Named CRS object indicates a coordinate reference system by name. In this case, the value of its "type" member +/// is the string "Name". The value of its "properties" member is a JSON object containing a "name" member whose +/// value is a string identifying a coordinate reference system (not JSON null value). The value of "href" and "type" +/// is a JSON null value. This standard recommends an EPSG[3] code as the value of "name", such as "EPSG::4326." +/// +/// See (7.2.3.1 Named CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct NamedCrs { + name: String, +} +/// A linked CRS object has one required member "href" and one optional member "type". The value of the required "href" +/// member is a dereferenceable URI. The value of the optional "type" member is a string that hints at the format used +/// to represent CRS parameters at the provided URI. Suggested values are: "Proj4", "OGCWKT", "ESRIWKT", but others can +/// be used. +/// +/// See (7.2.3.2. Linked CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_2_linked_crs] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct LinkedCrs { + r#type: Option, + href: String, +} + +impl Default for Crs { + fn default() -> Self { + Self::Name { + properties: Default::default(), + } + } +} + +impl Default for NamedCrs { + fn default() -> Self { + Self { + name: "urn:ogc:def:crs:OGC:1.3:CRS84".to_string(), + } + } +} + +impl TryFrom for common::Crs { + type Error = String; + + fn try_from(value: Crs) -> Result { + match value { + // TODO this might not work for names like "EPSG:4326" + Crs::Name { properties } => Self::from_str(properties.name.as_str()), + Crs::Link { properties } => Self::from_str(properties.href.as_str()), + } + } +} + +impl From for Crs { + fn from(value: common::Crs) -> Self { + Self::Name { + properties: NamedCrs { + name: value.to_urn(), + }, + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_json() { + // TODO this contradicts example from https://developer.ogc.org/api/movingfeatures/index.html#tag/MovingFeatures/operation/retrieveMovingFeatures + // Example from https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs + let trs: Crs = serde_json::from_str( + r#" + { + "type": "Name", + "properties": { + "name": "urn:ogc:def:crs:OGC:1.3:CRS84" + } + } + "#, + ) + .expect("Failed to parse Crs"); + let expected_trs = Crs::default(); + assert_eq!(trs, expected_trs); + } + + #[test] + fn into_common_crs(){ + // assert_eq!(common::Crs::try_from(Crs::default()).unwrap(), common::Crs::default()); + assert_eq!(common::Crs::default(), Crs::default().try_into().unwrap()); + + // assert_eq!(Crs::from(common::Crs::default()), Crs::default()); + assert_eq!(Crs::default(), common::Crs::default().into()); + } + + +} diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index d544bd8..a2b349b 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -1,5 +1,6 @@ pub mod temporal_primitive_geometry; pub mod trs; +mod crs; mod json_utils; diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 9ee5cf2..0337b4b 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -5,9 +5,7 @@ use serde::{ de::{self, Error}, }; -use crate::common::Crs; - -use super::{json_utils, trs::Trs}; +use super::{json_utils, trs::Trs, crs::Crs}; /// TemporalPrimitiveGeometry Object /// @@ -21,7 +19,6 @@ pub struct TemporalPrimitiveGeometry { pub value: Value, #[serde(default)] pub interpolation: Interpolation, - // FIXME apparently specification of moving features CRS and TRS is different to common::CRS ?! pub crs: Option, pub trs: Option, pub foreign_members: Option, diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs index 0732b50..f4c2561 100644 --- a/ogcapi-types/src/movingfeatures/trs.rs +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -1,35 +1,56 @@ use serde::{Deserialize, Serialize}; -// TODO enforce variants linkedTRS vs namedCRS +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(tag = "type")] +pub enum Trs { + Name { properties: NamedTrs }, // r#type: String, + Link { properties: LinkedTrs }, // r#type: String, + // properties: TrsProperties, +} -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct Trs { - r#type: String, - properties: TrsProperties, +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct NamedTrs{ + name: String, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TrsProperties { +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct LinkedTrs{ r#type: Option, - name: Option, - href: Option, + href: String } impl Default for Trs { fn default() -> Self { - Self { - r#type: "Name".to_string(), - properties: Default::default(), - } + Self::Name { properties: Default::default() } } } -impl Default for TrsProperties { +impl Default for NamedTrs { fn default() -> Self { Self { - r#type: None, - name: Some("urn:ogc:data:time:iso8601".to_string()), - href: None, + name: "urn:ogc:data:time:iso8601".to_string(), } } } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_json(){ + + // TODO this contradicts example from https://developer.ogc.org/api/movingfeatures/index.html#tag/MovingFeatures/operation/retrieveMovingFeatures + // Example from https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs + let trs: Trs = serde_json::from_str(r#" + { + "type": "Name", + "properties": {"name": "urn:ogc:data:time:iso8601"} + } + "#).expect("Failed to parse Trs"); + let expected_trs = Trs::default(); + assert_eq!(trs, expected_trs); + + } +} From 00ecbf5225664aae672013050dfd92b3c82477d9 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 18 May 2025 21:05:34 +0200 Subject: [PATCH 03/31] Use serde(try_from ...) instead of custom deserialization for temporal primitive geometry --- ogcapi-types/src/movingfeatures/json_utils.rs | 60 --- ogcapi-types/src/movingfeatures/mod.rs | 5 +- .../temporal_primitive_geometry.rs | 361 +++++------------- 3 files changed, 99 insertions(+), 327 deletions(-) delete mode 100644 ogcapi-types/src/movingfeatures/json_utils.rs diff --git a/ogcapi-types/src/movingfeatures/json_utils.rs b/ogcapi-types/src/movingfeatures/json_utils.rs deleted file mode 100644 index 843ee18..0000000 --- a/ogcapi-types/src/movingfeatures/json_utils.rs +++ /dev/null @@ -1,60 +0,0 @@ -use serde_json; - -use serde::de::{self, DeserializeOwned, Error}; - -use serde::Serialize; - -use geojson::JsonValue; - -use geojson::JsonObject; - -pub(crate) fn expect_type(value: &mut JsonObject) -> Result { - let prop = expect_property(value, "type")?; - expect_string(prop) -} - -pub(crate) fn expect_vec(value: JsonValue) -> Result, serde_json::Error> { - match value { - JsonValue::Array(s) => Ok(s), - _ => Err(Error::invalid_type( - de::Unexpected::Other("type"), - &"an array", - )), - } -} - -pub(crate) fn expect_string(value: JsonValue) -> Result { - match value { - JsonValue::String(s) => Ok(s), - _ => Err(Error::invalid_type( - de::Unexpected::Other("type"), - &"a string", - )), - } -} - -pub(crate) fn expect_property( - obj: &mut JsonObject, - name: &'static str, -) -> Result { - match obj.remove(name) { - Some(v) => Ok(v), - None => Err(Error::missing_field(name)), - } -} - -pub(crate) fn expect_named_vec( - value: &mut JsonObject, - name: &'static str, -) -> Result, serde_json::Error> { - let prop = expect_property(value, name)?; - expect_vec(prop) -} - -pub(crate) fn deserialize_iter( - json_vec: Vec, -) -> impl Iterator { - json_vec - .into_iter() - .map(|v: JsonValue| serde_json::from_value(v).unwrap()) -} diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index a2b349b..d2c4dd0 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -1,6 +1,3 @@ +mod crs; pub mod temporal_primitive_geometry; pub mod trs; -mod crs; - -mod json_utils; - diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 0337b4b..2d34448 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -1,11 +1,8 @@ use chrono::{DateTime, Utc}; -use geojson::{JsonObject, JsonValue, LineStringType, PointType, PolygonType}; -use serde::{ - Deserialize, Deserializer, Serialize, Serializer, - de::{self, Error}, -}; +use geojson::{JsonObject, LineStringType, PointType, PolygonType}; +use serde::{Deserialize, Serialize}; -use super::{json_utils, trs::Trs, crs::Crs}; +use super::{crs::Crs, trs::Trs}; /// TemporalPrimitiveGeometry Object /// @@ -48,7 +45,8 @@ where ///The value specifies the variants of a TemporalPrimitiveGeometry object with constraints on the interpretation of the ///array value of the "coordinates" member, the same-length "datetimes" array member and the optional members "base" and ///"orientations". -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] pub enum Value { ///The type represents a trajectory of a time-parametered 0-dimensional (0D) geometric primitive (Point), ///representing a single position at a time position (instant) within its temporal domain. Intuitively a temporal @@ -56,29 +54,77 @@ pub enum Value { ///It supports more complex movements of moving features, as well as linear movement like MF-JSON Trajectory. ///For example, the non-linear movement information of people, vehicles, or hurricanes can be shared as a ///TemporalPrimitiveGeometry object with the "MovingPoint" type. - MovingPoint(Vec<(DateTime, PointType)>), + MovingPoint { + #[serde(flatten)] + dt_coords: SameLengthDateTimeCoordinatesVecs, PointType>, + #[serde(flatten)] + base_representation: Option, + }, ///The type represents the prism of a time-parametered 1-dimensional (1D) geometric primitive (LineString), whose ///leaf geometry at a time position is a 1D linear object in a particular period. Intuitively a temporal geometry ///of a continuous movement of curve depicts a set of surfaces in a spatiotemporal domain. For example, the ///movement information of weather fronts or traffic congestion on roads can be shared as a ///TemporalPrimitiveGeometry object with the "MovingLineString" type. - MovingLineString(Vec<(DateTime, LineStringType)>), + MovingLineString { + #[serde(flatten)] + dt_coords: SameLengthDateTimeCoordinatesVecs, LineStringType>, + }, ///The type represents the prism of a time-parameterized 2-dimensional (2D) geometric primitive (Polygon), whose ///leaf geometry at a time position is a 2D polygonal object in a particular period. The list of points are in ///counterclockwise order. Intuitively a temporal geometry of a continuous movement of polygon depicts a set of ///volumes in a spatiotemporal domain. For example, the changes of flooding areas or the movement information of ///air pollution can be shared as a TemporalPrimitiveGeometry object with the "MovingPolygon" type. - MovingPolygon(Vec<(DateTime, PolygonType)>), + MovingPolygon { + #[serde(flatten)] + dt_coords: SameLengthDateTimeCoordinatesVecs, PolygonType>, + }, ///The type represents the prism of a time-parameterized point cloud whose leaf geometry at a time position is a ///set of points in a particular period. Intuitively a temporal geometry of a continuous movement of point set ///depicts a set of curves in a spatiotemporal domain. For example, the tacking information by using Light ///Detection and Ranging (LiDAR) can be shared as a TemporalPrimitiveGeometry object with the "MovingPointCloud" ///type. - MovingPointCloud(Vec<(DateTime, Vec)>), - ///The constraints on the "base" and "orientation" members are represented in the additional variant "BaseRepresentation" - ///where a 3D Model given as "Base" is moved along a trajectory of "MovingPoint" and rotated and scaled according to the - ///"orientations" member. - BaseRepresentation(Base, Vec<(DateTime, PointType, Orientations)>), + MovingPointCloud { + #[serde(flatten)] + dt_coords: SameLengthDateTimeCoordinatesVecs, Vec>, + }, +} + +#[derive(Deserialize)] +struct SameLengthDateTimeCoordinatesVecsUnchecked { + datetimes: Vec, + coordinates: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[serde(try_from = "SameLengthDateTimeCoordinatesVecsUnchecked")] +pub struct SameLengthDateTimeCoordinatesVecs { + datetimes: Vec, + coordinates: Vec, +} + +impl SameLengthDateTimeCoordinatesVecs { + fn try_new(datetimes: Vec, coordinates: Vec) -> Result { + if coordinates.len() != datetimes.len() { + Err("coordinates and datetimes must be of same length!".to_string()) + } else { + Ok(Self { + datetimes, + coordinates, + }) + } + } +} + +impl TryFrom> + for SameLengthDateTimeCoordinatesVecs +{ + type Error = String; + + fn try_from( + value: SameLengthDateTimeCoordinatesVecsUnchecked, + ) -> Result { + Self::try_new(value.datetimes, value.coordinates) + } } ///MF-JSON Prism separates out translational motion and rotational motion. The "interpolation" member is default and @@ -110,14 +156,20 @@ pub enum Interpolation { /// ///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) Cubic, - ///If applications need to define their own interpolation methods, the "interpolation" member in the + ///If applications need to define their own interpolation methods, the "interpolation" member in the ///TemporalPrimitiveGeometry object has a URL to a JSON array of parametric equations defined on a set of intervals of parameter t-value. /// ///See [7.2.10.2 URLs for user-defined parametric curve](https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_10_2_urls_for_user_defined_parametric_curve) Url(String), } -///The 3D model represents a base geometry of a 3D shape, and the combination of the "base" and "orientations" members +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BaseRepresentation { + base: Base, + orientations: Vec, +} + +///The 3D model represents a base geometry of a 3D shape, and the combination of the "base" and "orientations" members ///represents a 3D temporal geometry of the MF_RigidTemporalGeometry type in ISO 19141. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Base { @@ -130,228 +182,17 @@ pub struct Base { ///Orientations represents rotational motion of the base representation of a member named "base" ///as a transform matrix of the base representation at each time of the elements in "datetimes". #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct Orientations { +pub struct Orientation { ///The "scales" member has a array value of numbers along the x, y and z axis in order as three scale factors. scales: ScaleType, - ///the "angles" member has a JSON array value of numbers along the x, y and z axis in order as Euler angles in degree. - ///Angles are defined according to the right-hand rule; a positive value represents a rotation that appears clockwise + ///the "angles" member has a JSON array value of numbers along the x, y and z axis in order as Euler angles in degree. + ///Angles are defined according to the right-hand rule; a positive value represents a rotation that appears clockwise ///when looking in the positive direction of the axis and a negative value represents a counter-clockwise rotation. angles: AngleType, } type ScaleType = Vec; type AngleType = Vec; -impl Value { - fn type_name(&self) -> &'static str { - match self { - Value::MovingPoint(_) => "MovingPoint", - Value::MovingLineString(_) => "MovingLineString", - Value::MovingPolygon(_) => "MovingPolygon", - Value::MovingPointCloud(_) => "MovingPointCloud", - Value::BaseRepresentation(_, _) => "MovingPoint", - } - } - - fn unzip( - &self, - ) -> ( - Vec<&DateTime>, - Vec, - Option<(&Base, Vec<&Orientations>)>, - ) { - match self { - Value::MovingPoint(x) => { - let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x - .iter() - .map(|(a, b)| (a, geojson::Value::Point(b.to_vec()))) - .unzip(); - (datetimes, coordinates, None) - } - Value::MovingLineString(x) => { - let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x - .iter() - .map(|(a, b)| (a, geojson::Value::LineString(b.to_vec()))) - .unzip(); - (datetimes, coordinates, None) - } - Value::MovingPolygon(x) => { - let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x - .iter() - .map(|(a, b)| (a, geojson::Value::Polygon(b.to_vec()))) - .unzip(); - (datetimes, coordinates, None) - } - Value::MovingPointCloud(x) => { - let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x - .iter() - .map(|(a, b)| (a, geojson::Value::MultiPoint(b.to_vec()))) - .unzip(); - (datetimes, coordinates, None) - } - Value::BaseRepresentation(base, x) => { - let (datetimes, coordinates): (Vec<&DateTime>, Vec) = x - .iter() - .map(|(a, b, _)| (a, geojson::Value::Point(b.to_vec()))) - .unzip(); - let orientations: Vec<&Orientations> = - x.iter().map(|(_, _, orientations)| orientations).collect(); - (datetimes, coordinates, Some((base, orientations))) - } - } - } -} - -impl<'de> Deserialize<'de> for Value { - fn deserialize(deserializer: D) -> std::result::Result - where - D: Deserializer<'de>, - { - use serde::de::Error as SerdeError; - - let mut val = JsonObject::deserialize(deserializer)?; - - Value::try_from(&mut val).map_err(|e| D::Error::custom(e.to_string())) - } -} - -impl TryFrom<&mut JsonObject> for Value { - type Error = serde_json::Error; - - fn try_from(object: &mut JsonObject) -> Result { - let res = &*json_utils::expect_type(object)?; - let coordinates = json_utils::expect_named_vec(object, "coordinates")?; - let dt = json_utils::expect_named_vec(object, "datetimes")?; - let base: Option = object - .remove("base") - .map(serde_json::from_value) - .transpose()?; - if coordinates.len() != dt.len() { - Err(serde_json::Error::invalid_length( - dt.len(), - &"coordinates and datetimes must be of same length!", - ))?; - } - let datetimes = json_utils::deserialize_iter::>(dt); - match res { - "MovingPoint" if base.is_some() => { - let orientations = json_utils::expect_named_vec(object, "orientations")?; - if coordinates.len() != orientations.len() { - Err(serde_json::Error::invalid_length( - coordinates.len(), - &"orientations, coordinates and datetimes must be of same length!", - ))?; - } - Ok(Value::BaseRepresentation( - base.unwrap(), - datetimes - .zip(json_utils::deserialize_iter::(coordinates)) - .zip(json_utils::deserialize_iter::(orientations)) - .map(|((dt, coord), orientations)| (dt, coord, orientations)) - .collect(), - )) - } - "MovingPoint" => Ok(Value::MovingPoint( - datetimes - .zip(json_utils::deserialize_iter::(coordinates)) - .collect(), - )), - "MovingLineString" => Ok(Value::MovingLineString( - datetimes - .into_iter() - .zip(json_utils::deserialize_iter::(coordinates)) - .collect(), - )), - "MovingPolygon" => Ok(Value::MovingPolygon( - datetimes - .into_iter() - .zip(json_utils::deserialize_iter::(coordinates)) - .collect(), - )), - "MovingPointCloud" => Ok(Value::MovingPointCloud( - datetimes - .into_iter() - .zip(json_utils::deserialize_iter::>(coordinates)) - .collect(), - )), - unknown_variant => Err(serde_json::Error::unknown_variant( - unknown_variant, - &[ - "MovingPoint", - "MovingLineString", - "MovingPolygon", - "MovingPointCloud", - ], - )), - } - } -} - -impl TryFrom for Value { - type Error = serde_json::Error; - - fn try_from(value: JsonValue) -> Result { - if let JsonValue::Object(mut obj) = value { - Self::try_from(&mut obj) - } else { - Err(serde_json::Error::invalid_type( - de::Unexpected::Other("type"), - &"object", - )) - } - } -} - -impl<'a> From<&'a Value> for JsonValue { - fn from(value: &'a Value) -> JsonValue { - serde_json::to_value(value).unwrap() - } -} - -impl<'a> From<&'a Value> for JsonObject { - fn from(value: &'a Value) -> JsonObject { - let (datetimes, coordinates, base_rep) = value.unzip(); - let mut map = JsonObject::new(); - map.insert( - String::from("type"), - // The unwrap() should never panic, because &str always serializes to JSON - serde_json::to_value(value.type_name()).unwrap(), - ); - map.insert( - String::from("coordinates"), - // The unwrap() should never panic, because coordinates contains only JSON-serializable types - serde_json::to_value(coordinates).unwrap(), - ); - map.insert( - String::from("datetimes"), - // The unwrap() should never panic, because Value contains only JSON-serializable types - serde_json::to_value(datetimes).unwrap(), - ); - if let Some(base_rep) = base_rep { - map.insert( - String::from("base"), - // The unwrap() should never panic, because Base contains only JSON-serializable types - serde_json::to_value(base_rep.0).unwrap(), - ); - map.insert( - String::from("orientations"), - // The unwrap() should never panic, because Orientations contains only JSON-serializable types - serde_json::to_value(base_rep.1).unwrap(), - ); - } - map - } -} - -impl Serialize for Value { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let json_object: JsonObject = self.into(); - json_object.serialize(serializer) - } -} - #[cfg(test)] mod tests { @@ -365,8 +206,11 @@ mod tests { coordinates.push(vec![0., i as f64]); datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); } - let moving_point = Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()); - let mut jo: JsonObject = serde_json::from_str( + let moving_point = Value::MovingPoint { + dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates).unwrap(), + base_representation: None, + }; + let jo: JsonObject = serde_json::from_str( r#"{ "type": "MovingPoint", "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], @@ -374,27 +218,7 @@ mod tests { }"#, ) .unwrap(); - assert_eq!(moving_point, Value::try_from(&mut jo).unwrap()); - } - - #[test] - fn from_json_value() { - let mut coordinates = vec![]; - let mut datetimes = vec![]; - for i in 0..3 { - coordinates.push(vec![0., i as f64]); - datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); - } - let moving_point = Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()); - let jv: JsonValue = serde_json::from_str( - r#"{ - "type": "MovingPoint", - "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], - "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] - }"#, - ) - .unwrap(); - assert_eq!(moving_point, Value::try_from(jv).unwrap()); + assert_eq!(moving_point, serde_json::from_value(jo.into()).unwrap()); } #[test] @@ -406,7 +230,11 @@ mod tests { datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); } let geometry: TemporalPrimitiveGeometry = - Value::MovingPoint(datetimes.into_iter().zip(coordinates).collect()).into(); + TemporalPrimitiveGeometry::new(Value::MovingPoint { + dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates) + .unwrap(), + base_representation: None, + }); let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( r#"{ "type": "MovingPoint", @@ -446,15 +274,15 @@ mod tests { let mut coordinates = vec![]; let mut datetimes = vec![]; let orientations = vec![ - Orientations { + Orientation { scales: vec![1.0, 1.0, 1.0], angles: vec![0.0, 0.0, 0.0], }, - Orientations { + Orientation { scales: vec![1.0, 1.0, 1.0], angles: vec![0.0, 355.0, 0.0], }, - Orientations { + Orientation { scales: vec![1.0, 1.0, 1.0], angles: vec![0.0, 0.0, 330.0], }, @@ -463,12 +291,19 @@ mod tests { coordinates.push(vec![0., i as f64]); datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); } - let geometry: TemporalPrimitiveGeometry = - Value::BaseRepresentation( - Base{r#type: "glTF".to_string(), href: "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf".to_string()}, - datetimes.into_iter().zip(coordinates).zip(orientations).map(|((a,b), c)| (a,b,c)).collect()).into(); + let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new( + Value::MovingPoint{ + dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates).unwrap(), + base_representation: Some(BaseRepresentation{ + base: Base{ + r#type: "glTF".to_string(), + href: "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf".to_string() + }, + orientations + }), + }); let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( - r#"{ + r#"{ "type": "MovingPoint", "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"], From 4a0c5de9debb35cd6fed4d97d482f9806b64095a Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 18 May 2025 22:02:10 +0200 Subject: [PATCH 04/31] Inline AnglesType and ScaleType in Orientation --- .../temporal_primitive_geometry.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 2d34448..613bc3f 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -184,15 +184,12 @@ pub struct Base { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Orientation { ///The "scales" member has a array value of numbers along the x, y and z axis in order as three scale factors. - scales: ScaleType, + scales: [f64; 3], ///the "angles" member has a JSON array value of numbers along the x, y and z axis in order as Euler angles in degree. ///Angles are defined according to the right-hand rule; a positive value represents a rotation that appears clockwise ///when looking in the positive direction of the axis and a negative value represents a counter-clockwise rotation. - angles: AngleType, + angles: [f64; 3], } -type ScaleType = Vec; -type AngleType = Vec; - #[cfg(test)] mod tests { @@ -275,16 +272,16 @@ mod tests { let mut datetimes = vec![]; let orientations = vec![ Orientation { - scales: vec![1.0, 1.0, 1.0], - angles: vec![0.0, 0.0, 0.0], + scales: [1.0, 1.0, 1.0], + angles: [0.0, 0.0, 0.0], }, Orientation { - scales: vec![1.0, 1.0, 1.0], - angles: vec![0.0, 355.0, 0.0], + scales: [1.0, 1.0, 1.0], + angles: [0.0, 355.0, 0.0], }, Orientation { - scales: vec![1.0, 1.0, 1.0], - angles: vec![0.0, 0.0, 330.0], + scales: [1.0, 1.0, 1.0], + angles: [0.0, 0.0, 330.0], }, ]; for i in 0..3 { From 9719dd4bcd14f29561b3aed34290e6c1bd7eba08 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 19 May 2025 17:56:12 +0200 Subject: [PATCH 05/31] Used adjacent tags for Trs and Crs --- ogcapi-types/src/movingfeatures/crs.rs | 63 +++++++++----------------- ogcapi-types/src/movingfeatures/trs.rs | 27 ++--------- 2 files changed, 26 insertions(+), 64 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs index afe446a..1f3310c 100644 --- a/ogcapi-types/src/movingfeatures/crs.rs +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -7,45 +7,30 @@ use crate::common; /// See (7.2.3 CoordinateReferenceSystem Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#crs] /// See (6. Overview of Moving features JSON Encodings)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_overview_of_moving_features_json_encodings_informative] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(tag = "type")] +#[serde(tag = "type", content = "properties")] pub enum Crs { - Name { properties: NamedCrs }, - Link { properties: LinkedCrs }, -} - -/// A Named CRS object indicates a coordinate reference system by name. In this case, the value of its "type" member -/// is the string "Name". The value of its "properties" member is a JSON object containing a "name" member whose -/// value is a string identifying a coordinate reference system (not JSON null value). The value of "href" and "type" -/// is a JSON null value. This standard recommends an EPSG[3] code as the value of "name", such as "EPSG::4326." -/// -/// See (7.2.3.1 Named CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct NamedCrs { - name: String, -} -/// A linked CRS object has one required member "href" and one optional member "type". The value of the required "href" -/// member is a dereferenceable URI. The value of the optional "type" member is a string that hints at the format used -/// to represent CRS parameters at the provided URI. Suggested values are: "Proj4", "OGCWKT", "ESRIWKT", but others can -/// be used. -/// -/// See (7.2.3.2. Linked CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_2_linked_crs] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct LinkedCrs { - r#type: Option, - href: String, + /// A Named CRS object indicates a coordinate reference system by name. In this case, the value of its "type" member + /// is the string "Name". The value of its "properties" member is a JSON object containing a "name" member whose + /// value is a string identifying a coordinate reference system (not JSON null value). The value of "href" and "type" + /// is a JSON null value. This standard recommends an EPSG[3] code as the value of "name", such as "EPSG::4326." + /// + /// See (7.2.3.1 Named CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs] + Name { name: String }, + /// A linked CRS object has one required member "href" and one optional member "type". The value of the required "href" + /// member is a dereferenceable URI. The value of the optional "type" member is a string that hints at the format used + /// to represent CRS parameters at the provided URI. Suggested values are: "Proj4", "OGCWKT", "ESRIWKT", but others can + /// be used. + /// + /// See (7.2.3.2. Linked CRS)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_2_linked_crs] + Link { + r#type: Option, + href: String, + }, } impl Default for Crs { fn default() -> Self { Self::Name { - properties: Default::default(), - } - } -} - -impl Default for NamedCrs { - fn default() -> Self { - Self { name: "urn:ogc:def:crs:OGC:1.3:CRS84".to_string(), } } @@ -57,8 +42,8 @@ impl TryFrom for common::Crs { fn try_from(value: Crs) -> Result { match value { // TODO this might not work for names like "EPSG:4326" - Crs::Name { properties } => Self::from_str(properties.name.as_str()), - Crs::Link { properties } => Self::from_str(properties.href.as_str()), + Crs::Name { name } => Self::from_str(name.as_str()), + Crs::Link { href, .. } => Self::from_str(href.as_str()), } } } @@ -66,9 +51,7 @@ impl TryFrom for common::Crs { impl From for Crs { fn from(value: common::Crs) -> Self { Self::Name { - properties: NamedCrs { - name: value.to_urn(), - }, + name: value.to_urn(), } } } @@ -98,13 +81,11 @@ mod tests { } #[test] - fn into_common_crs(){ + fn into_common_crs() { // assert_eq!(common::Crs::try_from(Crs::default()).unwrap(), common::Crs::default()); assert_eq!(common::Crs::default(), Crs::default().try_into().unwrap()); // assert_eq!(Crs::from(common::Crs::default()), Crs::default()); assert_eq!(Crs::default(), common::Crs::default().into()); } - - } diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs index f4c2561..1658518 100644 --- a/ogcapi-types/src/movingfeatures/trs.rs +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -1,35 +1,16 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(tag = "type")] +#[serde(tag = "type", content = "properties")] pub enum Trs { - Name { properties: NamedTrs }, // r#type: String, - Link { properties: LinkedTrs }, // r#type: String, + Name { name: String }, // r#type: String, + Link { r#type: Option, href: String }, // r#type: String, // properties: TrsProperties, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct NamedTrs{ - name: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct LinkedTrs{ - r#type: Option, - href: String -} - impl Default for Trs { fn default() -> Self { - Self::Name { properties: Default::default() } - } -} - -impl Default for NamedTrs { - fn default() -> Self { - Self { - name: "urn:ogc:data:time:iso8601".to_string(), - } + Self::Name { name: "urn:ogc:data:time:iso8601".to_string() } } } From 33a09b287748a642af348503f25ddd1380964071 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 19 May 2025 21:08:52 +0200 Subject: [PATCH 06/31] Implement TemporalGeometry and TemporalComplexGeometry --- ogcapi-types/src/movingfeatures/mod.rs | 2 + .../temporal_complex_geometry.rs | 56 +++++++++++++ .../src/movingfeatures/temporal_geometry.rs | 78 +++++++++++++++++++ .../temporal_primitive_geometry.rs | 8 ++ 4 files changed, 144 insertions(+) create mode 100644 ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs create mode 100644 ogcapi-types/src/movingfeatures/temporal_geometry.rs diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index d2c4dd0..6ffe445 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -1,3 +1,5 @@ mod crs; +pub mod temporal_geometry; pub mod temporal_primitive_geometry; +pub mod temporal_complex_geometry; pub mod trs; diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs new file mode 100644 index 0000000..248756f --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -0,0 +1,56 @@ +use serde::{Deserialize, Serialize}; + +use super::{crs::Crs, temporal_primitive_geometry::TemporalPrimitiveGeometry, trs::Trs}; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +pub enum Type { + #[default] + MovingGeometryCollection, +} + +/// A TemporalComplexGeometry object represents a set of TemporalPrimitiveGeometry objects. When a TemporalGeometry +/// object has a "type" member is "MovingGeometryCollection", the object is specialized as a TemporalComplexGeometry +/// object with one additional mandatory member named "prisms". The value of the "prisms" member is represented by a +/// JSON array of a set of TemporalPrimitiveGeometry instances having at least one element in the array. +/// +/// See (7.2.1.2. TemporalComplexGeometry Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct TemporalComplexGeometry{ + r#type: Type, + prisms: Prisms, + crs: Option, + trs: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(try_from = "PrismsUnchecked")] +pub struct Prisms(Vec); + +#[derive(Deserialize, Clone, Debug, PartialEq)] +struct PrismsUnchecked(Vec); + +impl TryFrom> for TemporalComplexGeometry{ + type Error = &'static str; + + fn try_from(value: Vec) -> Result { + Ok(Self{ + r#type: Default::default(), + prisms: PrismsUnchecked(value).try_into()?, + crs: Default::default(), + trs: Default::default(), + }) + } +} + +impl TryFrom for Prisms { + type Error = &'static str; + + fn try_from(value: PrismsUnchecked) -> Result { + if !value.0.is_empty(){ + Ok(Prisms(value.0)) + }else{ + Err("Prisms must have at least one value") + } + } +} + diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs new file mode 100644 index 0000000..972a979 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; + +use super::{ + temporal_complex_geometry::TemporalComplexGeometry, + temporal_primitive_geometry::TemporalPrimitiveGeometry, +}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(untagged)] +enum TemporalGeometry { + Primitive(TemporalPrimitiveGeometry), + Complex(TemporalComplexGeometry), +} + +#[cfg(test)] +mod tests { + + use chrono::DateTime; + + use super::*; + + #[test] + fn moving_complex_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let primitive_geometry = + TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()); + let geometry = TemporalGeometry::Complex(TemporalComplexGeometry::try_from(vec![ + primitive_geometry.clone(), + primitive_geometry, + ]).unwrap()); + let deserialized_geometry: TemporalGeometry = serde_json::from_str( + r#"{ + "type": "MovingGeometryCollection", + "prisms": [ + { + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }, + { + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + } + ] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } + + #[test] + fn moving_primitive_geometry_from_json_value() { + let mut coordinates = vec![]; + let mut datetimes = vec![]; + for i in 0..3 { + coordinates.push(vec![0., i as f64]); + datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); + } + let geometry: TemporalGeometry = TemporalGeometry::Primitive( + TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()), + ); + let deserialized_geometry: TemporalGeometry = serde_json::from_str( + r#"{ + "type": "MovingPoint", + "coordinates": [[0.0, 0.0],[0.0, 1.0],[0.0, 2.0]], + "datetimes": ["1970-01-01T00:00:00Z", "1970-01-01T00:00:01Z", "1970-01-01T00:00:02Z"] + }"#, + ) + .unwrap(); + assert_eq!(geometry, deserialized_geometry); + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 613bc3f..78e4559 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -89,6 +89,14 @@ pub enum Value { }, } +impl TryFrom<(Vec>, Vec)> for Value { + type Error = String; + fn try_from(value: (Vec>, Vec)) -> Result { + let dt_coords = SameLengthDateTimeCoordinatesVecs::try_new(value.0, value.1)?; + Ok(Self::MovingPoint { dt_coords, base_representation: None }) + } +} + #[derive(Deserialize)] struct SameLengthDateTimeCoordinatesVecsUnchecked { datetimes: Vec, From 61a1b4858ea56c53dc03dfb84fefb05bf5417df8 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Fri, 23 May 2025 22:17:53 +0200 Subject: [PATCH 07/31] Add moving features property types Not sure if all of this is correct as it's a wild mix of almost identical types. --- ogcapi-types/src/movingfeatures/mod.rs | 6 +++ .../temporal_primitive_value.rs | 18 ++++++++ .../src/movingfeatures/temporal_properties.rs | 42 +++++++++++++++++++ .../temporal_properties_list.rs | 22 ++++++++++ .../src/movingfeatures/temporal_property.rs | 27 ++++++++++++ 5 files changed, 115 insertions(+) create mode 100644 ogcapi-types/src/movingfeatures/temporal_primitive_value.rs create mode 100644 ogcapi-types/src/movingfeatures/temporal_properties.rs create mode 100644 ogcapi-types/src/movingfeatures/temporal_properties_list.rs create mode 100644 ogcapi-types/src/movingfeatures/temporal_property.rs diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index 6ffe445..875b262 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -2,4 +2,10 @@ mod crs; pub mod temporal_geometry; pub mod temporal_primitive_geometry; pub mod temporal_complex_geometry; + +pub mod temporal_properties; +pub mod temporal_property; +pub mod temporal_primitive_value; +pub mod temporal_properties_list; + pub mod trs; diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs new file mode 100644 index 0000000..523092d --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs @@ -0,0 +1,18 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use super::temporal_properties::Interpolation; + +/// The TemporalPrimitiveValue resource represents the dynamic change of a non-spatial attribute’s value with time. An +/// abbreviated copy of this information is returned for each TemporalPrimitiveValue in the +/// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. +/// +/// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TemporalPrimitiveValue { + id: String, + datetimes: Vec>, + values: Vec, + interpolation: Interpolation, +} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs new file mode 100644 index 0000000..c160625 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TemporalProperties { + datetimes: Vec>, + values: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "type")] +pub enum TemporalPropertiesValue { + Measure { + values: f64, + interpolation: Option, + description: Option, + form: Option, + }, + Text { + values: String, + interpolation: Option, + description: Option, + form: Option, + }, + Image { + values: String, + interpolation: Option, + description: Option, + form: Option, + }, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum Interpolation { + Discrete, + Step, + Linear, + Regression, + Url(String), +} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties_list.rs b/ogcapi-types/src/movingfeatures/temporal_properties_list.rs new file mode 100644 index 0000000..3442378 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_properties_list.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +use crate::common::Links; + +use super::{temporal_properties::TemporalProperties, temporal_property::TemporalProperty}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TemporalPropertiesList { + pub temporal_properties: TemporalPropertiesListValue, + pub links: Option, + pub time_stamp: Option, + pub number_matched: Option, + pub number_returned: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum TemporalPropertiesListValue { + MFJsonTemporalProperties(Vec), + TemporalProperty(Vec), +} diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs new file mode 100644 index 0000000..6aaeae4 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +use super::temporal_primitive_value::TemporalPrimitiveValue; + +/// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct TemporalProperty { + /// An identifier for the resource assigned by an external entity. + name: String, + /// A predefined temporal property type. + r#type: Type, + value_sequence: Vec, + /// A unit of measure + form: Option, + /// A short description + description: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum Type { + TBoolean, + TText, + TInteger, + TReal, + TImage, +} From 99072bf537a1de088f4f8e75b6a8d89cf2b0add7 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sat, 24 May 2025 10:13:49 +0200 Subject: [PATCH 08/31] Refactor and comment temporal properties --- .../mfjson_temporal_properties.rs | 62 +++++++++++++++ ogcapi-types/src/movingfeatures/mod.rs | 5 +- .../temporal_primitive_value.rs | 18 ----- .../src/movingfeatures/temporal_properties.rs | 54 +++++-------- .../temporal_properties_list.rs | 22 ------ .../src/movingfeatures/temporal_property.rs | 77 ++++++++++++++++--- 6 files changed, 150 insertions(+), 88 deletions(-) create mode 100644 ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs delete mode 100644 ogcapi-types/src/movingfeatures/temporal_primitive_value.rs delete mode 100644 ogcapi-types/src/movingfeatures/temporal_properties_list.rs diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs new file mode 100644 index 0000000..3e4b0d1 --- /dev/null +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use super::temporal_property::Interpolation; + +/// MF-JSON TemporalProperties +/// +/// A TemporalProperties object is a JSON array of ParametricValues objects that groups a collection of dynamic +/// non-spatial attributes and its parametric values with time. +/// +/// See [7.2.2 MF-JSON TemporalProperties](https://docs.ogc.org/is/19-045r3/19-045r3.html#tproperties) +/// +/// Opposed to [TemporalProperty](super::temporal_property::TemporalProperty) values for all +/// represented properties are all measured at the same points in time. +// TODO enforce same length of datetimes and values +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct MFJsonTemporalProperties { + datetimes: Vec>, + #[serde(flatten)] + values: HashMap, +} + +/// A ParametricValues object is a JSON object that represents a collection of parametric values of dynamic non-spatial +/// attributes that are ascertained at the same times. A parametric value may be a time-varying measure, a sequence of +/// texts, or a sequence of images. Even though the parametric value may depend on the spatiotemporal location, +/// MF-JSON Prism only considers the temporal dependencies of their changes of value. +/// +/// See [7.2.2.1 MF-JSON ParametricValues](https://docs.ogc.org/is/19-045r3/19-045r3.html#pvalues) +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "type")] +pub enum ParametricValues { + /// The "values" member contains any numeric values. + Measure { + values: Vec, + /// Allowed Interpolations: Discrete, Step, Linear, Regression + interpolation: Option, + description: Option, + /// The "form" member is optional and its value is a JSON string as a common code (3 characters) described in + /// the [Code List Rec 20 by the UN Centre for Trade Facilitation and Electronic Business (UN/CEFACT)](https://www.unece.org/uncefact/codelistrecs.html) or a + /// URL specifying the unit of measurement. This member is applied only for a temporal property whose value + /// type is Measure. + form: Option, + }, + /// The "values" member contains any strings. + Text { + values: Vec, + /// Allowed Interpolations: Discrete, Step + // TODO enforce? + interpolation: Option, + description: Option, + }, + /// The "values" member contains Base64 strings converted from images or URLs to address images. + Image { + values: String, + /// Allowed Interpolations: Discrete, Step + // TODO enforce? + interpolation: Option, + description: Option, + }, +} diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index 875b262..6951430 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -1,11 +1,10 @@ mod crs; +pub mod temporal_complex_geometry; pub mod temporal_geometry; pub mod temporal_primitive_geometry; -pub mod temporal_complex_geometry; pub mod temporal_properties; +pub mod mfjson_temporal_properties; pub mod temporal_property; -pub mod temporal_primitive_value; -pub mod temporal_properties_list; pub mod trs; diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs deleted file mode 100644 index 523092d..0000000 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_value.rs +++ /dev/null @@ -1,18 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use super::temporal_properties::Interpolation; - -/// The TemporalPrimitiveValue resource represents the dynamic change of a non-spatial attribute’s value with time. An -/// abbreviated copy of this information is returned for each TemporalPrimitiveValue in the -/// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. -/// -/// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TemporalPrimitiveValue { - id: String, - datetimes: Vec>, - values: Vec, - interpolation: Interpolation, -} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs index c160625..2ae7f29 100644 --- a/ogcapi-types/src/movingfeatures/temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -1,42 +1,28 @@ -use std::collections::HashMap; - -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +use crate::common::Links; + +use super::{mfjson_temporal_properties::MFJsonTemporalProperties, temporal_property::TemporalProperty}; + +/// A TemporalProperties object consists of the set of [TemporalProperty] or a set of [MFJsonTemporalProperties]. +/// +/// See [8.8 TemporalProperties](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperties-section) +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct TemporalProperties { - datetimes: Vec>, - values: HashMap, + pub temporal_properties: TemporalPropertiesValue, + pub links: Option, + pub time_stamp: Option, + pub number_matched: Option, + pub number_returned: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(tag = "type")] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(untagged)] pub enum TemporalPropertiesValue { - Measure { - values: f64, - interpolation: Option, - description: Option, - form: Option, - }, - Text { - values: String, - interpolation: Option, - description: Option, - form: Option, - }, - Image { - values: String, - interpolation: Option, - description: Option, - form: Option, - }, + /// [MFJsonTemporalProperties] allows to represent multiple property values all measured at the same points in time. + MFJsonTemporalProperties(Vec), + /// [TemporalProperty] allows to represent a property value at independent points in time. + TemporalProperty(Vec), } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum Interpolation { - Discrete, - Step, - Linear, - Regression, - Url(String), -} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties_list.rs b/ogcapi-types/src/movingfeatures/temporal_properties_list.rs deleted file mode 100644 index 3442378..0000000 --- a/ogcapi-types/src/movingfeatures/temporal_properties_list.rs +++ /dev/null @@ -1,22 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use crate::common::Links; - -use super::{temporal_properties::TemporalProperties, temporal_property::TemporalProperty}; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct TemporalPropertiesList { - pub temporal_properties: TemporalPropertiesListValue, - pub links: Option, - pub time_stamp: Option, - pub number_matched: Option, - pub number_returned: Option, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -#[serde(untagged)] -pub enum TemporalPropertiesListValue { - MFJsonTemporalProperties(Vec), - TemporalProperty(Vec), -} diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index 6aaeae4..effdfcd 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -1,27 +1,82 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use super::temporal_primitive_value::TemporalPrimitiveValue; +use crate::common::Links; + +/// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. +/// An abbreviated copy of this information is returned for each TemporalProperty in the +/// [{root}/collections/{collectionId}/items/{mFeatureId}/tproperties](TemporalProperties) response. +/// The schema for the temporal property object presented in this clause is an extension of the [ParametricValues Object](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) defined in [MF-JSON](https://docs.ogc.org/is/22-003r3/22-003r3.html#OGC_19-045r3). +/// /// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] pub struct TemporalProperty { /// An identifier for the resource assigned by an external entity. name: String, - /// A predefined temporal property type. - r#type: Type, - value_sequence: Vec, + value_sequence: TemporalPropertyValue, /// A unit of measure form: Option, /// A short description description: Option, + links: Option } +/// A predefined temporal property type. +/// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub enum Type { - TBoolean, - TText, - TInteger, - TReal, - TImage, +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +pub enum TemporalPropertyValue { + TBoolean { + value_sequence: Vec>, + }, + TText { + value_sequence: Vec>, + }, + TInteger { + value_sequence: Vec>, + }, + TReal { + value_sequence: Vec>, + }, + TImage { + value_sequence: Vec>, + }, +} + +/// The TemporalPrimitiveValue resource represents the dynamic change of a non-spatial attribute’s value with time. An +/// abbreviated copy of this information is returned for each TemporalPrimitiveValue in the +/// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. +/// +/// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct TemporalPrimitiveValue { + id: String, + datetimes: Vec>, + values: Vec, + interpolation: Interpolation, +} + + +/// See [ParametricValues Object -> "interpolation"](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub enum Interpolation { + /// The sampling of the attribute occurs such that it is not possible to regard the series as continuous; thus, + /// there is no interpolated value if t is not an element in "datetimes". + #[default] + Discrete, + /// The values are not connected at the end of a subinterval with two successive instants. The value just jumps from + /// one value to the other at the end of a subinterval. + Step, + /// The values are essentially connected and a linear interpolation estimates the value of the property at the + /// indicated instant during a subinterval. + Linear, + /// The value of the attribute at the indicated instant is extrapolated from a simple linear regression model with + /// the whole values corresponding to the all elements in "datetimes". + Regression, + /// For a URL, this standard refers to the [InterpolationCode Codelist](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html#75) defined in [OGC TimeseriesML 1.0](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html)[OGC 15-042r3] + /// between neighboring points in a timeseries, e.g., "http://www.opengis.net/def/timeseries/InterpolationCode/Continuous", + /// "http://www.opengis.net/def/timeseries/InterpolationCode/Discontinuous", and etc. + Url(String), } From 2b3e9d2371b0ed9288a622474c96be9221a1c808 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 25 May 2025 12:55:13 +0200 Subject: [PATCH 09/31] Some doc comment fixes --- .../src/movingfeatures/temporal_complex_geometry.rs | 2 +- .../src/movingfeatures/temporal_primitive_geometry.rs | 2 +- ogcapi-types/src/movingfeatures/temporal_property.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index 248756f..a3db44d 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -13,7 +13,7 @@ pub enum Type { /// object with one additional mandatory member named "prisms". The value of the "prisms" member is represented by a /// JSON array of a set of TemporalPrimitiveGeometry instances having at least one element in the array. /// -/// See (7.2.1.2. TemporalComplexGeometry Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex] +/// See [7.2.1.2. TemporalComplexGeometry Object](https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex) #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TemporalComplexGeometry{ r#type: Type, diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 78e4559..85f6674 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -138,7 +138,7 @@ impl TryFrom> ///MF-JSON Prism separates out translational motion and rotational motion. The "interpolation" member is default and ///represents the translational motion of the geometry described by the "coordinates" value. Its value is a MotionCurve ///object described by one of predefined five motion curves (i.e., "Discrete", "Step", "Linear", "Quadratic", and -///"Cubic") or a URL (e.g., "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/motioncurve") +///"Cubic") or a URL (e.g., "") /// ///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index effdfcd..d5d0714 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -6,7 +6,7 @@ use crate::common::Links; /// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. /// An abbreviated copy of this information is returned for each TemporalProperty in the -/// [{root}/collections/{collectionId}/items/{mFeatureId}/tproperties](TemporalProperties) response. +/// [{root}/collections/{collectionId}/items/{mFeatureId}/tproperties](super::temporal_properties::TemporalProperties) response. /// The schema for the temporal property object presented in this clause is an extension of the [ParametricValues Object](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) defined in [MF-JSON](https://docs.ogc.org/is/22-003r3/22-003r3.html#OGC_19-045r3). /// /// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) @@ -76,7 +76,7 @@ pub enum Interpolation { /// the whole values corresponding to the all elements in "datetimes". Regression, /// For a URL, this standard refers to the [InterpolationCode Codelist](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html#75) defined in [OGC TimeseriesML 1.0](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html)[OGC 15-042r3] - /// between neighboring points in a timeseries, e.g., "http://www.opengis.net/def/timeseries/InterpolationCode/Continuous", - /// "http://www.opengis.net/def/timeseries/InterpolationCode/Discontinuous", and etc. + /// between neighboring points in a timeseries, e.g., "", + /// "", and etc. Url(String), } From 98064b09f286f01db3e34df2168eabe276153117 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 25 May 2025 14:45:45 +0200 Subject: [PATCH 10/31] Add test to temporal properties and fix some bugs --- .../mfjson_temporal_properties.rs | 72 ++++++++++- .../src/movingfeatures/temporal_properties.rs | 118 +++++++++++++++++- .../src/movingfeatures/temporal_property.rs | 101 +++++++++------ 3 files changed, 251 insertions(+), 40 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs index 3e4b0d1..cac42d3 100644 --- a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -17,9 +17,9 @@ use super::temporal_property::Interpolation; // TODO enforce same length of datetimes and values #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct MFJsonTemporalProperties { - datetimes: Vec>, + pub datetimes: Vec>, #[serde(flatten)] - values: HashMap, + pub values: HashMap, } /// A ParametricValues object is a JSON object that represents a collection of parametric values of dynamic non-spatial @@ -53,10 +53,76 @@ pub enum ParametricValues { }, /// The "values" member contains Base64 strings converted from images or URLs to address images. Image { - values: String, + values: Vec, /// Allowed Interpolations: Discrete, Step // TODO enforce? interpolation: Option, description: Option, }, } + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn serde_mfjson_temporal_properties() { + + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/insertTemporalProperty + let tp_json = r#"[ + { + "datetimes": [ + "2011-07-14T22:01:01.450Z", + "2011-07-14T23:01:01.450Z", + "2011-07-15T00:01:01.450Z" + ], + "length": { + "type": "Measure", + "form": "http://qudt.org/vocab/quantitykind/Length", + "values": [ + 1, + 2.4, + 1 + ], + "interpolation": "Linear" + }, + "discharge": { + "type": "Measure", + "form": "MQS", + "values": [ + 3, + 4, + 5 + ], + "interpolation": "Step" + } + }, + { + "datetimes": [ + "2011-07-14T22:01:01.450Z", + "2011-07-14T23:01:01.450Z" + ], + "camera": { + "type": "Image", + "values": [ + "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/image1", + "iVBORw0KGgoAAAANSUhEU......" + ], + "interpolation": "Discrete" + }, + "labels": { + "type": "Text", + "values": [ + "car", + "human" + ], + "interpolation": "Discrete" + } + } + ]"#; + + let _: Vec = serde_json::from_str(tp_json).expect("Failed to parse MF-JSON Temporal Properties"); + + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs index 2ae7f29..053dab4 100644 --- a/ogcapi-types/src/movingfeatures/temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use crate::common::Links; -use super::{mfjson_temporal_properties::MFJsonTemporalProperties, temporal_property::TemporalProperty}; +use super::{ + mfjson_temporal_properties::MFJsonTemporalProperties, temporal_property::TemporalProperty, +}; /// A TemporalProperties object consists of the set of [TemporalProperty] or a set of [MFJsonTemporalProperties]. /// @@ -26,3 +28,117 @@ pub enum TemporalPropertiesValue { TemporalProperty(Vec), } +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{ + common::Link, + movingfeatures::{ + mfjson_temporal_properties::ParametricValues, temporal_property::Interpolation, + }, + }; + + use super::*; + + #[test] + fn serde_temporal_properties() { + let links: Links = vec![ + Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties","self").mediatype("application/json"), + Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2","next").mediatype("application/json"), + ]; + + let temporal_properties = TemporalProperties { + temporal_properties: TemporalPropertiesValue::MFJsonTemporalProperties(vec![ + MFJsonTemporalProperties { + datetimes: vec![ + // TODO does type actually need to be UTC or could it be FixedOffset + // aswell? Converting to UTC loses the information of the original offset! + chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:06.000Z") + .unwrap() + .into(), + chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:07.000Z") + .unwrap() + .into(), + chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:08.000Z") + .unwrap() + .into(), + ], + values: HashMap::from([ + ( + "length".to_string(), + ParametricValues::Measure { + values: vec![1.0, 2.4, 1.0], + interpolation: Some(Interpolation::Linear), + description: None, + form: Some("http://qudt.org/vocab/quantitykind/Length".to_string()), + }, + ), + ( + "speed".to_string(), + ParametricValues::Measure { + values: vec![65.0, 70.0, 80.0], + interpolation: Some(Interpolation::Linear), + form: Some("KMH".to_string()), + description: None + }, + ), + ]), + }, + ]), + links: Some(links), + time_stamp: Some("2021-09-01T12:00:00Z".into()), + number_matched: Some(10), + number_returned: Some(2), + }; + + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperties + let tp_json = r#"{ + "temporalProperties": [ + { + "datetimes": [ + "2011-07-14T22:01:06.000Z", + "2011-07-14T22:01:07.000Z", + "2011-07-14T22:01:08.000Z" + ], + "length": { + "type": "Measure", + "form": "http://qudt.org/vocab/quantitykind/Length", + "values": [ + 1, + 2.4, + 1 + ], + "interpolation": "Linear" + }, + "speed": { + "type": "Measure", + "form": "KMH", + "values": [ + 65, + 70, + 80 + ], + "interpolation": "Linear" + } + } + ], + "links": [ + { + "href": "https://data.example.org/collections/mfc-1/items/mf-1/tproperties", + "rel": "self", + "type": "application/json" + }, + { + "href": "https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2", + "rel": "next", + "type": "application/json" + } + ], + "timeStamp": "2021-09-01T12:00:00Z", + "numberMatched": 10, + "numberReturned": 2 + }"#; + assert_eq!(temporal_properties, serde_json::from_str(tp_json).unwrap()); + } +} diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index d5d0714..bacc6d1 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -3,46 +3,36 @@ use serde::{Deserialize, Serialize}; use crate::common::Links; - -/// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. -/// An abbreviated copy of this information is returned for each TemporalProperty in the +/// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. +/// An abbreviated copy of this information is returned for each TemporalProperty in the /// [{root}/collections/{collectionId}/items/{mFeatureId}/tproperties](super::temporal_properties::TemporalProperties) response. /// The schema for the temporal property object presented in this clause is an extension of the [ParametricValues Object](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) defined in [MF-JSON](https://docs.ogc.org/is/22-003r3/22-003r3.html#OGC_19-045r3). /// /// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct TemporalProperty { /// An identifier for the resource assigned by an external entity. - name: String, - value_sequence: TemporalPropertyValue, + pub name: String, + #[serde(flatten)] + pub value_sequence: TemporalPropertyValue, /// A unit of measure - form: Option, + pub form: Option, /// A short description - description: Option, - links: Option + pub description: Option, + pub links: Option, } /// A predefined temporal property type. /// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] +#[serde(tag = "type", content = "valueSequence" )] pub enum TemporalPropertyValue { - TBoolean { - value_sequence: Vec>, - }, - TText { - value_sequence: Vec>, - }, - TInteger { - value_sequence: Vec>, - }, - TReal { - value_sequence: Vec>, - }, - TImage { - value_sequence: Vec>, - }, + TBoolean(Vec>), + TText(Vec>), + TInteger(Vec>), + TReal(Vec>), + TImage(Vec>), } /// The TemporalPrimitiveValue resource represents the dynamic change of a non-spatial attribute’s value with time. An @@ -52,31 +42,70 @@ pub enum TemporalPropertyValue { /// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub struct TemporalPrimitiveValue { - id: String, - datetimes: Vec>, - values: Vec, - interpolation: Interpolation, + /// A unique identifier to the temporal primitive value. + // TODO mandatory according to https://docs.ogc.org/is/22-003r3/22-003r3.html#_overview_13 + // but missing in response sample at https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperty + pub id: Option, + /// A sequence of monotonic increasing instants. + pub datetimes: Vec>, + /// A sequence of dynamic values having the same number of elements as “datetimes”. + // TODO enforce length + pub values: Vec, + /// A predefined type for a dynamic value (i.e., one of ‘Discrete’, ‘Step’, ‘Linear’, or ‘Regression’). + pub interpolation: Interpolation, } - /// See [ParametricValues Object -> "interpolation"](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub enum Interpolation { - /// The sampling of the attribute occurs such that it is not possible to regard the series as continuous; thus, + /// The sampling of the attribute occurs such that it is not possible to regard the series as continuous; thus, /// there is no interpolated value if t is not an element in "datetimes". #[default] Discrete, - /// The values are not connected at the end of a subinterval with two successive instants. The value just jumps from + /// The values are not connected at the end of a subinterval with two successive instants. The value just jumps from /// one value to the other at the end of a subinterval. Step, - /// The values are essentially connected and a linear interpolation estimates the value of the property at the + /// The values are essentially connected and a linear interpolation estimates the value of the property at the /// indicated instant during a subinterval. Linear, - /// The value of the attribute at the indicated instant is extrapolated from a simple linear regression model with + /// The value of the attribute at the indicated instant is extrapolated from a simple linear regression model with /// the whole values corresponding to the all elements in "datetimes". Regression, - /// For a URL, this standard refers to the [InterpolationCode Codelist](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html#75) defined in [OGC TimeseriesML 1.0](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html)[OGC 15-042r3] - /// between neighboring points in a timeseries, e.g., "", + /// For a URL, this standard refers to the [InterpolationCode Codelist](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html#75) defined in [OGC TimeseriesML 1.0](http://docs.opengeospatial.org/is/15-042r3/15-042r3.html)[OGC 15-042r3] + /// between neighboring points in a timeseries, e.g., "", /// "", and etc. Url(String), } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde_temporal_property() { + // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperty + let tp_json = r#"{ + "name": "speed", + "type": "TReal", + "form": "KMH", + "valueSequence": [ + { + "datetimes": [ + "2011-07-15T08:00:00Z", + "2011-07-15T08:00:01Z", + "2011-07-15T08:00:02Z" + ], + "values": [ + 0, + 20, + 50 + ], + "interpolation": "Linear" + } + ] + }"#; + + let _: TemporalProperty = + serde_json::from_str(tp_json).expect("Failed to deserialize Temporal Property"); + } +} From d5200aa1dc785e93755f4eb6368fb8f05abdd4a9 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 25 May 2025 16:14:37 +0200 Subject: [PATCH 11/31] Integrate with moving features with collection, feature collection and features --- ogcapi-types/Cargo.toml | 2 +- ogcapi-types/src/common/collection.rs | 22 ++++++++++++++ ogcapi-types/src/features/feature.rs | 30 +++++++++++++++++++ .../src/features/feature_collection.rs | 9 ++++++ ogcapi-types/src/movingfeatures/mod.rs | 2 +- .../src/movingfeatures/temporal_geometry.rs | 10 +++---- .../src/movingfeatures/temporal_properties.rs | 6 ++-- .../src/movingfeatures/temporal_property.rs | 4 +-- 8 files changed, 73 insertions(+), 12 deletions(-) diff --git a/ogcapi-types/Cargo.toml b/ogcapi-types/Cargo.toml index 19bf3b9..17df587 100644 --- a/ogcapi-types/Cargo.toml +++ b/ogcapi-types/Cargo.toml @@ -22,7 +22,7 @@ stac = ["features"] styles = [] tiles = ["common"] coverages = [] -movingfeatures = ["common"] +movingfeatures = ["common", "features"] [dependencies] chrono = { version = "0.4.41", features = ["serde"] } diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index d05893e..cfd8f1b 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -26,7 +26,14 @@ pub struct Collection { pub attribution: Option, pub extent: Option, /// An indicator about the type of the items in the collection. + #[cfg(not(feature = "movingfeatures"))] pub item_type: Option, + #[cfg(feature = "movingfeatures")] + // TODO not sure if this is the best way to solve the requirement by moving features + // to make itemType mandatory and still allowing to produce collections with other + // itemTypes + #[serde(flatten)] + pub item_type: ItemType, /// The list of coordinate reference systems supported by the API; the first item is the default coordinate reference system. #[serde(default)] #[serde_as(as = "Vec")] @@ -87,6 +94,10 @@ pub struct Collection { #[cfg(feature = "stac")] #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")] pub assets: std::collections::HashMap, + #[cfg(feature = "movingfeatures")] + #[serde(rename = "updateFrequency")] + /// A time interval of sampling location. The time unit of this property is millisecond. + update_frequency: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] pub additional_properties: Map, } @@ -96,6 +107,15 @@ fn collection() -> String { "Collection".to_string() } +#[cfg(feature = "movingfeatures")] +#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] +#[serde(untagged)] +pub enum ItemType { + #[default] + MovingFeature, + Other(Option) +} + #[allow(clippy::derivable_impls)] impl Default for Collection { fn default() -> Self { @@ -131,6 +151,8 @@ impl Default for Collection { summaries: Default::default(), #[cfg(feature = "stac")] assets: Default::default(), + #[cfg(feature = "movingfeatures")] + update_frequency: Default::default(), additional_properties: Default::default(), } } diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 91d56ea..5406b7e 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -4,6 +4,16 @@ use std::fmt::Display; #[cfg(feature = "stac")] use crate::common::Bbox; + +#[cfg(feature = "movingfeatures")] +use crate::movingfeatures::{ + crs::Crs, temporal_geometry::TemporalGeometry, temporal_properties::TemporalProperties, + trs::Trs, +}; + +#[cfg(feature = "movingfeatures")] +use chrono::{DateTime, Utc}; + use geojson::Geometry; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; @@ -40,7 +50,12 @@ pub fn geometry() -> Schema { /// Abstraction of real world phenomena (ISO 19101-1:2014) #[serde_with::skip_serializing_none] +<<<<<<< HEAD #[derive(Deserialize, Serialize, ToSchema, Debug, Clone, PartialEq)] +======= +#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] +#[serde(rename = "camelCase")] +>>>>>>> ba7e7ad (Integrate with moving features with collection, feature collection and features) pub struct Feature { #[serde(default)] pub id: Option, @@ -69,6 +84,21 @@ pub struct Feature { #[cfg(feature = "stac")] #[serde(default)] pub assets: HashMap, + #[cfg(feature = "movingfeatures")] + #[serde(serialize_with = "crate::common::serialize_interval")] + /// Life span information for the moving feature. + /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) + pub time: Vec>>>, + #[cfg(feature = "movingfeatures")] + // TODO should this be #[serde(default)] instead of option? + pub crs: Option, + #[cfg(feature = "movingfeatures")] + // TODO should this be #[serde(default)] instead of option? + pub trs: Option, + #[cfg(feature = "movingfeatures")] + pub temporal_geometry: Option, + #[cfg(feature = "movingfeatures")] + pub temporal_properties: Option, } impl Feature { diff --git a/ogcapi-types/src/features/feature_collection.rs b/ogcapi-types/src/features/feature_collection.rs index 7bd4118..282535f 100644 --- a/ogcapi-types/src/features/feature_collection.rs +++ b/ogcapi-types/src/features/feature_collection.rs @@ -4,6 +4,9 @@ use utoipa::ToSchema; use crate::common::Link; +#[cfg(feature = "movingfeatures")] +use crate::common::Bbox; + use super::Feature; #[derive(Serialize, Deserialize, ToSchema, Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -26,6 +29,12 @@ pub struct FeatureCollection { pub time_stamp: Option, pub number_matched: Option, pub number_returned: Option, + #[cfg(feature = "movingfeatures")] + pub crs: Option, + #[cfg(feature = "movingfeatures")] + pub trs: Option, + #[cfg(feature = "movingfeatures")] + pub bbox: Option, } impl FeatureCollection { diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index 6951430..20ae3df 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -1,4 +1,3 @@ -mod crs; pub mod temporal_complex_geometry; pub mod temporal_geometry; pub mod temporal_primitive_geometry; @@ -7,4 +6,5 @@ pub mod temporal_properties; pub mod mfjson_temporal_properties; pub mod temporal_property; +pub mod crs; pub mod trs; diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs index 972a979..cd6d9e8 100644 --- a/ogcapi-types/src/movingfeatures/temporal_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -7,7 +7,7 @@ use super::{ #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(untagged)] -enum TemporalGeometry { +pub enum TemporalGeometry { Primitive(TemporalPrimitiveGeometry), Complex(TemporalComplexGeometry), } @@ -29,10 +29,10 @@ mod tests { } let primitive_geometry = TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()); - let geometry = TemporalGeometry::Complex(TemporalComplexGeometry::try_from(vec![ - primitive_geometry.clone(), - primitive_geometry, - ]).unwrap()); + let geometry = TemporalGeometry::Complex( + TemporalComplexGeometry::try_from(vec![primitive_geometry.clone(), primitive_geometry]) + .unwrap(), + ); let deserialized_geometry: TemporalGeometry = serde_json::from_str( r#"{ "type": "MovingGeometryCollection", diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs index 053dab4..d783409 100644 --- a/ogcapi-types/src/movingfeatures/temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -9,7 +9,7 @@ use super::{ /// A TemporalProperties object consists of the set of [TemporalProperty] or a set of [MFJsonTemporalProperties]. /// /// See [8.8 TemporalProperties](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperties-section) -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub struct TemporalProperties { pub temporal_properties: TemporalPropertiesValue, @@ -19,7 +19,7 @@ pub struct TemporalProperties { pub number_returned: Option, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(untagged)] pub enum TemporalPropertiesValue { /// [MFJsonTemporalProperties] allows to represent multiple property values all measured at the same points in time. @@ -80,7 +80,7 @@ mod tests { values: vec![65.0, 70.0, 80.0], interpolation: Some(Interpolation::Linear), form: Some("KMH".to_string()), - description: None + description: None, }, ), ]), diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index bacc6d1..53aa9bb 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -26,7 +26,7 @@ pub struct TemporalProperty { /// A predefined temporal property type. /// #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[serde(tag = "type", content = "valueSequence" )] +#[serde(tag = "type", content = "valueSequence")] pub enum TemporalPropertyValue { TBoolean(Vec>), TText(Vec>), @@ -42,7 +42,7 @@ pub enum TemporalPropertyValue { /// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub struct TemporalPrimitiveValue { - /// A unique identifier to the temporal primitive value. + /// A unique identifier to the temporal primitive value. // TODO mandatory according to https://docs.ogc.org/is/22-003r3/22-003r3.html#_overview_13 // but missing in response sample at https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/retrieveTemporalProperty pub id: Option, From 83f77ec2089b2455018621afbbca6e288ab5c754 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 25 May 2025 16:55:28 +0200 Subject: [PATCH 12/31] Add traits for moving features transactions to drivers --- ogcapi-drivers/Cargo.toml | 1 + ogcapi-drivers/src/lib.rs | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/ogcapi-drivers/Cargo.toml b/ogcapi-drivers/Cargo.toml index 4d0c674..ebd1f4d 100644 --- a/ogcapi-drivers/Cargo.toml +++ b/ogcapi-drivers/Cargo.toml @@ -25,6 +25,7 @@ processes = ["common", "ogcapi-types/processes"] stac = ["features", "ogcapi-types/stac", "s3"] styles = ["ogcapi-types/styles"] tiles = ["common", "ogcapi-types/tiles"] +movingfeatures = ["common", "features"] [dependencies] anyhow = { workspace = true } diff --git a/ogcapi-drivers/src/lib.rs b/ogcapi-drivers/src/lib.rs index 9b728d3..0db1f7d 100644 --- a/ogcapi-drivers/src/lib.rs +++ b/ogcapi-drivers/src/lib.rs @@ -7,6 +7,8 @@ pub mod s3; use ogcapi_types::common::{Collection, Collections, Query as CollectionQuery}; #[cfg(feature = "edr")] use ogcapi_types::edr::{Query as EdrQuery, QueryType}; +#[cfg(feature = "movingfeatures")] +use ogcapi_types::movingfeatures::{temporal_geometry::TemporalGeometry, temporal_properties::TemporalProperties, temporal_primitive_geometry::TemporalPrimitiveGeometry}; #[cfg(feature = "processes")] use ogcapi_types::processes::{Results, StatusInfo}; #[cfg(feature = "stac")] @@ -116,3 +118,48 @@ pub trait TileTransactions: Send + Sync { col: u32, ) -> anyhow::Result>; } + + +#[cfg(feature = "movingfeatures")] +#[async_trait::async_trait] +pub trait TemporalGeometryTransactions: Send + Sync { + async fn create_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + temporal_geometry: &TemporalPrimitiveGeometry + ); + async fn read_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + ) -> anyhow::Result; + async fn delete_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + t_geometry_id: &str + ) -> anyhow::Result<()>; +} + +#[cfg(feature = "movingfeatures")] +#[async_trait::async_trait] +pub trait TemporalPropertyTransactions: Send + Sync { + async fn create_temporal_property( + &self, + collection: &str, + m_feature_id: &str, + temporal_geometry: &TemporalPrimitiveGeometry + ); + async fn read_temporal_property( + &self, + collection: &str, + m_feature_id: &str, + ) -> anyhow::Result; + async fn delete_temporal_property( + &self, + collection: &str, + m_feature_id: &str, + t_properties_name: &str + ) -> anyhow::Result<()>; +} From c4a7978b64b9898162dfc81bbb5f652a14e2f839 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 25 May 2025 18:55:55 +0200 Subject: [PATCH 13/31] Add a blanket implementation of moving feature geometry transactions to drivers for testing --- ogcapi-drivers/Cargo.toml | 3 +- ogcapi-drivers/src/blanket/mod.rs | 100 ++++++++++++++++++ ogcapi-drivers/src/lib.rs | 10 +- ogcapi-types/src/common/collection.rs | 2 +- .../temporal_complex_geometry.rs | 60 ++++++++--- .../temporal_primitive_geometry.rs | 2 + 6 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 ogcapi-drivers/src/blanket/mod.rs diff --git a/ogcapi-drivers/Cargo.toml b/ogcapi-drivers/Cargo.toml index ebd1f4d..f2235e0 100644 --- a/ogcapi-drivers/Cargo.toml +++ b/ogcapi-drivers/Cargo.toml @@ -16,6 +16,7 @@ default = ["common", "edr", "features", "processes", "tiles"] # drivers postgres = ["sqlx", "rink-core", "url"] s3 = ["aws-config", "aws-sdk-s3"] +blanket = [] # standards common = ["ogcapi-types/common"] @@ -25,7 +26,7 @@ processes = ["common", "ogcapi-types/processes"] stac = ["features", "ogcapi-types/stac", "s3"] styles = ["ogcapi-types/styles"] tiles = ["common", "ogcapi-types/tiles"] -movingfeatures = ["common", "features"] +movingfeatures = ["common", "features", "ogcapi-types/movingfeatures"] [dependencies] anyhow = { workspace = true } diff --git a/ogcapi-drivers/src/blanket/mod.rs b/ogcapi-drivers/src/blanket/mod.rs new file mode 100644 index 0000000..e514587 --- /dev/null +++ b/ogcapi-drivers/src/blanket/mod.rs @@ -0,0 +1,100 @@ +use anyhow::anyhow; + +#[cfg(feature = "movingfeatures")] +use ogcapi_types::movingfeatures::{ + temporal_complex_geometry::Prisms, temporal_complex_geometry::TemporalComplexGeometry, + temporal_geometry::TemporalGeometry, temporal_primitive_geometry::TemporalPrimitiveGeometry, +}; + +#[cfg(feature = "movingfeatures")] +use crate::{FeatureTransactions, TemporalGeometryTransactions}; + +#[cfg(feature = "movingfeatures")] +#[async_trait::async_trait] +impl TemporalGeometryTransactions for T +where + T: FeatureTransactions, +{ + async fn create_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + temporal_geometry: &TemporalPrimitiveGeometry, + ) -> anyhow::Result { + let crs = temporal_geometry + .crs + .clone() + .unwrap_or_default() + .try_into() + .map_err(|e: String| anyhow!(e))?; + let mut feature = self + .read_feature(collection, m_feature_id, &crs) + .await? + .ok_or(anyhow!("Feature not found!"))?; + let mut temporal_geometry = temporal_geometry.clone(); + let id = match feature.temporal_geometry { + None => { + temporal_geometry.id = Some(0.to_string()); + feature.temporal_geometry = + Some(TemporalGeometry::Primitive(temporal_geometry.clone())); + temporal_geometry.id.unwrap() + } + Some(TemporalGeometry::Primitive(tg)) => { + temporal_geometry.id = Some(1.to_string()); + feature.temporal_geometry = + Some(TemporalGeometry::Complex(TemporalComplexGeometry { + prisms: Prisms::new(vec![tg, temporal_geometry.clone()]) + // TODO should errors returned from ogcapi_types be std::Errors to + // avoid the map_err? + .map_err(|e| anyhow!(e))?, + r#type: Default::default(), + crs: Default::default(), + trs: Default::default(), + })); + temporal_geometry.id.unwrap() + } + Some(TemporalGeometry::Complex(ref mut tg)) => { + // This re-uses ids, might lead to surprising behaviour when deleting a tg and then + // adding a new one + temporal_geometry.id = Some(tg.prisms.len().to_string()); + tg.prisms.push(temporal_geometry.clone()); + temporal_geometry.id.unwrap() + } + }; + self.update_feature(&feature).await?; + Ok(id) + } + async fn read_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + ) -> anyhow::Result> { + let feature = self + .read_feature(collection, m_feature_id, &Default::default()) + .await? + .ok_or(anyhow!("Feature not found!"))?; + Ok(feature.temporal_geometry) + } + async fn delete_temporal_geometry( + &self, + collection: &str, + m_feature_id: &str, + t_geometry_id: &str, + ) -> anyhow::Result<()> { + let mut feature = self + .read_feature(collection, m_feature_id, &Default::default()) + .await? + .ok_or(anyhow!("Feature not found!"))?; + match feature.temporal_geometry { + Some(TemporalGeometry::Primitive(tg)) if tg.id.as_ref().is_some_and(|id| id == t_geometry_id) => { + feature.temporal_geometry = None; + Ok(()) + } + Some(TemporalGeometry::Complex(ref mut tg)) => { + tg.prisms.try_remove(t_geometry_id).map_err(|e| anyhow!(e))?; + Ok(()) + } + _ => Err(anyhow!(format!("TemporalGeometry with id {t_geometry_id} not found"))), + } + } +} diff --git a/ogcapi-drivers/src/lib.rs b/ogcapi-drivers/src/lib.rs index 0db1f7d..01caec3 100644 --- a/ogcapi-drivers/src/lib.rs +++ b/ogcapi-drivers/src/lib.rs @@ -2,6 +2,8 @@ pub mod postgres; #[cfg(feature = "s3")] pub mod s3; +#[cfg(feature = "blanket")] +pub mod blanket; #[cfg(feature = "common")] use ogcapi_types::common::{Collection, Collections, Query as CollectionQuery}; @@ -128,12 +130,12 @@ pub trait TemporalGeometryTransactions: Send + Sync { collection: &str, m_feature_id: &str, temporal_geometry: &TemporalPrimitiveGeometry - ); + ) -> anyhow::Result; async fn read_temporal_geometry( &self, collection: &str, m_feature_id: &str, - ) -> anyhow::Result; + ) -> anyhow::Result>; async fn delete_temporal_geometry( &self, collection: &str, @@ -150,12 +152,12 @@ pub trait TemporalPropertyTransactions: Send + Sync { collection: &str, m_feature_id: &str, temporal_geometry: &TemporalPrimitiveGeometry - ); + ) -> anyhow::Result; async fn read_temporal_property( &self, collection: &str, m_feature_id: &str, - ) -> anyhow::Result; + ) -> anyhow::Result>; async fn delete_temporal_property( &self, collection: &str, diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index cfd8f1b..c31f518 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -97,7 +97,7 @@ pub struct Collection { #[cfg(feature = "movingfeatures")] #[serde(rename = "updateFrequency")] /// A time interval of sampling location. The time unit of this property is millisecond. - update_frequency: Option, + pub update_frequency: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] pub additional_properties: Map, } diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index a3db44d..33b3e36 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -8,32 +8,65 @@ pub enum Type { MovingGeometryCollection, } -/// A TemporalComplexGeometry object represents a set of TemporalPrimitiveGeometry objects. When a TemporalGeometry -/// object has a "type" member is "MovingGeometryCollection", the object is specialized as a TemporalComplexGeometry -/// object with one additional mandatory member named "prisms". The value of the "prisms" member is represented by a +/// A TemporalComplexGeometry object represents a set of TemporalPrimitiveGeometry objects. When a TemporalGeometry +/// object has a "type" member is "MovingGeometryCollection", the object is specialized as a TemporalComplexGeometry +/// object with one additional mandatory member named "prisms". The value of the "prisms" member is represented by a /// JSON array of a set of TemporalPrimitiveGeometry instances having at least one element in the array. /// /// See [7.2.1.2. TemporalComplexGeometry Object](https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex) #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct TemporalComplexGeometry{ - r#type: Type, - prisms: Prisms, - crs: Option, - trs: Option, +pub struct TemporalComplexGeometry { + pub r#type: Type, + pub prisms: Prisms, + pub crs: Option, + pub trs: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] #[serde(try_from = "PrismsUnchecked")] pub struct Prisms(Vec); +impl Prisms { + pub fn new(value: Vec) -> Result { + if !value.is_empty() { + Ok(Self(value)) + } else { + Err("Prisms must have at least one value") + } + } + + pub fn push(&mut self, value: TemporalPrimitiveGeometry) { + self.0.push(value) + } + + pub fn is_empty(&self) -> bool { + // this should never be true + self.0.is_empty() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn try_remove(&mut self, id: &str) -> Result { + if self.len() > 2 { + self.0 + .pop_if(|tg| tg.id.as_ref().is_some_and(|tg_id| tg_id == id)) + .ok_or("Temporal Geometry not found.") + } else { + Err("Prisms must have at least one value. Try to delete the whole prism.") + } + } +} + #[derive(Deserialize, Clone, Debug, PartialEq)] struct PrismsUnchecked(Vec); -impl TryFrom> for TemporalComplexGeometry{ +impl TryFrom> for TemporalComplexGeometry { type Error = &'static str; fn try_from(value: Vec) -> Result { - Ok(Self{ + Ok(Self { r#type: Default::default(), prisms: PrismsUnchecked(value).try_into()?, crs: Default::default(), @@ -46,11 +79,6 @@ impl TryFrom for Prisms { type Error = &'static str; fn try_from(value: PrismsUnchecked) -> Result { - if !value.0.is_empty(){ - Ok(Prisms(value.0)) - }else{ - Err("Prisms must have at least one value") - } + Self::new(value.0) } } - diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 85f6674..5a4eb6f 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -12,6 +12,7 @@ use super::{crs::Crs, trs::Trs}; /// three-dimensional spatial coordinate system. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TemporalPrimitiveGeometry { + pub id: Option, #[serde(flatten)] pub value: Value, #[serde(default)] @@ -24,6 +25,7 @@ pub struct TemporalPrimitiveGeometry { impl TemporalPrimitiveGeometry { pub fn new(value: Value) -> Self { Self { + id: None, value, interpolation: Interpolation::default(), crs: Default::default(), From a32f31c29cfc67b7a88d7cebb15beada18e0b8fd Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Thu, 29 May 2025 10:19:24 +0200 Subject: [PATCH 14/31] Drop unnecessary complexity on ItemType for MovingFeatures --- ogcapi-types/src/common/collection.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index c31f518..6341c07 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -26,14 +26,7 @@ pub struct Collection { pub attribution: Option, pub extent: Option, /// An indicator about the type of the items in the collection. - #[cfg(not(feature = "movingfeatures"))] pub item_type: Option, - #[cfg(feature = "movingfeatures")] - // TODO not sure if this is the best way to solve the requirement by moving features - // to make itemType mandatory and still allowing to produce collections with other - // itemTypes - #[serde(flatten)] - pub item_type: ItemType, /// The list of coordinate reference systems supported by the API; the first item is the default coordinate reference system. #[serde(default)] #[serde_as(as = "Vec")] @@ -107,15 +100,6 @@ fn collection() -> String { "Collection".to_string() } -#[cfg(feature = "movingfeatures")] -#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Clone)] -#[serde(untagged)] -pub enum ItemType { - #[default] - MovingFeature, - Other(Option) -} - #[allow(clippy::derivable_impls)] impl Default for Collection { fn default() -> Self { From 06b206711c81209ab74760581d8de0f6d8da0d1e Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Thu, 29 May 2025 10:20:44 +0200 Subject: [PATCH 15/31] Add convenience method to SameLengthDateTimeCoordinatesVec --- .../src/movingfeatures/temporal_primitive_geometry.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 5a4eb6f..21e6520 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -113,7 +113,7 @@ pub struct SameLengthDateTimeCoordinatesVecs { } impl SameLengthDateTimeCoordinatesVecs { - fn try_new(datetimes: Vec, coordinates: Vec) -> Result { + pub fn try_new(datetimes: Vec, coordinates: Vec) -> Result { if coordinates.len() != datetimes.len() { Err("coordinates and datetimes must be of same length!".to_string()) } else { @@ -123,6 +123,11 @@ impl SameLengthDateTimeCoordinatesVecs { }) } } + + pub fn append(&mut self, other: &mut SameLengthDateTimeCoordinatesVecs) { + self.datetimes.append(&mut other.datetimes); + self.coordinates.append(&mut other.coordinates); + } } impl TryFrom> From 4b94480aeae2a615451c201b97d3afefcbcac83d Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Thu, 29 May 2025 10:24:43 +0200 Subject: [PATCH 16/31] Somehow serde(rename = camelCase) does not work here, resort to serde(rename) on individual members for now --- ogcapi-types/src/features/feature.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 5406b7e..3dbdda2 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -96,8 +96,10 @@ pub struct Feature { // TODO should this be #[serde(default)] instead of option? pub trs: Option, #[cfg(feature = "movingfeatures")] + #[serde(rename = "temporalGeometry")] pub temporal_geometry: Option, #[cfg(feature = "movingfeatures")] + #[serde(rename = "temporalProperties")] pub temporal_properties: Option, } From ad6aaca496bb9f00b6b310340cd3241f2e4b54e2 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sat, 31 May 2025 09:39:42 +0200 Subject: [PATCH 17/31] Add accessors to SameLengthCoordinatesVec --- .../src/movingfeatures/temporal_primitive_geometry.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 21e6520..a069c91 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -128,6 +128,15 @@ impl SameLengthDateTimeCoordinatesVecs { self.datetimes.append(&mut other.datetimes); self.coordinates.append(&mut other.coordinates); } + + pub fn datetimes(&self) -> &[A] { + self.datetimes.as_slice() + } + + pub fn coordinates(&self) -> &[B] { + self.coordinates.as_slice() + } + } impl TryFrom> From ed998a5a1daa27f35dc7d7d9a83bbdd52bf537e3 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sat, 31 May 2025 12:23:11 +0200 Subject: [PATCH 18/31] Add into_inner to Prisms --- .../src/movingfeatures/temporal_complex_geometry.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index 33b3e36..bc21f5f 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -57,6 +57,14 @@ impl Prisms { Err("Prisms must have at least one value. Try to delete the whole prism.") } } + + pub fn inner(&self) -> &[TemporalPrimitiveGeometry] { + self.0.as_slice() + } + + pub fn into_inner(self) -> Vec { + self.0 + } } #[derive(Deserialize, Clone, Debug, PartialEq)] From d60220c2a9d6be606c7a112b10482712cb9dda3a Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 22 Jun 2025 09:29:32 +0200 Subject: [PATCH 19/31] Drop enforcment of Prisms having >= 1 geometry --- ogcapi-drivers/src/blanket/mod.rs | 17 +++-- ogcapi-types/src/lib.rs | 3 +- .../temporal_complex_geometry.rs | 69 ++----------------- .../src/movingfeatures/temporal_geometry.rs | 3 +- 4 files changed, 20 insertions(+), 72 deletions(-) diff --git a/ogcapi-drivers/src/blanket/mod.rs b/ogcapi-drivers/src/blanket/mod.rs index e514587..3bd55c9 100644 --- a/ogcapi-drivers/src/blanket/mod.rs +++ b/ogcapi-drivers/src/blanket/mod.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; #[cfg(feature = "movingfeatures")] use ogcapi_types::movingfeatures::{ - temporal_complex_geometry::Prisms, temporal_complex_geometry::TemporalComplexGeometry, + temporal_complex_geometry::TemporalComplexGeometry, temporal_geometry::TemporalGeometry, temporal_primitive_geometry::TemporalPrimitiveGeometry, }; @@ -43,10 +43,7 @@ where temporal_geometry.id = Some(1.to_string()); feature.temporal_geometry = Some(TemporalGeometry::Complex(TemporalComplexGeometry { - prisms: Prisms::new(vec![tg, temporal_geometry.clone()]) - // TODO should errors returned from ogcapi_types be std::Errors to - // avoid the map_err? - .map_err(|e| anyhow!(e))?, + prisms: vec![tg, temporal_geometry.clone()], r#type: Default::default(), crs: Default::default(), trs: Default::default(), @@ -91,8 +88,14 @@ where Ok(()) } Some(TemporalGeometry::Complex(ref mut tg)) => { - tg.prisms.try_remove(t_geometry_id).map_err(|e| anyhow!(e))?; - Ok(()) + if tg.prisms.len() > 2 { + tg.prisms + .pop_if(|tg| tg.id.as_ref().is_some_and(|tg_id| tg_id == t_geometry_id)) + .ok_or(anyhow!("Temporal Geometry not found."))?; + Ok(()) + } else { + Err(anyhow!("Prisms must have at least one value. Try to delete the whole prism.")) + } } _ => Err(anyhow!(format!("TemporalGeometry with id {t_geometry_id} not found"))), } diff --git a/ogcapi-types/src/lib.rs b/ogcapi-types/src/lib.rs index 35576fe..89d3f37 100644 --- a/ogcapi-types/src/lib.rs +++ b/ogcapi-types/src/lib.rs @@ -23,7 +23,8 @@ pub mod styles; pub mod tiles; /// Types specified in the `OGC API - Moving Features` standard. -#[cfg(feature = "movingfeatures")] +// FIXME +// #[cfg(feature = "movingfeatures")] pub mod movingfeatures; #[cfg(feature = "coverages")] diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index bc21f5f..bdcd0a1 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -17,76 +17,21 @@ pub enum Type { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct TemporalComplexGeometry { pub r#type: Type, - pub prisms: Prisms, + pub prisms: Vec, pub crs: Option, pub trs: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -#[serde(try_from = "PrismsUnchecked")] -pub struct Prisms(Vec); - -impl Prisms { - pub fn new(value: Vec) -> Result { - if !value.is_empty() { - Ok(Self(value)) - } else { - Err("Prisms must have at least one value") - } - } - - pub fn push(&mut self, value: TemporalPrimitiveGeometry) { - self.0.push(value) - } - - pub fn is_empty(&self) -> bool { - // this should never be true - self.0.is_empty() - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn try_remove(&mut self, id: &str) -> Result { - if self.len() > 2 { - self.0 - .pop_if(|tg| tg.id.as_ref().is_some_and(|tg_id| tg_id == id)) - .ok_or("Temporal Geometry not found.") - } else { - Err("Prisms must have at least one value. Try to delete the whole prism.") - } - } - - pub fn inner(&self) -> &[TemporalPrimitiveGeometry] { - self.0.as_slice() - } - - pub fn into_inner(self) -> Vec { - self.0 - } -} -#[derive(Deserialize, Clone, Debug, PartialEq)] -struct PrismsUnchecked(Vec); +impl From> for TemporalComplexGeometry { -impl TryFrom> for TemporalComplexGeometry { - type Error = &'static str; - - fn try_from(value: Vec) -> Result { - Ok(Self { + fn from(value: Vec) -> Self { + debug_assert!(!value.is_empty()); + Self { r#type: Default::default(), - prisms: PrismsUnchecked(value).try_into()?, + prisms: value, crs: Default::default(), trs: Default::default(), - }) - } -} - -impl TryFrom for Prisms { - type Error = &'static str; - - fn try_from(value: PrismsUnchecked) -> Result { - Self::new(value.0) + } } } diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs index cd6d9e8..b6fa989 100644 --- a/ogcapi-types/src/movingfeatures/temporal_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -30,8 +30,7 @@ mod tests { let primitive_geometry = TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()); let geometry = TemporalGeometry::Complex( - TemporalComplexGeometry::try_from(vec![primitive_geometry.clone(), primitive_geometry]) - .unwrap(), + TemporalComplexGeometry::from(vec![primitive_geometry.clone(), primitive_geometry]), ); let deserialized_geometry: TemporalGeometry = serde_json::from_str( r#"{ From 5b11edde695afec073d6ea7fe34ec9c34945b36a Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:35:37 +0200 Subject: [PATCH 20/31] Implement same length checks at ser/de for datetime aligned types --- .../mfjson_temporal_properties.rs | 73 ++++++++++--- .../temporal_primitive_geometry.rs | 101 ++++++++++-------- .../src/movingfeatures/temporal_property.rs | 46 +++++++- 3 files changed, 161 insertions(+), 59 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs index cac42d3..9ba8fca 100644 --- a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -1,13 +1,14 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{ser, Deserialize, Serialize, Serializer}; +use serde_json::json; use super::temporal_property::Interpolation; /// MF-JSON TemporalProperties /// -/// A TemporalProperties object is a JSON array of ParametricValues objects that groups a collection of dynamic +/// A TemporalProperties object is a JSON array of ParametricValues objects that groups a collection of dynamic /// non-spatial attributes and its parametric values with time. /// /// See [7.2.2 MF-JSON TemporalProperties](https://docs.ogc.org/is/19-045r3/19-045r3.html#tproperties) @@ -15,15 +16,54 @@ use super::temporal_property::Interpolation; /// Opposed to [TemporalProperty](super::temporal_property::TemporalProperty) values for all /// represented properties are all measured at the same points in time. // TODO enforce same length of datetimes and values -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Debug, Clone, PartialEq)] pub struct MFJsonTemporalProperties { pub datetimes: Vec>, #[serde(flatten)] pub values: HashMap, } -/// A ParametricValues object is a JSON object that represents a collection of parametric values of dynamic non-spatial -/// attributes that are ascertained at the same times. A parametric value may be a time-varying measure, a sequence of +#[derive(Debug, Clone, PartialEq, Deserialize)] +struct MFJsonTemporalPropertiesUnchecked { + datetimes: Vec>, + values: HashMap, +} + +impl TryFrom for MFJsonTemporalProperties { + type Error = &'static str; + + fn try_from(value: MFJsonTemporalPropertiesUnchecked) -> Result { + let dt_len = value.datetimes.len(); + if value.values.values().all(|property| property.len() == dt_len) { + Err("all values and datetimes must be of same length") + } else { + Ok(Self { + datetimes: value.datetimes, + values: value.values, + }) + } + } +} + +impl Serialize for MFJsonTemporalProperties { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let dt_len = self.datetimes.len(); + if self.values.values().all(|property| property.len() == dt_len) { + Err(ser::Error::custom( + "all values and datetimes must be of same length", + )) + } else { + let value = json!(self); + value.serialize(serializer) + } + } +} + +/// A ParametricValues object is a JSON object that represents a collection of parametric values of dynamic non-spatial +/// attributes that are ascertained at the same times. A parametric value may be a time-varying measure, a sequence of /// texts, or a sequence of images. Even though the parametric value may depend on the spatiotemporal location, /// MF-JSON Prism only considers the temporal dependencies of their changes of value. /// @@ -37,9 +77,9 @@ pub enum ParametricValues { /// Allowed Interpolations: Discrete, Step, Linear, Regression interpolation: Option, description: Option, - /// The "form" member is optional and its value is a JSON string as a common code (3 characters) described in - /// the [Code List Rec 20 by the UN Centre for Trade Facilitation and Electronic Business (UN/CEFACT)](https://www.unece.org/uncefact/codelistrecs.html) or a - /// URL specifying the unit of measurement. This member is applied only for a temporal property whose value + /// The "form" member is optional and its value is a JSON string as a common code (3 characters) described in + /// the [Code List Rec 20 by the UN Centre for Trade Facilitation and Electronic Business (UN/CEFACT)](https://www.unece.org/uncefact/codelistrecs.html) or a + /// URL specifying the unit of measurement. This member is applied only for a temporal property whose value /// type is Measure. form: Option, }, @@ -61,14 +101,23 @@ pub enum ParametricValues { }, } +impl ParametricValues { + fn len(&self) -> usize { + match self { + Self::Measure{values, ..} => values.len(), + Self::Text{values, ..} => values.len(), + Self::Image{values, ..} => values.len(), + } + } +} + #[cfg(test)] mod tests { - use super::*; + use super::*; #[test] fn serde_mfjson_temporal_properties() { - // https://developer.ogc.org/api/movingfeatures/index.html#tag/TemporalProperty/operation/insertTemporalProperty let tp_json = r#"[ { @@ -122,7 +171,7 @@ mod tests { } ]"#; - let _: Vec = serde_json::from_str(tp_json).expect("Failed to parse MF-JSON Temporal Properties"); - + let _: Vec = + serde_json::from_str(tp_json).expect("Failed to parse MF-JSON Temporal Properties"); } } diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index a069c91..f464823 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -1,6 +1,7 @@ use chrono::{DateTime, Utc}; use geojson::{JsonObject, LineStringType, PointType, PolygonType}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer, ser}; +use serde_json::json; use super::{crs::Crs, trs::Trs}; @@ -58,7 +59,7 @@ pub enum Value { ///TemporalPrimitiveGeometry object with the "MovingPoint" type. MovingPoint { #[serde(flatten)] - dt_coords: SameLengthDateTimeCoordinatesVecs, PointType>, + dt_coords: DateTimeCoords, PointType>, #[serde(flatten)] base_representation: Option, }, @@ -69,7 +70,7 @@ pub enum Value { ///TemporalPrimitiveGeometry object with the "MovingLineString" type. MovingLineString { #[serde(flatten)] - dt_coords: SameLengthDateTimeCoordinatesVecs, LineStringType>, + dt_coords: DateTimeCoords, LineStringType>, }, ///The type represents the prism of a time-parameterized 2-dimensional (2D) geometric primitive (Polygon), whose ///leaf geometry at a time position is a 2D polygonal object in a particular period. The list of points are in @@ -78,7 +79,7 @@ pub enum Value { ///air pollution can be shared as a TemporalPrimitiveGeometry object with the "MovingPolygon" type. MovingPolygon { #[serde(flatten)] - dt_coords: SameLengthDateTimeCoordinatesVecs, PolygonType>, + dt_coords: DateTimeCoords, PolygonType>, }, ///The type represents the prism of a time-parameterized point cloud whose leaf geometry at a time position is a ///set of points in a particular period. Intuitively a temporal geometry of a continuous movement of point set @@ -87,67 +88,78 @@ pub enum Value { ///type. MovingPointCloud { #[serde(flatten)] - dt_coords: SameLengthDateTimeCoordinatesVecs, Vec>, + dt_coords: DateTimeCoords, Vec>, }, } impl TryFrom<(Vec>, Vec)> for Value { type Error = String; fn try_from(value: (Vec>, Vec)) -> Result { - let dt_coords = SameLengthDateTimeCoordinatesVecs::try_new(value.0, value.1)?; + let dt_coords = DateTimeCoordsUnchecked{ + datetimes: value.0, + coordinates: value.1 + }.try_into()?; Ok(Self::MovingPoint { dt_coords, base_representation: None }) } } +impl TryFrom<(Vec, Vec)> for DateTimeCoords{ + type Error = &'static str; + fn try_from(value: (Vec, Vec)) -> Result { + // TODO does it make sense to keep this check as + // attributes are now pub anyways and same length is now checked + // at serialization? Maybe this should be just From? + DateTimeCoordsUnchecked{ + datetimes: value.0, + coordinates: value.1 + }.try_into() + } +} + #[derive(Deserialize)] -struct SameLengthDateTimeCoordinatesVecsUnchecked { +struct DateTimeCoordsUnchecked { datetimes: Vec, coordinates: Vec, } -#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] -#[serde(try_from = "SameLengthDateTimeCoordinatesVecsUnchecked")] -pub struct SameLengthDateTimeCoordinatesVecs { - datetimes: Vec, - coordinates: Vec, +#[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] +#[serde(try_from = "DateTimeCoordsUnchecked")] +pub struct DateTimeCoords { + pub datetimes: Vec, + pub coordinates: Vec, } -impl SameLengthDateTimeCoordinatesVecs { - pub fn try_new(datetimes: Vec, coordinates: Vec) -> Result { - if coordinates.len() != datetimes.len() { - Err("coordinates and datetimes must be of same length!".to_string()) - } else { - Ok(Self { - datetimes, - coordinates, - }) +impl Serialize for DateTimeCoords{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.coordinates.len() != self.datetimes.len() { + Err(ser::Error::custom("coordinates and datetimes must be of same length")) + }else{ + let value = json!(self); + value.serialize(serializer) } + } - - pub fn append(&mut self, other: &mut SameLengthDateTimeCoordinatesVecs) { - self.datetimes.append(&mut other.datetimes); - self.coordinates.append(&mut other.coordinates); - } - - pub fn datetimes(&self) -> &[A] { - self.datetimes.as_slice() - } - - pub fn coordinates(&self) -> &[B] { - self.coordinates.as_slice() - } - } -impl TryFrom> - for SameLengthDateTimeCoordinatesVecs +impl TryFrom> + for DateTimeCoords { - type Error = String; + type Error = &'static str; fn try_from( - value: SameLengthDateTimeCoordinatesVecsUnchecked, + value: DateTimeCoordsUnchecked, ) -> Result { - Self::try_new(value.datetimes, value.coordinates) + if value.coordinates.len() != value.datetimes.len() { + Err("coordinates and datetimes must be of same length") + }else{ + Ok(Self{ + datetimes: value.datetimes, + coordinates: value.coordinates + }) + } } } @@ -228,7 +240,7 @@ mod tests { datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); } let moving_point = Value::MovingPoint { - dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates).unwrap(), + dt_coords: (datetimes, coordinates).try_into().unwrap(), base_representation: None, }; let jo: JsonObject = serde_json::from_str( @@ -252,9 +264,8 @@ mod tests { } let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new(Value::MovingPoint { - dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates) - .unwrap(), - base_representation: None, + dt_coords: (datetimes, coordinates).try_into().unwrap(), + base_representation: None, }); let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( r#"{ @@ -314,7 +325,7 @@ mod tests { } let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new( Value::MovingPoint{ - dt_coords: SameLengthDateTimeCoordinatesVecs::try_new(datetimes, coordinates).unwrap(), + dt_coords: (datetimes, coordinates).try_into().unwrap(), base_representation: Some(BaseRepresentation{ base: Base{ r#type: "glTF".to_string(), diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index 53aa9bb..98b5129 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; +use serde::{ser, Deserialize, Serialize, Serializer}; +use serde_json::json; use crate::common::Links; @@ -40,7 +41,8 @@ pub enum TemporalPropertyValue { /// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. /// /// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Deserialize, Debug, Default, Clone, PartialEq)] +#[serde(try_from = "TemporalPrimitiveValueUnchecked")] pub struct TemporalPrimitiveValue { /// A unique identifier to the temporal primitive value. // TODO mandatory according to https://docs.ogc.org/is/22-003r3/22-003r3.html#_overview_13 @@ -55,6 +57,46 @@ pub struct TemporalPrimitiveValue { pub interpolation: Interpolation, } +#[derive(Debug, Clone, PartialEq, Deserialize)] +struct TemporalPrimitiveValueUnchecked { + id: Option, + datetimes: Vec>, + values: Vec, + interpolation: Interpolation, +} + +impl TryFrom> for TemporalPrimitiveValue{ + type Error = &'static str; + + fn try_from(value: TemporalPrimitiveValueUnchecked) -> Result { + if value.values.len() != value.datetimes.len() { + Err("values and datetimes must be of same length") + }else{ + Ok(Self{ + id: value.id, + interpolation: value.interpolation, + datetimes: value.datetimes, + values: value.values + }) + } + } +} + +impl Serialize for TemporalPrimitiveValue{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + if self.values.len() != self.datetimes.len() { + Err(ser::Error::custom("values and datetimes must be of same length")) + }else{ + let value = json!(self); + value.serialize(serializer) + } + + } +} + /// See [ParametricValues Object -> "interpolation"](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] pub enum Interpolation { From 93d6eb0c0bf7ea420081c8b268ee744a523d69d7 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:17:37 +0200 Subject: [PATCH 21/31] Drop foreign members for temporal primitive geometry as it can't be flattend due to flattening of 'Value' --- .../src/movingfeatures/temporal_primitive_geometry.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index f464823..3a50448 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use geojson::{JsonObject, LineStringType, PointType, PolygonType}; +use geojson::{LineStringType, PointType, PolygonType}; use serde::{Deserialize, Serialize, Serializer, ser}; use serde_json::json; @@ -20,7 +20,6 @@ pub struct TemporalPrimitiveGeometry { pub interpolation: Interpolation, pub crs: Option, pub trs: Option, - pub foreign_members: Option, } impl TemporalPrimitiveGeometry { @@ -31,7 +30,6 @@ impl TemporalPrimitiveGeometry { interpolation: Interpolation::default(), crs: Default::default(), trs: Default::default(), - foreign_members: Default::default(), } } } @@ -229,6 +227,8 @@ pub struct Orientation { #[cfg(test)] mod tests { + use geojson::JsonObject; + use super::*; #[test] From 7c5938c11327fe3f47e368f5ea2f2a3bb4ed8cf2 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:34:28 +0200 Subject: [PATCH 22/31] Enforce Same Length DateTimeCoords and add convenience functions --- .../temporal_primitive_geometry.rs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 3a50448..4fde7d2 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -104,9 +104,6 @@ impl TryFrom<(Vec>, Vec)> for Value { impl TryFrom<(Vec, Vec)> for DateTimeCoords{ type Error = &'static str; fn try_from(value: (Vec, Vec)) -> Result { - // TODO does it make sense to keep this check as - // attributes are now pub anyways and same length is now checked - // at serialization? Maybe this should be just From? DateTimeCoordsUnchecked{ datetimes: value.0, coordinates: value.1 @@ -123,8 +120,23 @@ struct DateTimeCoordsUnchecked { #[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] #[serde(try_from = "DateTimeCoordsUnchecked")] pub struct DateTimeCoords { - pub datetimes: Vec, - pub coordinates: Vec, + datetimes: Vec, + coordinates: Vec, +} + +impl DateTimeCoords { + pub fn append(&mut self, other: &mut Self) { + self.datetimes.append(&mut other.datetimes); + self.coordinates.append(&mut other.coordinates); + } + + pub fn datetimes(&self) -> &[A] { + self.datetimes.as_slice() + } + + pub fn coordinates(&self) -> &[B] { + self.coordinates.as_slice() + } } impl Serialize for DateTimeCoords{ From 53c1f809fddc367ad6a0ba971109ae8a6245ba3a Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:43:58 +0200 Subject: [PATCH 23/31] Fix time type in feature --- ogcapi-types/src/features/feature.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 3dbdda2..b15126e 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -85,10 +85,9 @@ pub struct Feature { #[serde(default)] pub assets: HashMap, #[cfg(feature = "movingfeatures")] - #[serde(serialize_with = "crate::common::serialize_interval")] /// Life span information for the moving feature. /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) - pub time: Vec>>>, + pub time: [Option>; 2], #[cfg(feature = "movingfeatures")] // TODO should this be #[serde(default)] instead of option? pub crs: Option, From 14b4f0c48878f6acca895898cde17be75c750b22 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:10:02 +0200 Subject: [PATCH 24/31] Add utoipa support --- ogcapi-types/src/common/extent.rs | 2 +- ogcapi-types/src/features/feature.rs | 8 ++------ ogcapi-types/src/movingfeatures/crs.rs | 3 ++- .../movingfeatures/mfjson_temporal_properties.rs | 5 +++-- .../src/movingfeatures/temporal_complex_geometry.rs | 5 +++-- .../src/movingfeatures/temporal_geometry.rs | 3 ++- .../movingfeatures/temporal_primitive_geometry.rs | 5 +++-- .../src/movingfeatures/temporal_properties.rs | 11 ++++++----- .../src/movingfeatures/temporal_property.rs | 13 +++++++------ ogcapi-types/src/movingfeatures/trs.rs | 3 ++- 10 files changed, 31 insertions(+), 27 deletions(-) diff --git a/ogcapi-types/src/common/extent.rs b/ogcapi-types/src/common/extent.rs index cb06d54..cc46f80 100644 --- a/ogcapi-types/src/common/extent.rs +++ b/ogcapi-types/src/common/extent.rs @@ -73,7 +73,7 @@ impl Default for TemporalExtent { } } -fn serialize_interval( +pub(crate) fn serialize_interval( interval: &Vec<[Option>; 2]>, serializer: S, ) -> Result diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index b15126e..ebc27b0 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -50,12 +50,7 @@ pub fn geometry() -> Schema { /// Abstraction of real world phenomena (ISO 19101-1:2014) #[serde_with::skip_serializing_none] -<<<<<<< HEAD #[derive(Deserialize, Serialize, ToSchema, Debug, Clone, PartialEq)] -======= -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] -#[serde(rename = "camelCase")] ->>>>>>> ba7e7ad (Integrate with moving features with collection, feature collection and features) pub struct Feature { #[serde(default)] pub id: Option, @@ -85,9 +80,10 @@ pub struct Feature { #[serde(default)] pub assets: HashMap, #[cfg(feature = "movingfeatures")] + #[serde(serialize_with="crate::common::serialize_interval")] /// Life span information for the moving feature. /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) - pub time: [Option>; 2], + pub time: Vec<[Option>; 2]>, #[cfg(feature = "movingfeatures")] // TODO should this be #[serde(default)] instead of option? pub crs: Option, diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs index 1f3310c..83d2eb7 100644 --- a/ogcapi-types/src/movingfeatures/crs.rs +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use std::str::FromStr; use crate::common; @@ -6,7 +7,7 @@ use crate::common; /// MF-JSON uses a CRS as described in in (GeoJSON:2008)[https://geojson.org/geojson-spec#coordinate-reference-system-objects] /// See (7.2.3 CoordinateReferenceSystem Object)[https://docs.ogc.org/is/19-045r3/19-045r3.html#crs] /// See (6. Overview of Moving features JSON Encodings)[https://docs.ogc.org/is/19-045r3/19-045r3.html#_overview_of_moving_features_json_encodings_informative] -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] #[serde(tag = "type", content = "properties")] pub enum Crs { /// A Named CRS object indicates a coordinate reference system by name. In this case, the value of its "type" member diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs index 9ba8fca..0dc1ee6 100644 --- a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use chrono::{DateTime, Utc}; use serde::{ser, Deserialize, Serialize, Serializer}; use serde_json::json; +use utoipa::ToSchema; use super::temporal_property::Interpolation; @@ -16,7 +17,7 @@ use super::temporal_property::Interpolation; /// Opposed to [TemporalProperty](super::temporal_property::TemporalProperty) values for all /// represented properties are all measured at the same points in time. // TODO enforce same length of datetimes and values -#[derive(Deserialize, Debug, Clone, PartialEq)] +#[derive(Deserialize, Debug, Clone, PartialEq, ToSchema)] pub struct MFJsonTemporalProperties { pub datetimes: Vec>, #[serde(flatten)] @@ -68,7 +69,7 @@ impl Serialize for MFJsonTemporalProperties { /// MF-JSON Prism only considers the temporal dependencies of their changes of value. /// /// See [7.2.2.1 MF-JSON ParametricValues](https://docs.ogc.org/is/19-045r3/19-045r3.html#pvalues) -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] #[serde(tag = "type")] pub enum ParametricValues { /// The "values" member contains any numeric values. diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index bdcd0a1..0866ed7 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use super::{crs::Crs, temporal_primitive_geometry::TemporalPrimitiveGeometry, trs::Trs}; -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, ToSchema)] pub enum Type { #[default] MovingGeometryCollection, @@ -14,7 +15,7 @@ pub enum Type { /// JSON array of a set of TemporalPrimitiveGeometry instances having at least one element in the array. /// /// See [7.2.1.2. TemporalComplexGeometry Object](https://docs.ogc.org/is/19-045r3/19-045r3.html#tcomplex) -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] pub struct TemporalComplexGeometry { pub r#type: Type, pub prisms: Vec, diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs index b6fa989..092965c 100644 --- a/ogcapi-types/src/movingfeatures/temporal_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -1,11 +1,12 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; use super::{ temporal_complex_geometry::TemporalComplexGeometry, temporal_primitive_geometry::TemporalPrimitiveGeometry, }; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] #[serde(untagged)] pub enum TemporalGeometry { Primitive(TemporalPrimitiveGeometry), diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 4fde7d2..8784d31 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -2,6 +2,7 @@ use chrono::{DateTime, Utc}; use geojson::{LineStringType, PointType, PolygonType}; use serde::{Deserialize, Serialize, Serializer, ser}; use serde_json::json; +use utoipa::ToSchema; use super::{crs::Crs, trs::Trs}; @@ -11,7 +12,7 @@ use super::{crs::Crs, trs::Trs}; /// movement of a geographic feature whose leaf geometry at a time instant is drawn by a primitive geometry such as a /// point, linestring, and polygon in the two- or three-dimensional spatial coordinate system, or a point cloud in the /// three-dimensional spatial coordinate system. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] pub struct TemporalPrimitiveGeometry { pub id: Option, #[serde(flatten)] @@ -179,7 +180,7 @@ impl TryFrom> ///"Cubic") or a URL (e.g., "") /// ///See [7.2.10 MotionCurve Objects](https://docs.ogc.org/is/19-045r3/19-045r3.html#interpolation) -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] pub enum Interpolation { ///The positions are NOT connected. The position is valid only at the time instant in datetimes Discrete, diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs index d783409..327cce1 100644 --- a/ogcapi-types/src/movingfeatures/temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -use crate::common::Links; +use crate::common::Link; use super::{ mfjson_temporal_properties::MFJsonTemporalProperties, temporal_property::TemporalProperty, @@ -9,17 +10,17 @@ use super::{ /// A TemporalProperties object consists of the set of [TemporalProperty] or a set of [MFJsonTemporalProperties]. /// /// See [8.8 TemporalProperties](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperties-section) -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TemporalProperties { pub temporal_properties: TemporalPropertiesValue, - pub links: Option, + pub links: Option>, pub time_stamp: Option, pub number_matched: Option, pub number_returned: Option, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, ToSchema)] #[serde(untagged)] pub enum TemporalPropertiesValue { /// [MFJsonTemporalProperties] allows to represent multiple property values all measured at the same points in time. @@ -43,7 +44,7 @@ mod tests { #[test] fn serde_temporal_properties() { - let links: Links = vec![ + let links: Vec = vec![ Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties","self").mediatype("application/json"), Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2","next").mediatype("application/json"), ]; diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index 98b5129..4389916 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -1,8 +1,9 @@ use chrono::{DateTime, Utc}; use serde::{ser, Deserialize, Serialize, Serializer}; use serde_json::json; +use utoipa::ToSchema; -use crate::common::Links; +use crate::common::Link; /// A temporal property object is a collection of dynamic non-spatial attributes and their temporal values with time. /// An abbreviated copy of this information is returned for each TemporalProperty in the @@ -10,7 +11,7 @@ use crate::common::Links; /// The schema for the temporal property object presented in this clause is an extension of the [ParametricValues Object](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) defined in [MF-JSON](https://docs.ogc.org/is/22-003r3/22-003r3.html#OGC_19-045r3). /// /// See [8.9. TemporalProperty](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalProperty-section) -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TemporalProperty { /// An identifier for the resource assigned by an external entity. @@ -21,12 +22,12 @@ pub struct TemporalProperty { pub form: Option, /// A short description pub description: Option, - pub links: Option, + pub links: Option>, } /// A predefined temporal property type. /// -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] #[serde(tag = "type", content = "valueSequence")] pub enum TemporalPropertyValue { TBoolean(Vec>), @@ -41,7 +42,7 @@ pub enum TemporalPropertyValue { /// {root}/collections/{collectionId}/items/{mFeatureId}/tproperties/{tPropertyName} response. /// /// See [8.10. TemporalPrimitiveValue](https://docs.ogc.org/is/22-003r3/22-003r3.html#resource-temporalPrimitiveValue-section) -#[derive(Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] #[serde(try_from = "TemporalPrimitiveValueUnchecked")] pub struct TemporalPrimitiveValue { /// A unique identifier to the temporal primitive value. @@ -98,7 +99,7 @@ impl Serialize for TemporalPrimitiveValue{ } /// See [ParametricValues Object -> "interpolation"](https://docs.opengeospatial.org/is/19-045r3/19-045r3.html#tproperties) -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, ToSchema)] pub enum Interpolation { /// The sampling of the attribute occurs such that it is not possible to regard the series as continuous; thus, /// there is no interpolated value if t is not an element in "datetimes". diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs index 1658518..f41a5ab 100644 --- a/ogcapi-types/src/movingfeatures/trs.rs +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] #[serde(tag = "type", content = "properties")] pub enum Trs { Name { name: String }, // r#type: String, From 7e70a7632eb781e5bb53e452ad3793649379c9d0 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:15:32 +0200 Subject: [PATCH 25/31] Move moving features driver to separate PR #27 --- ogcapi-drivers/Cargo.toml | 2 - ogcapi-drivers/src/blanket/mod.rs | 103 ------------------------------ ogcapi-drivers/src/lib.rs | 49 -------------- 3 files changed, 154 deletions(-) delete mode 100644 ogcapi-drivers/src/blanket/mod.rs diff --git a/ogcapi-drivers/Cargo.toml b/ogcapi-drivers/Cargo.toml index f2235e0..4d0c674 100644 --- a/ogcapi-drivers/Cargo.toml +++ b/ogcapi-drivers/Cargo.toml @@ -16,7 +16,6 @@ default = ["common", "edr", "features", "processes", "tiles"] # drivers postgres = ["sqlx", "rink-core", "url"] s3 = ["aws-config", "aws-sdk-s3"] -blanket = [] # standards common = ["ogcapi-types/common"] @@ -26,7 +25,6 @@ processes = ["common", "ogcapi-types/processes"] stac = ["features", "ogcapi-types/stac", "s3"] styles = ["ogcapi-types/styles"] tiles = ["common", "ogcapi-types/tiles"] -movingfeatures = ["common", "features", "ogcapi-types/movingfeatures"] [dependencies] anyhow = { workspace = true } diff --git a/ogcapi-drivers/src/blanket/mod.rs b/ogcapi-drivers/src/blanket/mod.rs deleted file mode 100644 index 3bd55c9..0000000 --- a/ogcapi-drivers/src/blanket/mod.rs +++ /dev/null @@ -1,103 +0,0 @@ -use anyhow::anyhow; - -#[cfg(feature = "movingfeatures")] -use ogcapi_types::movingfeatures::{ - temporal_complex_geometry::TemporalComplexGeometry, - temporal_geometry::TemporalGeometry, temporal_primitive_geometry::TemporalPrimitiveGeometry, -}; - -#[cfg(feature = "movingfeatures")] -use crate::{FeatureTransactions, TemporalGeometryTransactions}; - -#[cfg(feature = "movingfeatures")] -#[async_trait::async_trait] -impl TemporalGeometryTransactions for T -where - T: FeatureTransactions, -{ - async fn create_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - temporal_geometry: &TemporalPrimitiveGeometry, - ) -> anyhow::Result { - let crs = temporal_geometry - .crs - .clone() - .unwrap_or_default() - .try_into() - .map_err(|e: String| anyhow!(e))?; - let mut feature = self - .read_feature(collection, m_feature_id, &crs) - .await? - .ok_or(anyhow!("Feature not found!"))?; - let mut temporal_geometry = temporal_geometry.clone(); - let id = match feature.temporal_geometry { - None => { - temporal_geometry.id = Some(0.to_string()); - feature.temporal_geometry = - Some(TemporalGeometry::Primitive(temporal_geometry.clone())); - temporal_geometry.id.unwrap() - } - Some(TemporalGeometry::Primitive(tg)) => { - temporal_geometry.id = Some(1.to_string()); - feature.temporal_geometry = - Some(TemporalGeometry::Complex(TemporalComplexGeometry { - prisms: vec![tg, temporal_geometry.clone()], - r#type: Default::default(), - crs: Default::default(), - trs: Default::default(), - })); - temporal_geometry.id.unwrap() - } - Some(TemporalGeometry::Complex(ref mut tg)) => { - // This re-uses ids, might lead to surprising behaviour when deleting a tg and then - // adding a new one - temporal_geometry.id = Some(tg.prisms.len().to_string()); - tg.prisms.push(temporal_geometry.clone()); - temporal_geometry.id.unwrap() - } - }; - self.update_feature(&feature).await?; - Ok(id) - } - async fn read_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - ) -> anyhow::Result> { - let feature = self - .read_feature(collection, m_feature_id, &Default::default()) - .await? - .ok_or(anyhow!("Feature not found!"))?; - Ok(feature.temporal_geometry) - } - async fn delete_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - t_geometry_id: &str, - ) -> anyhow::Result<()> { - let mut feature = self - .read_feature(collection, m_feature_id, &Default::default()) - .await? - .ok_or(anyhow!("Feature not found!"))?; - match feature.temporal_geometry { - Some(TemporalGeometry::Primitive(tg)) if tg.id.as_ref().is_some_and(|id| id == t_geometry_id) => { - feature.temporal_geometry = None; - Ok(()) - } - Some(TemporalGeometry::Complex(ref mut tg)) => { - if tg.prisms.len() > 2 { - tg.prisms - .pop_if(|tg| tg.id.as_ref().is_some_and(|tg_id| tg_id == t_geometry_id)) - .ok_or(anyhow!("Temporal Geometry not found."))?; - Ok(()) - } else { - Err(anyhow!("Prisms must have at least one value. Try to delete the whole prism.")) - } - } - _ => Err(anyhow!(format!("TemporalGeometry with id {t_geometry_id} not found"))), - } - } -} diff --git a/ogcapi-drivers/src/lib.rs b/ogcapi-drivers/src/lib.rs index 01caec3..9b728d3 100644 --- a/ogcapi-drivers/src/lib.rs +++ b/ogcapi-drivers/src/lib.rs @@ -2,15 +2,11 @@ pub mod postgres; #[cfg(feature = "s3")] pub mod s3; -#[cfg(feature = "blanket")] -pub mod blanket; #[cfg(feature = "common")] use ogcapi_types::common::{Collection, Collections, Query as CollectionQuery}; #[cfg(feature = "edr")] use ogcapi_types::edr::{Query as EdrQuery, QueryType}; -#[cfg(feature = "movingfeatures")] -use ogcapi_types::movingfeatures::{temporal_geometry::TemporalGeometry, temporal_properties::TemporalProperties, temporal_primitive_geometry::TemporalPrimitiveGeometry}; #[cfg(feature = "processes")] use ogcapi_types::processes::{Results, StatusInfo}; #[cfg(feature = "stac")] @@ -120,48 +116,3 @@ pub trait TileTransactions: Send + Sync { col: u32, ) -> anyhow::Result>; } - - -#[cfg(feature = "movingfeatures")] -#[async_trait::async_trait] -pub trait TemporalGeometryTransactions: Send + Sync { - async fn create_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - temporal_geometry: &TemporalPrimitiveGeometry - ) -> anyhow::Result; - async fn read_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - ) -> anyhow::Result>; - async fn delete_temporal_geometry( - &self, - collection: &str, - m_feature_id: &str, - t_geometry_id: &str - ) -> anyhow::Result<()>; -} - -#[cfg(feature = "movingfeatures")] -#[async_trait::async_trait] -pub trait TemporalPropertyTransactions: Send + Sync { - async fn create_temporal_property( - &self, - collection: &str, - m_feature_id: &str, - temporal_geometry: &TemporalPrimitiveGeometry - ) -> anyhow::Result; - async fn read_temporal_property( - &self, - collection: &str, - m_feature_id: &str, - ) -> anyhow::Result>; - async fn delete_temporal_property( - &self, - collection: &str, - m_feature_id: &str, - t_properties_name: &str - ) -> anyhow::Result<()>; -} From ad4edeafbcfb2d626bbef097aa9b39904cd8ea57 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:18:04 +0200 Subject: [PATCH 26/31] Add movingfeatures to changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 859dde1..ed2632b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Dynamic OpenAPI definition extration. +- Dynamic OpenAPI definition extraction. +- Types for `OGCAPI - Moving Features` ### Changed From 49f3c105a0e92865e41543dbddd6cd22db5c0377 Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:26:36 +0200 Subject: [PATCH 27/31] Add serde(default) --- ogcapi-types/src/common/collection.rs | 2 +- ogcapi-types/src/features/feature.rs | 10 +++++----- ogcapi-types/src/features/feature_collection.rs | 3 +++ ogcapi-types/src/lib.rs | 3 +-- ogcapi-types/src/movingfeatures/crs.rs | 2 +- ogcapi-types/src/movingfeatures/temporal_property.rs | 1 - 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index 6341c07..954b068 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -88,7 +88,7 @@ pub struct Collection { #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")] pub assets: std::collections::HashMap, #[cfg(feature = "movingfeatures")] - #[serde(rename = "updateFrequency")] + #[serde(default, rename = "updateFrequency", skip_serializing_if = "Option::is_none")] /// A time interval of sampling location. The time unit of this property is millisecond. pub update_frequency: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index ebc27b0..437cbd1 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -80,21 +80,21 @@ pub struct Feature { #[serde(default)] pub assets: HashMap, #[cfg(feature = "movingfeatures")] - #[serde(serialize_with="crate::common::serialize_interval")] + #[serde(default, serialize_with="crate::common::serialize_interval", skip_serializing_if = "Vec::is_empty")] /// Life span information for the moving feature. /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) pub time: Vec<[Option>; 2]>, #[cfg(feature = "movingfeatures")] - // TODO should this be #[serde(default)] instead of option? + #[serde(default)] pub crs: Option, #[cfg(feature = "movingfeatures")] - // TODO should this be #[serde(default)] instead of option? + #[serde(default)] pub trs: Option, #[cfg(feature = "movingfeatures")] - #[serde(rename = "temporalGeometry")] + #[serde(default, rename = "temporalGeometry")] pub temporal_geometry: Option, #[cfg(feature = "movingfeatures")] - #[serde(rename = "temporalProperties")] + #[serde(default, rename = "temporalProperties")] pub temporal_properties: Option, } diff --git a/ogcapi-types/src/features/feature_collection.rs b/ogcapi-types/src/features/feature_collection.rs index 282535f..546a05c 100644 --- a/ogcapi-types/src/features/feature_collection.rs +++ b/ogcapi-types/src/features/feature_collection.rs @@ -30,10 +30,13 @@ pub struct FeatureCollection { pub number_matched: Option, pub number_returned: Option, #[cfg(feature = "movingfeatures")] + #[serde(default)] pub crs: Option, #[cfg(feature = "movingfeatures")] + #[serde(default)] pub trs: Option, #[cfg(feature = "movingfeatures")] + #[serde(default)] pub bbox: Option, } diff --git a/ogcapi-types/src/lib.rs b/ogcapi-types/src/lib.rs index 89d3f37..35576fe 100644 --- a/ogcapi-types/src/lib.rs +++ b/ogcapi-types/src/lib.rs @@ -23,8 +23,7 @@ pub mod styles; pub mod tiles; /// Types specified in the `OGC API - Moving Features` standard. -// FIXME -// #[cfg(feature = "movingfeatures")] +#[cfg(feature = "movingfeatures")] pub mod movingfeatures; #[cfg(feature = "coverages")] diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs index 83d2eb7..1821c57 100644 --- a/ogcapi-types/src/movingfeatures/crs.rs +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -32,7 +32,7 @@ pub enum Crs { impl Default for Crs { fn default() -> Self { Self::Name { - name: "urn:ogc:def:crs:OGC:1.3:CRS84".to_string(), + name: common::Crs::default().to_urn(), } } } diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index 4389916..a99378d 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -52,7 +52,6 @@ pub struct TemporalPrimitiveValue { /// A sequence of monotonic increasing instants. pub datetimes: Vec>, /// A sequence of dynamic values having the same number of elements as “datetimes”. - // TODO enforce length pub values: Vec, /// A predefined type for a dynamic value (i.e., one of ‘Discrete’, ‘Step’, ‘Linear’, or ‘Regression’). pub interpolation: Interpolation, From 940321e66ce7efdac71ab359776eb7ff5587e8ee Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:27:43 +0200 Subject: [PATCH 28/31] When CRS or TRS are missing use the default spatial or temporal CRS --- ogcapi-types/src/features/feature.rs | 4 ++-- ogcapi-types/src/features/feature_collection.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 437cbd1..34d982f 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -86,10 +86,10 @@ pub struct Feature { pub time: Vec<[Option>; 2]>, #[cfg(feature = "movingfeatures")] #[serde(default)] - pub crs: Option, + pub crs: Crs, #[cfg(feature = "movingfeatures")] #[serde(default)] - pub trs: Option, + pub trs: Trs, #[cfg(feature = "movingfeatures")] #[serde(default, rename = "temporalGeometry")] pub temporal_geometry: Option, diff --git a/ogcapi-types/src/features/feature_collection.rs b/ogcapi-types/src/features/feature_collection.rs index 546a05c..4291ce6 100644 --- a/ogcapi-types/src/features/feature_collection.rs +++ b/ogcapi-types/src/features/feature_collection.rs @@ -31,10 +31,10 @@ pub struct FeatureCollection { pub number_returned: Option, #[cfg(feature = "movingfeatures")] #[serde(default)] - pub crs: Option, + pub crs: crate::movingfeatures::crs::Crs, #[cfg(feature = "movingfeatures")] #[serde(default)] - pub trs: Option, + pub trs: crate::movingfeatures::trs::Trs, #[cfg(feature = "movingfeatures")] #[serde(default)] pub bbox: Option, From 2b9ca5ec3d2af5df4c0711485a82e43aa8143ec9 Mon Sep 17 00:00:00 2001 From: Balthasar Teuscher Date: Sat, 29 Nov 2025 13:48:51 +0100 Subject: [PATCH 29/31] Address some todos and propositions --- ogcapi-types/Cargo.toml | 2 +- ogcapi-types/src/common/collection.rs | 6 +- ogcapi-types/src/features/feature.rs | 6 +- ogcapi-types/src/movingfeatures/crs.rs | 3 +- .../mfjson_temporal_properties.rs | 53 +++++----- ogcapi-types/src/movingfeatures/mod.rs | 2 +- .../temporal_complex_geometry.rs | 2 - .../src/movingfeatures/temporal_geometry.rs | 7 +- .../temporal_primitive_geometry.rs | 99 +++++++++---------- .../src/movingfeatures/temporal_properties.rs | 65 ++++++------ .../src/movingfeatures/temporal_property.rs | 23 ++--- ogcapi-types/src/movingfeatures/trs.rs | 26 +++-- 12 files changed, 150 insertions(+), 144 deletions(-) diff --git a/ogcapi-types/Cargo.toml b/ogcapi-types/Cargo.toml index 17df587..a3dfe42 100644 --- a/ogcapi-types/Cargo.toml +++ b/ogcapi-types/Cargo.toml @@ -11,7 +11,7 @@ keywords.workspace = true include = ["/src", "/assets"] [features] -default = ["common", "edr", "features", "processes", "tiles"] +default = ["common", "edr", "features", "movingfeatures", "processes", "tiles"] # standards common = [] diff --git a/ogcapi-types/src/common/collection.rs b/ogcapi-types/src/common/collection.rs index 954b068..22fb168 100644 --- a/ogcapi-types/src/common/collection.rs +++ b/ogcapi-types/src/common/collection.rs @@ -88,7 +88,11 @@ pub struct Collection { #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")] pub assets: std::collections::HashMap, #[cfg(feature = "movingfeatures")] - #[serde(default, rename = "updateFrequency", skip_serializing_if = "Option::is_none")] + #[serde( + default, + rename = "updateFrequency", + skip_serializing_if = "Option::is_none" + )] /// A time interval of sampling location. The time unit of this property is millisecond. pub update_frequency: Option, #[serde(flatten, default, skip_serializing_if = "Map::is_empty")] diff --git a/ogcapi-types/src/features/feature.rs b/ogcapi-types/src/features/feature.rs index 34d982f..c8f42ae 100644 --- a/ogcapi-types/src/features/feature.rs +++ b/ogcapi-types/src/features/feature.rs @@ -80,7 +80,11 @@ pub struct Feature { #[serde(default)] pub assets: HashMap, #[cfg(feature = "movingfeatures")] - #[serde(default, serialize_with="crate::common::serialize_interval", skip_serializing_if = "Vec::is_empty")] + #[serde( + default, + serialize_with = "crate::common::serialize_interval", + skip_serializing_if = "Vec::is_empty" + )] /// Life span information for the moving feature. /// See [MF-Json 7.2.3 LifeSpan](https://docs.ogc.org/is/19-045r3/19-045r3.html#time) pub time: Vec<[Option>; 2]>, diff --git a/ogcapi-types/src/movingfeatures/crs.rs b/ogcapi-types/src/movingfeatures/crs.rs index 1821c57..bec0e35 100644 --- a/ogcapi-types/src/movingfeatures/crs.rs +++ b/ogcapi-types/src/movingfeatures/crs.rs @@ -1,6 +1,7 @@ +use std::str::FromStr; + use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use std::str::FromStr; use crate::common; diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs index 0dc1ee6..437c4b5 100644 --- a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use chrono::{DateTime, Utc}; -use serde::{ser, Deserialize, Serialize, Serializer}; +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize, Serializer, ser::Error}; use serde_json::json; use utoipa::ToSchema; @@ -16,34 +16,33 @@ use super::temporal_property::Interpolation; /// /// Opposed to [TemporalProperty](super::temporal_property::TemporalProperty) values for all /// represented properties are all measured at the same points in time. -// TODO enforce same length of datetimes and values #[derive(Deserialize, Debug, Clone, PartialEq, ToSchema)] pub struct MFJsonTemporalProperties { - pub datetimes: Vec>, + datetimes: Vec>, #[serde(flatten)] - pub values: HashMap, -} - -#[derive(Debug, Clone, PartialEq, Deserialize)] -struct MFJsonTemporalPropertiesUnchecked { - datetimes: Vec>, values: HashMap, } -impl TryFrom for MFJsonTemporalProperties { - type Error = &'static str; - - fn try_from(value: MFJsonTemporalPropertiesUnchecked) -> Result { - let dt_len = value.datetimes.len(); - if value.values.values().all(|property| property.len() == dt_len) { +impl MFJsonTemporalProperties { + pub fn new( + datetimes: Vec>, + values: HashMap, + ) -> Result { + let dt_len = datetimes.len(); + if values.values().any(|property| property.len() != dt_len) { Err("all values and datetimes must be of same length") } else { - Ok(Self { - datetimes: value.datetimes, - values: value.values, - }) + Ok(Self { datetimes, values }) } } + + pub fn datatimes(&self) -> &[DateTime] { + &self.datetimes + } + + pub fn values(&self) -> &HashMap { + &self.values + } } impl Serialize for MFJsonTemporalProperties { @@ -52,8 +51,12 @@ impl Serialize for MFJsonTemporalProperties { S: Serializer, { let dt_len = self.datetimes.len(); - if self.values.values().all(|property| property.len() == dt_len) { - Err(ser::Error::custom( + if self + .values + .values() + .any(|property| property.len() != dt_len) + { + Err(S::Error::custom( "all values and datetimes must be of same length", )) } else { @@ -105,9 +108,9 @@ pub enum ParametricValues { impl ParametricValues { fn len(&self) -> usize { match self { - Self::Measure{values, ..} => values.len(), - Self::Text{values, ..} => values.len(), - Self::Image{values, ..} => values.len(), + Self::Measure { values, .. } => values.len(), + Self::Text { values, .. } => values.len(), + Self::Image { values, .. } => values.len(), } } } diff --git a/ogcapi-types/src/movingfeatures/mod.rs b/ogcapi-types/src/movingfeatures/mod.rs index 20ae3df..a2913b0 100644 --- a/ogcapi-types/src/movingfeatures/mod.rs +++ b/ogcapi-types/src/movingfeatures/mod.rs @@ -2,8 +2,8 @@ pub mod temporal_complex_geometry; pub mod temporal_geometry; pub mod temporal_primitive_geometry; -pub mod temporal_properties; pub mod mfjson_temporal_properties; +pub mod temporal_properties; pub mod temporal_property; pub mod crs; diff --git a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs index 0866ed7..974b92c 100644 --- a/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_complex_geometry.rs @@ -23,9 +23,7 @@ pub struct TemporalComplexGeometry { pub trs: Option, } - impl From> for TemporalComplexGeometry { - fn from(value: Vec) -> Self { debug_assert!(!value.is_empty()); Self { diff --git a/ogcapi-types/src/movingfeatures/temporal_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_geometry.rs index 092965c..4af7984 100644 --- a/ogcapi-types/src/movingfeatures/temporal_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_geometry.rs @@ -30,9 +30,10 @@ mod tests { } let primitive_geometry = TemporalPrimitiveGeometry::new((datetimes, coordinates).try_into().unwrap()); - let geometry = TemporalGeometry::Complex( - TemporalComplexGeometry::from(vec![primitive_geometry.clone(), primitive_geometry]), - ); + let geometry = TemporalGeometry::Complex(TemporalComplexGeometry::from(vec![ + primitive_geometry.clone(), + primitive_geometry, + ])); let deserialized_geometry: TemporalGeometry = serde_json::from_str( r#"{ "type": "MovingGeometryCollection", diff --git a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs index 8784d31..0bf175c 100644 --- a/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs +++ b/ogcapi-types/src/movingfeatures/temporal_primitive_geometry.rs @@ -94,30 +94,14 @@ pub enum Value { impl TryFrom<(Vec>, Vec)> for Value { type Error = String; fn try_from(value: (Vec>, Vec)) -> Result { - let dt_coords = DateTimeCoordsUnchecked{ - datetimes: value.0, - coordinates: value.1 - }.try_into()?; - Ok(Self::MovingPoint { dt_coords, base_representation: None }) - } -} - -impl TryFrom<(Vec, Vec)> for DateTimeCoords{ - type Error = &'static str; - fn try_from(value: (Vec, Vec)) -> Result { - DateTimeCoordsUnchecked{ - datetimes: value.0, - coordinates: value.1 - }.try_into() + let dt_coords = DateTimeCoords::new(value.0, value.1)?; + Ok(Self::MovingPoint { + dt_coords, + base_representation: None, + }) } } -#[derive(Deserialize)] -struct DateTimeCoordsUnchecked { - datetimes: Vec, - coordinates: Vec, -} - #[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] #[serde(try_from = "DateTimeCoordsUnchecked")] pub struct DateTimeCoords { @@ -125,10 +109,21 @@ pub struct DateTimeCoords { coordinates: Vec, } -impl DateTimeCoords { - pub fn append(&mut self, other: &mut Self) { - self.datetimes.append(&mut other.datetimes); - self.coordinates.append(&mut other.coordinates); +impl DateTimeCoords { + pub fn new(datetimes: Vec, coordinates: Vec) -> Result { + if coordinates.len() != datetimes.len() { + Err("coordinates and datetimes must be of same length") + } else { + Ok(Self { + datetimes, + coordinates, + }) + } + } + + pub fn append(&mut self, other: &mut Self) { + self.datetimes.append(&mut other.datetimes); + self.coordinates.append(&mut other.coordinates); } pub fn datetimes(&self) -> &[A] { @@ -140,36 +135,32 @@ impl DateTimeCoords { } } -impl Serialize for DateTimeCoords{ +#[derive(Deserialize)] +struct DateTimeCoordsUnchecked { + datetimes: Vec, + coordinates: Vec, +} + +impl TryFrom> for DateTimeCoords { + type Error = &'static str; + + fn try_from(value: DateTimeCoordsUnchecked) -> Result { + DateTimeCoords::new(value.datetimes, value.coordinates) + } +} + +impl Serialize for DateTimeCoords { fn serialize(&self, serializer: S) -> Result where S: Serializer, { if self.coordinates.len() != self.datetimes.len() { - Err(ser::Error::custom("coordinates and datetimes must be of same length")) - }else{ + Err(ser::Error::custom( + "coordinates and datetimes must be of same length", + )) + } else { let value = json!(self); - value.serialize(serializer) - } - - } -} - -impl TryFrom> - for DateTimeCoords -{ - type Error = &'static str; - - fn try_from( - value: DateTimeCoordsUnchecked, - ) -> Result { - if value.coordinates.len() != value.datetimes.len() { - Err("coordinates and datetimes must be of same length") - }else{ - Ok(Self{ - datetimes: value.datetimes, - coordinates: value.coordinates - }) + value.serialize(serializer) } } } @@ -253,7 +244,7 @@ mod tests { datetimes.push(DateTime::from_timestamp(i, 0).unwrap()); } let moving_point = Value::MovingPoint { - dt_coords: (datetimes, coordinates).try_into().unwrap(), + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), base_representation: None, }; let jo: JsonObject = serde_json::from_str( @@ -277,8 +268,8 @@ mod tests { } let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new(Value::MovingPoint { - dt_coords: (datetimes, coordinates).try_into().unwrap(), - base_representation: None, + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), + base_representation: None, }); let deserialized_geometry: TemporalPrimitiveGeometry = serde_json::from_str( r#"{ @@ -338,12 +329,12 @@ mod tests { } let geometry: TemporalPrimitiveGeometry = TemporalPrimitiveGeometry::new( Value::MovingPoint{ - dt_coords: (datetimes, coordinates).try_into().unwrap(), + dt_coords: DateTimeCoords::new(datetimes, coordinates).unwrap(), base_representation: Some(BaseRepresentation{ base: Base{ r#type: "glTF".to_string(), href: "http://www.opengis.net/spec/movingfeatures/json/1.0/prism/example/car3dmodel.gltf".to_string() - }, + }, orientations }), }); diff --git a/ogcapi-types/src/movingfeatures/temporal_properties.rs b/ogcapi-types/src/movingfeatures/temporal_properties.rs index 327cce1..197bf9a 100644 --- a/ogcapi-types/src/movingfeatures/temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/temporal_properties.rs @@ -33,6 +33,8 @@ pub enum TemporalPropertiesValue { mod tests { use std::collections::HashMap; + use chrono::DateTime; + use crate::{ common::Link, movingfeatures::{ @@ -49,43 +51,36 @@ mod tests { Link::new("https://data.example.org/collections/mfc-1/items/mf-1/tproperties&offset=2&limit=2","next").mediatype("application/json"), ]; + let datetimes = vec![ + DateTime::parse_from_rfc3339("2011-07-14T22:01:06.000Z").unwrap(), + DateTime::parse_from_rfc3339("2011-07-14T22:01:07.000Z").unwrap(), + DateTime::parse_from_rfc3339("2011-07-14T22:01:08.000Z").unwrap(), + ]; + + let values = HashMap::from([ + ( + "length".to_string(), + ParametricValues::Measure { + values: vec![1.0, 2.4, 1.0], + interpolation: Some(Interpolation::Linear), + description: None, + form: Some("http://qudt.org/vocab/quantitykind/Length".to_string()), + }, + ), + ( + "speed".to_string(), + ParametricValues::Measure { + values: vec![65.0, 70.0, 80.0], + interpolation: Some(Interpolation::Linear), + form: Some("KMH".to_string()), + description: None, + }, + ), + ]); + let temporal_properties = TemporalProperties { temporal_properties: TemporalPropertiesValue::MFJsonTemporalProperties(vec![ - MFJsonTemporalProperties { - datetimes: vec![ - // TODO does type actually need to be UTC or could it be FixedOffset - // aswell? Converting to UTC loses the information of the original offset! - chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:06.000Z") - .unwrap() - .into(), - chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:07.000Z") - .unwrap() - .into(), - chrono::DateTime::parse_from_rfc3339("2011-07-14T22:01:08.000Z") - .unwrap() - .into(), - ], - values: HashMap::from([ - ( - "length".to_string(), - ParametricValues::Measure { - values: vec![1.0, 2.4, 1.0], - interpolation: Some(Interpolation::Linear), - description: None, - form: Some("http://qudt.org/vocab/quantitykind/Length".to_string()), - }, - ), - ( - "speed".to_string(), - ParametricValues::Measure { - values: vec![65.0, 70.0, 80.0], - interpolation: Some(Interpolation::Linear), - form: Some("KMH".to_string()), - description: None, - }, - ), - ]), - }, + MFJsonTemporalProperties::new(datetimes, values).unwrap(), ]), links: Some(links), time_stamp: Some("2021-09-01T12:00:00Z".into()), diff --git a/ogcapi-types/src/movingfeatures/temporal_property.rs b/ogcapi-types/src/movingfeatures/temporal_property.rs index a99378d..58fb9d6 100644 --- a/ogcapi-types/src/movingfeatures/temporal_property.rs +++ b/ogcapi-types/src/movingfeatures/temporal_property.rs @@ -1,5 +1,5 @@ use chrono::{DateTime, Utc}; -use serde::{ser, Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer, ser::Error}; use serde_json::json; use utoipa::ToSchema; @@ -65,35 +65,36 @@ struct TemporalPrimitiveValueUnchecked { interpolation: Interpolation, } -impl TryFrom> for TemporalPrimitiveValue{ +impl TryFrom> for TemporalPrimitiveValue { type Error = &'static str; fn try_from(value: TemporalPrimitiveValueUnchecked) -> Result { if value.values.len() != value.datetimes.len() { Err("values and datetimes must be of same length") - }else{ - Ok(Self{ + } else { + Ok(Self { id: value.id, interpolation: value.interpolation, - datetimes: value.datetimes, - values: value.values + datetimes: value.datetimes, + values: value.values, }) } } } -impl Serialize for TemporalPrimitiveValue{ +impl Serialize for TemporalPrimitiveValue { fn serialize(&self, serializer: S) -> Result where S: Serializer, { if self.values.len() != self.datetimes.len() { - Err(ser::Error::custom("values and datetimes must be of same length")) - }else{ + Err(S::Error::custom( + "values and datetimes must be of same length", + )) + } else { let value = json!(self); - value.serialize(serializer) + value.serialize(serializer) } - } } diff --git a/ogcapi-types/src/movingfeatures/trs.rs b/ogcapi-types/src/movingfeatures/trs.rs index f41a5ab..9794b20 100644 --- a/ogcapi-types/src/movingfeatures/trs.rs +++ b/ogcapi-types/src/movingfeatures/trs.rs @@ -4,14 +4,21 @@ use utoipa::ToSchema; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] #[serde(tag = "type", content = "properties")] pub enum Trs { - Name { name: String }, // r#type: String, - Link { r#type: Option, href: String }, // r#type: String, - // properties: TrsProperties, + Name { + name: String, + }, // r#type: String, + Link { + r#type: Option, + href: String, + }, // r#type: String, + // properties: TrsProperties, } impl Default for Trs { fn default() -> Self { - Self::Name { name: "urn:ogc:data:time:iso8601".to_string() } + Self::Name { + name: "urn:ogc:data:time:iso8601".to_string(), + } } } @@ -21,18 +28,19 @@ mod tests { use super::*; #[test] - fn serde_json(){ - + fn serde_json() { // TODO this contradicts example from https://developer.ogc.org/api/movingfeatures/index.html#tag/MovingFeatures/operation/retrieveMovingFeatures // Example from https://docs.ogc.org/is/19-045r3/19-045r3.html#_7_2_3_1_named_crs - let trs: Trs = serde_json::from_str(r#" + let trs: Trs = serde_json::from_str( + r#" { "type": "Name", "properties": {"name": "urn:ogc:data:time:iso8601"} } - "#).expect("Failed to parse Trs"); + "#, + ) + .expect("Failed to parse Trs"); let expected_trs = Trs::default(); assert_eq!(trs, expected_trs); - } } From b4e6a1f1faa5c5ae1e7759ba9085b7078671db52 Mon Sep 17 00:00:00 2001 From: Balthasar Teuscher Date: Sat, 29 Nov 2025 13:52:14 +0100 Subject: [PATCH 30/31] Merge `main` --- .env | 6 +- .github/workflows/ci.yml | 47 +- .gitignore | 3 +- CHANGELOG.md | 15 + Cargo.lock | 1525 ++++++++--------- README.md | 1 + examples/cite-service/Cargo.toml | 16 + examples/cite-service/src/main.rs | 32 + examples/data-loader/Cargo.toml | 11 +- examples/data-loader/src/boundaries.rs | 44 +- examples/data-loader/src/geojson.rs | 2 +- examples/data-loader/src/main.rs | 2 +- examples/demo-service/Cargo.toml | 6 +- examples/demo-service/src/main.rs | 34 +- ogcapi-client/Cargo.toml | 2 +- ogcapi-client/src/processes.rs | 104 +- ogcapi-drivers/Cargo.toml | 4 +- .../20201113131516_initialization.sql | 5 +- ogcapi-drivers/src/lib.rs | 47 +- ogcapi-drivers/src/postgres/collection.rs | 1 - ogcapi-drivers/src/postgres/feature.rs | 43 +- ogcapi-drivers/src/postgres/job.rs | 141 +- ogcapi-drivers/src/postgres/stac.rs | 4 +- ogcapi-drivers/src/s3/collection.rs | 16 +- ogcapi-drivers/tests/job.rs | 89 +- ogcapi-processes/Cargo.toml | 12 +- ogcapi-processes/src/echo.rs | 383 +++++ ogcapi-processes/src/gdal_loader.rs | 43 +- ogcapi-processes/src/geojson_loader.rs | 49 +- ogcapi-processes/src/greeter.rs | 101 +- ogcapi-processes/src/lib.rs | 10 +- ogcapi-processes/src/processor.rs | 14 +- ogcapi-services/Cargo.toml | 22 +- ogcapi-services/assets/openapi/openapi.yaml | 593 ++++++- ogcapi-services/src/config.rs | 5 +- ogcapi-services/src/error.rs | 58 +- ogcapi-services/src/extractors.rs | 12 +- ogcapi-services/src/lib.rs | 6 +- ogcapi-services/src/main.rs | 4 +- ogcapi-services/src/openapi.rs | 7 +- ogcapi-services/src/processes.rs | 546 +++++- ogcapi-services/src/routes/collections.rs | 9 +- ogcapi-services/src/routes/common.rs | 16 +- ogcapi-services/src/routes/features.rs | 81 +- ogcapi-services/src/routes/processes.rs | 413 ++++- ogcapi-services/src/routes/stac.rs | 44 +- ogcapi-services/src/routes/tiles.rs | 5 +- ogcapi-services/src/service.rs | 23 +- ogcapi-services/src/state.rs | 80 +- ogcapi-services/tests/setup.rs | 15 +- ogcapi-types/Cargo.toml | 4 +- ogcapi-types/src/common/bbox.rs | 27 +- ogcapi-types/src/common/exception.rs | 6 + ogcapi-types/src/common/link_rel.rs | 2 +- ogcapi-types/src/features/mod.rs | 2 + ogcapi-types/src/features/queryables.rs | 27 +- ogcapi-types/src/lib.rs | 11 +- ogcapi-types/src/processes/description.rs | 15 +- ogcapi-types/src/processes/execute.rs | 23 +- ogcapi-types/src/processes/job.rs | 26 +- ogcapi-types/src/processes/mod.rs | 2 +- ogcapi-types/src/processes/process.rs | 54 +- ogcapi-types/src/styles/mod.rs | 4 +- 63 files changed, 3517 insertions(+), 1437 deletions(-) create mode 100644 examples/cite-service/Cargo.toml create mode 100644 examples/cite-service/src/main.rs create mode 100644 ogcapi-processes/src/echo.rs diff --git a/.env b/.env index 8486623..779c645 100644 --- a/.env +++ b/.env @@ -3,13 +3,15 @@ RUST_LOG=ogcapi=debug,ogcapi_services=debug,ogcapi_client=debug,tower_http=debug APP_HOST=0.0.0.0 APP_PORT=8484 +# PUBLIC_URL=http://example.org + DB_USER=postgres DB_PASSWORD=password DB_HOST=localhost -DB_PORT=5433 +DB_PORT=5432 DB_NAME=ogcapi -DATABASE_URL=postgresql://postgres:password@localhost:5433/ogcapi +DATABASE_URL=postgresql://postgres:password@localhost:5432/ogcapi AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87efe2e..e0b71ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,12 @@ name: CI on: push: + branches: + - main pull_request: + merge_group: + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: env: CARGO_TERM_COLOR: always @@ -15,7 +20,7 @@ jobs: steps: - name: Dependencies run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config libclang-dev libgdal-dev - + - name: Checkout uses: actions/checkout@v4 @@ -27,9 +32,47 @@ jobs: - name: Format run: cargo fmt --all -- --check - + - name: Clippy run: cargo clippy --workspace --all-features --all-targets -- -D warnings - name: Test run: cargo test --workspace --all-features --all-targets + + ogc-compliance: + name: Validate OGC API Compliance + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup + run: docker compose up db -d + + - name: Load data + run: cargo run -p data-loader -- --input data/ne_110m_populated_places.geojson --collection places + + - name: Start service + run: | + cargo run -p cite-service & + npx wait-on http://localhost:8484/ + + - name: Validate service + uses: geo-engine/ogc-validator@v2 + with: + service-url: http://localhost:8484/ + ogc-api-processes: true + ogc-api-processes-container-tag: 1.3-teamengine-6.0.0-RC2 + echoprocessid: echo + # cf. + ogc-api-processes-ignore: |- + testJobCreationInputValidation + testJobCreationInputRef + ogc-api-features: true + ogc-api-features-container-tag: 1.9-teamengine-6.0.0-RC2 + ogc-api-features-ignore: |- + validateFeaturesWithLimitResponse_NumberMatched + validateFeaturesResponse_NumberMatched + validateFeaturesWithBoundingBoxResponse_NumberMatched + validateFeaturesWithLimitResponse_NumberMatched diff --git a/.gitignore b/.gitignore index 34b6021..79a188e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target -minio \ No newline at end of file +minio +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2632b..0ab6df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dynamic OpenAPI definition extraction. - Types for `OGCAPI - Moving Features` +- Async OGC API - Processes execution (jobs). +- Multipart/related response support for raw OGC API - Processes results with multiple outputs. +- Echo process for testing. +- Dedicate `cite-service` for OGC Cite testsuite in CI. +- Make public base url configurable via `PUBLIC_URL` environment variable. + +### Fixed + +- Respect process execution `response` parameter. +- Service URL for OGC API - Features. +- Minor issues with OGC API - Features conformance. ### Changed - Make features opt-out rather than opt-in for released standards. - Allow integers for feature id. - Build documentation for all features. +- Output type for OGC API - Processes trait (execute). +- Changed fields to status database model for OGC API - Processes. +- Consolidate API definition for OGC cite validation. +- Decouple drivers from app state. ## [0.3.0] - 2025-04-05 diff --git a/Cargo.lock b/Cargo.lock index 646ad07..8cc39bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -25,7 +16,7 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "version_check", "zerocopy", @@ -33,9 +24,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -46,12 +37,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -63,9 +48,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -78,9 +63,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -93,29 +78,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "approx" @@ -128,18 +113,18 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "arrow" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd798aea3553913a5986813e9c6ad31a2d2b04e931fe8ea4a37155eb541cebb5" +checksum = "4df8bb5b0bd64c0b9bc61317fcc480bad0f00e56d3bc32c69a4c8dada4786bae" dependencies = [ "arrow-arith", "arrow-array", @@ -156,23 +141,23 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "508dafb53e5804a238cab7fd97a59ddcbfab20cc4d9814b1ab5465b9fa147f2e" +checksum = "a1a640186d3bd30a24cb42264c2dafb30e236a6f50d510e56d40b708c9582491" dependencies = [ "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", "chrono", - "num", + "num-traits", ] [[package]] name = "arrow-array" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2730bc045d62bb2e53ef8395b7d4242f5c8102f41ceac15e8395b9ac3d08461" +checksum = "219fe420e6800979744c8393b687afb0252b3f8a89b91027d27887b72aa36d31" dependencies = [ "ahash", "arrow-buffer", @@ -180,26 +165,29 @@ dependencies = [ "arrow-schema", "chrono", "half", - "hashbrown 0.15.4", - "num", + "hashbrown 0.16.1", + "num-complex", + "num-integer", + "num-traits", ] [[package]] name = "arrow-buffer" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54295b93beb702ee9a6f6fbced08ad7f4d76ec1c297952d4b83cf68755421d1d" +checksum = "76885a2697a7edf6b59577f568b456afc94ce0e2edc15b784ce3685b6c3c5c27" dependencies = [ "bytes", "half", - "num", + "num-bigint", + "num-traits", ] [[package]] name = "arrow-cast" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e8bcb7dc971d779a7280593a1bf0c2743533b8028909073e804552e85e75b5" +checksum = "9c9ebb4c987e6b3b236fb4a14b20b34835abfdd80acead3ccf1f9bf399e1f168" dependencies = [ "arrow-array", "arrow-buffer", @@ -211,27 +199,28 @@ dependencies = [ "chrono", "half", "lexical-core", - "num", + "num-traits", "ryu", ] [[package]] name = "arrow-data" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c22fe3da840039c69e9f61f81e78092ea36d57037b4900151f063615a2f6b4" +checksum = "727681b95de313b600eddc2a37e736dcb21980a40f640314dcf360e2f36bc89b" dependencies = [ "arrow-buffer", "arrow-schema", "half", - "num", + "num-integer", + "num-traits", ] [[package]] name = "arrow-json" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3860db334fe7b19fcf81f6b56f8d9d95053f3839ffe443d56b5436f7a29a1794" +checksum = "b969b4a421ae83828591c6bf5450bd52e6d489584142845ad6a861f42fe35df8" dependencies = [ "arrow-array", "arrow-buffer", @@ -240,20 +229,22 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.10.0", + "indexmap 2.12.1", + "itoa", "lexical-core", "memchr", - "num", - "serde", + "num-traits", + "ryu", + "serde_core", "serde_json", "simdutf8", ] [[package]] name = "arrow-ord" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "425fa0b42a39d3ff55160832e7c25553e7f012c3f187def3d70313e7a29ba5d9" +checksum = "141c05298b21d03e88062317a1f1a73f5ba7b6eb041b350015b1cd6aabc0519b" dependencies = [ "arrow-array", "arrow-buffer", @@ -264,9 +255,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9c9423c9e71abd1b08a7f788fcd203ba2698ac8e72a1f236f1faa1a06a7414" +checksum = "c5f3c06a6abad6164508ed283c7a02151515cef3de4b4ff2cebbcaeb85533db2" dependencies = [ "arrow-array", "arrow-buffer", @@ -277,32 +268,32 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fa1babc4a45fdc64a92175ef51ff00eba5ebbc0007962fecf8022ac1c6ce28" +checksum = "9cfa7a03d1eee2a4d061476e1840ad5c9867a544ca6c4c59256496af5d0a8be5" dependencies = [ "bitflags", ] [[package]] name = "arrow-select" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8854d15f1cf5005b4b358abeb60adea17091ff5bdd094dca5d3f73787d81170" +checksum = "bafa595babaad59f2455f4957d0f26448fb472722c186739f4fac0823a1bdb47" dependencies = [ "ahash", "arrow-array", "arrow-buffer", "arrow-data", "arrow-schema", - "num", + "num-traits", ] [[package]] name = "arrow-string" -version = "56.0.0" +version = "57.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c477e8b89e1213d5927a2a84a72c384a9bf4dd0dbf15f9fd66d821aafd9e95e" +checksum = "32f46457dbbb99f2650ff3ac23e46a929e0ab81db809b02aa5511c258348bef2" dependencies = [ "arrow-array", "arrow-buffer", @@ -310,29 +301,29 @@ dependencies = [ "arrow-schema", "arrow-select", "memchr", - "num", + "num-traits", "regex", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "async-compression" -version = "0.4.27" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" dependencies = [ - "flate2", + "compression-codecs", + "compression-core", "futures-core", - "memchr", "pin-project-lite", "tokio", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -362,9 +353,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.3" +version = "1.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0baa720ebadea158c5bda642ac444a2af0cdf7bb66b46d1e4533de5d1f449d0" +checksum = "a0149602eeaf915158e14029ba0c78dedb8c08d554b024d54c8f239aab46511d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -392,9 +383,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.4" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b68c2194a190e1efc999612792e25b1ab3abfefe4306494efaaabc25933c0cbe" +checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -404,9 +395,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" dependencies = [ "aws-lc-sys", "zeroize", @@ -414,11 +405,10 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" dependencies = [ - "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -427,9 +417,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.9" +version = "1.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2090e664216c78e766b6bac10fe74d2f451c02441d43484cd76ac9a295075f7" +checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -452,9 +442,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.100.0" +version = "1.115.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c5eafbdcd898114b839ba68ac628e31c4cfc3e11dfca38dc1b2de2f35bb6270" +checksum = "fdaa0053cbcbc384443dd24569bd5d1664f86427b9dc04677bd0ab853954baec" dependencies = [ "aws-credential-types", "aws-runtime", @@ -486,9 +476,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.78.0" +version = "1.90.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd7bc4bd34303733bded362c4c997a39130eac4310257c79aae8484b1c4b724" +checksum = "4f18e53542c522459e757f81e274783a78f8c81acdfc8d1522ee8a18b5fb1c66" dependencies = [ "aws-credential-types", "aws-runtime", @@ -508,9 +498,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.79.0" +version = "1.92.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77358d25f781bb106c1a69531231d4fd12c6be904edb0c47198c604df5a2dbca" +checksum = "532f4d866012ffa724a4385c82e8dd0e59f0ca0e600f3f22d4c03b6824b34e4a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -530,9 +520,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.80.0" +version = "1.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e3ed2a9b828ae7763ddaed41d51724d2661a50c45f845b08967e52f4939cfc" +checksum = "1be6fbbfa1a57724788853a623378223fe828fc4c09b146c992f0c95b6256174" dependencies = [ "aws-credential-types", "aws-runtime", @@ -553,9 +543,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.3" +version = "1.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfb9021f581b71870a17eac25b52335b82211cdc092e02b6876b2bcefa61666" +checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -581,9 +571,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" dependencies = [ "futures-util", "pin-project-lite", @@ -592,9 +582,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.5" +version = "0.63.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab9472f7a8ec259ddb5681d2ef1cb1cf16c0411890063e67cdc7b62562cc496" +checksum = "95bd108f7b3563598e4dc7b62e1388c9982324a2abd622442167012690184591" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -612,9 +602,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.10" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604c7aec361252b8f1c871a7641d5e0ba3a7f5a586e51b66bc9510a5519594d9" +checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658" dependencies = [ "aws-smithy-types", "bytes", @@ -623,9 +613,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.2" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c82ba4cab184ea61f6edaafc1072aad3c2a17dcf4c0fce19ac5694b90d8b5f" +checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -633,6 +623,7 @@ dependencies = [ "bytes", "bytes-utils", "futures-core", + "futures-util", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", @@ -644,56 +635,57 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.6" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.27", - "h2 0.4.11", + "h2 0.4.12", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", + "tokio-rustls 0.26.4", "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.4" +version = "0.61.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" +checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" dependencies = [ "aws-smithy-types", "urlencoding", @@ -701,9 +693,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.5" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660f70d9d8af6876b4c9aa8dcb0dbaf0f89b04ee9a4455bea1b4ba03b15f26f6" +checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -725,9 +717,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.5" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "937a49ecf061895fca4a6dd8e864208ed9be7546c0527d04bc07d502ec5fba1c" +checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -742,9 +734,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.2" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" +checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e" dependencies = [ "base64-simd", "bytes", @@ -768,18 +760,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.10" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.8" +version = "1.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" +checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -791,9 +783,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -802,7 +794,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", "itoa", "matchit", @@ -811,8 +803,7 @@ dependencies = [ "multer", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -826,9 +817,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -837,7 +828,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -846,39 +836,23 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b" dependencies = [ "axum", "axum-core", "bytes", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", - "rustversion", - "serde", - "tower", "tower-layer", "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "tracing", ] [[package]] @@ -915,29 +889,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", - "which", -] - [[package]] name = "bindgen" version = "0.71.1" @@ -953,18 +904,18 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn", ] [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -990,9 +941,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bytes-utils" @@ -1006,10 +957,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.31" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1026,9 +978,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -1038,11 +990,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -1070,6 +1021,14 @@ dependencies = [ "parse-zoneinfo", ] +[[package]] +name = "cite-service" +version = "0.1.0" +dependencies = [ + "ogcapi", + "tokio", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1083,9 +1042,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.42" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -1093,9 +1052,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.42" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -1105,9 +1064,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -1117,9 +1076,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmake" @@ -1136,6 +1095,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compression-codecs" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1223,15 +1199,15 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.3.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" dependencies = [ "crc", "digest", - "libc", "rand 0.9.2", "regex", + "rustversion", ] [[package]] @@ -1307,9 +1283,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1317,9 +1293,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -1327,9 +1303,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", @@ -1341,9 +1317,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", @@ -1357,7 +1333,6 @@ dependencies = [ "anyhow", "arrow", "clap", - "dotenvy", "gdal", "geo", "geojson", @@ -1376,12 +1351,8 @@ dependencies = [ name = "demo-service" version = "0.1.0" dependencies = [ - "clap", - "dotenvy", "ogcapi", "tokio", - "tracing", - "tracing-subscriber", ] [[package]] @@ -1407,19 +1378,19 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -1535,12 +1506,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1556,9 +1527,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -1581,6 +1552,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "flat_map" version = "0.0.10" @@ -1589,9 +1566,9 @@ checksum = "e4fa2a56a33c493fc81acbad4676c599cf2b128f21462a020043ea1eee46244f" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -1629,9 +1606,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -1648,6 +1625,21 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1670,7 +1662,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" dependencies = [ - "futures", + "futures 0.1.31", "num_cpus", ] @@ -1702,6 +1694,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -1720,8 +1723,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1741,7 +1746,7 @@ dependencies = [ "gdal-sys", "geo-types", "semver", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -1750,7 +1755,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "febef67dc08a956a9ecb04de2b40dbd15ad56be49421aad9ae0cdcbe9a24166c" dependencies = [ - "bindgen 0.71.1", + "bindgen", "pkg-config", "semver", ] @@ -1767,9 +1772,9 @@ dependencies = [ [[package]] name = "geo" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4416397671d8997e9a3e7ad99714f4f00a22e9eaa9b966a5985d2194fc9e02e1" +checksum = "2fc1a1678e54befc9b4bcab6cd43b8e7f834ae8ea121118b0fd8c42747675b4a" dependencies = [ "earcutr", "float_next_after", @@ -1824,7 +1829,17 @@ dependencies = [ "log", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link", ] [[package]] @@ -1836,35 +1851,29 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" @@ -1889,7 +1898,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -1898,9 +1907,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -1908,7 +1917,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -1917,13 +1926,14 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "num-traits", + "zerocopy", ] [[package]] @@ -1943,22 +1953,28 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -2009,11 +2025,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2110,20 +2126,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.11", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -2152,22 +2170,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", + "rustls 0.23.35", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -2176,12 +2194,12 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.8.1", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2189,24 +2207,24 @@ dependencies = [ [[package]] name = "i_float" -version = "1.7.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85df3a416829bb955fdc2416c7b73680c8dcea8d731f2c7aa23e1042fe1b8343" +checksum = "010025c2c532c8d82e42d0b8bb5184afa449fa6f06c709ea9adcb16c49ae405b" dependencies = [ - "serde", + "libm", ] [[package]] name = "i_key_sort" -version = "0.2.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "347c253b4748a1a28baf94c9ce133b6b166f08573157e05afe718812bc599fcd" +checksum = "9190f86706ca38ac8add223b2aed8b1330002b5cdbbce28fb58b10914d38fc27" [[package]] name = "i_overlay" -version = "2.0.5" +version = "4.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0542dfef184afdd42174a03dcc0625b6147fb73e1b974b1a08a2a42ac35cee49" +checksum = "0fcccbd4e4274e0f80697f5fbc6540fdac533cce02f2081b328e68629cce24f9" dependencies = [ "i_float", "i_key_sort", @@ -2217,25 +2235,24 @@ dependencies = [ [[package]] name = "i_shape" -version = "1.7.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a38f5a42678726718ff924f6d4a0e79b129776aeed298f71de4ceedbd091bce" +checksum = "1ea154b742f7d43dae2897fcd5ead86bc7b5eefcedd305a7ebf9f69d44d61082" dependencies = [ "i_float", - "serde", ] [[package]] name = "i_tree" -version = "0.8.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155181bc97d770181cf9477da51218a19ee92a8e5be642e796661aee2b601139" +checksum = "35e6d558e6d4c7b82bc51d9c771e7a927862a161a7d87bf2b0541450e0e20915" [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2257,9 +2274,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2270,9 +2287,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2283,11 +2300,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2298,42 +2314,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2349,9 +2361,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -2381,24 +2393,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", -] - -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags", - "cfg-if", - "libc", + "serde_core", ] [[package]] @@ -2409,9 +2411,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2419,9 +2421,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2432,15 +2434,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2458,19 +2451,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2485,17 +2478,11 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lexical-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b765c31809609075565a70b4b71402281283aeda7ecaf4818ac14a7b2ade8958" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" dependencies = [ "lexical-parse-float", "lexical-parse-integer", @@ -2506,69 +2493,62 @@ dependencies = [ [[package]] name = "lexical-parse-float" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6f9cb01fb0b08060209a057c048fcbab8717b4c1ecd2eac66ebfe39a65b0f2" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" dependencies = [ "lexical-parse-integer", "lexical-util", - "static_assertions", ] [[package]] name = "lexical-parse-integer" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72207aae22fc0a121ba7b6d479e42cbfea549af1479c3f3a4f12c70dd66df12e" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" dependencies = [ "lexical-util", - "static_assertions", ] [[package]] name = "lexical-util" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a82e24bf537fd24c177ffbbdc6ebcc8d54732c35b50a3f28cc3f4e4c949a0b3" -dependencies = [ - "static_assertions", -] +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" [[package]] name = "lexical-write-float" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5afc668a27f460fb45a81a757b6bf2f43c2d7e30cb5a2dcd3abf294c78d62bd" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" dependencies = [ "lexical-util", "lexical-write-integer", - "static_assertions", ] [[package]] name = "lexical-write-integer" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629ddff1a914a836fb245616a7888b62903aae58fa771e1d83943035efa0f978" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" dependencies = [ "lexical-util", - "static_assertions", ] [[package]] name = "libc" -version = "0.2.174" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-link", ] [[package]] @@ -2577,6 +2557,17 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2589,9 +2580,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" dependencies = [ "zlib-rs", ] @@ -2604,31 +2595,30 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru" @@ -2636,7 +2626,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] @@ -2645,13 +2635,22 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mail-builder" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900998f307338c4013a28ab14d760b784067324b164448c6d98a89e44810473b" +dependencies = [ + "gethostname", +] + [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -2672,9 +2671,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2705,17 +2704,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -2747,26 +2747,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num" -version = "0.4.3" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", + "windows-sys 0.61.2", ] [[package]] @@ -2782,11 +2767,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2866,9 +2850,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -2876,9 +2860,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2886,15 +2870,6 @@ dependencies = [ "syn", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "ogcapi" version = "0.3.1" @@ -2918,7 +2893,7 @@ dependencies = [ "serde", "serde_json", "serde_qs", - "thiserror 2.0.12", + "thiserror 2.0.17", "url", ] @@ -2952,7 +2927,7 @@ dependencies = [ "http-body 1.0.1", "ogcapi-drivers", "ogcapi-types", - "schemars 1.0.4", + "schemars 1.1.0", "serde", "serde_json", "sqlx", @@ -2972,20 +2947,22 @@ dependencies = [ "data-loader", "dotenvy", "dyn-clone", + "futures 0.3.31", "geojson", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-util", + "mail-builder", "ogcapi-drivers", "ogcapi-processes", "ogcapi-types", "openapiv3", - "schemars 1.0.4", + "schemars 1.1.0", "serde", "serde_json", "serde_qs", "serde_yaml", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tower", "tower-http", @@ -3022,9 +2999,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openapiv3" @@ -3032,7 +3009,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8d427828b22ae1fff2833a03d8486c2c881367f1c336349f307f321e7f4d05" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", "serde_json", ] @@ -3066,12 +3043,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.11.1" @@ -3089,7 +3060,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f05b290991702bb8140cf70915b82b0ae1ec7fe478db97305af990048040095" dependencies = [ - "futures", + "futures 0.1.31", "futures-cpupool", "num_cpus", "pub-iterator-type", @@ -3103,9 +3074,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3113,15 +3084,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -3150,9 +3121,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -3205,9 +3176,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3229,9 +3200,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -3239,18 +3210,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3288,7 +3259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" dependencies = [ "anyhow", - "indexmap 2.10.0", + "indexmap 2.12.1", "log", "protobuf", "protobuf-support", @@ -3314,19 +3285,19 @@ checksum = "858afdbecdce657c6e32031348cf7326da7700c869c368a136d31565972f7018" [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", - "rustls 0.23.31", - "socket2 0.5.10", - "thiserror 2.0.12", + "rustc-hash", + "rustls 0.23.35", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3334,20 +3305,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", - "rustls 0.23.31", + "rustc-hash", + "rustls 0.23.35", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3355,23 +3326,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3438,14 +3409,14 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -3453,9 +3424,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -3463,27 +3434,27 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -3492,59 +3463,44 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.6.29" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -3554,7 +3510,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.8.1", "hyper-rustls 0.27.7", "hyper-util", "js-sys", @@ -3562,14 +3518,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.31", + "rustls 0.23.35", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower", "tower-http", "tower-service", @@ -3577,7 +3533,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] @@ -3614,7 +3570,7 @@ dependencies = [ "chrono", "chrono-humanize", "chrono-tz", - "indexmap 2.10.0", + "indexmap 2.12.1", "num-bigint", "num-rational", "num-traits", @@ -3631,9 +3587,9 @@ checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -3662,9 +3618,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3673,9 +3629,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", @@ -3686,26 +3642,14 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3736,15 +3680,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] @@ -3761,15 +3705,15 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.8", "subtle", "zeroize", ] @@ -3788,14 +3732,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.5.1", ] [[package]] @@ -3809,9 +3753,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -3829,9 +3773,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "aws-lc-rs", "ring", @@ -3841,9 +3785,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -3862,11 +3806,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3883,9 +3827,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -3896,9 +3840,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633" dependencies = [ "proc-macro2", "quote", @@ -3951,9 +3895,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation 0.10.1", @@ -3964,9 +3908,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3974,30 +3918,40 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4017,24 +3971,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -4047,7 +4003,7 @@ dependencies = [ "percent-encoding", "ryu", "serde", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -4075,19 +4031,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.12.1", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -4095,9 +4050,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling", "proc-macro2", @@ -4111,7 +4066,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "itoa", "ryu", "serde", @@ -4157,9 +4112,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -4198,9 +4153,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -4234,21 +4189,21 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "spade" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14e31a007e9f85c32784b04f89e6e194bb252a4d41b4a8ccd9e77245d901c8c" +checksum = "fb313e1c8afee5b5647e00ee0fe6855e3d529eb863a0fdae1d60006c4d1e9990" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", "num-traits", "robust", "smallvec", @@ -4312,19 +4267,19 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "hashlink", - "indexmap 2.10.0", + "indexmap 2.12.1", "log", "memchr", "once_cell", "percent-encoding", - "rustls 0.23.31", + "rustls 0.23.35", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -4407,7 +4362,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -4444,7 +4399,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -4468,16 +4423,16 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -4516,9 +4471,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -4547,15 +4502,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -4569,11 +4524,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -4589,9 +4544,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -4609,9 +4564,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -4624,15 +4579,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -4649,9 +4604,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -4659,9 +4614,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -4674,29 +4629,26 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -4715,11 +4667,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.31", + "rustls 0.23.35", "tokio", ] @@ -4736,9 +4688,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -4749,18 +4701,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] @@ -4863,14 +4828,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -4887,9 +4852,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" @@ -4905,24 +4870,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unsafe-libyaml" @@ -4938,9 +4903,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -4972,7 +4937,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.12.1", "serde", "serde_json", "utoipa-gen", @@ -5024,11 +4989,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -5084,12 +5049,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -5100,35 +5065,22 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -5139,9 +5091,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5149,31 +5101,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -5195,14 +5147,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.4", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -5221,50 +5173,28 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall", + "libredox", "wasite", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -5275,9 +5205,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -5286,9 +5216,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -5297,24 +5227,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -5352,7 +5282,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -5388,19 +5327,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -5417,9 +5356,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -5435,9 +5374,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -5453,9 +5392,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -5465,9 +5404,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -5483,9 +5422,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -5501,9 +5440,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -5519,9 +5458,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -5537,33 +5476,30 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wkb" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff9eff6aebac4c64f9c7c057a68f6359284e2a80acf102dffe041fe219b3a082" +checksum = "a120b336c7ad17749026d50427c23d838ecb50cd64aaea6254b5030152f890a9" dependencies = [ "byteorder", "geo-traits", @@ -5573,9 +5509,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xmlparser" @@ -5585,11 +5521,10 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -5597,9 +5532,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -5609,18 +5544,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", @@ -5650,15 +5585,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -5667,9 +5602,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -5678,9 +5613,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -5696,22 +5631,22 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.10.0", + "indexmap 2.12.1", "memchr", "zopfli", ] [[package]] name = "zlib-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/README.md b/README.md index 85f541a..2466bb4 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ cargo clippy --workspace --all-features --all-targets ```bash podman run --network host docker.io/ogccite/ets-ogcapi-features10 # podman run --network host docker.io/ogccite/ets-ogcapi-edr10 +# podman run --network host docker.io/ogccite/ets-ogcapi-processes10 ``` Navigate to to execute the test suite. For documentation and more info see . diff --git a/examples/cite-service/Cargo.toml b/examples/cite-service/Cargo.toml new file mode 100644 index 0000000..c5bc1a2 --- /dev/null +++ b/examples/cite-service/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cite-service" +version = "0.1.0" +edition.workspace = true +publish = false + + +[dependencies] +tokio = { version = "1.48", features = ["full"] } + +ogcapi = { path = "../../ogcapi", version = "0.3", default-features = false, features = [ + "services", + "common", + "features", + "processes", +] } diff --git a/examples/cite-service/src/main.rs b/examples/cite-service/src/main.rs new file mode 100644 index 0000000..075c5f8 --- /dev/null +++ b/examples/cite-service/src/main.rs @@ -0,0 +1,32 @@ +use ogcapi::{ + processes::echo::Echo, + services::{AppState, Config, ConfigParser, Drivers, Service}, +}; + +#[tokio::main] +async fn main() { + // setup env + ogcapi::services::setup_env(); + + // setup tracing + ogcapi::services::telemetry::init(); + + // Config + let config = Config::try_parse().unwrap(); + + // Drivers + let drivers = Drivers::try_new_from_env().await.unwrap(); + + // Application state + let state = AppState::new(drivers).await; + + // Register processes/processors + let state = state.processors(vec![Box::new(Echo)]); + + // Build & run with hyper + Service::try_new_with(&config, state) + .await + .unwrap() + .serve() + .await; +} diff --git a/examples/data-loader/Cargo.toml b/examples/data-loader/Cargo.toml index 5e73123..af48aee 100644 --- a/examples/data-loader/Cargo.toml +++ b/examples/data-loader/Cargo.toml @@ -15,20 +15,19 @@ stac = ["ogcapi/stac"] [dependencies] anyhow = { workspace = true } -arrow = { version = "56.0", optional = true, default-features = false } +arrow = { version = "57.0", optional = true, default-features = false } clap = { version = "4.5", features = ["derive", "env"] } -dotenvy = "0.15.7" gdal = { version = "0.18.0", optional = true, features = ["bindgen"] } -geo = { version = "0.30.0", optional = true } +geo = { version = "0.31.0", optional = true } geojson = { workspace = true, optional = true, features = ["geo-types"] } osmpbfreader = { version = "0.19.1", optional = true } serde_json = { workspace = true } sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres"] } -tokio = { version = "1.47", features = ["full"] } +tokio = { version = "1.48", features = ["full"] } tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } url = { workspace = true, features = ["serde"] } -wkb = { version = "0.9.0", optional = true } +wkb = { version = "0.9.2", optional = true } ogcapi = { path = "../../ogcapi", version = "0.3", features = [ diff --git a/examples/data-loader/src/boundaries.rs b/examples/data-loader/src/boundaries.rs index 22d83cf..c34a685 100644 --- a/examples/data-loader/src/boundaries.rs +++ b/examples/data-loader/src/boundaries.rs @@ -72,29 +72,29 @@ pub fn build_boundary>( let mut outer_polys = build_boundary_parts(relation, objects, vec!["outer", "enclave", ""]); let inner_polys = build_boundary_parts(relation, objects, vec!["inner"]); - if let Some(ref mut outers) = outer_polys { - if let Some(inners) = inner_polys { - inners.into_iter().for_each(|inner| { - /* - It's assumed here that the 'inner' ring is contained into - exactly ONE outer ring. To find it among all 'outers', all - we need is to find a candidate 'outer' area that shares a point - point with (i.e 'intersects') all 'inner' segments. - Using 'contains' is not suitable here, as 'inner' may touch its outer - ring at a single point. - - NB: this algorithm cannot handle "donut inside donut" boundaries - (where 'inner' would be contained into multiple concentric outer rings). - */ - let (exterior, _) = inner.into_inner(); - for ref mut outer in outers.0.iter_mut() { - if exterior.lines().all(|line| outer.intersects(&line)) { - outer.interiors_push(exterior); - break; - } + if let Some(ref mut outers) = outer_polys + && let Some(inners) = inner_polys + { + inners.into_iter().for_each(|inner| { + /* + It's assumed here that the 'inner' ring is contained into + exactly ONE outer ring. To find it among all 'outers', all + we need is to find a candidate 'outer' area that shares a point + point with (i.e 'intersects') all 'inner' segments. + Using 'contains' is not suitable here, as 'inner' may touch its outer + ring at a single point. + + NB: this algorithm cannot handle "donut inside donut" boundaries + (where 'inner' would be contained into multiple concentric outer rings). + */ + let (exterior, _) = inner.into_inner(); + for ref mut outer in outers.0.iter_mut() { + if exterior.lines().all(|line| outer.intersects(&line)) { + outer.interiors_push(exterior); + break; } - }) - } + } + }) } outer_polys } diff --git a/examples/data-loader/src/geojson.rs b/examples/data-loader/src/geojson.rs index 758649a..00d4518 100644 --- a/examples/data-loader/src/geojson.rs +++ b/examples/data-loader/src/geojson.rs @@ -40,7 +40,7 @@ pub async fn load(args: Args) -> anyhow::Result<()> { ..Default::default() }) .or_else(|| Some(Extent::default())), - crs: vec![Crs::default(), Crs::from_epsg(3857), Crs::from_epsg(2056)], + crs: vec![Crs::default(), Crs::from_epsg(3857)], storage_crs: Some(Crs::default()), #[cfg(feature = "stac")] assets: crate::asset::load_asset_from_path(&args.input).await?, diff --git a/examples/data-loader/src/main.rs b/examples/data-loader/src/main.rs index c799c8e..c6610f7 100644 --- a/examples/data-loader/src/main.rs +++ b/examples/data-loader/src/main.rs @@ -4,7 +4,7 @@ use tracing_subscriber::{EnvFilter, prelude::*}; #[tokio::main] async fn main() -> anyhow::Result<()> { // setup env - dotenvy::dotenv().ok(); + ogcapi::services::setup_env(); // setup tracing tracing_subscriber::registry() diff --git a/examples/demo-service/Cargo.toml b/examples/demo-service/Cargo.toml index df37e13..2ef47a2 100644 --- a/examples/demo-service/Cargo.toml +++ b/examples/demo-service/Cargo.toml @@ -8,11 +8,7 @@ publish = false stac = [] [dependencies] -clap = { version = "4.5", features = ["derive", "env"] } -dotenvy = "0.15.7" -tokio = { version = "1.47", features = ["full"] } -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tokio = { version = "1.48", features = ["full"] } ogcapi = { path = "../../ogcapi", version = "0.3", features = [ "services", diff --git a/examples/demo-service/src/main.rs b/examples/demo-service/src/main.rs index 5f45c37..10e9fbd 100644 --- a/examples/demo-service/src/main.rs +++ b/examples/demo-service/src/main.rs @@ -1,35 +1,37 @@ -use clap::Parser; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - use ogcapi::{ - processes::{gdal_loader::GdalLoader, geojson_loader::GeoJsonLoader, greeter::Greeter}, - services::{AppState, Config, Service}, + processes::echo::Echo, + services::{AppState, Config, ConfigParser, Drivers, Service}, }; #[tokio::main] async fn main() { // setup env - dotenvy::dotenv().ok(); + ogcapi::services::setup_env(); // setup tracing - tracing_subscriber::registry() - .with(tracing_subscriber::EnvFilter::from_default_env()) - .with(tracing_subscriber::fmt::layer().pretty()) - .init(); + ogcapi::services::telemetry::init(); // Config - let config = Config::parse(); + let config = Config::try_parse().unwrap(); + + // Drivers + let drivers = Drivers::try_new_from_env().await.unwrap(); // Application state - let state = AppState::new_from(&config).await; + let state = AppState::new(drivers).await; // Register processes/processors let state = state.processors(vec![ - Box::new(Greeter), - Box::new(GeoJsonLoader), - Box::new(GdalLoader), + // Box::new(Greeter), + // Box::new(GeoJsonLoader), + // Box::new(GdalLoader), + Box::new(Echo), ]); // Build & run with hyper - Service::new_with(&config, state).await.serve().await; + Service::try_new_with(&config, state) + .await + .unwrap() + .serve() + .await; } diff --git a/ogcapi-client/Cargo.toml b/ogcapi-client/Cargo.toml index 670e4d6..1de28f3 100644 --- a/ogcapi-client/Cargo.toml +++ b/ogcapi-client/Cargo.toml @@ -19,7 +19,7 @@ stac = ["ogcapi-types/stac"] [dependencies] geojson = { workspace = true } log = { workspace = true } -reqwest = { version = "0.12.22", default-features = false, features = ["json", "blocking", "rustls-tls"] } +reqwest = { version = "0.12.24", default-features = false, features = ["json", "blocking", "rustls-tls"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_qs = { workspace = true } diff --git a/ogcapi-client/src/processes.rs b/ogcapi-client/src/processes.rs index 36dbe74..d957961 100644 --- a/ogcapi-client/src/processes.rs +++ b/ogcapi-client/src/processes.rs @@ -1,7 +1,6 @@ use crate::{Client, Error}; - -use ogcapi_processes::ProcessResponseBody; -use ogcapi_types::processes::{Execute, Response, Results, StatusInfo, TransmissionMode}; +use ogcapi_types::processes::{Execute, Output, Response, Results, StatusInfo, TransmissionMode}; +use std::collections::HashMap; impl Client { #[cfg(feature = "processes")] @@ -28,9 +27,10 @@ impl Client { if execute.outputs.len() == 1 { let (_k, v) = execute.outputs.iter().next().unwrap(); match v.transmission_mode { - TransmissionMode::Value => { - Ok(ProcessResponseBody::Requested(response.bytes()?.to_vec())) - } + TransmissionMode::Value => Ok(ProcessResponseBody::Requested { + outputs: execute.outputs.clone(), + parts: vec![response.bytes()?.to_vec()], + }), TransmissionMode::Reference => todo!(), } } else { @@ -55,9 +55,20 @@ impl Client { } } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum ProcessResponseBody { + Requested { + outputs: HashMap, + parts: Vec>, + }, + Results(Results), + Empty(String), + StatusInfo(StatusInfo), +} + #[cfg(test)] mod tests { - use ogcapi_processes::gdal_loader::GdalLoaderOutputs; + // use ogcapi_processes::gdal_loader::GdalLoaderOutputs; use ogcapi_types::processes::Execute; use super::*; @@ -85,38 +96,55 @@ mod tests { let response = client.execute(Greeter {}.id(), &execute).unwrap(); - let output: GreeterOutputs = response.try_into().unwrap(); - assert_eq!(output.greeting, "Hello, client!\n") - } - - #[test] - #[ignore = "needs running demo service"] - fn execute_gdal_loader() { - use ogcapi_processes::{ - Processor, - gdal_loader::{GdalLoader, GdalLoaderInputs}, + let ProcessResponseBody::Requested { + outputs: _outputs, + parts, + } = response + else { + panic!() }; - let endpoint = "http://0.0.0.0:8484/"; - let client = Client::new(endpoint).unwrap(); - - let input = GdalLoaderInputs { - input: "/data/ne_10m_railroads_north_america.geojson".to_owned(), - collection: "streets".to_string(), - filter: None, - s_srs: None, - database_url: "postgresql://postgres:password@db:5432/ogcapi".to_string(), - }; - - let execute = Execute { - inputs: input.execute_input(), - outputs: GdalLoaderOutputs::execute_output(), - ..Default::default() - }; - - let response = client.execute(GdalLoader {}.id(), &execute).unwrap(); - - let output: GdalLoaderOutputs = response.try_into().unwrap(); - dbg!(output); + assert_eq!( + String::from_utf8(parts[0].clone()).unwrap(), + "Hello, client!\n" + ) } + + // #[test] + // #[ignore = "needs running demo service"] + // fn execute_gdal_loader() { + // use ogcapi_processes::{ + // Processor, + // gdal_loader::{GdalLoader, GdalLoaderInputs}, + // }; + + // let endpoint = "http://0.0.0.0:8484/"; + // let client = Client::new(endpoint).unwrap(); + + // let input = GdalLoaderInputs { + // input: "/data/ne_10m_railroads_north_america.geojson".to_owned(), + // collection: "streets".to_string(), + // filter: None, + // s_srs: None, + // database_url: "postgresql://postgres:password@db:5432/ogcapi".to_string(), + // }; + + // let execute = Execute { + // inputs: input.execute_input(), + // outputs: GdalLoaderOutputs::execute_output(), + // ..Default::default() + // }; + + // let response = client.execute(GdalLoader {}.id(), &execute).unwrap(); + + // let ProcessResponseBody::Requested { + // outputs: _outputs, + // parts, + // } = response + // else { + // panic!() + // }; + + // assert_eq!(String::from_utf8(parts[0].clone()).unwrap(), "streets"); + // } } diff --git a/ogcapi-drivers/Cargo.toml b/ogcapi-drivers/Cargo.toml index 4d0c674..616c150 100644 --- a/ogcapi-drivers/Cargo.toml +++ b/ogcapi-drivers/Cargo.toml @@ -31,10 +31,10 @@ anyhow = { workspace = true } aws-config = { version = "1.8", optional = true, features = [ "behavior-version-latest", ] } -aws-sdk-s3 = { version = "1.100", optional = true, features = [ +aws-sdk-s3 = { version = "1.115.0", optional = true, features = [ "behavior-version-latest", ] } -async-trait = "0.1.88" +async-trait = "0.1.89" http = "1.3" rink-core = { version = "0.8.0", optional = true, features = ["bundle-files"] } serde_json = { workspace = true } diff --git a/ogcapi-drivers/migrations/20201113131516_initialization.sql b/ogcapi-drivers/migrations/20201113131516_initialization.sql index 28c3d33..eb48fe9 100644 --- a/ogcapi-drivers/migrations/20201113131516_initialization.sql +++ b/ogcapi-drivers/migrations/20201113131516_initialization.sql @@ -32,6 +32,8 @@ CREATE TABLE meta.styles ( value jsonb NOT NULL ); +CREATE TYPE response_type AS ENUM ('raw', 'document'); + CREATE TABLE meta.jobs ( job_id text PRIMARY KEY, process_id text, @@ -42,5 +44,6 @@ CREATE TABLE meta.jobs ( updated timestamptz, progress smallint, links jsonb, - results jsonb + results jsonb, + response response_type NOT NULL ); diff --git a/ogcapi-drivers/src/lib.rs b/ogcapi-drivers/src/lib.rs index 9b728d3..6a3a734 100644 --- a/ogcapi-drivers/src/lib.rs +++ b/ogcapi-drivers/src/lib.rs @@ -7,8 +7,6 @@ pub mod s3; use ogcapi_types::common::{Collection, Collections, Query as CollectionQuery}; #[cfg(feature = "edr")] use ogcapi_types::edr::{Query as EdrQuery, QueryType}; -#[cfg(feature = "processes")] -use ogcapi_types::processes::{Results, StatusInfo}; #[cfg(feature = "stac")] use ogcapi_types::stac::SearchParams; #[cfg(feature = "styles")] @@ -18,7 +16,12 @@ use ogcapi_types::tiles::TileMatrixSet; #[cfg(feature = "features")] use ogcapi_types::{ common::Crs, - features::{Feature, Query as FeatureQuery}, + features::{Feature, Query as FeatureQuery, Queryables}, +}; +#[cfg(feature = "processes")] +use ogcapi_types::{ + common::Link, + processes::{Response, StatusCode, StatusInfo}, }; #[cfg(any(feature = "features", feature = "stac", feature = "edr"))] @@ -60,12 +63,20 @@ pub trait FeatureTransactions: Send + Sync { collection: &str, query: &FeatureQuery, ) -> anyhow::Result; + + async fn queryables(&self, _collection: &str) -> anyhow::Result { + // Default to nothing is queryable + Ok(Queryables { + queryables: Default::default(), + additional_properties: false, + }) + } } /// Trait for `STAC` search #[cfg(feature = "stac")] #[async_trait::async_trait] -pub trait StacSeach: Send + Sync { +pub trait StacSearch: Send + Sync { async fn search(&self, query: &SearchParams) -> anyhow::Result; } @@ -85,13 +96,37 @@ pub trait EdrQuerier: Send + Sync { #[cfg(feature = "processes")] #[async_trait::async_trait] pub trait JobHandler: Send + Sync { - async fn register(&self, job: &StatusInfo) -> anyhow::Result; + async fn register(&self, job: &StatusInfo, response_mode: Response) -> anyhow::Result; + + async fn update(&self, job: &StatusInfo) -> anyhow::Result<()>; + + async fn status_list(&self, offset: usize, limit: usize) -> anyhow::Result>; async fn status(&self, id: &str) -> anyhow::Result>; + async fn finish( + &self, + job_id: &str, + status: &StatusCode, + message: Option, + links: Vec, + results: Option, + ) -> anyhow::Result<()>; + async fn dismiss(&self, id: &str) -> anyhow::Result>; - async fn results(&self, id: &str) -> anyhow::Result>; + async fn results(&self, id: &str) -> anyhow::Result; +} + +#[cfg(feature = "processes")] +#[derive(Debug)] +pub enum ProcessResult { + NoSuchJob, + NotReady, + Results { + results: ogcapi_types::processes::ExecuteResults, + response_mode: Response, + }, } /// Trait for `Style` transactions diff --git a/ogcapi-drivers/src/postgres/collection.rs b/ogcapi-drivers/src/postgres/collection.rs index 35edc3c..4eea50d 100644 --- a/ogcapi-drivers/src/postgres/collection.rs +++ b/ogcapi-drivers/src/postgres/collection.rs @@ -107,7 +107,6 @@ impl CollectionTransactions for Db { } async fn list_collections(&self, _query: &Query) -> anyhow::Result { - println!("Query collections"); let collections: Option>> = if cfg!(feature = "stac") { sqlx::query_scalar( r#" diff --git a/ogcapi-drivers/src/postgres/feature.rs b/ogcapi-drivers/src/postgres/feature.rs index 2cfe0ab..9790a7c 100644 --- a/ogcapi-drivers/src/postgres/feature.rs +++ b/ogcapi-drivers/src/postgres/feature.rs @@ -1,5 +1,5 @@ use ogcapi_types::{ - common::{Bbox, Crs, Datetime, IntervalDatetime}, + common::{Authority, Bbox, Crs, Datetime, IntervalDatetime}, features::{Feature, FeatureCollection, Query}, }; @@ -140,8 +140,11 @@ impl FeatureTransactions for Db { // bbox if let Some(bbox) = query.bbox.as_ref() { - // TODO: Properly handle crs and bbox transformation - let bbox_srid: i32 = query.bbox_crs.as_srid(); + // coordinate system axis order (OGC and Postgis is lng, lat | EPSG is lat, lng) + let order = match query.bbox_crs.authority { + Authority::OGC => [0, 1, 2, 3], + Authority::EPSG => [1, 0, 3, 2], + }; let c = self.read_collection(collection).await?; let storage_srid = c @@ -150,17 +153,39 @@ impl FeatureTransactions for Db { .unwrap_or_default() .as_srid(); - let envelope = match bbox { + // TODO: handle antimeridian (lower > upper on axis 1) + let intersection = match bbox { Bbox::Bbox2D(bbox) => format!( - "ST_MakeEnvelope({}, {}, {}, {}, {})", - bbox[0], bbox[1], bbox[2], bbox[3], bbox_srid + "ST_Intersects(geom, ST_Transform(ST_MakeEnvelope({}, {}, {}, {}, {}), {storage_srid}))", + bbox[order[0]], + bbox[order[1]], + bbox[order[2]], + bbox[order[3]], + query.bbox_crs.as_srid() ), Bbox::Bbox3D(bbox) => format!( - "ST_MakeEnvelope({}, {}, {}, {}, {})", - bbox[0], bbox[1], bbox[3], bbox[4], bbox_srid + // FIXME: ensure proper height/box transformation handling + r#"ST_3DIntersects(geom, ST_Envelope(ST_Transform(ST_SetSRID(ST_MakeLine(ARRAY[ + ST_MakePoint({x1}, {y1}, {z1}), + ST_MakePoint({x2}, {y1}, {z1}), + ST_MakePoint({x1}, {y2}, {z1}), + ST_MakePoint({x2}, {y2}, {z1}), + ST_MakePoint({x1}, {y1}, {z2}), + ST_MakePoint({x2}, {y1}, {z2}), + ST_MakePoint({x1}, {y2}, {z2}), + ST_MakePoint({x2}, {y2}, {z2}) + ]), {srid}), {storage_srid})))"#, + x1 = bbox[order[0]], + y1 = bbox[order[1]], + z1 = bbox[2], + x2 = bbox[order[2] + 1], + y2 = bbox[order[3] + 1], + z2 = bbox[5], + srid = query.bbox_crs.as_srid() ), }; - where_conditions.push(format!("geom && ST_Transform({envelope}, {storage_srid})")); + + where_conditions.push(intersection); } // datetime diff --git a/ogcapi-drivers/src/postgres/job.rs b/ogcapi-drivers/src/postgres/job.rs index db45fd4..ac47fbe 100644 --- a/ogcapi-drivers/src/postgres/job.rs +++ b/ogcapi-drivers/src/postgres/job.rs @@ -1,34 +1,134 @@ -use ogcapi_types::processes::{Results, StatusCode, StatusInfo}; +use ogcapi_types::{ + common::Link, + processes::{ExecuteResults, Response, StatusCode, StatusInfo}, +}; +use sqlx::types::Json; -use crate::JobHandler; +use crate::{JobHandler, ProcessResult}; use super::Db; #[async_trait::async_trait] impl JobHandler for Db { - async fn register(&self, job: &StatusInfo) -> anyhow::Result { + async fn register(&self, job: &StatusInfo, response_mode: Response) -> anyhow::Result { let (id,): (String,) = sqlx::query_as( r#" INSERT INTO meta.jobs( - job_id, process_id, status, created, updated, links + job_id, + process_id, + status, + created, + updated, + links, + progress, + message, + response ) VALUES ( - $1 ->> 'jobID', $1 ->> 'processID', $1 -> 'status', NOW(), NOW(), $1 -> 'links' + CASE WHEN(($1 ->> 'jobID') <> '') THEN $1 ->> 'jobID' ELSE gen_random_uuid()::text END, + $1 ->> 'processID', + $1 -> 'status', + NOW(), + NOW(), + $1 -> 'links', + COALESCE(($1 ->> 'progress')::smallint, 0), + COALESCE($1 ->> 'message', ''), + ($2 #>> '{}')::response_type ) RETURNING job_id "#, ) .bind(sqlx::types::Json(job)) + .bind(sqlx::types::Json(response_mode)) .fetch_one(&self.pool) .await?; Ok(id) } + async fn update(&self, job: &StatusInfo) -> anyhow::Result<()> { + sqlx::query( + r#" + UPDATE meta.jobs + SET status = $1 -> 'status', + message = $1 -> 'message', + finished = NOW(), -- TODO: only set if status is successful or failed + updated = NOW(), + progress = ($1 -> 'progress')::smallint, + links = $1 -> 'links' + WHERE job_id = $1 ->> 'jobID' + "#, + ) + .bind(sqlx::types::Json(job)) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn finish( + &self, + job_id: &str, + status: &StatusCode, + message: Option, + links: Vec, + results: Option, + ) -> anyhow::Result<()> { + sqlx::query( + r#" + UPDATE meta.jobs + SET status = $2, + message = COALESCE($3, ''), + links = $4, + results = $5, + finished = NOW(), + updated = NOW(), + progress = 100 + WHERE job_id = $1 + "#, + ) + .bind(job_id) + .bind(Json(status)) + .bind(message) + .bind(Json(links)) + .bind(results.map(Json)) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn status_list(&self, offset: usize, limit: usize) -> anyhow::Result> { + let status_list: Vec> = sqlx::query_scalar( + r#" + SELECT row_to_json(jobs) as "status_info!" + FROM meta.jobs + ORDER BY created DESC + OFFSET $1 + LIMIT $2 + "#, + ) + .bind(offset as i64) + .bind(limit as i64) + .fetch_all(&self.pool) + .await?; + + Ok(status_list.into_iter().map(|s| s.0).collect()) + } + async fn status(&self, id: &str) -> anyhow::Result> { let status: Option> = sqlx::query_scalar( r#" - SELECT row_to_json(jobs) as "status_info!" - FROM meta.jobs WHERE job_id = $1 + SELECT json_object( + 'process_id': process_id, + 'job_id': job_id, + 'status': status, + 'message': message, + 'created': created, + 'finished': finished, + 'updated': updated, + 'progress': progress, + 'links': COALESCE(links, '[]'::jsonb) + ) as "status_info!" + FROM meta.jobs + WHERE job_id = $1 "#, ) .bind(id) @@ -45,7 +145,17 @@ impl JobHandler for Db { SET status = $2, message = 'Job dismissed' WHERE job_id = $1 AND status <@ '["accepted", "running"]'::jsonb - RETURNING row_to_json(jobs) as "status_info!" + RETURNING json_object( + 'process_id': process_id, + 'job_id': job_id, + 'status': status, + 'message': message, + 'created': created, + 'finished': finished, + 'updated': updated, + 'progress': progress, + 'links': COALESCE(links, '[]'::jsonb) + ) as "status_info!" "#, ) .bind(id) @@ -56,10 +166,10 @@ impl JobHandler for Db { Ok(status.map(|s| s.0)) } - async fn results(&self, id: &str) -> anyhow::Result> { - let results: Option> = sqlx::query_scalar( + async fn results(&self, id: &str) -> anyhow::Result { + let results: Option<(Option>, Json)> = sqlx::query_as( r#" - SELECT results as "results!" + SELECT results, to_jsonb(response) FROM meta.jobs WHERE job_id = $1 "#, @@ -68,6 +178,13 @@ impl JobHandler for Db { .fetch_optional(&self.pool) .await?; - Ok(results.map(|r| r.0)) + Ok(match results { + None => ProcessResult::NoSuchJob, + Some((None, _)) => ProcessResult::NotReady, + Some((Some(Json(results)), Json(response_mode))) => ProcessResult::Results { + results, + response_mode, + }, + }) } } diff --git a/ogcapi-drivers/src/postgres/stac.rs b/ogcapi-drivers/src/postgres/stac.rs index c012bb0..ae19e53 100644 --- a/ogcapi-drivers/src/postgres/stac.rs +++ b/ogcapi-drivers/src/postgres/stac.rs @@ -4,12 +4,12 @@ use ogcapi_types::{ stac::SearchParams, }; -use crate::StacSeach; +use crate::StacSearch; use super::Db; #[async_trait::async_trait] -impl StacSeach for Db { +impl StacSearch for Db { async fn search(&self, query: &SearchParams) -> anyhow::Result { let mut tx = self.pool.begin().await?; diff --git a/ogcapi-drivers/src/s3/collection.rs b/ogcapi-drivers/src/s3/collection.rs index ca17114..f2737a0 100644 --- a/ogcapi-drivers/src/s3/collection.rs +++ b/ogcapi-drivers/src/s3/collection.rs @@ -79,16 +79,16 @@ impl CollectionTransactions for S3 { .await?; for object in resp.contents.unwrap() { - if let Some(key) = object.key() { - if key.ends_with("collection.json") { - let r = self - .get_object(self.bucket.clone().unwrap_or_default(), key) - .await?; + if let Some(key) = object.key() + && key.ends_with("collection.json") + { + let r = self + .get_object(self.bucket.clone().unwrap_or_default(), key) + .await?; - let c = serde_json::from_slice(&r.body.collect().await?.into_bytes()[..])?; + let c = serde_json::from_slice(&r.body.collect().await?.into_bytes()[..])?; - collections.push(c); - } + collections.push(c); } } diff --git a/ogcapi-drivers/tests/job.rs b/ogcapi-drivers/tests/job.rs index 1b40bc3..45e9cba 100644 --- a/ogcapi-drivers/tests/job.rs +++ b/ogcapi-drivers/tests/job.rs @@ -1,7 +1,12 @@ #[cfg(feature = "processes")] mod postgres { - use ogcapi_drivers::{JobHandler, postgres::Db}; - use ogcapi_types::processes::{StatusCode, StatusInfo}; + use std::collections::HashMap; + + use ogcapi_drivers::{JobHandler, ProcessResult, postgres::Db}; + use ogcapi_types::processes::{ + ExecuteResult, InlineOrRefData, InputValueNoObject, Output, Response, StatusCode, + StatusInfo, + }; #[sqlx::test] async fn job_handling(pool: sqlx::PgPool) -> () { @@ -13,7 +18,7 @@ mod postgres { }; // register - let job_id = db.register(&job).await.unwrap(); + let job_id = db.register(&job, Response::default()).await.unwrap(); assert_eq!(job_id, job.job_id); @@ -25,4 +30,82 @@ mod postgres { assert_eq!(info.unwrap().status, StatusCode::Dismissed) } + + #[sqlx::test] + async fn job_result(pool: sqlx::PgPool) -> () { + let db = Db { pool }; + + let job = StatusInfo { + job_id: "test-job".to_string(), + ..Default::default() + }; + + matches!( + db.results(&job.job_id).await.unwrap(), + ProcessResult::NoSuchJob + ); + + assert_eq!( + db.register(&job, Response::Document).await.unwrap(), + job.job_id + ); + + matches!( + db.results(&job.job_id).await.unwrap(), + ProcessResult::NotReady + ); + + db.finish( + &job.job_id, + &StatusCode::Successful, + Some("it is ready".to_string()), + vec![], + Some(HashMap::from([( + "key1".to_string(), + ExecuteResult { + output: Output { + format: None, + transmission_mode: Default::default(), + }, + data: InlineOrRefData::InputValueNoObject(InputValueNoObject::String( + "foobar".into(), + )), + }, + )])), + ) + .await + .unwrap(); + + matches!( + db.results(&job.job_id).await.unwrap(), + ProcessResult::Results { + results: _, + response_mode: Response::Document, + } + ); + } + + #[sqlx::test] + async fn job_result_failed(pool: sqlx::PgPool) -> () { + let db = Db { pool }; + + let job = StatusInfo { + job_id: "test-job".to_string(), + ..Default::default() + }; + + let _ = db.register(&job, Response::Document).await.unwrap(); + + db.finish(&job.job_id, &StatusCode::Failed, None, vec![], None) + .await + .unwrap(); + + matches!( + db.results(&job.job_id).await.unwrap(), + ProcessResult::Results { + results: _, + response_mode: Response::Document, + } + ); + } } diff --git a/ogcapi-processes/Cargo.toml b/ogcapi-processes/Cargo.toml index e418b24..3514c31 100644 --- a/ogcapi-processes/Cargo.toml +++ b/ogcapi-processes/Cargo.toml @@ -18,21 +18,21 @@ gdal-loader = ["dep:arrow", "arrow/ffi", "arrow/json", "dep:gdal", "dep:geo", "d [dependencies] anyhow = "1.0" -arrow ={ version = "56.0", optional = true, default-features = false } -async-trait = "0.1.88" +arrow ={ version = "57.0", optional = true, default-features = false } +async-trait = "0.1.89" dyn-clone = "1.0" gdal = { version = "0.18.0", optional = true, features = ["bindgen"] } -geo = { version = "0.30.0", optional = true, default-features = false } +geo = { version = "0.31.0", optional = true, default-features = false } geojson = { workspace = true, optional = true, features = ["geo-types"]} http-body = "1.0" -schemars = "1.0" +schemars = "1.1" serde = { workspace = true } serde_json = { workspace = true } -tokio = { version = "1.47", default-features = false, features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.48", default-features = false, features = ["rt-multi-thread", "macros"] } url = { workspace = true } sqlx = { version = "0.8.6", optional = true } -wkb = { version = "0.9.0", optional = true } +wkb = { version = "0.9.2", optional = true } ogcapi-types = { path = "../ogcapi-types", version = "0.3", default-features = false, features = ["processes"] } ogcapi-drivers = { path = "../ogcapi-drivers", version = "0.3", optional = true, default-features = false, features = ["postgres", "common"] } diff --git a/ogcapi-processes/src/echo.rs b/ogcapi-processes/src/echo.rs new file mode 100644 index 0000000..1dd0dc5 --- /dev/null +++ b/ogcapi-processes/src/echo.rs @@ -0,0 +1,383 @@ +use std::collections::HashMap; + +use crate::Processor; +use anyhow::Result; +use ogcapi_types::{ + common::Link, + processes::{ + Execute, ExecuteResult, ExecuteResults, Format, InlineOrRefData, InputValueNoObject, + JobControlOptions, Output, Process, ProcessSummary, TransmissionMode, + description::{DescriptionType, InputDescription, OutputDescription}, + }, +}; +use schemars::{JsonSchema, generate::SchemaSettings}; +use serde::{Deserialize, Serialize}; + +/// Echo is a simple process that echoes back the inputs it receives. +/// It is used to verify that the OGC API Processes implementation is working correctly. +/// +/// Definition: https://docs.ogc.org/is/18-062r2/18-062r2.html#_443805da-dfcc-84bd-1820-4a41a69f629a +#[derive(Clone)] +pub struct Echo; + +#[derive(Deserialize, Serialize, Debug, JsonSchema)] +pub struct StringInput(String); + +#[derive(Deserialize, Serialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EchoInputs { + pub string_input: Option, + // pub measure_input: Option, + // pub date_input: Option, + pub double_input: Option, + // pub array_input: Option>, + // pub complex_object_input: Option, + // pub geometry_input: Option>, + // pub bounding_box_input: Option, + // pub images_input: Option>, + // pub feature_collection_input: Option, + pub pause: Option, +} + +#[derive(Deserialize, Serialize, Debug, JsonSchema)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct EchoOutputs { + pub string_output: Option, + // pub measure_output: Option, + // pub date_output: Option, + pub double_output: Option, + // pub array_output: Option>, + // pub complex_object_output: Option, + // pub geometry_output: Option>, + // pub bounding_box_output: Option, + // pub images_output: Option>, + // pub feature_collection_output: Option, +} + +#[derive(Deserialize, Serialize, Debug, JsonSchema)] +#[serde(untagged)] +pub enum StringOutput { + Value1(String), + Value2(String), + Value3(String), +} + +impl EchoOutputs { + pub fn compute_output_metadata(&self) -> HashMap { + let mut outputs = HashMap::new(); + + if self.string_output.is_some() { + outputs.insert( + "stringOutput".to_string(), + Output { + format: Some(Format { + media_type: Some("text/plain".to_string()), + encoding: Some("utf8".to_string()), + schema: None, + }), + transmission_mode: TransmissionMode::Value, + }, + ); + } + + if self.double_output.is_some() { + outputs.insert( + "doubleOutput".to_string(), + Output { + format: Some(Format { + media_type: Some("text/plain".to_string()), + encoding: Some("utf8".to_string()), + schema: None, + }), + transmission_mode: TransmissionMode::Value, + }, + ); + } + + // TODO: implement for other types + + outputs + } + + fn to_execute_results(&self, outputs: &HashMap) -> ExecuteResults { + let mut execute_results = HashMap::with_capacity(outputs.len()); + + if let Some(string_output) = &self.string_output + && let Some(string_output_meta) = outputs.get("stringOutput") + { + execute_results.insert( + "stringOutput".to_string(), + ExecuteResult { + output: string_output_meta.clone(), + data: InlineOrRefData::InputValueNoObject(InputValueNoObject::String( + string_output.clone(), + )), + }, + ); + } + + if let Some(double_output) = &self.double_output + && let Some(double_output_meta) = outputs.get("doubleOutput") + { + execute_results.insert( + "doubleOutput".to_string(), + ExecuteResult { + output: double_output_meta.clone(), + data: InlineOrRefData::InputValueNoObject(InputValueNoObject::Number( + *double_output, + )), + }, + ); + } + + execute_results + } +} + +#[async_trait::async_trait] +impl Processor for Echo { + fn id(&self) -> &'static str { + "echo" + } + + fn version(&self) -> &'static str { + "1.0.0" + } + + fn process(&self) -> Result { + let mut settings = SchemaSettings::default(); + settings.meta_schema = None; + + let mut generator = settings.into_generator(); + Ok(Process { + summary: ProcessSummary { + id: self.id().to_string(), + version: self.version().to_string(), + job_control_options: vec![ + JobControlOptions::SyncExecute, + JobControlOptions::AsyncExecute, + // TODO: implement "dismiss extension" + // JobControlOptions::Dismiss, + ], + output_transmission: vec![ + TransmissionMode::Value, + // TODO: implement reference mode + // TransmissionMode::Reference, + ], + links: vec![ + Link::new( + format!("./{}/execution", self.id()), + "http://www.opengis.net/def/rel/ogc/1.0/execute", + ) + .title("Execution endpoint"), + ], + }, + inputs: HashMap::from([ + ( + "stringInput".to_string(), + InputDescription { + description_type: DescriptionType { + title: Some("String Literal Input Example".to_string()), + description: Some( + "This is an example of a STRING literal input.".to_string(), + ), + ..Default::default() + }, + schema: generator.root_schema_for::().to_value(), + ..Default::default() + }, + ), + ( + "doubleInput".to_string(), + InputDescription { + description_type: DescriptionType { + title: Some("Double Literal Input Example".to_string()), + description: Some( + "This is an example of a DOUBLE literal input.".to_string(), + ), + ..Default::default() + }, + schema: generator.root_schema_for::().to_value(), + ..Default::default() + }, + ), + ( + "pause".to_string(), + InputDescription { + description_type: DescriptionType { + title: Some("Pause Duration".to_string()), + description: Some( + "Optional pause duration in seconds before responding.".to_string(), + ), + ..Default::default() + }, + schema: generator.root_schema_for::().to_value(), + ..Default::default() + }, + ), + ]), + outputs: HashMap::from([( + "stringOutput".to_string(), + OutputDescription { + description_type: DescriptionType::default(), + schema: generator.root_schema_for::().to_value(), + }, + )]), + }) + } + + async fn execute(&self, execute: Execute) -> Result { + let value = serde_json::to_value(execute.inputs)?; + let inputs: EchoInputs = serde_json::from_value(value)?; + + if let Some(pause_duration) = inputs.pause { + tokio::time::sleep(std::time::Duration::from_secs(pause_duration)).await; + } + + let output_values = EchoOutputs { + string_output: inputs.string_input, + double_output: inputs.double_input, + }; + + // validate requested outputs + if !execute.outputs.is_empty() { + for (output_name, output) in &execute.outputs { + if !["stringOutput", "doubleOutput"].contains(&output_name.as_str()) { + anyhow::bail!( + "Requested output '{}' is not available in echo process", + output_name + ); + } + + if output.format.is_some() { + anyhow::bail!("Custom output formats are not supported in echo process"); + } + + if !matches!(output.transmission_mode, TransmissionMode::Value) { + anyhow::bail!("Only 'value' transmission mode is supported in echo process"); + } + } + } + + let all_outputs = output_values.compute_output_metadata(); + let outputs = if execute.outputs.is_empty() { + all_outputs + } else { + let mut outputs = execute.outputs; + for (name, output) in &mut outputs { + let Some(default_output) = all_outputs.get(name) else { + anyhow::bail!( + "Requested output '{}' is not available in echo process", + name + ); + }; + if output.format.is_none() { + output.format = default_output.format.clone(); + } + } + outputs + }; + + Ok(output_values.to_execute_results(&outputs)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ogcapi_types::processes::Input; + + #[tokio::test] + async fn test_string_value_sync() { + let echo = Echo; + assert_eq!(echo.id(), "echo"); + + eprintln!( + "Process:\n{}", + serde_json::to_string_pretty(&echo.process().unwrap()).unwrap() + ); + + let execute = Execute { + inputs: HashMap::from([( + "stringInput".to_string(), + Input::InlineOrRefData(InlineOrRefData::InputValueNoObject( + InputValueNoObject::String("testtest".to_string()), + )), + )]), + outputs: HashMap::from([( + "stringOutput".to_string(), + Output { + format: None, + transmission_mode: TransmissionMode::Value, + }, + )]), + ..Default::default() + }; + + let output = echo.execute(execute).await.unwrap(); + + assert_eq!(output.len(), 1); + + assert_eq!( + output["stringOutput"].data, + InlineOrRefData::InputValueNoObject(InputValueNoObject::String("testtest".to_string())) + ); + } + + #[tokio::test] + async fn test_multi_value_sync() { + let echo = Echo; + assert_eq!(echo.id(), "echo"); + + eprintln!( + "Process:\n{}", + serde_json::to_string_pretty(&echo.process().unwrap()).unwrap() + ); + + let execute = Execute { + inputs: HashMap::from([ + ( + "stringInput".to_string(), + Input::InlineOrRefData(InlineOrRefData::InputValueNoObject( + InputValueNoObject::String("testtest".to_string()), + )), + ), + ( + "doubleInput".to_string(), + Input::InlineOrRefData(InlineOrRefData::InputValueNoObject( + InputValueNoObject::Number(42.0), + )), + ), + ]), + outputs: HashMap::from([ + ( + "stringOutput".to_string(), + Output { + format: None, + transmission_mode: TransmissionMode::Value, + }, + ), + ( + "doubleOutput".to_string(), + Output { + format: None, + transmission_mode: TransmissionMode::Value, + }, + ), + ]), + ..Default::default() + }; + + let output = echo.execute(execute).await.unwrap(); + + assert_eq!(output.len(), 2); + assert_eq!( + output["stringOutput"].data, + InlineOrRefData::InputValueNoObject(InputValueNoObject::String("testtest".to_string())) + ); + assert_eq!( + output["doubleOutput"].data, + InlineOrRefData::InputValueNoObject(InputValueNoObject::Number(42.0)) + ); + } +} diff --git a/ogcapi-processes/src/gdal_loader.rs b/ogcapi-processes/src/gdal_loader.rs index ea1c805..3a4a192 100644 --- a/ogcapi-processes/src/gdal_loader.rs +++ b/ogcapi-processes/src/gdal_loader.rs @@ -118,20 +118,6 @@ impl GdalLoaderOutputs { } } -impl TryFrom for GdalLoaderOutputs { - type Error = Exception; - - fn try_from(value: ProcessResponseBody) -> Result { - if let ProcessResponseBody::Requested(buf) = value { - Ok(GdalLoaderOutputs { - collection: String::from_utf8(buf).unwrap(), - }) - } else { - Err(Exception::new("500")) - } - } -} - #[async_trait::async_trait] impl Processor for GdalLoader { fn id(&self) -> &'static str { @@ -356,9 +342,10 @@ impl Processor for GdalLoader { })?; } - Ok(ProcessResponseBody::Requested( - inputs.collection.as_bytes().to_owned(), - )) + Ok(ProcessResponseBody::Requested { + outputs: GdalLoaderOutputs::execute_output(), + parts: vec![inputs.collection.as_bytes().to_owned()], + }) } } @@ -366,10 +353,9 @@ impl Processor for GdalLoader { mod tests { use ogcapi_types::processes::Execute; - use crate::{ - Processor, - gdal_loader::{GdalLoader, GdalLoaderInputs, GdalLoaderOutputs}, - }; + use crate::{ProcessResponseBody, Processor}; + + use super::*; #[tokio::test(flavor = "multi_thread")] async fn test_loader() { @@ -386,7 +372,7 @@ mod tests { collection: "streets-gdal".to_string(), filter: None, s_srs: None, - database_url: "postgresql://postgres:password@localhost:5433/ogcapi".to_string(), + database_url: "postgresql://postgres:password@localhost:5432/ogcapi".to_string(), }; let execute = Execute { @@ -394,7 +380,16 @@ mod tests { ..Default::default() }; - let output: GdalLoaderOutputs = loader.execute(execute).await.unwrap().try_into().unwrap(); - assert_eq!(output.collection, "streets-gdal"); + let output = loader.execute(execute).await.unwrap(); + + let ProcessResponseBody::Requested { + outputs: _outputs, + parts, + } = output + else { + panic!() + }; + + assert_eq!(String::from_utf8(parts[0].clone()).unwrap(), "streets-gdal"); } } diff --git a/ogcapi-processes/src/geojson_loader.rs b/ogcapi-processes/src/geojson_loader.rs index 6b97ebf..5da8780 100644 --- a/ogcapi-processes/src/geojson_loader.rs +++ b/ogcapi-processes/src/geojson_loader.rs @@ -11,7 +11,7 @@ use wkb::{Endianness, writer::WriteOptions}; use ogcapi_drivers::{CollectionTransactions, postgres::Db}; use ogcapi_types::{ - common::{Collection, Crs, Exception, Extent, SpatialExtent}, + common::{Collection, Crs, Extent, SpatialExtent}, processes::{ Execute, Format, InlineOrRefData, Input, InputValueNoObject, Output, Process, TransmissionMode, @@ -100,20 +100,6 @@ impl GeoJsonLoaderOutputs { } } -impl TryFrom for GeoJsonLoaderOutputs { - type Error = Exception; - - fn try_from(value: ProcessResponseBody) -> Result { - if let ProcessResponseBody::Requested(buf) = value { - Ok(GeoJsonLoaderOutputs { - collection_id: String::from_utf8(buf).unwrap(), - }) - } else { - Err(Exception::new("500")) - } - } -} - #[async_trait::async_trait] impl Processor for GeoJsonLoader { fn id(&self) -> &'static str { @@ -234,9 +220,10 @@ impl Processor for GeoJsonLoader { .execute(&db.pool) .await?; } - Ok(ProcessResponseBody::Requested( - inputs.collection.as_bytes().to_owned(), - )) + Ok(ProcessResponseBody::Requested { + outputs: GeoJsonLoaderOutputs::execute_output(), + parts: vec![inputs.collection.as_bytes().to_owned()], + }) } } @@ -244,10 +231,9 @@ impl Processor for GeoJsonLoader { mod tests { use ogcapi_types::processes::Execute; - use crate::{ - Processor, - geojson_loader::{GeoJsonLoader, GeoJsonLoaderInputs, GeoJsonLoaderOutputs}, - }; + use crate::Processor; + + use super::*; #[tokio::test] async fn test_loader() { @@ -263,7 +249,7 @@ mod tests { input: "../data/ne_10m_railroads_north_america.geojson".to_owned(), collection: "streets-geojson".to_string(), s_srs: None, - database_url: "postgresql://postgres:password@localhost:5433/ogcapi".to_string(), + database_url: "postgresql://postgres:password@localhost:5432/ogcapi".to_string(), }; let execute = Execute { @@ -271,8 +257,19 @@ mod tests { ..Default::default() }; - let output: GeoJsonLoaderOutputs = - loader.execute(execute).await.unwrap().try_into().unwrap(); - assert_eq!(output.collection_id, "streets-geojson"); + let output = loader.execute(execute).await.unwrap(); + + let ProcessResponseBody::Requested { + outputs: _outputs, + parts, + } = output + else { + panic!() + }; + + assert_eq!( + String::from_utf8(parts[0].clone()).unwrap(), + "streets-geojson" + ); } } diff --git a/ogcapi-processes/src/greeter.rs b/ogcapi-processes/src/greeter.rs index 41e0b9a..478415f 100644 --- a/ogcapi-processes/src/greeter.rs +++ b/ogcapi-processes/src/greeter.rs @@ -4,15 +4,13 @@ use anyhow::Result; use schemars::{JsonSchema, schema_for}; use serde::Deserialize; -use ogcapi_types::{ - common::Exception, - processes::{ - Execute, Format, InlineOrRefData, Input, InputValueNoObject, Output, Process, - TransmissionMode, - }, +use ogcapi_types::processes::{ + Execute, ExecuteResult, ExecuteResults, Format, InlineOrRefData, Input, InputValueNoObject, + JobControlOptions, Output, Process, ProcessSummary, TransmissionMode, + description::{DescriptionType, InputDescription, OutputDescription}, }; -use crate::{ProcessResponseBody, Processor}; +use crate::Processor; /// Greeter `Processor` /// @@ -68,20 +66,6 @@ impl GreeterOutputs { } } -impl TryFrom for GreeterOutputs { - type Error = Exception; - - fn try_from(value: ProcessResponseBody) -> Result { - if let ProcessResponseBody::Requested(buf) = value { - Ok(GreeterOutputs { - greeting: String::from_utf8(buf).unwrap(), - }) - } else { - Err(Exception::new("500")) - } - } -} - #[async_trait::async_trait] impl Processor for Greeter { fn id(&self) -> &'static str { @@ -93,32 +77,63 @@ impl Processor for Greeter { } fn process(&self) -> Result { - Process::try_new( - self.id(), - self.version(), - &schema_for!(GreeterInputs), - &schema_for!(GreeterOutputs), - ) - .map_err(Into::into) + Ok(Process { + summary: ProcessSummary { + id: self.id().to_string(), + version: self.version().to_string(), + job_control_options: vec![ + JobControlOptions::SyncExecute, + JobControlOptions::AsyncExecute, + JobControlOptions::Dismiss, + ], + output_transmission: vec![TransmissionMode::Value, TransmissionMode::Reference], + links: Vec::new(), + }, + inputs: HashMap::from([( + "name".to_string(), + InputDescription { + description_type: DescriptionType::default(), + schema: schema_for!(GreeterInputs).to_value(), + ..Default::default() + }, + )]), + outputs: HashMap::from([( + "greeting".to_string(), + OutputDescription { + description_type: DescriptionType::default(), + schema: schema_for!(GreeterOutputs).to_value(), + }, + )]), + }) } - async fn execute(&self, execute: Execute) -> Result { + async fn execute(&self, execute: Execute) -> Result { let value = serde_json::to_value(execute.inputs).unwrap(); let inputs: GreeterInputs = serde_json::from_value(value).unwrap(); - Ok(ProcessResponseBody::Requested( - format!("Hello, {}!\n", inputs.name).as_bytes().to_owned(), - )) + let greeting = format!("Hello, {}!\n", inputs.name); + + Ok(HashMap::from([( + "greeting".to_string(), + ExecuteResult { + data: InlineOrRefData::InputValueNoObject(InputValueNoObject::String(greeting)), + output: Output { + format: Some(Format { + media_type: Some("text/plain".to_string()), + encoding: Some("utf8".to_string()), + schema: None, + }), + transmission_mode: TransmissionMode::Value, + }, + }, + )])) } } #[cfg(test)] mod tests { - use ogcapi_types::processes::Execute; - - use crate::{ - Processor, - greeter::{Greeter, GreeterInputs, GreeterOutputs}, - }; + use super::*; + use crate::Processor; + use ogcapi_types::processes::{Execute, ExecuteResult, InlineOrRefData, InputValueNoObject}; #[tokio::test] async fn test_greeter() { @@ -140,7 +155,13 @@ mod tests { ..Default::default() }; - let output: GreeterOutputs = greeter.execute(execute).await.unwrap().try_into().unwrap(); - assert_eq!(output.greeting, "Hello, Greeter!\n"); + let output = greeter.execute(execute).await.unwrap(); + + let ExecuteResult { data, output: _ } = output.get("greeting").unwrap(); + let InlineOrRefData::InputValueNoObject(InputValueNoObject::String(greeting)) = data else { + panic!("Unexpected output data type"); + }; + + assert_eq!(greeting, "Hello, Greeter!\n"); } } diff --git a/ogcapi-processes/src/lib.rs b/ogcapi-processes/src/lib.rs index 7c88e22..d26775f 100644 --- a/ogcapi-processes/src/lib.rs +++ b/ogcapi-processes/src/lib.rs @@ -1,11 +1,13 @@ #[cfg(feature = "greeter")] pub mod greeter; -#[cfg(feature = "gdal-loader")] -pub mod gdal_loader; +// #[cfg(feature = "gdal-loader")] +// pub mod gdal_loader; -#[cfg(feature = "geojson-loader")] -pub mod geojson_loader; +// #[cfg(feature = "geojson-loader")] +// pub mod geojson_loader; + +pub mod echo; mod processor; pub use processor::*; diff --git a/ogcapi-processes/src/processor.rs b/ogcapi-processes/src/processor.rs index 11d50d6..5f45a30 100644 --- a/ogcapi-processes/src/processor.rs +++ b/ogcapi-processes/src/processor.rs @@ -1,8 +1,6 @@ use anyhow::Result; use dyn_clone::DynClone; - -use ogcapi_types::processes::{Execute, Process, Results, StatusInfo}; -use serde::{Deserialize, Serialize}; +use ogcapi_types::processes::{Execute, ExecuteResults, Process}; /// Trait for defining and executing a [Process] #[async_trait::async_trait] @@ -17,15 +15,7 @@ pub trait Processor: Send + Sync + DynClone { fn process(&self) -> Result; /// Executes the Process and returns [Results] - async fn execute(&self, execute: Execute) -> Result; + async fn execute(&self, execute: Execute) -> Result; } dyn_clone::clone_trait_object!(Processor); - -#[derive(Debug, Serialize, Deserialize)] -pub enum ProcessResponseBody { - Requested(Vec), - Results(Results), - Empty(String), - StatusInfo(StatusInfo), -} diff --git a/ogcapi-services/Cargo.toml b/ogcapi-services/Cargo.toml index 83fe66c..fa5f3e6 100644 --- a/ogcapi-services/Cargo.toml +++ b/ogcapi-services/Cargo.toml @@ -17,33 +17,35 @@ default = ["common", "edr", "features", "processes", "tiles"] common = ["ogcapi-types/common", "ogcapi-drivers/common"] features = ["ogcapi-types/features", "ogcapi-drivers/features"] edr = ["ogcapi-types/edr", "ogcapi-drivers/edr"] -processes = ["ogcapi-types/processes", "ogcapi-drivers/processes", "dyn-clone", "schemars", "ogcapi-processes"] +processes = ["ogcapi-types/processes", "ogcapi-drivers/processes", "ogcapi-processes", "dyn-clone", "schemars", "mail-builder"] stac = ["ogcapi-types/stac", "ogcapi-drivers/stac"] styles = ["ogcapi-types/styles", "ogcapi-drivers/styles"] tiles = ["ogcapi-types/tiles", "ogcapi-drivers/tiles"] [dependencies] anyhow = { workspace = true } -axum = { version = "0.8.4", features = ["multipart"] } -axum-extra = { version = "0.10.1" } +axum = { version = "0.8.7", features = ["multipart"] } +axum-extra = { version = "0.12.2" } clap = { version = "4.5", features = ["derive", "env"] } dyn-clone = { version = "1.0", optional = true } dotenvy = "0.15.7" -hyper = { version = "1.6", features = ["full"] } +futures = "0.3.31" +hyper = { version = "1.8", features = ["full"] } +mail-builder = { version = "0.4.4", optional = true } openapiv3 = "2.2" -schemars = { version = "1.0", optional = true } +schemars = { version = "1.1", optional = true } serde = { workspace = true } serde_json = { workspace = true } serde_qs = { workspace = true } serde_yaml = "0.9.33" thiserror = { workspace = true } -tokio = { version = "1.47", features = ["full"] } +tokio = { version = "1.48", features = ["full"] } tower = "0.5.2" tower-http = { version = "0.6.6", features = ["compression-gzip", "catch-panic", "cors", "request-id", "sensitive-headers", "trace", "util"] } tracing = "0.1.41" -tracing-subscriber = { version="0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } url = { workspace = true, features = ["serde"] } -utoipa = { version = "5.3", features = ["axum_extras", "preserve_order", "preserve_path_order"] } +utoipa = { version = "5.4", features = ["axum_extras", "preserve_order", "preserve_path_order"] } utoipa-axum = "0.2.0" utoipa-swagger-ui = { version = "9.0", features = ["axum"] } @@ -53,9 +55,9 @@ ogcapi-processes = { path = "../ogcapi-processes", version = "0.3", optional = t [dev-dependencies] geojson = { workspace = true } -hyper-util = { version = "0.1.16", features = ["client-legacy"] } +hyper-util = { version = "0.1.18", features = ["client-legacy"] } http-body-util = "0.1.3" -uuid = { version = "1.17", features = ["serde", "v4"] } +uuid = { version = "1.18", features = ["serde", "v4"] } data-loader = { path = "../examples/data-loader" } diff --git a/ogcapi-services/assets/openapi/openapi.yaml b/ogcapi-services/assets/openapi/openapi.yaml index 82feb3c..56aeb0f 100644 --- a/ogcapi-services/assets/openapi/openapi.yaml +++ b/ogcapi-services/assets/openapi/openapi.yaml @@ -6,8 +6,8 @@ info: This is an OpenAPI definition of various OGC API specifications as well as the SpatioTemporal Asset Catalog (STAC) API specification. contact: - name: Balthasar Teuscher - email: balthasar.teuscher@gmail.com + name: "GeoRust `ogcapi` project on GitHub" + url: "https://github.com/georust/ogcapi" license: name: CC-BY 4.0 license url: https://creativecommons.org/licenses/by/4.0/ @@ -20,6 +20,10 @@ tags: description: essential characteristics of this API - name: Data description: access to data (features) + - name: Processes + description: OGC API Processes. + - name: Jobs + description: OGC API Processes (Jobs). paths: /: get: @@ -31,9 +35,9 @@ paths: 200: $ref: "#/components/responses/LandingPage" 400: - $ref: "#/components/responses/400" + $ref: "#/components/responses/BadRequest" 500: - $ref: "#/components/responses/500" + $ref: "#/components/responses/ServerError" summary: Landing page tags: - server @@ -42,11 +46,11 @@ paths: description: This document responses: 200: - $ref: "#/components/responses/200" + $ref: "#/components/responses/Success" 400: - $ref: "#/components/responses/400" + $ref: "#/components/responses/BadRequest" default: - $ref: "#/components/responses/500" + $ref: "#/components/responses/ServerError" summary: This document tags: - server @@ -60,9 +64,9 @@ paths: 200: $ref: "#/components/responses/ConformanceDeclaration" 400: - $ref: "#/components/responses/400" + $ref: "#/components/responses/BadRequest" 500: - $ref: "#/components/responses/500" + $ref: "#/components/responses/ServerError" summary: API conformance definition tags: - server @@ -76,7 +80,7 @@ paths: 200: $ref: "#/components/schemas/collections" 500: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/ServerError" /collections/{collectionId}: get: tags: @@ -89,9 +93,9 @@ paths: 200: $ref: "#/components/schemas/collectionDesc" 404: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/NotFound" 500: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/ServerError" /collections/{collectionId}/items: get: tags: @@ -117,11 +121,11 @@ paths: 200: $ref: "#/components/responses/FeatureCollection" 400: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/BadRequest" 404: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/NotFound" 500: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/ServerError" /collections/{collectionId}/items/{featureId}: get: tags: @@ -141,9 +145,176 @@ paths: 200: $ref: "#/components/responses/Feature" 404: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/NotFound" 500: - $ref: "#/components/schemas/exception" + $ref: "#/components/responses/ServerError" + + # OGC API - Processes - Part 1: Core + /processes: + get: + summary: retrieve the list of available processes + description: | + The list of processes contains a summary of each process the OGC API - Processes offers, including the link to a more detailed description of the process. + + For more information, see [Section 7.7]https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_process_list). + operationId: getProcesses + tags: + - Processes + responses: + 200: + $ref: "#/components/responses/ProcessList" + + /processes/{processID}: + get: + summary: retrieve a process description + description: | + The process description contains information about inputs and outputs and a link to the execution-endpoint for the process. The Core does not mandate the use of a specific process description to specify the interface of a process. That said, the Core requirements class makes the following recommendation: + + Implementations SHOULD consider supporting the OGC process description. + + For more information, see [Section 7.8](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_process_description). + operationId: getProcessDescription + tags: + - Processes + parameters: + - $ref: "#/components/parameters/processID-path" + responses: + 200: + $ref: "#/components/responses/ProcessDescription" + 404: + $ref: "#/components/responses/NotFound" + + /processes/{processID}/execution: + post: + summary: execute a process. + description: | + Executes a process (this may result in the creation of a job resource e.g., for _asynchronous execution_). + + For more information, see [Section 7.9](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_create_job). + operationId: execute + tags: + - Processes + parameters: + - $ref: "#/components/parameters/processID-path" + - $ref: "#/components/parameters/prefer-header-execution" + requestBody: + description: |- + An execution request specifying any inputs for the process to execute, and optionally to select specific outputs. + With support for _Part 3: Workflows and chaining_, this execution request may specify a complex processing workflow + e.g., including nested processes and OGC API collections as inputs. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/execute" + responses: + 200: + $ref: "#/components/responses/ExecuteSync" + 201: + $ref: "#/components/responses/ExecuteAsync" + 204: + $ref: "#/components/responses/Empty" + 404: + $ref: "#/components/responses/NotFound" + 500: + $ref: "#/components/responses/ServerError" + # callbacks: + # jobCompleted: + # "{$request.body#/subscriber/successUri}": + # post: + # requestBody: + # content: + # application/json: + # schema: + # $ref: "../../schemas/processes-core/results.yaml" + # responses: + # 200: + # description: Results received successfully + + # OGC API - Processes - Part 1: Core (Jobs, required for Async execution) + /jobs: + get: + summary: retrieve the list of jobs. + description: | + Lists available jobs. + + For more information, see [Section 12](https://docs.ogc.org/is/18-062r2/18-062r2.html#Job_list). + operationId: getJobs + tags: + - Jobs + parameters: + - $ref: "#/components/parameters/limit" + - $ref: "#/components/parameters/type" + - $ref: "#/components/parameters/processID-query" + - $ref: "#/components/parameters/status" + - $ref: "#/components/parameters/datetime" + - $ref: "#/components/parameters/minDuration" + - $ref: "#/components/parameters/maxDuration" + responses: + 200: + $ref: "#/components/responses/JobList" + 404: + $ref: "#/components/responses/NotFound" + + /jobs/{jobID}: + get: + summary: retrieve the status of a job + description: | + Shows the status of a job. + + For more information, see [Section 7.10](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_retrieve_status_info). + operationId: getStatus + tags: + - Jobs + parameters: + - $ref: "#/components/parameters/jobID" + responses: + 200: + $ref: "#/components/responses/Status" + 404: + $ref: "#/components/responses/NotFound" + 500: + $ref: "#/components/responses/ServerError" + delete: + summary: cancel a job execution, remove a finished job + description: | + Cancel a job execution and remove it from the jobs list. + + For more information, see [Section 14]https://docs.ogc.org/is/18-062r2/18-062r2.html#Dismiss). + operationId: dismiss + tags: + - Jobs + parameters: + - $ref: "#/components/parameters/jobID" + responses: + 200: + $ref: "#/components/responses/Status" + 404: + $ref: "#/components/responses/NotFound" + 500: + $ref: "#/components/responses/ServerError" + + /jobs/{jobID}/results: + get: + summary: retrieve the result(s) of a job + description: | + Lists available results of a job. In case of a failure, lists exceptions instead. + + For more information, see [Section 7.11](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_retrieve_job_results). + operationId: getResult + tags: + - Jobs + parameters: + - $ref: "#/components/parameters/jobID" + - $ref: "#/components/parameters/prefer-header-results" + responses: + 200: + $ref: "#/components/responses/Results" + 404: + $ref: "#/components/responses/NotFound" + 500: + $ref: "#/components/responses/ServerError" + components: headers: Content-Crs: @@ -280,7 +451,89 @@ components: default: 10 style: form explode: false + processID-path: + name: processID + in: path + required: true + schema: + type: string + prefer-header-execution: + in: header + description: |- + Indicates client preferences, including whether the client is capable of asynchronous processing. + A `respond-async` preference indicates a preference for asynchronous processing. + A `wait: s` preference indicates that the client prefers to wait up to x seconds to receive a reponse synchronously before the server falls back to asynchronous processing. + name: Prefer + schema: + type: string + processID-query: + name: processID + in: query + required: false + schema: + type: array + items: + type: string + type: + name: type + in: query + required: false + schema: + type: array + items: + type: string + enum: + - process + status: + name: status + in: query + required: false + schema: + type: array + items: + $ref: "#/components/schemas/statusCode" + minDuration: + name: minDuration + in: query + required: false + schema: + type: integer + maxDuration: + name: maxDuration + in: query + required: false + schema: + type: integer + jobID: + name: jobID + in: path + description: local identifier of a job + required: true + schema: + type: string + prefer-header-results: + in: header + description: |- + Indicates client preferences, such as whether the client wishes a self-contained or minimal response. + A `return=minimal` preference indicates that the client would prefer that links be returned to larger object to minimize the response payload. + A `return=representation` indicates that the client would prefer if the server can return a self-contained response. + name: Prefer + schema: + type: string responses: + LandingPage: + description: |- + The landing page provides links to the API definition + (link relations `service-desc` and `service-doc`), + and the Conformance declaration (path `/conformance`, + link relation `conformance`). + content: + application/json: + schema: + $ref: "#/components/schemas/landingPage" + text/html: + schema: + type: string ConformanceDeclaration: description: |- The URIs of all conformance classes supported by the server. @@ -320,21 +573,93 @@ components: application/geo+json: schema: $ref: "#/components/schemas/featureGeoJSON" - LandingPage: - description: |- - The landing page provides links to the API definition - (link relations `service-desc` and `service-doc`), - and the Conformance declaration (path `/conformance`, - link relation `conformance`). + ProcessList: + description: Information about the available processes content: application/json: schema: - $ref: "#/components/schemas/landingPage" - text/html: + $ref: "#/components/schemas/processList" + ProcessDescription: + description: A process description. + content: + application/json: + schema: + $ref: "../../schemas/processes-core/process.yaml" + ExecuteSync: + description: Result of synchronous execution + content: + application/json: + schema: + oneOf: + - type: string + - type: number + - type: integer + - type: object + nullable: true + - type: array + items: {} + - type: boolean + - type: string + format: binary + - $ref: "../../schemas/processes-core/results.yaml" + image/png: schema: type: string - exception: - description: An error occurred. + format: binary + image/jpeg: + schema: + type: string + format: binary + image/tiff; application=geotiff: + schema: + type: string + format: binary + application/geo+json: + schema: + allOf: + - format: geojson-feature-collection + # JSON Schema not supported in OpenAPI 3.0 + # - $ref: https://geojson.org/schema/FeatureCollection.json + - $ref: "../../schemas/geojson/FeatureCollection.yaml" + ExecuteAsync: + description: Started asynchronous execution. Created job. + headers: + Location: + schema: + type: string + description: URL to check the status of the execution/job. + Preference-Applied: + schema: + type: string + description: The preference applied to execute the process asynchronously (see. RFC 2740). + content: + application/json: + schema: + $ref: "../../schemas/processes-core/statusInfo.yaml" + Empty: + description: successful operation (no response body) + JobList: + description: A list of jobs for this process. + content: + application/json: + schema: + $ref: "../../schemas/processes-core/jobList.yaml" + Status: + description: The status of a job. + content: + application/json: + schema: + $ref: "../../schemas/processes-core/statusInfo.yaml" + Results: + description: The processing results of a job. + content: + application/json: + schema: + $ref: "../../schemas/processes-core/results.yaml" + Success: + description: General Success response. + BadRequest: + description: Bad Request. content: application/json: schema: @@ -342,12 +667,8 @@ components: text/html: schema: type: string - 200: - description: |- - General Success response. - 400: - description: |- - General HTTP error response. + NotFound: + description: The requested resource does not exist on the server. For example, a path parameter had an incorrect value. content: application/json: schema: @@ -355,9 +676,8 @@ components: text/html: schema: type: string - 500: - description: |- - A server error occurred. + ServerError: + description: A server error occurred. content: application/json: schema: @@ -876,3 +1196,204 @@ components: type: string format: date-time example: "2017-08-17T08:05:32Z" + processList: + type: object + required: + - processes + - links + properties: + processes: + type: array + items: + $ref: "#/components/schemas/processSummary" + links: + type: array + items: + $ref: "#/components/schemas/link" + processSummary: + allOf: + - $ref: "#/components/schemas/descriptionType" + - type: object + required: + - id + - version + properties: + id: + type: string + version: + type: string + jobControlOptions: + type: array + items: + $ref: "#/components/schemas/jobControlOptions" + links: + type: array + items: + $ref: "#/components/schemas/link" + descriptionType: + type: object + properties: + title: + type: string + description: + type: string + keywords: + type: array + items: + type: string + metadata: + type: array + items: + $ref: "#/components/schemas/metadata" + jobControlOptions: + type: string + enum: + - sync-execute + - async-execute + - dismiss + metadata: + oneOf: + - allOf: + - $ref: "#/components/schemas/link" + - type: object + properties: + role: + type: string + - type: object + properties: + role: + type: string + title: + type: string + lang: + type: string + value: + oneOf: + - type: string + - type: object + execute: + type: object + properties: + processID: + type: string + format: uri + inputs: + additionalProperties: + $ref: "#/components/schemas/input" + outputs: + additionalProperties: + $ref: "#/components/schemas/output" + subscriber: + $ref: "#/components/schemas/subscriber" + input: + oneOf: + - $ref: "#/components/schemas/inlineOrRefValue" + - type: array + items: + $ref: "#/components/schemas/inlineOrRefValue" + inlineOrRefValue: + oneOf: + - $ref: "#/components/schemas/valueNoObject" + - $ref: "#/components/schemas/qualifiedValue" + - $ref: "#/components/schemas/link" + valueNoObject: + anyOf: + - type: string + - type: number + - type: integer + - type: boolean + - type: array + items: + oneOf: + - $ref: "#/components/schemas/inputValue" + - $ref: "#/components/schemas/qualifiedValue" + - $ref: "#/components/schemas/binaryValue" + - $ref: "#/components/schemas/bbox" + # - $ref: "collectionValue.yaml" + qualifiedValue: + allOf: + - $ref: "#/components/schemas/format" + - type: object + required: + - value + properties: + value: + $ref: "#/components/schemas/inputValue" + format: + type: object + properties: + mediaType: + type: string + encoding: + type: string + schema: + oneOf: + - type: string + format: url + - type: object + inputValue: + anyOf: + - $ref: "#/components/schemas/valueNoObject" + - type: object + binaryValue: + type: string + format: byte + bbox: + type: object + required: + - bbox + properties: + bbox: + type: array + oneOf: + - minItems: 4 + maxItems: 4 + - minItems: 6 + maxItems: 6 + items: + type: number + crs: + $ref: "#/components/schemas/bbox-def-crs" + bbox-def-crs: + anyOf: + - type: string + format: uri + enum: + - "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + - "http://www.opengis.net/def/crs/OGC/0/CRS84h" + default: "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + - type: string + format: uri + default: "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + output: + type: object + properties: + format: + $ref: "#/components/schemas/format" + subscriber: + description: |- + Optional URIs for callbacks for this job. + + Support for this parameter is not required and the parameter may be + removed from the API definition, if conformance class **'callback'** + is not listed in the conformance declaration under `/conformance`. + type: object + properties: + successUri: + type: string + format: uri + inProgressUri: + type: string + format: uri + failedUri: + type: string + format: uri + statusCode: + type: string + nullable: false + enum: + - accepted + - running + - successful + - failed + - dismissed diff --git a/ogcapi-services/src/config.rs b/ogcapi-services/src/config.rs index e350f0b..ff70199 100644 --- a/ogcapi-services/src/config.rs +++ b/ogcapi-services/src/config.rs @@ -6,12 +6,9 @@ pub struct Config { /// Listening port of the server #[clap(long, env("APP_PORT"), default_value = "8484")] pub port: u16, - /// istening host address of the server + /// listening host address of the server #[clap(long, env("APP_HOST"), default_value = "0.0.0.0")] pub host: String, - /// Postgres database url - #[clap(long, env, hide_env_values = true, value_parser)] - pub database_url: url::Url, /// OpenAPI definition #[clap(long, env, value_parser)] pub openapi: Option, diff --git a/ogcapi-services/src/error.rs b/ogcapi-services/src/error.rs index 257526b..aa33130 100644 --- a/ogcapi-services/src/error.rs +++ b/ogcapi-services/src/error.rs @@ -4,6 +4,7 @@ use axum::{ response::{IntoResponse, Response}, }; use hyper::HeaderMap; +use tracing::error; use ogcapi_types::common::{Exception, media_type::PROBLEM_JSON}; @@ -12,9 +13,6 @@ use ogcapi_types::common::{Exception, media_type::PROBLEM_JSON}; /// Can be returned in a `Result` from an API handler function. #[derive(thiserror::Error, Debug)] pub enum Error { - // /// Automatically return `500 Internal Server Error` on a `sqlx::Error`. - // #[error("an error occurred with the database")] - // Sqlx(#[from] sqlx::Error), /// Return `404 Not Found` #[error("not found")] NotFound, @@ -32,15 +30,18 @@ pub enum Error { Qs(#[from] serde_qs::Error), /// Custom Exception - #[error("an ogcapi exception occurred")] - Exception(StatusCode, String), + #[error("an OGC API exception occurred")] + ApiException(#[from] Exception), } impl Error { fn status_code(&self) -> StatusCode { match self { Self::NotFound => StatusCode::NOT_FOUND, - Self::Exception(status, _) => *status, + Self::ApiException(exception) => exception + .status + .and_then(|status| StatusCode::from_u16(status).ok()) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), _ => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -53,35 +54,38 @@ impl Error { /// to the client. impl IntoResponse for Error { fn into_response(self) -> Response { - let (status, message) = match self { - // Self::Sqlx(ref e) => { - // tracing::error!("SQLx error: {:?}", e); - // (self.status_code(), self.to_string()) - // } - Self::NotFound => (self.status_code(), self.to_string()), - Self::Anyhow(ref e) => { + let status = self.status_code(); + let exception = Exception::from(self); + + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, PROBLEM_JSON.parse().unwrap()); + + (status, headers, Json(exception)).into_response() + } +} + +impl From for Exception { + fn from(value: Error) -> Self { + let (status, message) = match value { + Error::NotFound => (value.status_code(), value.to_string()), + Error::Anyhow(ref e) => { tracing::error!("Generic error: {:?}", e); - (self.status_code(), self.to_string()) + (value.status_code(), e.to_string()) } - Self::Url(ref e) => { + Error::Url(ref e) => { tracing::error!("Url error: {:?}", e); - (self.status_code(), self.to_string()) + (value.status_code(), e.to_string()) } - Self::Qs(ref e) => { + Error::Qs(ref e) => { tracing::error!("Query string error: {:?}", e); - (self.status_code(), self.to_string()) + (value.status_code(), e.to_string()) } - Self::Exception(status, message) => { - tracing::debug!("OGCAPI exception: {}", message); - (status, message) + Error::ApiException(exception) => { + tracing::debug!("OGCAPI exception: {exception}"); + return exception; } }; - let exception = Exception::new(status.as_u16()).detail(message); - - let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, PROBLEM_JSON.parse().unwrap()); - - (status, headers, Json(exception)).into_response() + Exception::new(status.as_u16()).detail(message) } } diff --git a/ogcapi-services/src/extractors.rs b/ogcapi-services/src/extractors.rs index 5806bed..b7d6e0e 100644 --- a/ogcapi-services/src/extractors.rs +++ b/ogcapi-services/src/extractors.rs @@ -20,7 +20,13 @@ where async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let uri = OriginalUri::from_request_parts(parts, state).await.unwrap(); - let url = if uri.0.scheme().is_some() { + let url = if let Ok(url) = std::env::var("PUBLIC_URL") { + format!( + "{}{}", + url.trim_end_matches('/'), + uri.path_and_query().unwrap() + ) + } else if uri.0.scheme().is_some() { uri.0.to_string() } else { let host = Host::from_request_parts(parts, state) @@ -54,7 +60,9 @@ where let qs = parts.uri.query().unwrap_or(""); match serde_qs::from_str(qs) { Ok(query) => Ok(Self(query)), - Err(e) => Err(Error::Exception(StatusCode::BAD_REQUEST, e.to_string())), + Err(e) => Err(Error::ApiException( + (StatusCode::BAD_REQUEST, e.to_string()).into(), + )), } } } diff --git a/ogcapi-services/src/lib.rs b/ogcapi-services/src/lib.rs index 473d683..17d0c82 100644 --- a/ogcapi-services/src/lib.rs +++ b/ogcapi-services/src/lib.rs @@ -13,9 +13,13 @@ pub use config::Config; pub use error::Error; pub use openapi::ApiDoc; pub use service::Service; -pub use state::AppState; +pub use state::{AppState, Drivers}; #[doc(hidden)] pub use clap::Parser as ConfigParser; pub type Result = std::result::Result; + +pub fn setup_env() { + dotenvy::dotenv().ok(); +} diff --git a/ogcapi-services/src/main.rs b/ogcapi-services/src/main.rs index 0cded38..de8677f 100644 --- a/ogcapi-services/src/main.rs +++ b/ogcapi-services/src/main.rs @@ -1,13 +1,13 @@ #[tokio::main] async fn main() -> anyhow::Result<()> { // setup env - dotenvy::dotenv().ok(); + ogcapi_services::setup_env(); // setup tracing ogcapi_services::telemetry::init(); // build & run our application with hyper - ogcapi_services::Service::new().await.serve().await; + ogcapi_services::Service::try_new().await?.serve().await; Ok(()) } diff --git a/ogcapi-services/src/openapi.rs b/ogcapi-services/src/openapi.rs index cf479b1..3c236a7 100644 --- a/ogcapi-services/src/openapi.rs +++ b/ogcapi-services/src/openapi.rs @@ -1,8 +1,11 @@ use utoipa::OpenApi; /// TODO: remove once Open API 3.1 is supported -#[cfg(all(feature = "features", not(feature = "edr")))] -pub(crate) static OPENAPI: &[u8; 29696] = include_bytes!("../assets/openapi/openapi.yaml"); +#[cfg(all( + any(feature = "common", feature = "features", feature = "processes"), + not(feature = "edr") +))] +pub(crate) static OPENAPI: &[u8; 45786] = include_bytes!("../assets/openapi/openapi.yaml"); /// TODO: remove once Open API 3.1 is supported #[cfg(feature = "edr")] diff --git a/ogcapi-services/src/processes.rs b/ogcapi-services/src/processes.rs index e28da32..f86a422 100644 --- a/ogcapi-services/src/processes.rs +++ b/ogcapi-services/src/processes.rs @@ -1,32 +1,532 @@ +use crate::Error; use axum::{ + Json, body::Body, - http::StatusCode, - response::{IntoResponse, Response}, + extract::{FromRequest, Request}, + http::{HeaderValue, StatusCode}, + response::{IntoResponse, IntoResponseParts, Response, ResponseParts}, }; -use ogcapi_processes::ProcessResponseBody; +use hyper::header::{self, InvalidHeaderValue, LINK, LOCATION}; +use mail_builder::headers::HeaderType; +use mail_builder::headers::content_type::ContentType; +use mail_builder::headers::text::Text; +use mail_builder::mime::{BodyPart, MimePart}; +use ogcapi_types::{ + common::{Exception, Link}, + processes::{ExecuteResult, ExecuteResults, InlineOrRefData, StatusInfo}, +}; +use std::borrow::Cow; +use std::convert::Infallible; +use std::fmt::Write; -pub(crate) struct ProcessResponse(pub(crate) ProcessResponseBody); +pub(crate) struct ProcessResultsResponse { + pub results: ExecuteResults, + pub response_mode: ogcapi_types::processes::Response, +} -impl IntoResponse for ProcessResponse { - fn into_response(self) -> Response { +pub(crate) enum ProcessExecuteResponse { + Synchronous { + results: ProcessResultsResponse, + was_preferred_execution_mode: bool, + }, + Asynchronous { + status_info: StatusInfo, + was_preferred_execution_mode: bool, + base_url: String, + }, +} + +impl ProcessExecuteResponse { + fn preference_header(&self) -> PreferredResponseHeader { + match self { + ProcessExecuteResponse::Synchronous { + was_preferred_execution_mode: true, + .. + } => PreferredResponseHeader::RespondSync, + ProcessExecuteResponse::Asynchronous { + was_preferred_execution_mode: true, + .. + } => PreferredResponseHeader::RespondAsync, + _ => PreferredResponseHeader::None, + } + } +} + +/// Add Preference-Applied header to response based on negotiated execution mode. +/// +/// cf. +enum PreferredResponseHeader { + RespondSync, + RespondAsync, + None, +} + +impl IntoResponseParts for PreferredResponseHeader { + type Error = Infallible; + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + const PREFERENCE_KEY: &str = "Preference-Applied"; + + match self { + PreferredResponseHeader::RespondSync => { + res.headers_mut() + .insert(PREFERENCE_KEY, HeaderValue::from_static("respond-sync")); + } + PreferredResponseHeader::RespondAsync => { + res.headers_mut() + .insert(PREFERENCE_KEY, HeaderValue::from_static("respond-async")); + } + PreferredResponseHeader::None => { /* nothing to do */ } + }; + + Ok(res) + } +} + +struct LocationHeader(Result); + +impl LocationHeader { + fn new(base_url: &str, job_id: &str) -> Self { + Self(format!("{base_url}/jobs/{job_id}").parse()) + } +} + +impl IntoResponseParts for LocationHeader { + type Error = (StatusCode, String); + + fn into_response_parts(self, mut res: ResponseParts) -> Result { match self.0 { - ProcessResponseBody::Requested(body) => Response::new(body.into()), - ProcessResponseBody::Results(results) => Response::builder() - .status(StatusCode::OK) - .header("Content-Type", "application/json") - .body(Body::from(serde_json::to_vec(&results).unwrap())) - .unwrap(), - ProcessResponseBody::Empty(link) => Response::builder() - .status(StatusCode::NO_CONTENT) - .header("Link", link) - .body(Body::empty()) - .unwrap(), - ProcessResponseBody::StatusInfo(status_info) => Response::builder() - .status(StatusCode::CREATED) - .header("Content-Type", "application/json") - .header("Location", &format!("../../jobs/{}", status_info.job_id)) - .body(Body::from(serde_json::to_vec(&status_info).unwrap())) - .unwrap(), + Err(e) => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to create Location header: {}", e), + )); + } + Ok(location) => res.headers_mut().insert(LOCATION, location), + }; + + Ok(res) + } +} + +impl IntoResponse for ProcessResultsResponse { + fn into_response(self) -> Response { + if let ogcapi_types::processes::Response::Document = self.response_mode { + let results = ogcapi_types::processes::Results { + results: self + .results + .into_iter() + .map(|(name, result)| (name, result.data)) + .collect(), + }; + return Json(results).into_response(); + }; + + match self.results.len() { + 0 => (StatusCode::NO_CONTENT, Body::empty()).into_response(), + 1 => { + let (_, result) = self.results.into_iter().next().expect("checked"); + SingleResponse(result).into_response() + } + _ => MultipartResponse::new(self.results).into_response(), + } + } +} + +impl IntoResponse for ProcessExecuteResponse { + fn into_response(self) -> Response { + let preference_header = self.preference_header(); + + match self { + ProcessExecuteResponse::Synchronous { + results, + was_preferred_execution_mode: _, + } => (preference_header, results).into_response(), + ProcessExecuteResponse::Asynchronous { + status_info, + was_preferred_execution_mode: _, + base_url, + } => { + let location_header = LocationHeader::new(&base_url, &status_info.job_id); + + // `/req/core/process-execute-success-async` + ( + StatusCode::CREATED, + location_header, + preference_header, + Json(status_info), + ) + .into_response() + } + } + } +} + +struct SingleResponse(ExecuteResult); + +impl IntoResponse for SingleResponse { + fn into_response(self) -> Response { + let ExecuteResult { output: _, data } = self.0; + + match data { + InlineOrRefData::Link(link) => { + // Cf. link header + + (StatusCode::NO_CONTENT, LinkHeader(link), Body::empty()).into_response() + } + _ => { + let body = to_binary(data); + ( + StatusCode::OK, + [(header::CONTENT_TYPE, body.content_type)], + Body::from(body.data), + ) + .into_response() + } + } + } +} + +struct LinkHeader(Link); + +impl IntoResponseParts for LinkHeader { + type Error = (StatusCode, String); + + fn into_response_parts(self, mut res: ResponseParts) -> Result { + fn add_quoted_string(header: &mut String, key: &str, value: &str) { + header.push_str("; "); + header.push_str(key); + header.push_str("=\""); + for c in value.chars() { + if c == '"' || c == '\\' { + header.push('\\'); + } + header.push(c); + } + header.push('"'); + } + + let link = self.0; + + let mut link_header = String::from("<"); + link_header.push_str(&link.href); + link_header.push('>'); + + add_quoted_string(&mut link_header, "rel", &link.rel); + + if let Some(type_) = &link.r#type { + add_quoted_string(&mut link_header, "type", type_); + } + if let Some(href_lang) = &link.hreflang { + add_quoted_string(&mut link_header, "hreflang", href_lang); + } + + if let Some(title) = &link.title { + add_quoted_string(&mut link_header, "title", title); + } + + if let Some(length) = link.length { + let _ = write!(link_header, "; length={}", length); + } + + match link_header.parse() { + Ok(header_value) => { + res.headers_mut().insert(LINK, header_value); + Ok(res) + } + Err(e) => Err(( + StatusCode::INTERNAL_SERVER_ERROR, + format!("failed to create Link header: {}", e), + )), + } + } +} + +struct MultipartResponse { + parts: ExecuteResults, + boundary: Option, +} +impl MultipartResponse { + fn new(parts: ExecuteResults) -> Self { + Self { + parts, + boundary: None, + } + } + + #[cfg(test)] + fn new_with_boundary(parts: ExecuteResults, boundary: String) -> Self { + Self { + parts, + boundary: Some(boundary), + } + } +} + +impl IntoResponse for MultipartResponse { + fn into_response(self) -> Response { + let mut mime_parts = Vec::::with_capacity(self.parts.len()); + + let parts = self.parts; + #[cfg(test)] + let parts = parts + .into_iter() + .collect::>(); + + for (name, result) in parts { + let mut mime_part = to_mime_part(result.data); + mime_part = mime_part.header("Content-ID", Text::new(name)); + mime_parts.push(mime_part); + } + + let mut content_type = ContentType::new("multipart/related"); + if let Some(boundary) = self.boundary { + content_type = content_type.attribute("boundary", boundary); + } + let multipart_mime_part = MimePart::new(content_type, mime_parts); + + let mut body = Vec::new(); + let write_result = multipart_mime_part.write_part(&mut body); + + if let Err(e) = write_result { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to write multipart MIME response: {}", e), + ) + .into_response(); + } + + ( + StatusCode::OK, + [(header::CONTENT_TYPE, "multipart/related")], + Body::from(body), + ) + .into_response() + } +} + +struct BinaryData { + data: Vec, + content_type: String, +} + +fn to_mime_part(data: InlineOrRefData) -> MimePart<'static> { + fn extract_header( + (name, value): (&axum::http::HeaderName, &axum::http::HeaderValue), + ) -> (Cow<'static, str>, HeaderType<'static>) { + let name = name.as_str().to_string(); + let value = value.to_str().unwrap_or_default().to_string(); + (Cow::Owned(name), HeaderType::Text(Text::new(value))) + } + + match data { + InlineOrRefData::Link(link) => { + let link_response = (LinkHeader(link), ()).into_response(); + + MimePart { + headers: link_response + .headers() + .into_iter() + .map(extract_header) + .collect(), + contents: BodyPart::from(&[] as &[u8]), + } + } + data => { + let body = to_binary(data); + MimePart::new(body.content_type, body.data) + } + } +} + +fn to_binary(data: InlineOrRefData) -> BinaryData { + match data { + InlineOrRefData::InputValueNoObject(ivno) => input_value_no_object_to_binary(ivno), + InlineOrRefData::Link(_link) => BinaryData { + data: Vec::new(), + content_type: String::new(), + }, + InlineOrRefData::QualifiedInputValue(qiv) => qualified_input_value_to_binary(qiv), + } +} + +fn input_value_no_object_to_binary( + ivno: ogcapi_types::processes::InputValueNoObject, +) -> BinaryData { + match ivno { + ogcapi_types::processes::InputValueNoObject::String(s) => BinaryData { + data: s.into_bytes(), + content_type: "text/plain; charset=utf-8".to_string(), + }, + ogcapi_types::processes::InputValueNoObject::Integer(i) => BinaryData { + data: i.to_string().into_bytes(), + content_type: "text/plain; charset=utf-8".to_string(), + }, + ogcapi_types::processes::InputValueNoObject::Number(f) => BinaryData { + data: f.to_string().into_bytes(), + content_type: "text/plain; charset=utf-8".to_string(), + }, + ogcapi_types::processes::InputValueNoObject::Boolean(b) => BinaryData { + data: b.to_string().into_bytes(), + content_type: "text/plain; charset=utf-8".to_string(), + }, + ogcapi_types::processes::InputValueNoObject::Array(items) => { + // TODO: verify correct serialization + BinaryData { + data: items.join(",").into_bytes(), + content_type: "application/json".to_string(), + } + } + ogcapi_types::processes::InputValueNoObject::Bbox(bounding_box) => { + // TODO: verify correct serialization + BinaryData { + data: serde_json::to_vec(&bounding_box).unwrap_or_default(), + content_type: "application/json".to_string(), + } + } + } +} + +fn qualified_input_value_to_binary( + qiv: ogcapi_types::processes::QualifiedInputValue, +) -> BinaryData { + match qiv.value { + ogcapi_types::processes::InputValue::InputValueNoObject(value) => { + let mut binary_data = input_value_no_object_to_binary(value); + // TODO: verify that this is enough to respect the format + if let Some(media_type) = qiv.format.media_type { + binary_data.content_type = media_type; + + if let Some(encoding) = qiv.format.encoding { + binary_data.content_type.push_str("; charset="); + binary_data.content_type.push_str(&encoding); + } + } + binary_data } + ogcapi_types::processes::InputValue::Object(object) => { + // TODO: verify correct serialization + BinaryData { + data: serde_json::to_vec(&object).unwrap_or_default(), + content_type: "application/json".to_string(), + } + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +#[must_use] +pub struct ValidParams(pub T); + +impl FromRequest for ValidParams +where + T: FromRequest, + T::Rejection: std::fmt::Display, + S: Send + Sync, +{ + type Rejection = Error; + + async fn from_request(req: Request, state: &S) -> Result { + let json = T::from_request(req, state).await; + + match json { + Ok(value) => Ok(ValidParams(value)), + Err(rejection) => { + // let response_body = rejection.body_text(); + Err(Error::ApiException( + Exception::new("InvalidParameterValue") + .status(404) + .title("InvalidParameterValue") + .detail(format!( + "The following parameters are not recognized: {rejection}", + )), + )) + } + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use ogcapi_types::processes::Output; + use std::collections::HashMap; + + #[test] + fn it_creates_link_headers() { + let link = Link::new("http://example.com", "examp\"le"); + let response = (LinkHeader(link), ()).into_response(); + assert_eq!( + response.headers().get("Link").unwrap(), + "; rel=\"examp\\\"le\"" + ); + } + + #[tokio::test] + async fn it_creates_multipart_response() { + let mut results = HashMap::new(); + results.insert( + "output1".to_string(), + ExecuteResult { + output: Output { + format: None, + transmission_mode: ogcapi_types::processes::TransmissionMode::Value, + }, + data: InlineOrRefData::InputValueNoObject( + ogcapi_types::processes::InputValueNoObject::String("Hello".to_string()), + ), + }, + ); + results.insert( + "output2".to_string(), + ExecuteResult { + output: Output { + format: None, + transmission_mode: ogcapi_types::processes::TransmissionMode::Value, + }, + data: InlineOrRefData::InputValueNoObject( + ogcapi_types::processes::InputValueNoObject::Integer(42), + ), + }, + ); + + let multipart_response = + MultipartResponse::new_with_boundary(results, "boundary-boundary-boundary".into()); + let response = multipart_response.into_response(); + + assert_eq!(response.status(), StatusCode::OK); + assert!( + response + .headers() + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap() + .starts_with("multipart/related") + ); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body_str: String = String::from_utf8(body.as_ref().to_vec()).unwrap(); + + assert_eq!( + body_str, + concat!( + "Content-Type: multipart/related; boundary=\"boundary-boundary-boundary\"\r\n\r\n\r\n", + "--boundary-boundary-boundary\r\n", + "Content-Type: text/plain; charset=utf-8\r\n", + "Content-ID: output1\r\n", + "Content-Transfer-Encoding: 7bit\r\n\r\n", + "Hello\r\n", + "--boundary-boundary-boundary\r\n", + "Content-Type: text/plain; charset=utf-8\r\n", + "Content-ID: output2\r\n", + "Content-Transfer-Encoding: 7bit\r\n\r\n", + "42\r\n", + "--boundary-boundary-boundary--\r\n" + ) + ) } } diff --git a/ogcapi-services/src/routes/collections.rs b/ogcapi-services/src/routes/collections.rs index eebfa2d..4464ff5 100755 --- a/ogcapi-services/src/routes/collections.rs +++ b/ogcapi-services/src/routes/collections.rs @@ -58,9 +58,12 @@ async fn create( .await? .is_some() { - return Err(Error::Exception( - StatusCode::CONFLICT, - format!("Collection with id `{}` already exists.", collection.id), + return Err(Error::ApiException( + ( + StatusCode::CONFLICT, + format!("Collection with id `{}` already exists.", collection.id), + ) + .into(), )); } diff --git a/ogcapi-services/src/routes/common.rs b/ogcapi-services/src/routes/common.rs index 0d59793..1d73df7 100644 --- a/ogcapi-services/src/routes/common.rs +++ b/ogcapi-services/src/routes/common.rs @@ -90,11 +90,23 @@ pub async fn root( ) ) )] -pub(crate) async fn api() -> (HeaderMap, Json) { +pub(crate) async fn api(RemoteUrl(url): RemoteUrl) -> (HeaderMap, Json) { let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, OPEN_API_JSON.parse().unwrap()); - (headers, Json(OpenAPI::from_slice(OPENAPI).0)) + let mut open_api = OpenAPI::from_slice(OPENAPI).0; + + let base_url = url[..url::Position::BeforePath].to_string(); + + // replace servers with relative server + open_api.servers = vec![openapiv3::Server { + url: base_url, + description: None, + variables: None, + extensions: Default::default(), + }]; + + (headers, Json(open_api)) } /// API conformance definition diff --git a/ogcapi-services/src/routes/features.rs b/ogcapi-services/src/routes/features.rs index 422becf..072ee68 100755 --- a/ogcapi-services/src/routes/features.rs +++ b/ogcapi-services/src/routes/features.rs @@ -23,9 +23,9 @@ use crate::{ extractors::{Qs, RemoteUrl}, }; -const CONFORMANCE: [&str; 3] = [ +const CONFORMANCE: [&str; 4] = [ "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", - // "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30", "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs", ]; @@ -128,9 +128,7 @@ async fn read( let mut headers = HeaderMap::new(); headers.insert( "Content-Crs", - query - .crs - .to_string() + format!("<{}>", query.crs) .parse() .context("Unable to parse `Content-Crs` header value")?, ); @@ -261,11 +259,15 @@ async fn items( // Limit if let Some(limit) = query.limit { + // TODO: sync with opanapi specification if limit > 10000 { query.limit = Some(10000); } + if limit == 0 { + query.limit = Some(1) + } } else { - query.limit = Some(100); + query.limit = Some(10); } let collection = state @@ -276,7 +278,18 @@ async fn items( .ok_or(Error::NotFound)?; is_supported_crs(&collection, &query.crs).await?; - // TODO: validate additional parameters + // validate additional parameters + let queryables = state.drivers.features.queryables(&collection_id).await?; + if !queryables.additional_properties { + for prop in query.additional_parameters.keys() { + if !queryables.queryables.contains_key(prop) { + return Err(Error::ApiException( + Exception::new_from_status(StatusCode::BAD_REQUEST.as_u16()) + .detail(format!("Property {prop} is not queryable!")), + )); + } + } + } let mut fc = state .drivers @@ -304,13 +317,13 @@ async fn items( fc.links.insert_or_update(&[previous]); } - if let Some(number_matched) = fc.number_matched { - if number_matched > (offset + limit) as u64 { - query.offset = Some(offset + limit); - url.set_query(serde_qs::to_string(&query).ok().as_deref()); - let next = Link::new(&url, NEXT).mediatype(GEO_JSON); - fc.links.insert_or_update(&[next]); - } + if let Some(number_matched) = fc.number_matched + && number_matched > (offset + limit) as u64 + { + query.offset = Some(offset + limit); + url.set_query(serde_qs::to_string(&query).ok().as_deref()); + let next = Link::new(&url, NEXT).mediatype(GEO_JSON); + fc.links.insert_or_update(&[next]); } } } @@ -328,19 +341,51 @@ async fn items( } let mut headers = HeaderMap::new(); - headers.insert("Content-Crs", query.crs.to_string().parse().unwrap()); + headers.insert("Content-Crs", format!("<{}>", query.crs).parse().unwrap()); headers.insert(CONTENT_TYPE, GEO_JSON.parse().unwrap()); Ok((headers, Json(fc))) } +// /// Fetch queriables of a collection +// /// +// /// Fetch the feature with id `featureId` in the feature collection with id +// /// `collectionId`. +// #[utoipa::path(get, path = "/collections/{collectionId}/queryables", tag = "Schema", +// params( +// ("collectionId" = String, Path, description = "local identifier of a collection") +// ), +// responses( +// ( +// status = 200, +// description = "Fetch the queryable properties of the collection with id `collectionId`", +// body = Queryables), +// ( +// status = 404, description = "The requested resource does not exist \ +// on the server. For example, a path parameter had an incorrect value.", +// body = Exception, example = json!(Exception::new_from_status(404)) +// ), +// ( +// status = 500, description = "A server error occurred.", +// body = Exception, example = json!(Exception::new_from_status(500)) +// ) +// ) +// )] +// async fn queryables( +// State(state): State, +// Path(collection_id): Path, +// ) -> Result> { +// let queryables = state.drivers.features.queryables(&collection_id).await?; + +// Ok(Json(queryables)) +// } + async fn is_supported_crs(collection: &Collection, crs: &Crs) -> Result<(), Error> { if collection.crs.contains(crs) { Ok(()) } else { - Err(Error::Exception( - StatusCode::BAD_REQUEST, - format!("Unsuported CRS `{crs}`"), + Err(Error::ApiException( + (StatusCode::BAD_REQUEST, format!("Unsuported CRS `{crs}`")).into(), )) } } diff --git a/ogcapi-services/src/routes/processes.rs b/ogcapi-services/src/routes/processes.rs index 817009d..a2b1ea3 100644 --- a/ogcapi-services/src/routes/processes.rs +++ b/ogcapi-services/src/routes/processes.rs @@ -1,38 +1,45 @@ -use std::collections::HashMap; - use axum::{ Json, extract::{Path, Query, State}, http::StatusCode, response::{IntoResponse, Response}, }; +use futures::TryFutureExt; +use hyper::HeaderMap; +use ogcapi_drivers::ProcessResult; +use tokio::spawn; +use tracing::error; use url::Position; use utoipa_axum::{router::OpenApiRouter, routes}; use ogcapi_types::{ common::{ Exception, Link, - link_rel::{NEXT, PREV, PROCESSES, SELF}, + link_rel::{NEXT, PREV, PROCESSES, RESULTS, SELF, STATUS}, media_type::JSON, query::LimitOffsetPagination, }, processes::{ - Execute, InlineOrRefData, JobList, Process, ProcessList, ProcessSummary, Results, + Execute, JobControlOptions, JobList, Process, ProcessList, ProcessSummary, Results, ResultsQuery, StatusInfo, }, }; -use crate::{AppState, Error, Result, extractors::RemoteUrl, processes::ProcessResponse}; +use crate::{ + AppState, Error, Result, + extractors::RemoteUrl, + processes::{ProcessExecuteResponse, ProcessResultsResponse, ValidParams}, +}; -const CONFORMANCE: [&str; 4] = [ +const CONFORMANCE: [&str; 5] = [ "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core", "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/ogc-process-description", "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/json", // "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/html", - // "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/oas30", - // "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/job-list", + "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/oas30", + "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/job-list", // "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/callback", - "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss", + // "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss", ]; /// Retrieve the list of available processes @@ -59,20 +66,20 @@ async fn processes( RemoteUrl(mut url): RemoteUrl, Query(mut query): Query, ) -> Result> { - let limit = query - .limit - .unwrap_or_else(|| state.processors.read().unwrap().len()); + let processors = read_lock(&state.processors); + let limit = query.limit.unwrap_or_else(|| processors.len()); let offset = query.offset.unwrap_or(0); - let mut summaries: Vec = state - .processors - .read() - .unwrap() - .clone() - .into_iter() + let mut summaries: Vec = processors + .iter() .skip(offset) .take(limit) - .map(|(_id, p)| p.process().unwrap().summary) + .filter_map(|(_id, p)| { + p.process() + .map(|p| p.summary) + .inspect_err(|e| error!("Error when accessing process: {e}")) + .ok() + }) .collect(); let mut links = vec![Link::new(&url, SELF).mediatype(JSON)]; @@ -139,17 +146,26 @@ async fn process( RemoteUrl(url): RemoteUrl, Path(process_id): Path, ) -> Result> { - match state.processors.read().unwrap().get(&process_id) { - Some(processor) => { - let mut process = processor.process().unwrap(); - - process.summary.links = vec![Link::new(url, SELF).mediatype(JSON)]; + match read_lock(&state.processors) + .get(&process_id) + .and_then(|processor| processor.process().ok()) + { + Some(mut process) => { + let self_link = Link::new(url.clone(), SELF).mediatype(JSON); + if let Some(link) = process.summary.links.iter_mut().find(|l| l.rel == SELF) { + *link = Link::new(url.clone(), SELF).mediatype(JSON); + } else { + process.summary.links.insert(0, self_link); + } Ok(Json(process)) } - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - format!("No process with id `{process_id}`"), + None => Err(Error::ApiException( + ( + StatusCode::NOT_FOUND, + format!("No process with id `{process_id}`"), + ) + .into(), )), } } @@ -175,20 +191,199 @@ async fn process( )] async fn execution( State(state): State, + RemoteUrl(url): RemoteUrl, Path(process_id): Path, - Json(execute): Json, -) -> Result { - let processors = state.processors.read().unwrap().clone(); - let processor = processors.get(&process_id); - match processor { - Some(processor) => match processor.execute(execute).await { - Ok(body) => Ok(ProcessResponse(body)), - Err(e) => Err(Error::Anyhow(anyhow::anyhow!(e))), + headers: HeaderMap, + ValidParams(Json(execute)): ValidParams>, +) -> Result { + let Some(processor) = read_lock(&state.processors).get(&process_id).cloned() else { + return Err(Error::ApiException( + ( + StatusCode::NOT_FOUND, + format!("No process with id `{process_id}`"), + ) + .into(), + )); + }; + + let process_description = processor.process()?; + + let response_mode = execute.response.clone(); + let negotiated_execution_mode = + negotiate_execution_mode(&headers, &process_description.summary.job_control_options); + + if negotiated_execution_mode.is_sync() { + let results = processor.execute(execute).await?; + return Ok(ProcessExecuteResponse::Synchronous { + results: ProcessResultsResponse { + results, + response_mode, + }, + was_preferred_execution_mode: negotiated_execution_mode.was_preferred(), + }); + } + + let base_url = url[..url::Position::BeforePath].to_string(); + + let mut status_info = StatusInfo { + process_id: Some(process_id), + status: ogcapi_types::processes::StatusCode::Accepted, + ..Default::default() + }; + + let job_id = state + .drivers + .jobs + .register(&status_info, response_mode) + .await?; + + status_info.job_id = job_id; + status_info.links.push( + Link::new(format!("{base_url}/jobs/{}", status_info.job_id), STATUS) + .title("Job status") + .mediatype(JSON), + ); + + { + let mut status_info = status_info.clone(); + spawn(async move { + status_info.status = ogcapi_types::processes::StatusCode::Running; + + let result = state + .drivers + .jobs + .update(&status_info) + .and_then(|_| processor.execute(execute)) + .await; + let mut results = None; + + match result { + Ok(res) => { + status_info.status = ogcapi_types::processes::StatusCode::Successful; + status_info.message = None; + status_info.progress = Some(100); + + if let Ok(results_link) = + url.join(&format!("/jobs/{}/results", status_info.job_id)) + { + status_info + .links + .push(Link::new(results_link, RESULTS).title("Job result")); + } + + results = Some(res); + } + Err(e) => { + status_info.status = ogcapi_types::processes::StatusCode::Failed; + status_info.message = e.to_string().into(); + } + }; + + let _ = state + .drivers + .jobs + .finish( + &status_info.job_id, + &status_info.status, + status_info.message.clone(), + status_info.links.clone(), + results, + ) + .await; + }); + } + + Ok(ProcessExecuteResponse::Asynchronous { + status_info, + was_preferred_execution_mode: negotiated_execution_mode.was_preferred(), + base_url, + }) +} + +/// Determine whether the client prefers synchronous execution +/// by inspecting the "Prefer" header. +fn client_execute_preference(headers: &HeaderMap) -> ClientExecutionModePreference { + let prefer = headers + .get("Prefer") + .and_then(|s| s.to_str().ok()) + .unwrap_or_default(); + + if prefer.contains("respond-sync") { + ClientExecutionModePreference::Sync + } else if prefer.contains("respond-async") { + ClientExecutionModePreference::Async + } else { + ClientExecutionModePreference::None + } +} + +enum ClientExecutionModePreference { + Sync, + Async, + None, +} + +enum NegotiatedExecutionMode { + Sync { was_preferred: bool }, + Async { was_preferred: bool }, +} + +impl NegotiatedExecutionMode { + fn is_sync(&self) -> bool { + matches!(self, NegotiatedExecutionMode::Sync { .. }) + } + + fn was_preferred(&self) -> bool { + match self { + NegotiatedExecutionMode::Sync { was_preferred } => *was_preferred, + NegotiatedExecutionMode::Async { was_preferred } => *was_preferred, + } + } +} + +/// Determine whether the execution should be synchronous or asynchronous. +/// +/// Requirements: +/// - `req/core/process-execute-default-execution-mode`: If the execute request is not accompanied with a preference: +/// - If the process supports only synchronous execution, execute synchronously. +/// - If the process supports only asynchronous execution, execute asynchronously. +/// - If the process supports both synchronous and asynchronous execution, execute synchronously. +/// - `/req/core/process-execute-auto-execution-mode`: If the execute request is accompanied with the preference `respond-async`: +/// - If the process supports only asynchronous execution, execute asynchronously. +/// - If the process supports only synchronous execution, execute synchronously. +/// - If the process supports both synchronous and asynchronous execution, execute asynchronously (or synchronously). +/// - `/rec/core/process-execute-preference-applied`: If the execute request is executed as preferred by the client, indicate this in the response (`Preference-Applied`). +/// +fn negotiate_execution_mode( + headers: &HeaderMap, + job_control_options: &[JobControlOptions], +) -> NegotiatedExecutionMode { + let client_preference = client_execute_preference(headers); + let (can_be_executed_sync, can_be_executed_async) = + job_control_options + .iter() + .fold((false, false), |(sync, async_), option| match option { + JobControlOptions::SyncExecute => (true, async_), + JobControlOptions::AsyncExecute => (sync, true), + _ => (sync, async_), + }); + match client_preference { + ClientExecutionModePreference::Sync if can_be_executed_sync => { + NegotiatedExecutionMode::Sync { + was_preferred: true, + } + } + ClientExecutionModePreference::Async if can_be_executed_async => { + NegotiatedExecutionMode::Async { + was_preferred: true, + } + } + _ if can_be_executed_sync => NegotiatedExecutionMode::Sync { + was_preferred: false, + }, + _ => NegotiatedExecutionMode::Async { + was_preferred: false, }, - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - format!("No process with id `{process_id}`"), - )), } } @@ -209,10 +404,28 @@ async fn execution( ) )] async fn jobs( - State(_state): State, - RemoteUrl(mut _url): RemoteUrl, + State(state): State, + RemoteUrl(url): RemoteUrl, + Query(query): Query, ) -> Result> { - todo!() + const DEFAULT_LIMIT: usize = 10; + const MAX_LIMIT: usize = 100; + + let offset = query.offset.unwrap_or_default(); + let limit = query.limit.unwrap_or(DEFAULT_LIMIT).max(MAX_LIMIT); + + let jobs = state.drivers.jobs.status_list(offset, limit).await?; + + let mut links = vec![Link::new(&url, SELF).mediatype(JSON)]; + + if jobs.len() >= limit { + let mut next_url = url.clone(); + let next_offset = offset + limit; + next_url.set_query(Some(&format!("limit={}&offset={}", limit, next_offset))); + links.push(Link::new(&next_url, NEXT).mediatype(JSON)); + } + + Ok(Json(JobList { jobs, links })) } /// Retrieve the status of a job @@ -233,24 +446,21 @@ async fn jobs( ) ) )] -async fn status( - State(state): State, - RemoteUrl(url): RemoteUrl, - Path(job_id): Path, -) -> Result { +async fn status(State(state): State, Path(job_id): Path) -> Result { let status = state.drivers.jobs.status(&job_id).await?; - match status { - Some(mut info) => { - info.links = vec![Link::new(url, SELF).mediatype(JSON)]; + let Some(info) = status else { + return Err(Error::ApiException( + Exception::new( + "http://www.opengis.net/def/exceptions/ogcapi-processes-1/1.0/no-such-job", + ) + .status(StatusCode::NOT_FOUND.as_u16()) + .title("NoSuchJob") + .detail(format!("No job with id `{job_id}`")), + )); + }; - Ok(Json(info).into_response()) - } - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - format!("No job with id `{job_id}`"), - )), - } + Ok(Json(info).into_response()) } /// Cancel a job execution, remove finished job @@ -278,9 +488,8 @@ async fn delete(State(state): State, Path(job_id): Path) -> Re match status { Some(info) => Ok(Json(info).into_response()), - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - format!("No job with id `{job_id}`"), + None => Err(Error::ApiException( + (StatusCode::NOT_FOUND, format!("No job with id `{job_id}`")).into(), )), } } @@ -289,7 +498,10 @@ async fn delete(State(state): State, Path(job_id): Path) -> Re /// /// Lists available results of a job. In case of a failure, lists exceptions instead. /// -/// For more information, see [Section 7.13](https://docs.ogc.org/is/18-062/18-062.html#sc_retrieve_job_results). +/// For more information, see [Section 7.13](https://docs.ogc.org/is/18-062r2/18-062r2.html#sc_retrieve_job_results). +/// +// On success, cf. `/req/core/job-results` +// On failure, cf. `/req/core/job-results-failed`. #[utoipa::path(get, path = "/jobs/{jobId}/results", tag = "Processes", responses( ( @@ -306,38 +518,69 @@ async fn delete(State(state): State, Path(job_id): Path) -> Re async fn results( State(state): State, Path(job_id): Path, - Query(query): Query, -) -> Result { + Query(_query): Query, +) -> Result { let results = state.drivers.jobs.results(&job_id).await?; - // TODO: check if job is finished + // TODO: use pagination, etc. from `_query` match results { - Some(results) => { - if let Some(outputs) = query.outputs { - let results: HashMap = outputs - .iter() - .filter_map(|output| { - results - .get(output) - .map(|result| (output.to_string(), result.to_owned())) - }) - .collect(); - - Ok(Json(Results { results }).into_response()) - } else { - Ok(Json(results).into_response()) - } + ProcessResult::NoSuchJob => { + // `/req/core/job-results-exception/no-such-job` + Err(Error::ApiException( + Exception::new( + "http://www.opengis.net/def/exceptions/ogcapi-processes-1/1.0/no-such-job", + ) + .status(404) + .title("NoSuchJob") + .detail(format!("No job with id `{job_id}`")), + )) + } + ProcessResult::NotReady => { + // `/req/core/job-results-exception/results-not-ready` + Err(Error::ApiException( + Exception::new( + "http://www.opengis.net/def/exceptions/ogcapi-processes-1/1.0/result-not-ready", + ) + .status(404) + .title("NotReady") + .detail(format!("Results for job `{job_id}` are not ready yet")), + )) + } + ProcessResult::Results { + results, + response_mode, + } => Ok(ProcessResultsResponse { + results, + response_mode, + }), + } +} + +/// Helper function to read-lock a RwLock, recovering from poisoning if necessary. +fn read_lock(mutex: &std::sync::RwLock) -> std::sync::RwLockReadGuard<'_, T> { + match mutex.read() { + Ok(guard) => guard, + Err(poisoned) => { + error!("Mutex was poisoned, attempting to recover."); + poisoned.into_inner() + } + } +} + +/// Helper function to write-lock a RwLock, recovering from poisoning if necessary. +fn write_lock(mutex: &std::sync::RwLock) -> std::sync::RwLockWriteGuard<'_, T> { + match mutex.write() { + Ok(guard) => guard, + Err(poisoned) => { + error!("Mutex was poisoned, attempting to recover."); + poisoned.into_inner() } - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - format!("No job with id `{job_id}`"), - )), } } pub(crate) fn router(state: &AppState) -> OpenApiRouter { - let mut root = state.root.write().unwrap(); + let mut root = write_lock(&state.root); root.links.append(&mut vec![ Link::new("processes", PROCESSES) .mediatype(JSON) @@ -347,7 +590,7 @@ pub(crate) fn router(state: &AppState) -> OpenApiRouter { // .title("The endpoint for job monitoring"), ]); - state.conformance.write().unwrap().extend(&CONFORMANCE); + write_lock(&state.conformance).extend(&CONFORMANCE); OpenApiRouter::new() .routes(routes!(processes)) diff --git a/ogcapi-services/src/routes/stac.rs b/ogcapi-services/src/routes/stac.rs index 348a44f..44b38cf 100644 --- a/ogcapi-services/src/routes/stac.rs +++ b/ogcapi-services/src/routes/stac.rs @@ -7,7 +7,6 @@ use hyper::header::CONTENT_TYPE; use url::Url; use utoipa_axum::{router::OpenApiRouter, routes}; -use ogcapi_drivers::StacSeach; use ogcapi_types::{ common::{ Bbox, Exception, Link, Linked, @@ -98,9 +97,12 @@ pub(crate) async fn search( // Limit if let Some(limit) = params.limit { if !(1..10001).contains(&limit) { - return Err(Error::Exception( - StatusCode::BAD_REQUEST, - "query parameter `limit` not in range 1 to 10000".to_string(), + return Err(Error::ApiException( + ( + StatusCode::BAD_REQUEST, + "query parameter `limit` not in range 1 to 10000".to_string(), + ) + .into(), )); } } else { @@ -113,24 +115,30 @@ pub(crate) async fn search( match bbox { Bbox::Bbox2D(bbox) => { if bbox[0] > bbox[2] || bbox[1] > bbox[3] { - return Err(Error::Exception( - StatusCode::BAD_REQUEST, - "query parameter `bbox` not valid".to_string(), + return Err(Error::ApiException( + ( + StatusCode::BAD_REQUEST, + "query parameter `bbox` not valid".to_string(), + ) + .into(), )); } } Bbox::Bbox3D(bbox) => { if bbox[0] > bbox[3] || bbox[1] > bbox[4] || bbox[2] > bbox[5] { - return Err(Error::Exception( - StatusCode::BAD_REQUEST, - "query parameter `bbox` not valid".to_string(), + return Err(Error::ApiException( + ( + StatusCode::BAD_REQUEST, + "query parameter `bbox` not valid".to_string(), + ) + .into(), )); } } } } - let mut fc = state.db.search(¶ms).await?; + let mut fc = state.drivers.stac.search(¶ms).await?; fc.links.insert_or_update(&[ Link::new(&url, SELF).mediatype(GEO_JSON), @@ -151,13 +159,13 @@ pub(crate) async fn search( fc.links.insert_or_update(&[previous]); } - if let Some(number_matched) = fc.number_matched { - if number_matched > offset + limit { - params.offset = Some(offset + limit); - url.set_query(serde_qs::to_string(¶ms).ok().as_deref()); - let next = Link::new(&url, NEXT).mediatype(GEO_JSON); - fc.links.insert_or_update(&[next]); - } + if let Some(number_matched) = fc.number_matched + && number_matched > offset + limit + { + params.offset = Some(offset + limit); + url.set_query(serde_qs::to_string(¶ms).ok().as_deref()); + let next = Link::new(&url, NEXT).mediatype(GEO_JSON); + fc.links.insert_or_update(&[next]); } } } diff --git a/ogcapi-services/src/routes/tiles.rs b/ogcapi-services/src/routes/tiles.rs index 089e683..319824e 100644 --- a/ogcapi-services/src/routes/tiles.rs +++ b/ogcapi-services/src/routes/tiles.rs @@ -113,9 +113,8 @@ async fn tile_matrix_sets(RemoteUrl(url): RemoteUrl) -> Result) -> Result> { match TMS.get().and_then(|tms| tms.get(&id)) { Some(tms) => Ok(Json(tms.to_owned())), - None => Err(Error::Exception( - StatusCode::NOT_FOUND, - "Unable to find resource".to_string(), + None => Err(Error::ApiException( + (StatusCode::NOT_FOUND, "Unable to find resource".to_string()).into(), )), } } diff --git a/ogcapi-services/src/service.rs b/ogcapi-services/src/service.rs index f7da73e..14326cc 100644 --- a/ogcapi-services/src/service.rs +++ b/ogcapi-services/src/service.rs @@ -26,7 +26,7 @@ use utoipa::OpenApi; use utoipa_axum::router::OpenApiRouter; use utoipa_swagger_ui::SwaggerUi; -use crate::{ApiDoc, AppState, Config, ConfigParser, Error, routes}; +use crate::{ApiDoc, AppState, Config, ConfigParser, Error, routes, state::Drivers}; /// OGC API Services pub struct Service { @@ -36,17 +36,20 @@ pub struct Service { } impl Service { - pub async fn new() -> Self { + pub async fn try_new() -> Result { // config let config = Config::parse(); + // drivers + let drivers = Drivers::try_new_from_env().await?; + // state - let state = AppState::new_from(&config).await; + let state = AppState::new(drivers).await; - Service::new_with(&config, state).await + Service::try_new_with(&config, state).await } - pub async fn new_with(config: &Config, state: AppState) -> Self { + pub async fn try_new_with(config: &Config, state: AppState) -> Result { // router let router = OpenApiRouter::::with_openapi(ApiDoc::openapi()); @@ -98,15 +101,13 @@ impl Service { ); // listener - let listener = TcpListener::bind((config.host.as_str(), config.port)) - .await - .expect("create listener"); + let listener = TcpListener::bind((config.host.as_str(), config.port)).await?; - Service { + Ok(Service { state, router, listener, - } + }) } /// Serve application @@ -117,7 +118,7 @@ impl Service { // serve tracing::info!( "listening on http://{}", - self.listener.local_addr().unwrap() + self.listener.local_addr().expect("local address") ); axum::serve::serve(self.listener, router) diff --git a/ogcapi-services/src/state.rs b/ogcapi-services/src/state.rs index 12d96e7..2b8b2e0 100644 --- a/ogcapi-services/src/state.rs +++ b/ogcapi-services/src/state.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "processes")] +use std::collections::HashMap; use std::sync::{Arc, RwLock}; #[cfg(feature = "edr")] @@ -6,6 +8,8 @@ use ogcapi_drivers::EdrQuerier; use ogcapi_drivers::FeatureTransactions; #[cfg(feature = "processes")] use ogcapi_drivers::JobHandler; +#[cfg(feature = "stac")] +use ogcapi_drivers::StacSearch; #[cfg(feature = "styles")] use ogcapi_drivers::StyleTransactions; #[cfg(feature = "tiles")] @@ -15,20 +19,16 @@ use ogcapi_drivers::{CollectionTransactions, postgres::Db}; #[cfg(feature = "processes")] use ogcapi_processes::Processor; use ogcapi_types::common::{Conformance, LandingPage}; - -use crate::{Config, ConfigParser}; +use url::Url; /// Application state #[derive(Clone)] pub struct AppState { - pub root: Arc>, - pub conformance: Arc>, - pub drivers: Arc, - pub db: Db, - #[cfg(feature = "stac")] - pub s3: ogcapi_drivers::s3::S3, + pub(crate) root: Arc>, + pub(crate) conformance: Arc>, + pub(crate) drivers: Arc, #[cfg(feature = "processes")] - pub processors: Arc>>>, + pub(crate) processors: Arc>>>, } // TODO: Introduce service trait @@ -44,33 +44,22 @@ pub struct Drivers { pub styles: Box, #[cfg(feature = "tiles")] pub tiles: Box, + #[cfg(feature = "stac")] + pub stac: Box, } -impl AppState { - pub async fn new() -> Self { - let config = Config::parse(); - AppState::new_from(&config).await - } - - pub async fn new_from(config: &Config) -> Self { - let db = Db::setup(&config.database_url).await.unwrap(); - AppState::new_with(db).await +impl Drivers { + /// Try to setup drivers from `DATABASE_URL` environment variable. + pub async fn try_new_from_env() -> Result { + let var = std::env::var("DATABASE_URL")?; + Self::try_new_db(&var).await } - pub async fn new_with(db: Db) -> Self { - // conformance - #[allow(unused_mut)] - let mut conformace = Conformance::default(); - #[cfg(feature = "stac")] - conformace.extend(&[ - "https://api.stacspec.org/v1.0.0-rc.1/core", - "https://api.stacspec.org/v1.0.0-rc.1/item-search", - "https://api.stacspec.org/v1.0.0-rc.1/collections", - "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features", - "https://api.stacspec.org/v1.0.0-rc.1/browseable", - ]); + /// Try to setup db driver from database url. + pub async fn try_new_db(url: &str) -> Result { + let database_url = Url::parse(url)?; + let db = Db::setup(&database_url).await?; - // drivers let drivers = Drivers { collections: Box::new(db.clone()), #[cfg(feature = "features")] @@ -83,15 +72,32 @@ impl AppState { styles: Box::new(db.clone()), #[cfg(feature = "tiles")] tiles: Box::new(db.clone()), + #[cfg(feature = "stac")] + stac: Box::new(db.clone()), }; + Ok(drivers) + } +} + +impl AppState { + pub async fn new(drivers: Drivers) -> Self { + // conformance + #[allow(unused_mut)] + let mut conformace = Conformance::default(); + #[cfg(feature = "stac")] + conformace.extend(&[ + "https://api.stacspec.org/v1.0.0-rc.1/core", + "https://api.stacspec.org/v1.0.0-rc.1/item-search", + "https://api.stacspec.org/v1.0.0-rc.1/collections", + "https://api.stacspec.org/v1.0.0-rc.1/ogcapi-features", + "https://api.stacspec.org/v1.0.0-rc.1/browseable", + ]); + AppState { root: Arc::new(RwLock::new(LandingPage::new("root").description("root"))), conformance: Arc::new(RwLock::new(conformace)), drivers: Arc::new(drivers), - db, - #[cfg(feature = "stac")] - s3: ogcapi_drivers::s3::S3::new().await, #[cfg(feature = "processes")] processors: Default::default(), } @@ -102,12 +108,6 @@ impl AppState { self } - #[cfg(feature = "stac")] - pub async fn s3_client(mut self, client: ogcapi_drivers::s3::S3) -> Self { - self.s3 = client; - self - } - #[cfg(feature = "processes")] pub fn processors(self, processors: Vec>) -> Self { for p in processors { diff --git a/ogcapi-services/tests/setup.rs b/ogcapi-services/tests/setup.rs index 76ee1c2..b9f2138 100644 --- a/ogcapi-services/tests/setup.rs +++ b/ogcapi-services/tests/setup.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use url::Url; use uuid::Uuid; -use ogcapi_services::{AppState, Config, ConfigParser, Service}; +use ogcapi_services::{AppState, Config, ConfigParser, Drivers, Service}; #[allow(dead_code)] pub async fn spawn_app() -> anyhow::Result<(SocketAddr, Url)> { @@ -12,12 +12,17 @@ pub async fn spawn_app() -> anyhow::Result<(SocketAddr, Url)> { // ogcapi_services::telemetry::init(); let mut config = Config::parse(); - config.database_url.set_path(&Uuid::new_v4().to_string()); config.port = 0; - let state = AppState::new_from(&config).await; + let var = std::env::var("DATABASE_URL")?; + let mut database_url = Url::parse(&var)?; + database_url.set_path(&Uuid::new_v4().to_string()); - let service = Service::new_with(&config, state).await; + let drivers = Drivers::try_new_db(database_url.as_str()).await?; + + let state = AppState::new(drivers).await; + + let service = Service::try_new_with(&config, state).await?; let addr = service.local_addr()?; @@ -25,5 +30,5 @@ pub async fn spawn_app() -> anyhow::Result<(SocketAddr, Url)> { service.serve().await; }); - Ok((addr, config.database_url)) + Ok((addr, database_url)) } diff --git a/ogcapi-types/Cargo.toml b/ogcapi-types/Cargo.toml index a3dfe42..8fa5023 100644 --- a/ogcapi-types/Cargo.toml +++ b/ogcapi-types/Cargo.toml @@ -25,13 +25,13 @@ coverages = [] movingfeatures = ["common", "features"] [dependencies] -chrono = { version = "0.4.41", features = ["serde"] } +chrono = { version = "0.4.42", features = ["serde"] } geojson = { workspace = true } log = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_repr = "0.1.20" -serde_with = { version = "3.14", features = ["json"] } +serde_with = { version = "3.16.0", features = ["json"] } serde_qs = { workspace = true } url = { workspace = true } utoipa = { workspace = true, features = ["preserve_order", "chrono", "url", "repr"] } diff --git a/ogcapi-types/src/common/bbox.rs b/ogcapi-types/src/common/bbox.rs index 2ad3332..fafeef7 100644 --- a/ogcapi-types/src/common/bbox.rs +++ b/ogcapi-types/src/common/bbox.rs @@ -3,9 +3,6 @@ use std::{fmt, str}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -type Bbox2D = [f64; 4]; -type Bbox3D = [f64; 6]; - /// Each bounding box is provided as four or six numbers, depending on /// whether the coordinate reference system includes a vertical axis /// (height or depth): @@ -41,9 +38,9 @@ type Bbox3D = [f64; 6]; #[serde(untagged)] pub enum Bbox { #[schema(value_type = Vec)] - Bbox2D(Bbox2D), + Bbox2D([f64; 4]), #[schema(value_type = Vec)] - Bbox3D(Bbox3D), + Bbox3D([f64; 6]), } impl fmt::Display for Bbox { @@ -79,10 +76,28 @@ impl str::FromStr for Bbox { fn from_str(s: &str) -> Result { let numbers: Vec = s .split(',') - .map(|d| d.trim().parse::()) + .map(|d| d.trim().trim_matches(['[', ']']).parse::()) .collect::, std::num::ParseFloatError>>() .map_err(|_| "Unable to convert bbox coordinates to float")?; + let n = numbers.len(); + + // check number of coordinates + if !(n == 4 || n == 6) { + return Err("Expected 4 or 6 numbers"); + } + + // // check lower <= upper on axis 2 + // if (n == 4 && numbers[1] > numbers[3]) || (n == 6 && numbers[1] > numbers[4]) { + // // TODO: ensure this assumption is correct + // return Err("Lower value of coordinate axis 2 must be larger than upper value!"); + // } + + // check lower <= upper on axis 3 + if n == 6 && numbers[2] > numbers[5] { + return Err("Lower value of coordinate axis 3 must be larger than upper value!"); + } + match numbers.len() { 4 => Ok(Bbox::Bbox2D([ numbers[0], numbers[1], numbers[2], numbers[3], diff --git a/ogcapi-types/src/common/exception.rs b/ogcapi-types/src/common/exception.rs index 77bafad..48ffe1d 100644 --- a/ogcapi-types/src/common/exception.rs +++ b/ogcapi-types/src/common/exception.rs @@ -74,3 +74,9 @@ impl Display for Exception { } impl Error for Exception {} + +impl> From<(T, String)> for Exception { + fn from((status_code, message): (T, String)) -> Self { + Exception::new_from_status(status_code.into()).detail(message) + } +} diff --git a/ogcapi-types/src/common/link_rel.rs b/ogcapi-types/src/common/link_rel.rs index 25eeb31..972dcf2 100644 --- a/ogcapi-types/src/common/link_rel.rs +++ b/ogcapi-types/src/common/link_rel.rs @@ -74,7 +74,7 @@ pub const RELATED: &str = "related"; /// The target URI points to the results of a job. /// /// See: -pub const RESULTS: &str = "results"; +pub const RESULTS: &str = "http://www.opengis.net/def/rel/ogc/1.0/results"; pub const ROOT: &str = "root"; diff --git a/ogcapi-types/src/features/mod.rs b/ogcapi-types/src/features/mod.rs index cb9d4d7..693ee26 100644 --- a/ogcapi-types/src/features/mod.rs +++ b/ogcapi-types/src/features/mod.rs @@ -1,9 +1,11 @@ mod feature; mod feature_collection; mod query; +mod queryables; pub use feature::{Feature, FeatureId, geometry}; pub use feature_collection::FeatureCollection; pub use query::Query; +pub use queryables::Queryables; pub use geojson::Geometry; diff --git a/ogcapi-types/src/features/queryables.rs b/ogcapi-types/src/features/queryables.rs index b85dd00..5a1b3b9 100644 --- a/ogcapi-types/src/features/queryables.rs +++ b/ogcapi-types/src/features/queryables.rs @@ -1,16 +1,23 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; -#[derive(Serialize, Deserialize, Debug)] -struct Queryables { - queryables: Vec +#[derive(Serialize, Deserialize, ToSchema, Debug, Default)] +pub struct Queryables { + #[serde(flatten)] + pub queryables: HashMap, + #[serde(rename = "additionalProperties", default = "default_true")] + pub additional_properties: bool, } -#[derive(Serialize, Deserialize, Debug)] -struct Queryable { - queryable: String, + +#[derive(Serialize, Deserialize, ToSchema, Debug)] +pub struct Queryable { title: Option, description: Option, - language: Option, // default en r#type: String, - #[serde(rename = "type-ref")] - type_ref: String, -} \ No newline at end of file +} + +fn default_true() -> bool { + true +} diff --git a/ogcapi-types/src/lib.rs b/ogcapi-types/src/lib.rs index 35576fe..378f87c 100644 --- a/ogcapi-types/src/lib.rs +++ b/ogcapi-types/src/lib.rs @@ -9,6 +9,9 @@ pub mod edr; /// Types specified in the `OGC API - Features` standard. #[cfg(feature = "features")] pub mod features; +/// Types specified in the `OGC API - Moving Features` standard. +#[cfg(feature = "movingfeatures")] +pub mod movingfeatures; /// Types specified in the `OGC API - Processes` standard. #[cfg(feature = "processes")] pub mod processes; @@ -22,9 +25,5 @@ pub mod styles; #[cfg(feature = "tiles")] pub mod tiles; -/// Types specified in the `OGC API - Moving Features` standard. -#[cfg(feature = "movingfeatures")] -pub mod movingfeatures; - -#[cfg(feature = "coverages")] -mod coverages; +// #[cfg(feature = "coverages")] +// mod coverages; diff --git a/ogcapi-types/src/processes/description.rs b/ogcapi-types/src/processes/description.rs index 645d7e1..33b8043 100644 --- a/ogcapi-types/src/processes/description.rs +++ b/ogcapi-types/src/processes/description.rs @@ -48,15 +48,15 @@ pub struct AdditionalParameter { } /// Process input description -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Default)] #[serde(rename_all = "camelCase")] pub struct InputDescription { #[serde(flatten)] pub description_type: DescriptionType, - #[serde(default = "min_occurs")] - pub min_occurs: u64, - #[serde(default)] - pub max_occurs: MaxOccurs, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub min_occurs: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_occurs: Option, pub schema: Value, } @@ -71,7 +71,7 @@ pub enum ValuePassing { #[serde(untagged)] pub enum MaxOccurs { Integer(u64), - Unbounded(String), + Unbounded(String), // FIXME: type string , enum "unbounded" } impl Default for MaxOccurs { @@ -79,9 +79,6 @@ impl Default for MaxOccurs { Self::Integer(1) } } -fn min_occurs() -> u64 { - 1 -} /// Process output description #[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] diff --git a/ogcapi-types/src/processes/execute.rs b/ogcapi-types/src/processes/execute.rs index b83d69d..60d226c 100644 --- a/ogcapi-types/src/processes/execute.rs +++ b/ogcapi-types/src/processes/execute.rs @@ -8,6 +8,7 @@ use crate::common::{Bbox, Link, OGC_CRS84}; /// Process execution #[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Default)] +#[serde(deny_unknown_fields)] pub struct Execute { #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub inputs: HashMap, @@ -28,7 +29,7 @@ pub enum Input { InlineOrRefDataArray(Vec), } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum InlineOrRefData { InputValueNoObject(InputValueNoObject), @@ -36,7 +37,7 @@ pub enum InlineOrRefData { Link(Link), } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum InputValueNoObject { String(String), @@ -49,7 +50,7 @@ pub enum InputValueNoObject { Bbox(BoundingBox), // bbox is actually an object } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] pub struct BoundingBox { pub bbox: Bbox, #[serde(default = "default_crs")] @@ -60,14 +61,14 @@ fn default_crs() -> String { OGC_CRS84.to_string() } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] pub struct QualifiedInputValue { pub value: InputValue, #[serde(flatten)] pub format: Format, } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum InputValue { InputValueNoObject(InputValueNoObject), @@ -85,7 +86,7 @@ pub struct Output { pub transmission_mode: TransmissionMode, } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Format { #[schema(nullable = false)] @@ -99,7 +100,7 @@ pub struct Format { pub schema: Option, } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq)] #[serde(untagged)] pub enum Schema { String(String), @@ -145,3 +146,11 @@ pub struct Subscriber { #[serde(default)] pub failed_uri: Option, } + +pub type ExecuteResults = HashMap; + +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +pub struct ExecuteResult { + pub output: Output, + pub data: InlineOrRefData, +} diff --git a/ogcapi-types/src/processes/job.rs b/ogcapi-types/src/processes/job.rs index 103e464..7c51b55 100644 --- a/ogcapi-types/src/processes/job.rs +++ b/ogcapi-types/src/processes/job.rs @@ -11,14 +11,18 @@ use super::execute::InlineOrRefData; #[derive(Serialize, Deserialize, ToSchema, Debug, Default)] pub struct JobList { - jobs: Vec, - links: Vec, + pub jobs: Vec, + pub links: Vec, } #[derive(Serialize, Deserialize, ToSchema, Debug, Clone, Default)] pub struct StatusInfo { #[schema(nullable = false)] - #[serde(rename = "processID", alias = "process_id")] + #[serde( + rename = "processID", + alias = "process_id", + skip_serializing_if = "Option::is_none" + )] pub process_id: Option, #[schema(required = false)] #[serde(default)] @@ -27,16 +31,21 @@ pub struct StatusInfo { pub job_id: String, pub status: StatusCode, #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub created: Option>, #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub finished: Option>, #[schema(nullable = false)] + #[serde(skip_serializing_if = "Option::is_none")] pub updated: Option>, #[schema(nullable = false, value_type = isize, required = false, minimum = 0, maximum = 100)] + #[serde(skip_serializing_if = "Option::is_none")] pub progress: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub links: Vec, } @@ -48,9 +57,10 @@ pub enum JobType { Process, } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq, Eq)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq, Eq, Default)] #[serde(rename_all = "lowercase")] pub enum StatusCode { + #[default] Accepted, Running, Successful, @@ -58,12 +68,6 @@ pub enum StatusCode { Dismissed, } -impl Default for StatusCode { - fn default() -> Self { - Self::Accepted - } -} - #[serde_with::serde_as] #[derive(Deserialize, Debug)] pub struct ResultsQuery { diff --git a/ogcapi-types/src/processes/mod.rs b/ogcapi-types/src/processes/mod.rs index d4aa2e6..6faf0cb 100644 --- a/ogcapi-types/src/processes/mod.rs +++ b/ogcapi-types/src/processes/mod.rs @@ -5,4 +5,4 @@ mod process; pub use execute::*; pub use job::*; -pub use process::{Process, ProcessList, ProcessSummary}; +pub use process::*; diff --git a/ogcapi-types/src/processes/process.rs b/ogcapi-types/src/processes/process.rs index 9acb014..7b82432 100644 --- a/ogcapi-types/src/processes/process.rs +++ b/ogcapi-types/src/processes/process.rs @@ -1,13 +1,11 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Error; -use utoipa::ToSchema; - -use crate::common::Link; - use super::{ TransmissionMode, - description::{DescriptionType, InputDescription, MaxOccurs, OutputDescription}, + description::{InputDescription, OutputDescription}, }; +use crate::common::Link; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use utoipa::ToSchema; /// Process summary #[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] @@ -17,15 +15,13 @@ pub struct ProcessSummary { pub version: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub job_control_options: Vec, - #[serde(default)] - pub output_transmission: TransmissionMode, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub output_transmission: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub links: Vec, - #[serde(flatten)] - pub description_type: DescriptionType, } -#[derive(Serialize, Deserialize, ToSchema, Debug, Clone)] +#[derive(Serialize, Deserialize, ToSchema, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub enum JobControlOptions { SyncExecute, @@ -46,37 +42,7 @@ pub struct Process { #[serde(flatten)] pub summary: ProcessSummary, #[schema(required = false)] - pub inputs: InputDescription, + pub inputs: HashMap, #[schema(required = false)] - pub outputs: OutputDescription, -} - -impl Process { - pub fn try_new( - id: impl ToString, - version: impl ToString, - inputs: &T, - outputs: &T, - ) -> Result { - Ok(Process { - summary: ProcessSummary { - id: id.to_string(), - version: version.to_string(), - job_control_options: Vec::new(), - output_transmission: TransmissionMode::default(), - links: Vec::new(), - description_type: DescriptionType::default(), - }, - inputs: InputDescription { - description_type: DescriptionType::default(), - min_occurs: 1, - max_occurs: MaxOccurs::default(), - schema: serde_json::to_value(inputs)?, - }, - outputs: OutputDescription { - description_type: DescriptionType::default(), - schema: serde_json::to_value(outputs)?, - }, - }) - } + pub outputs: HashMap, } diff --git a/ogcapi-types/src/styles/mod.rs b/ogcapi-types/src/styles/mod.rs index ca806a8..160db18 100644 --- a/ogcapi-types/src/styles/mod.rs +++ b/ogcapi-types/src/styles/mod.rs @@ -1,5 +1,5 @@ -mod mapbox; -mod symcore; +// mod mapbox; +// mod symcore; use serde::{Deserialize, Serialize}; use serde_json::Value; From 774456cb83032603549980d532d8bec618c91e3a Mon Sep 17 00:00:00 2001 From: Matthias Kraus <40459408+KrausMatthias@users.noreply.github.com> Date: Sun, 30 Nov 2025 13:18:10 +0100 Subject: [PATCH 31/31] Fix typo --- ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs index 437c4b5..21f01a7 100644 --- a/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs +++ b/ogcapi-types/src/movingfeatures/mfjson_temporal_properties.rs @@ -36,7 +36,7 @@ impl MFJsonTemporalProperties { } } - pub fn datatimes(&self) -> &[DateTime] { + pub fn datetimes(&self) -> &[DateTime] { &self.datetimes }