Skip to main content

oxigdal_stac/
collection.rs

1//! STAC Collection representation.
2
3use crate::{
4    error::{Result, StacError},
5    item::Link,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// A STAC Collection provides additional metadata about a set of Items.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct Collection {
14    /// Type must be "Collection".
15    #[serde(rename = "type")]
16    pub type_: String,
17
18    /// STAC version.
19    #[serde(rename = "stac_version")]
20    pub stac_version: String,
21
22    /// List of extensions used in the collection.
23    #[serde(skip_serializing_if = "Option::is_none", rename = "stac_extensions")]
24    pub stac_extensions: Option<Vec<String>>,
25
26    /// Unique identifier for the collection.
27    pub id: String,
28
29    /// Title of the collection.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub title: Option<String>,
32
33    /// Description of the collection.
34    pub description: String,
35
36    /// Keywords describing the collection.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub keywords: Option<Vec<String>>,
39
40    /// License of the collection (SPDX license identifier or URL).
41    pub license: String,
42
43    /// Providers of the collection.
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub providers: Option<Vec<Provider>>,
46
47    /// Extent of the collection.
48    pub extent: Extent,
49
50    /// Summaries of properties in the collection.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub summaries: Option<HashMap<String, serde_json::Value>>,
53
54    /// Links to other resources.
55    pub links: Vec<Link>,
56
57    /// Assets associated with this collection.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub assets: Option<HashMap<String, crate::asset::Asset>>,
60
61    /// Additional fields for extensions.
62    #[serde(flatten)]
63    pub additional_fields: HashMap<String, serde_json::Value>,
64}
65
66/// Provider of data in a STAC Collection.
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct Provider {
69    /// Name of the provider.
70    pub name: String,
71
72    /// Description of the provider.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub description: Option<String>,
75
76    /// Roles of the provider.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub roles: Option<Vec<String>>,
79
80    /// URL of the provider.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub url: Option<String>,
83}
84
85/// Extent of a STAC Collection.
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87pub struct Extent {
88    /// Spatial extent.
89    pub spatial: SpatialExtent,
90
91    /// Temporal extent.
92    pub temporal: TemporalExtent,
93}
94
95/// Spatial extent of a STAC Collection.
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct SpatialExtent {
98    /// Bounding boxes of the collection [west, south, east, north].
99    pub bbox: Vec<Vec<f64>>,
100}
101
102/// Temporal extent of a STAC Collection.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct TemporalExtent {
105    /// Time intervals of the collection [[start, end], ...].
106    /// `null` can be used for open-ended intervals.
107    pub interval: Vec<Vec<Option<DateTime<Utc>>>>,
108}
109
110impl Collection {
111    /// Creates a new STAC Collection.
112    ///
113    /// # Arguments
114    ///
115    /// * `id` - Unique identifier for the collection
116    /// * `description` - Description of the collection
117    /// * `license` - License identifier or URL
118    ///
119    /// # Returns
120    ///
121    /// A new Collection instance with default STAC version 1.0.0
122    pub fn new(
123        id: impl Into<String>,
124        description: impl Into<String>,
125        license: impl Into<String>,
126    ) -> Self {
127        Self {
128            type_: "Collection".to_string(),
129            stac_version: "1.0.0".to_string(),
130            stac_extensions: None,
131            id: id.into(),
132            title: None,
133            description: description.into(),
134            keywords: None,
135            license: license.into(),
136            providers: None,
137            extent: Extent {
138                spatial: SpatialExtent { bbox: vec![] },
139                temporal: TemporalExtent { interval: vec![] },
140            },
141            summaries: None,
142            links: Vec::new(),
143            assets: None,
144            additional_fields: HashMap::new(),
145        }
146    }
147
148    /// Sets the title of the collection.
149    ///
150    /// # Arguments
151    ///
152    /// * `title` - Title for the collection
153    ///
154    /// # Returns
155    ///
156    /// Self for method chaining
157    pub fn with_title(mut self, title: impl Into<String>) -> Self {
158        self.title = Some(title.into());
159        self
160    }
161
162    /// Sets the keywords of the collection.
163    ///
164    /// # Arguments
165    ///
166    /// * `keywords` - Vector of keywords
167    ///
168    /// # Returns
169    ///
170    /// Self for method chaining
171    pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
172        self.keywords = Some(keywords);
173        self
174    }
175
176    /// Adds a provider to the collection.
177    ///
178    /// # Arguments
179    ///
180    /// * `provider` - Provider to add
181    ///
182    /// # Returns
183    ///
184    /// Self for method chaining
185    pub fn add_provider(mut self, provider: Provider) -> Self {
186        match &mut self.providers {
187            Some(providers) => providers.push(provider),
188            None => self.providers = Some(vec![provider]),
189        }
190        self
191    }
192
193    /// Sets the spatial extent of the collection.
194    ///
195    /// # Arguments
196    ///
197    /// * `bbox` - Bounding box [west, south, east, north]
198    ///
199    /// # Returns
200    ///
201    /// Self for method chaining
202    pub fn with_spatial_extent(mut self, bbox: Vec<f64>) -> Self {
203        self.extent.spatial.bbox = vec![bbox];
204        self
205    }
206
207    /// Sets the temporal extent of the collection.
208    ///
209    /// # Arguments
210    ///
211    /// * `start` - Start datetime (None for open start)
212    /// * `end` - End datetime (None for open end)
213    ///
214    /// # Returns
215    ///
216    /// Self for method chaining
217    pub fn with_temporal_extent(
218        mut self,
219        start: Option<DateTime<Utc>>,
220        end: Option<DateTime<Utc>>,
221    ) -> Self {
222        self.extent.temporal.interval = vec![vec![start, end]];
223        self
224    }
225
226    /// Adds a link to the collection.
227    ///
228    /// # Arguments
229    ///
230    /// * `link` - Link to add
231    ///
232    /// # Returns
233    ///
234    /// Self for method chaining
235    pub fn add_link(mut self, link: Link) -> Self {
236        self.links.push(link);
237        self
238    }
239
240    /// Adds a STAC extension.
241    ///
242    /// # Arguments
243    ///
244    /// * `extension` - Extension schema URI
245    ///
246    /// # Returns
247    ///
248    /// Self for method chaining
249    pub fn add_extension(mut self, extension: impl Into<String>) -> Self {
250        match &mut self.stac_extensions {
251            Some(extensions) => extensions.push(extension.into()),
252            None => self.stac_extensions = Some(vec![extension.into()]),
253        }
254        self
255    }
256
257    /// Adds a summary field.
258    ///
259    /// # Arguments
260    ///
261    /// * `key` - Summary field name
262    /// * `value` - Summary value
263    ///
264    /// # Returns
265    ///
266    /// Self for method chaining
267    pub fn add_summary(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
268        match &mut self.summaries {
269            Some(summaries) => {
270                summaries.insert(key.into(), value);
271            }
272            None => {
273                let mut summaries = HashMap::new();
274                summaries.insert(key.into(), value);
275                self.summaries = Some(summaries);
276            }
277        }
278        self
279    }
280
281    /// Finds links by relationship type.
282    ///
283    /// # Arguments
284    ///
285    /// * `rel` - Relationship type
286    ///
287    /// # Returns
288    ///
289    /// Iterator over matching links
290    pub fn find_links(&self, rel: &str) -> impl Iterator<Item = &Link> {
291        self.links.iter().filter(move |link| link.rel == rel)
292    }
293
294    /// Validates the collection according to STAC specification.
295    ///
296    /// # Returns
297    ///
298    /// `Ok(())` if valid, otherwise an error
299    pub fn validate(&self) -> Result<()> {
300        // Check type
301        if self.type_ != "Collection" {
302            return Err(StacError::InvalidType {
303                expected: "Collection".to_string(),
304                found: self.type_.clone(),
305            });
306        }
307
308        // Check STAC version
309        if self.stac_version != "1.0.0" {
310            return Err(StacError::InvalidVersion(self.stac_version.clone()));
311        }
312
313        // Check ID
314        if self.id.is_empty() {
315            return Err(StacError::MissingField("id".to_string()));
316        }
317
318        // Check description
319        if self.description.is_empty() {
320            return Err(StacError::MissingField("description".to_string()));
321        }
322
323        // Check license
324        if self.license.is_empty() {
325            return Err(StacError::MissingField("license".to_string()));
326        }
327
328        // Validate spatial extent
329        for bbox in &self.extent.spatial.bbox {
330            if bbox.len() != 4 && bbox.len() != 6 {
331                return Err(StacError::InvalidBbox(format!(
332                    "bbox must have 4 or 6 elements, found {}",
333                    bbox.len()
334                )));
335            }
336        }
337
338        Ok(())
339    }
340}
341
342impl Provider {
343    /// Creates a new provider.
344    ///
345    /// # Arguments
346    ///
347    /// * `name` - Provider name
348    ///
349    /// # Returns
350    ///
351    /// A new Provider instance
352    pub fn new(name: impl Into<String>) -> Self {
353        Self {
354            name: name.into(),
355            description: None,
356            roles: None,
357            url: None,
358        }
359    }
360
361    /// Sets the description of the provider.
362    ///
363    /// # Arguments
364    ///
365    /// * `description` - Description of the provider
366    ///
367    /// # Returns
368    ///
369    /// Self for method chaining
370    pub fn with_description(mut self, description: impl Into<String>) -> Self {
371        self.description = Some(description.into());
372        self
373    }
374
375    /// Sets the roles of the provider.
376    ///
377    /// # Arguments
378    ///
379    /// * `roles` - Vector of role identifiers
380    ///
381    /// # Returns
382    ///
383    /// Self for method chaining
384    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
385        self.roles = Some(roles);
386        self
387    }
388
389    /// Sets the URL of the provider.
390    ///
391    /// # Arguments
392    ///
393    /// * `url` - Provider URL
394    ///
395    /// # Returns
396    ///
397    /// Self for method chaining
398    pub fn with_url(mut self, url: impl Into<String>) -> Self {
399        self.url = Some(url.into());
400        self
401    }
402}
403
404/// Common provider roles.
405pub mod provider_roles {
406    /// Licensor role.
407    pub const LICENSOR: &str = "licensor";
408
409    /// Producer role.
410    pub const PRODUCER: &str = "producer";
411
412    /// Processor role.
413    pub const PROCESSOR: &str = "processor";
414
415    /// Host role.
416    pub const HOST: &str = "host";
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_collection_new() {
425        let collection = Collection::new("test-collection", "Test description", "MIT");
426        assert_eq!(collection.id, "test-collection");
427        assert_eq!(collection.description, "Test description");
428        assert_eq!(collection.license, "MIT");
429        assert_eq!(collection.stac_version, "1.0.0");
430        assert_eq!(collection.type_, "Collection");
431    }
432
433    #[test]
434    fn test_collection_with_title() {
435        let collection =
436            Collection::new("test-collection", "Test description", "MIT").with_title("Test Title");
437        assert_eq!(collection.title, Some("Test Title".to_string()));
438    }
439
440    #[test]
441    fn test_collection_add_provider() {
442        let provider = Provider::new("Test Provider");
443        let collection = Collection::new("test-collection", "Test description", "MIT")
444            .add_provider(provider.clone());
445        assert_eq!(collection.providers, Some(vec![provider]));
446    }
447
448    #[test]
449    fn test_collection_with_spatial_extent() {
450        let collection = Collection::new("test-collection", "Test description", "MIT")
451            .with_spatial_extent(vec![-180.0, -90.0, 180.0, 90.0]);
452        assert_eq!(
453            collection.extent.spatial.bbox,
454            vec![vec![-180.0, -90.0, 180.0, 90.0]]
455        );
456    }
457
458    #[test]
459    fn test_collection_with_temporal_extent() {
460        let start = Utc::now();
461        let end = Utc::now();
462        let collection = Collection::new("test-collection", "Test description", "MIT")
463            .with_temporal_extent(Some(start), Some(end));
464        assert_eq!(
465            collection.extent.temporal.interval,
466            vec![vec![Some(start), Some(end)]]
467        );
468    }
469
470    #[test]
471    fn test_collection_validate() {
472        let collection = Collection::new("test-collection", "Test description", "MIT")
473            .with_spatial_extent(vec![-180.0, -90.0, 180.0, 90.0]);
474        assert!(collection.validate().is_ok());
475    }
476
477    #[test]
478    fn test_provider_builder() {
479        let provider = Provider::new("Test Provider")
480            .with_description("A test provider")
481            .with_roles(vec![provider_roles::PRODUCER.to_string()])
482            .with_url("https://example.com");
483
484        assert_eq!(provider.name, "Test Provider");
485        assert_eq!(provider.description, Some("A test provider".to_string()));
486        assert_eq!(
487            provider.roles,
488            Some(vec![provider_roles::PRODUCER.to_string()])
489        );
490        assert_eq!(provider.url, Some("https://example.com".to_string()));
491    }
492}