1use crate::{
4 error::{Result, StacError},
5 item::Link,
6};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct Collection {
14 #[serde(rename = "type")]
16 pub type_: String,
17
18 #[serde(rename = "stac_version")]
20 pub stac_version: String,
21
22 #[serde(skip_serializing_if = "Option::is_none", rename = "stac_extensions")]
24 pub stac_extensions: Option<Vec<String>>,
25
26 pub id: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub title: Option<String>,
32
33 pub description: String,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub keywords: Option<Vec<String>>,
39
40 pub license: String,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub providers: Option<Vec<Provider>>,
46
47 pub extent: Extent,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub summaries: Option<HashMap<String, serde_json::Value>>,
53
54 pub links: Vec<Link>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub assets: Option<HashMap<String, crate::asset::Asset>>,
60
61 #[serde(flatten)]
63 pub additional_fields: HashMap<String, serde_json::Value>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
68pub struct Provider {
69 pub name: String,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub description: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub roles: Option<Vec<String>>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub url: Option<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87pub struct Extent {
88 pub spatial: SpatialExtent,
90
91 pub temporal: TemporalExtent,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct SpatialExtent {
98 pub bbox: Vec<Vec<f64>>,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104pub struct TemporalExtent {
105 pub interval: Vec<Vec<Option<DateTime<Utc>>>>,
108}
109
110impl Collection {
111 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 pub fn with_title(mut self, title: impl Into<String>) -> Self {
158 self.title = Some(title.into());
159 self
160 }
161
162 pub fn with_keywords(mut self, keywords: Vec<String>) -> Self {
172 self.keywords = Some(keywords);
173 self
174 }
175
176 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 pub fn with_spatial_extent(mut self, bbox: Vec<f64>) -> Self {
203 self.extent.spatial.bbox = vec![bbox];
204 self
205 }
206
207 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 pub fn add_link(mut self, link: Link) -> Self {
236 self.links.push(link);
237 self
238 }
239
240 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 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 pub fn find_links(&self, rel: &str) -> impl Iterator<Item = &Link> {
291 self.links.iter().filter(move |link| link.rel == rel)
292 }
293
294 pub fn validate(&self) -> Result<()> {
300 if self.type_ != "Collection" {
302 return Err(StacError::InvalidType {
303 expected: "Collection".to_string(),
304 found: self.type_.clone(),
305 });
306 }
307
308 if self.stac_version != "1.0.0" {
310 return Err(StacError::InvalidVersion(self.stac_version.clone()));
311 }
312
313 if self.id.is_empty() {
315 return Err(StacError::MissingField("id".to_string()));
316 }
317
318 if self.description.is_empty() {
320 return Err(StacError::MissingField("description".to_string()));
321 }
322
323 if self.license.is_empty() {
325 return Err(StacError::MissingField("license".to_string()));
326 }
327
328 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 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
371 self.description = Some(description.into());
372 self
373 }
374
375 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
385 self.roles = Some(roles);
386 self
387 }
388
389 pub fn with_url(mut self, url: impl Into<String>) -> Self {
399 self.url = Some(url.into());
400 self
401 }
402}
403
404pub mod provider_roles {
406 pub const LICENSOR: &str = "licensor";
408
409 pub const PRODUCER: &str = "producer";
411
412 pub const PROCESSOR: &str = "processor";
414
415 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}