Skip to main content

oxigdal_stac/api/
features.rs

1//! OGC API – Features resource types used by STAC API.
2//!
3//! These lightweight types model the landing page, collections list, and
4//! single-collection response that a STAC API exposes in addition to the
5//! Item Search endpoint.
6
7use serde::{Deserialize, Serialize};
8
9use super::search::Link;
10
11/// OGC API / STAC API landing page response.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct LandingPage {
14    /// Service title.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub title: Option<String>,
17
18    /// Service description.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub description: Option<String>,
21
22    /// STAC API version.
23    #[serde(rename = "stac_version")]
24    pub stac_version: String,
25
26    /// Conformance URIs (mirrors `/conformance`).
27    #[serde(rename = "conformsTo", default, skip_serializing_if = "Vec::is_empty")]
28    pub conforms_to: Vec<String>,
29
30    /// Navigation / capability links.
31    pub links: Vec<Link>,
32}
33
34impl LandingPage {
35    /// Creates a minimal landing page.
36    pub fn new(stac_version: impl Into<String>, links: Vec<Link>) -> Self {
37        Self {
38            title: None,
39            description: None,
40            stac_version: stac_version.into(),
41            conforms_to: Vec::new(),
42            links,
43        }
44    }
45
46    /// Adds a title to the landing page.
47    pub fn with_title(mut self, title: impl Into<String>) -> Self {
48        self.title = Some(title.into());
49        self
50    }
51
52    /// Adds a description to the landing page.
53    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
54        self.description = Some(desc.into());
55        self
56    }
57
58    /// Adds conformance URIs.
59    pub fn with_conforms_to(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
60        self.conforms_to = uris.into_iter().map(Into::into).collect();
61        self
62    }
63}
64
65/// A list of collection summaries as returned by `GET /collections`.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct CollectionsList {
68    /// Collection summaries.
69    pub collections: Vec<CollectionSummary>,
70    /// Navigation links.
71    pub links: Vec<Link>,
72}
73
74/// Summary information for a STAC Collection.
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct CollectionSummary {
77    /// Unique collection identifier.
78    pub id: String,
79    /// Human-readable title.
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub title: Option<String>,
82    /// Brief description.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub description: Option<String>,
85    /// STAC version.
86    #[serde(rename = "stac_version")]
87    pub stac_version: String,
88    /// Navigation links (e.g., `"self"`, `"items"`).
89    pub links: Vec<Link>,
90    /// Spatial extent bounding box(es) `[[west, south, east, north]]`.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub extent: Option<serde_json::Value>,
93}
94
95impl CollectionSummary {
96    /// Creates a minimal collection summary.
97    pub fn new(id: impl Into<String>, stac_version: impl Into<String>) -> Self {
98        Self {
99            id: id.into(),
100            title: None,
101            description: None,
102            stac_version: stac_version.into(),
103            links: Vec::new(),
104            extent: None,
105        }
106    }
107
108    /// Adds a title.
109    pub fn with_title(mut self, title: impl Into<String>) -> Self {
110        self.title = Some(title.into());
111        self
112    }
113
114    /// Adds a description.
115    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
116        self.description = Some(desc.into());
117        self
118    }
119
120    /// Adds a navigation link.
121    pub fn add_link(mut self, link: Link) -> Self {
122        self.links.push(link);
123        self
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_landing_page_new() {
133        let page = LandingPage::new("1.0.0", vec![]).with_title("My STAC API");
134        assert_eq!(page.title, Some("My STAC API".to_string()));
135        assert_eq!(page.stac_version, "1.0.0");
136    }
137
138    #[test]
139    fn test_landing_page_roundtrip() {
140        let page = LandingPage::new("1.0.0", vec![Link::new("self", "https://example.com")])
141            .with_title("Test API")
142            .with_description("A test STAC API");
143        let json = serde_json::to_string(&page).expect("serialize");
144        let back: LandingPage = serde_json::from_str(&json).expect("deserialize");
145        assert_eq!(page, back);
146    }
147
148    #[test]
149    fn test_collection_summary() {
150        let cs = CollectionSummary::new("sentinel-2-l2a", "1.0.0")
151            .with_title("Sentinel-2 L2A")
152            .add_link(Link::new(
153                "items",
154                "https://example.com/collections/sentinel-2-l2a/items",
155            ));
156        assert_eq!(cs.id, "sentinel-2-l2a");
157        assert_eq!(cs.links.len(), 1);
158    }
159
160    #[test]
161    fn test_collections_list_roundtrip() {
162        let list = CollectionsList {
163            collections: vec![CollectionSummary::new("my-col", "1.0.0")],
164            links: vec![Link::new("self", "https://example.com/collections")],
165        };
166        let json = serde_json::to_string(&list).expect("serialize");
167        let back: CollectionsList = serde_json::from_str(&json).expect("deserialize");
168        assert_eq!(list, back);
169    }
170}