Skip to main content

oxigdal_stac/api/
conformance.rs

1//! STAC API conformance class definitions and validation.
2//!
3//! <https://github.com/radiantearth/stac-api-spec>
4
5use serde::{Deserialize, Serialize};
6
7/// Well-known STAC API v1.0 conformance class URIs.
8pub mod uris {
9    /// STAC API – Core.
10    pub const CORE: &str = "https://api.stacspec.org/v1.0.0/core";
11    /// STAC API – Browseable.
12    pub const BROWSEABLE: &str = "https://api.stacspec.org/v1.0.0/browseable";
13    /// STAC API – Item Search.
14    pub const ITEM_SEARCH: &str = "https://api.stacspec.org/v1.0.0/item-search";
15    /// STAC API – Item Search – Filter.
16    pub const ITEM_SEARCH_FILTER: &str = "https://api.stacspec.org/v1.0.0/item-search#filter";
17    /// STAC API – Item Search – Sort.
18    pub const ITEM_SEARCH_SORT: &str = "https://api.stacspec.org/v1.0.0/item-search#sort";
19    /// STAC API – Item Search – Fields.
20    pub const ITEM_SEARCH_FIELDS: &str = "https://api.stacspec.org/v1.0.0/item-search#fields";
21    /// OGC API – Features – Core.
22    pub const OGCAPI_FEATURES: &str = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core";
23    /// STAC API – Transaction Extension.
24    pub const TRANSACTION: &str =
25        "https://api.stacspec.org/v1.0.0/ogcapi-features/extensions/transaction";
26    /// STAC API – Children.
27    pub const CHILDREN: &str = "https://api.stacspec.org/v1.0.0/children";
28}
29
30/// Conformance declaration response (`GET /conformance`).
31///
32/// Lists the conformance classes that a server implements, enabling clients
33/// to discover which features and operations are available.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub struct ConformanceDeclaration {
36    /// List of conformance class URIs that the server conforms to.
37    #[serde(rename = "conformsTo")]
38    pub conforms_to: Vec<String>,
39}
40
41impl ConformanceDeclaration {
42    /// Creates a conformance declaration from an iterable of URI strings.
43    pub fn new(classes: impl IntoIterator<Item = impl Into<String>>) -> Self {
44        Self {
45            conforms_to: classes.into_iter().map(Into::into).collect(),
46        }
47    }
48
49    /// Returns `true` if the declaration includes the given conformance class URI.
50    pub fn supports(&self, class: &str) -> bool {
51        self.conforms_to.iter().any(|c| c == class)
52    }
53
54    /// Builds the standard STAC API v1.0 conformance declaration.
55    ///
56    /// Includes: Core, Browseable, Item Search (+ filter, sort, fields),
57    /// OGC API Features, and Children.
58    pub fn standard() -> Self {
59        Self::new([
60            uris::CORE,
61            uris::BROWSEABLE,
62            uris::ITEM_SEARCH,
63            uris::ITEM_SEARCH_FILTER,
64            uris::ITEM_SEARCH_SORT,
65            uris::ITEM_SEARCH_FIELDS,
66            uris::OGCAPI_FEATURES,
67            uris::CHILDREN,
68        ])
69    }
70
71    /// Adds the Transaction extension conformance class to this declaration.
72    pub fn with_transaction(mut self) -> Self {
73        self.conforms_to.push(uris::TRANSACTION.to_string());
74        self
75    }
76
77    /// Adds an arbitrary conformance class URI to this declaration.
78    pub fn with_class(mut self, uri: impl Into<String>) -> Self {
79        self.conforms_to.push(uri.into());
80        self
81    }
82
83    /// Returns the number of conformance classes declared.
84    pub fn len(&self) -> usize {
85        self.conforms_to.len()
86    }
87
88    /// Returns `true` if the declaration contains no conformance classes.
89    pub fn is_empty(&self) -> bool {
90        self.conforms_to.is_empty()
91    }
92}
93
94impl Default for ConformanceDeclaration {
95    fn default() -> Self {
96        Self::standard()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_standard_includes_core() {
106        let decl = ConformanceDeclaration::standard();
107        assert!(decl.supports(uris::CORE));
108    }
109
110    #[test]
111    fn test_standard_includes_item_search() {
112        let decl = ConformanceDeclaration::standard();
113        assert!(decl.supports(uris::ITEM_SEARCH));
114    }
115
116    #[test]
117    fn test_standard_includes_ogc() {
118        let decl = ConformanceDeclaration::standard();
119        assert!(decl.supports(uris::OGCAPI_FEATURES));
120    }
121
122    #[test]
123    fn test_supports_false_for_unknown() {
124        let decl = ConformanceDeclaration::standard();
125        assert!(!decl.supports("https://example.com/unknown"));
126    }
127
128    #[test]
129    fn test_with_transaction() {
130        let decl = ConformanceDeclaration::standard().with_transaction();
131        assert!(decl.supports(uris::TRANSACTION));
132    }
133
134    #[test]
135    fn test_custom_class() {
136        let decl = ConformanceDeclaration::new(["https://my.server/custom"]);
137        assert!(decl.supports("https://my.server/custom"));
138    }
139
140    #[test]
141    fn test_json_roundtrip() {
142        let decl = ConformanceDeclaration::standard();
143        let json = serde_json::to_string(&decl).expect("serialize");
144        let back: ConformanceDeclaration = serde_json::from_str(&json).expect("deserialize");
145        assert_eq!(decl, back);
146    }
147
148    #[test]
149    fn test_standard_count() {
150        let decl = ConformanceDeclaration::standard();
151        // Core, Browseable, Item Search, Filter, Sort, Fields, OGC Features, Children = 8
152        assert_eq!(decl.len(), 8);
153    }
154}