Skip to main content

stac/
item.rs

1//! STAC Items.
2
3use crate::{
4    Asset, Assets, Bbox, Error, Fields, Link, Result, STAC_VERSION, Version,
5    datetime::parse_datetime_permissively,
6};
7use chrono::{DateTime, Utc};
8use cql2::Expr;
9use geojson::{Feature, Geometry, feature::Id};
10use indexmap::IndexMap;
11use serde::{Deserialize, Deserializer, Serialize};
12use serde_json::{Map, Value};
13use stac_derive::{Links, Migrate, SelfHref};
14use std::path::Path;
15
16const TOP_LEVEL_ATTRIBUTES: [&str; 8] = [
17    "type",
18    "stac_extensions",
19    "id",
20    "geometry",
21    "bbox",
22    "links",
23    "assets",
24    "collection",
25];
26
27const ITEM_TYPE: &str = "Feature";
28
29fn item_type() -> String {
30    ITEM_TYPE.to_string()
31}
32
33fn deserialize_item_type<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
34where
35    D: Deserializer<'de>,
36{
37    let r#type = String::deserialize(deserializer)?;
38    if r#type != ITEM_TYPE {
39        Err(serde::de::Error::invalid_value(
40            serde::de::Unexpected::Str(&r#type),
41            &ITEM_TYPE,
42        ))
43    } else {
44        Ok(r#type)
45    }
46}
47
48/// An `Item` is a GeoJSON Feature augmented with foreign members relevant to a
49/// STAC object.
50///
51/// These include fields that identify the time range and assets of the `Item`. An
52/// `Item` is the core object in a STAC catalog, containing the core metadata that
53/// enables any client to search or crawl online catalogs of spatial 'assets'
54/// (e.g., satellite imagery, derived data, DEMs).
55#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, SelfHref, Links, Migrate)]
56pub struct Item {
57    #[serde(default = "item_type", deserialize_with = "deserialize_item_type")]
58    r#type: String,
59
60    /// The STAC version the `Item` implements.
61    #[serde(rename = "stac_version", default)]
62    pub version: Version,
63
64    /// A list of extensions the `Item` implements.
65    #[serde(
66        rename = "stac_extensions",
67        skip_serializing_if = "Vec::is_empty",
68        default
69    )]
70    pub extensions: Vec<String>,
71
72    /// Provider identifier.
73    ///
74    /// The ID should be unique within the [Collection](crate::Collection) that contains the `Item`.
75    #[serde(default)]
76    pub id: String,
77
78    /// Defines the full footprint of the asset represented by this item,
79    /// formatted according to [RFC 7946, section
80    /// 3.1](https://tools.ietf.org/html/rfc7946#section-3.1).
81    ///
82    /// The footprint should be the default GeoJSON geometry, though additional
83    /// geometries can be included. Coordinates are specified in
84    /// Longitude/Latitude or Longitude/Latitude/Elevation based on [WGS
85    /// 84](http://www.opengis.net/def/crs/OGC/1.3/CRS84).
86    pub geometry: Option<Geometry>,
87
88    /// Bounding Box of the asset represented by this `Item`, formatted according
89    /// to [RFC 7946, section 5](https://tools.ietf.org/html/rfc7946#section-5).
90    ///
91    /// REQUIRED if `geometry` is not `null`.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub bbox: Option<Bbox>,
94
95    /// A dictionary of additional metadata for the `Item`.
96    #[serde(default)]
97    pub properties: Properties,
98
99    /// List of link objects to resources and related URLs.
100    #[serde(default)]
101    pub links: Vec<Link>,
102
103    /// Dictionary of asset objects that can be downloaded, each with a unique key.
104    #[serde(default)]
105    pub assets: IndexMap<String, Asset>,
106
107    /// The `id` of the STAC [Collection](crate::Collection) this `Item`
108    /// references to.
109    ///
110    /// This field is *required* if such a relation type is present and is *not
111    /// allowed* otherwise. This field provides an easy way for a user to search
112    /// for any `Item`s that belong in a specified `Collection`. Must be a non-empty
113    /// string.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub collection: Option<String>,
116
117    /// Additional fields not part of the Item specification.
118    #[serde(flatten)]
119    pub additional_fields: Map<String, Value>,
120
121    #[serde(skip)]
122    self_href: Option<String>,
123}
124
125/// A [FlatItem] has all of its properties at the top level.
126///
127/// Some STAC representations, e.g.
128/// [stac-geoparquet](https://github.com/stac-utils/stac-geoparquet/blob/main/spec/stac-geoparquet-spec.md),
129/// use this "flat" representation.
130#[derive(Debug, Serialize, Deserialize)]
131pub struct FlatItem {
132    #[serde(default = "item_type", deserialize_with = "deserialize_item_type")]
133    r#type: String,
134
135    #[serde(rename = "stac_version", default = "default_stac_version")]
136    version: Version,
137
138    /// This column is required, but can be empty if no STAC extensions were used.
139    #[serde(
140        rename = "stac_extensions",
141        skip_serializing_if = "Vec::is_empty",
142        default
143    )]
144    pub extensions: Vec<String>,
145
146    /// Required, should be unique within each collection
147    pub id: String,
148
149    /// Defines the full footprint of the asset represented by this item,
150    /// formatted according to [RFC 7946, section
151    /// 3.1](https://tools.ietf.org/html/rfc7946#section-3.1).
152    ///
153    /// The footprint should be the default GeoJSON geometry, though additional
154    /// geometries can be included. Coordinates are specified in
155    /// Longitude/Latitude or Longitude/Latitude/Elevation based on [WGS
156    /// 84](http://www.opengis.net/def/crs/OGC/1.3/CRS84).
157    pub geometry: Option<Geometry>,
158
159    /// Can be a 4 or 6 value vector, depending on dimension of the data.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub bbox: Option<Bbox>,
162
163    /// List of link objects to resources and related URLs.
164    pub links: Vec<Link>,
165
166    /// Dictionary of asset objects that can be downloaded, each with a unique key.
167    #[serde(skip_serializing_if = "IndexMap::is_empty")]
168    pub assets: IndexMap<String, Asset>,
169
170    /// The ID of the collection this Item is a part of.
171    pub collection: Option<String>,
172
173    /// Each property should use the relevant Parquet type, and be pulled out of
174    /// the properties object to be a top-level Parquet field
175    #[serde(flatten)]
176    pub properties: Map<String, Value>,
177}
178
179/// Additional metadata fields can be added to the GeoJSON Object Properties.
180#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
181pub struct Properties {
182    /// The searchable date and time of the assets, which must be in UTC.
183    ///
184    /// It is formatted according to RFC 3339, section 5.6. null is allowed, but
185    /// requires `start_datetime` and `end_datetime` from common metadata to be set.
186    #[serde(default, deserialize_with = "deserialize_datetime_permissively")]
187    pub datetime: Option<DateTime<Utc>>,
188
189    /// The first or start date and time for the Item, in UTC.
190    ///
191    /// It is formatted as date-time according to RFC 3339, section 5.6.
192    ///
193    /// This is a [common
194    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
195    /// field.
196    #[serde(
197        skip_serializing_if = "Option::is_none",
198        default,
199        deserialize_with = "deserialize_datetime_permissively"
200    )]
201    pub start_datetime: Option<DateTime<Utc>>,
202
203    /// The last or end date and time for the Item, in UTC.
204    ///
205    /// It is formatted as date-time according to RFC 3339, section 5.6.
206    ///
207    /// This is a [common
208    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
209    /// field.
210    #[serde(
211        skip_serializing_if = "Option::is_none",
212        default,
213        deserialize_with = "deserialize_datetime_permissively"
214    )]
215    pub end_datetime: Option<DateTime<Utc>>,
216
217    /// A human readable title describing the Item.
218    ///
219    /// This is a [common
220    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
221    /// field.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub title: Option<String>,
224
225    /// Detailed multi-line description to fully explain the Item.
226    ///
227    /// CommonMark 0.29 syntax MAY be used for rich text representation.
228    ///
229    /// This is a [common
230    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
231    /// field.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub description: Option<String>,
234
235    /// Creation date and time of the corresponding data, in UTC.
236    ///
237    /// This identifies the creation time of the metadata.
238    ///
239    /// This is a [common
240    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
241    /// field.
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub created: Option<String>,
244
245    /// Date and time the metadata was updated last, in UTC.
246    ///
247    /// This identifies the updated time of the metadata.
248    ///
249    /// This is a [common
250    /// metadata](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md)
251    /// field.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub updated: Option<String>,
254
255    /// Additional fields on the properties.
256    #[serde(flatten)]
257    pub additional_fields: Map<String, Value>,
258}
259
260/// Builder for a STAC Item.
261#[derive(Debug)]
262pub struct Builder {
263    id: String,
264    canonicalize_paths: bool,
265    assets: IndexMap<String, Asset>,
266}
267
268impl Builder {
269    /// Creates a new builder.
270    ///
271    /// # Examples
272    ///
273    /// ```
274    /// use stac::item::Builder;
275    /// let builder = Builder::new("an-id");
276    /// ```
277    pub fn new(id: impl ToString) -> Builder {
278        Builder {
279            id: id.to_string(),
280            canonicalize_paths: true,
281            assets: IndexMap::new(),
282        }
283    }
284
285    /// Set to false to not canonicalize paths.
286    ///
287    /// Useful if you want relative paths, or the files don't actually exist.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use stac::item::Builder;
293    /// let builder = Builder::new("an-id").canonicalize_paths(false);
294    /// ```
295    pub fn canonicalize_paths(mut self, canonicalize_paths: bool) -> Builder {
296        self.canonicalize_paths = canonicalize_paths;
297        self
298    }
299
300    /// Adds an asset by href to this builder.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// use stac::item::Builder;
306    /// let builder = Builder::new("an-id").asset("data", "assets/dataset.tif");
307    /// ```
308    pub fn asset(mut self, key: impl ToString, asset: impl Into<Asset>) -> Builder {
309        let _ = self.assets.insert(key.to_string(), asset.into());
310        self
311    }
312
313    /// Builds an [Item] from this builder.
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use stac::item::Builder;
319    /// let builder = Builder::new("an-id").asset("data", "assets/dataset.tif");
320    /// let item = builder.build().unwrap();
321    /// assert_eq!(item.assets.len(), 1);
322    /// ```
323    pub fn build(self) -> Result<Item> {
324        let mut item = Item::new(self.id);
325        for (key, mut asset) in self.assets {
326            if self.canonicalize_paths {
327                asset.href = Path::new(&asset.href)
328                    .canonicalize()?
329                    .to_string_lossy()
330                    .into_owned();
331            }
332            let _ = item.assets.insert(key, asset);
333        }
334        Ok(item)
335    }
336}
337
338impl Default for Properties {
339    fn default() -> Properties {
340        Properties {
341            datetime: Some(Utc::now()),
342            start_datetime: None,
343            end_datetime: None,
344            title: None,
345            description: None,
346            created: None,
347            updated: None,
348            additional_fields: Map::new(),
349        }
350    }
351}
352
353impl Item {
354    /// Creates a new `Item` with the given `id`.
355    ///
356    /// The item properties' `datetime` field is set to the object creation
357    /// time.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use stac::Item;
363    /// let item = Item::new("an-id");
364    /// assert_eq!(item.id, "an-id");
365    /// ```
366    pub fn new(id: impl ToString) -> Item {
367        Item {
368            r#type: ITEM_TYPE.to_string(),
369            version: STAC_VERSION,
370            extensions: Vec::new(),
371            id: id.to_string(),
372            geometry: None,
373            bbox: None,
374            properties: Properties::default(),
375            links: Vec::new(),
376            assets: IndexMap::new(),
377            collection: None,
378            additional_fields: Map::new(),
379            self_href: None,
380        }
381    }
382
383    /// Sets this item's collection id in the builder pattern.
384    ///
385    /// # Examples
386    ///
387    /// ```
388    /// use stac::Item;
389    /// let item = Item::new("an-id").collection("a-collection");
390    /// assert_eq!(item.collection.unwrap(), "a-collection");
391    pub fn collection(mut self, id: impl ToString) -> Item {
392        self.collection = Some(id.to_string());
393        self
394    }
395
396    /// Returns this item's collection link.
397    ///
398    /// This is the first link with a rel="collection".
399    ///
400    /// # Examples
401    ///
402    /// ```
403    /// use stac::Item;
404    /// let item: Item = stac::read("examples/simple-item.json").unwrap();
405    /// let link = item.collection_link().unwrap();
406    /// ```
407    pub fn collection_link(&self) -> Option<&Link> {
408        self.links.iter().find(|link| link.is_collection())
409    }
410
411    /// Sets this item's geometry.
412    ///
413    /// Also sets this item's bounding box.
414    ///
415    /// # Examples
416    ///
417    /// ```
418    /// use stac::Item;
419    /// use geojson::Geometry;
420    ///
421    /// let mut item = Item::new("an-id");
422    /// item.set_geometry(Some(Geometry::new_point(vec![-105.1, 41.1])));
423    /// assert_eq!(item.bbox.unwrap(), vec![-105.1, 41.1, -105.1, 41.1].try_into().unwrap());
424    /// ```
425    #[cfg(feature = "geo")]
426    pub fn set_geometry(&mut self, geometry: impl Into<Option<Geometry>>) -> Result<()> {
427        use geo::BoundingRect;
428
429        let geometry = geometry.into();
430        self.bbox = geometry
431            .as_ref()
432            .and_then(|geometry| geo::Geometry::try_from(geometry).ok())
433            .and_then(|geometry| geometry.bounding_rect())
434            .map(Bbox::from);
435        self.geometry = serde_json::from_value(serde_json::to_value(geometry)?)?;
436        Ok(())
437    }
438
439    /// Returns true if this item's geometry intersects the provided geojson geometry.
440    ///
441    /// # Examples
442    ///
443    /// ```
444    /// use stac::Item;
445    /// use geojson::{Geometry, Value};
446    /// use geo::{Rect, coord};
447    ///
448    /// let mut item = Item::new("an-id");
449    /// item.set_geometry(Some(Geometry::new_point(vec![-105.1, 41.1])));
450    /// let intersects = Rect::new(
451    ///     coord! { x: -106.0, y: 40.0 },
452    ///     coord! { x: -105.0, y: 42.0 },
453    /// );
454    /// assert!(item.intersects(&intersects).unwrap());
455    /// ```
456    #[cfg(feature = "geo")]
457    pub fn intersects<T>(&self, intersects: &T) -> Result<bool>
458    where
459        T: geo::Intersects<geo::Geometry>,
460    {
461        match self.geometry.clone() {
462            Some(geometry) => {
463                let geometry: geo::Geometry = geometry.try_into().map_err(Box::new)?;
464                Ok(intersects.intersects(&geometry))
465            }
466            _ => Ok(false),
467        }
468    }
469
470    /// Returns true if this item's geometry intersects the provided bounding box.
471    ///
472    /// DEPRECATED Use `intersects` instead.
473    ///
474    /// # Examples
475    ///
476    /// ```
477    /// use stac::Item;
478    /// use geojson::{Geometry, Value};
479    ///
480    /// let mut item = Item::new("an-id");
481    /// item.set_geometry(Some(Geometry::new_point(vec![-105.1, 41.1])));
482    /// let bbox = stac::geo::bbox(&vec![-106.0, 41.0, -105.0, 42.0]).unwrap();
483    /// assert!(item.intersects(&bbox).unwrap());
484    /// ```
485    #[cfg(feature = "geo")]
486    #[deprecated(since = "0.5.2", note = "Use intersects instead")]
487    pub fn intersects_bbox(&self, bbox: geo::Rect) -> Result<bool> {
488        use geo::Intersects;
489
490        match self.geometry.clone() {
491            Some(geometry) => {
492                let geometry: geo::Geometry = geometry.try_into().map_err(Box::new)?;
493                Ok(geometry.intersects(&bbox))
494            }
495            _ => Ok(false),
496        }
497    }
498
499    /// Returns true if this item's datetime (or start and end datetime)
500    /// intersects the provided datetime string.
501    ///
502    /// # Examples
503    ///
504    /// ```
505    /// use stac::Item;
506    /// let mut item = Item::new("an-id");
507    /// item.properties.datetime = Some("2023-07-11T12:00:00Z".parse().unwrap());
508    /// assert!(item.intersects_datetime_str("2023-07-11T00:00:00Z/2023-07-12T00:00:00Z").unwrap());
509    /// ```
510    pub fn intersects_datetime_str(&self, datetime: &str) -> Result<bool> {
511        let (start, end) = crate::datetime::parse(datetime)?;
512        self.intersects_datetimes(start, end)
513    }
514
515    /// Returns true if this item's datetime (or start and end datetimes)
516    /// intersects the provided datetime.
517    ///
518    /// # Examples
519    ///
520    /// ```
521    /// use stac::Item;
522    /// let mut item = Item::new("an-id");
523    /// item.properties.datetime = Some("2023-07-11T12:00:00Z".parse().unwrap());
524    /// let (start, end) = stac::datetime::parse("2023-07-11T00:00:00Z/2023-07-12T00:00:00Z").unwrap();
525    /// assert!(item.intersects_datetimes(start, end).unwrap());
526    /// ```
527    pub fn intersects_datetimes(
528        &self,
529        start: Option<DateTime<Utc>>,
530        end: Option<DateTime<Utc>>,
531    ) -> Result<bool> {
532        let (item_start, item_end) = self.datetimes();
533        let mut intersects = true;
534        if let Some(start) = start
535            && let Some(item_end) = item_end
536            && item_end < start
537        {
538            intersects = false;
539        }
540        if let Some(end) = end
541            && let Some(item_start) = item_start
542            && item_start > end
543        {
544            intersects = false;
545        }
546        Ok(intersects)
547    }
548
549    pub(crate) fn datetimes(&self) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
550        let item_datetime = self.properties.datetime;
551        let item_start = self.properties.start_datetime.or(item_datetime);
552        let item_end = self.properties.end_datetime.or(item_datetime);
553        (item_start, item_end)
554    }
555
556    /// Converts this item into a [FlatItem].
557    ///
558    /// If `drop_invalid_attributes` is `True`, any properties that conflict
559    /// with top-level field names will be discarded with a warning. If it is
560    /// `False`, and error will be raised. The same is true for any top-level
561    /// fields that are not part of the spec.
562    ///
563    /// # Examples
564    ///
565    /// ```
566    /// use stac::Item;
567    ///
568    /// let mut item = Item::new("an-id");
569    /// let flat_item = item.into_flat_item(true).unwrap();
570    /// ```
571    pub fn into_flat_item(self, drop_invalid_attributes: bool) -> Result<FlatItem> {
572        let properties = match serde_json::to_value(self.properties)? {
573            Value::Object(object) => object,
574            _ => {
575                panic!("properties should always serialize to an object")
576            }
577        };
578        for (key, _) in properties.iter() {
579            if TOP_LEVEL_ATTRIBUTES.contains(&key.as_str()) {
580                if drop_invalid_attributes {
581                    log::warn!("dropping invalid property: {key}");
582                } else {
583                    return Err(Error::InvalidAttribute(key.to_string()));
584                }
585            }
586        }
587        for (key, _) in self.additional_fields {
588            if drop_invalid_attributes {
589                log::warn!("dropping out-of-spec top-level attribute: {key}");
590            } else {
591                return Err(Error::InvalidAttribute(key));
592            }
593        }
594        Ok(FlatItem {
595            r#type: self.r#type,
596            version: STAC_VERSION,
597            extensions: self.extensions,
598            id: self.id,
599            geometry: self.geometry,
600            bbox: self.bbox,
601            links: self.links,
602            assets: self.assets,
603            collection: self.collection,
604            properties,
605        })
606    }
607
608    /// Returns true if this item matches the given CQL2 expression.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// use stac::Item;
614    ///
615    /// let item = Item::new("an-item");
616    /// assert!(item.clone().matches_cql2("id = 'an-item'".parse().unwrap()).unwrap());
617    /// assert!(!item.matches_cql2("id = 'another-item'".parse().unwrap()).unwrap());
618    /// ```
619    pub fn matches_cql2(self, expr: Expr) -> Result<bool> {
620        let result = self.into_flat_item(true)?.matches_cql2(expr)?;
621        Ok(result)
622    }
623}
624
625impl Assets for Item {
626    fn assets(&self) -> &IndexMap<String, Asset> {
627        &self.assets
628    }
629    fn assets_mut(&mut self) -> &mut IndexMap<String, Asset> {
630        &mut self.assets
631    }
632}
633
634impl Fields for Item {
635    fn fields(&self) -> &Map<String, Value> {
636        &self.properties.additional_fields
637    }
638    fn fields_mut(&mut self) -> &mut Map<String, Value> {
639        &mut self.properties.additional_fields
640    }
641}
642
643impl TryFrom<Item> for Map<String, Value> {
644    type Error = Error;
645    fn try_from(item: Item) -> Result<Self> {
646        match serde_json::to_value(item)? {
647            Value::Object(object) => Ok(object),
648            _ => {
649                panic!("all STAC items should serialize to a serde_json::Value::Object")
650            }
651        }
652    }
653}
654
655impl TryFrom<Map<String, Value>> for Item {
656    type Error = serde_json::Error;
657    fn try_from(map: Map<String, Value>) -> std::result::Result<Self, Self::Error> {
658        serde_json::from_value(Value::Object(map))
659    }
660}
661
662impl TryFrom<Feature> for Item {
663    type Error = Error;
664
665    fn try_from(feature: Feature) -> Result<Item> {
666        if let Some(id) = feature.id {
667            let mut item = Item::new(match id {
668                Id::String(id) => id,
669                Id::Number(id) => id.to_string(),
670            });
671            item.bbox = feature.bbox.map(|bbox| bbox.try_into()).transpose()?;
672            item.geometry = feature.geometry;
673            item.properties = feature
674                .properties
675                .map(|properties| serde_json::from_value::<Properties>(Value::Object(properties)))
676                .transpose()?
677                .unwrap_or_default();
678            item.additional_fields = feature.foreign_members.unwrap_or_default();
679            Ok(item)
680        } else {
681            Err(Error::MissingField("id"))
682        }
683    }
684}
685
686impl TryFrom<Item> for Feature {
687    type Error = Error;
688    fn try_from(item: Item) -> Result<Feature> {
689        Ok(Feature {
690            bbox: item.bbox.map(Bbox::into),
691            geometry: item.geometry,
692            id: Some(Id::String(item.id)),
693            properties: match serde_json::to_value(item.properties)? {
694                Value::Object(object) => Some(object),
695                _ => panic!("properties should always serialize to an object"),
696            },
697            foreign_members: if item.additional_fields.is_empty() {
698                None
699            } else {
700                Some(item.additional_fields)
701            },
702        })
703    }
704}
705
706impl FlatItem {
707    /// Returns true if the item matches the given CQL2 expression.
708    pub fn matches_cql2(self, expr: Expr) -> Result<bool> {
709        let value = serde_json::to_value(self)?;
710        let result = expr.matches(Some(&value)).map_err(Box::new)?;
711        Ok(result)
712    }
713}
714
715fn default_stac_version() -> Version {
716    STAC_VERSION
717}
718
719fn deserialize_datetime_permissively<'de, D>(
720    deserializer: D,
721) -> std::result::Result<Option<DateTime<Utc>>, D::Error>
722where
723    D: Deserializer<'de>,
724{
725    use serde::de::Error;
726
727    if let Some(s) = Option::<String>::deserialize(deserializer)? {
728        parse_datetime_permissively(&s)
729            .map(Some)
730            .map_err(D::Error::custom)
731    } else {
732        Ok(None)
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::{Builder, FlatItem, Item};
739    use crate::{Asset, STAC_VERSION};
740    use geojson::{Feature, feature::Id};
741    use serde_json::json;
742
743    #[test]
744    fn new() {
745        let item = Item::new("an-id");
746        assert_eq!(item.geometry, None);
747        assert!(item.properties.datetime.is_some());
748        assert!(item.assets.is_empty());
749        assert!(item.collection.is_none());
750        assert_eq!(item.version, STAC_VERSION);
751        assert!(item.extensions.is_empty());
752        assert_eq!(item.id, "an-id");
753        assert!(item.links.is_empty());
754    }
755
756    #[test]
757    fn skip_serializing() {
758        let item = Item::new("an-id");
759        let value = serde_json::to_value(item).unwrap();
760        assert!(value.get("stac_extensions").is_none());
761        assert!(value.get("bbox").is_none());
762        assert!(value.get("collection").is_none());
763    }
764
765    #[test]
766    #[cfg(feature = "geo")]
767    fn set_geometry_sets_bbox() {
768        use geojson::Geometry;
769        let mut item = Item::new("an-id");
770        item.set_geometry(Some(Geometry::new(geojson::GeometryValue::new_point(
771            vec![-105.1, 41.1],
772        ))))
773        .unwrap();
774        assert_eq!(
775            item.bbox,
776            Some(vec![-105.1, 41.1, -105.1, 41.1].try_into().unwrap())
777        );
778    }
779
780    #[test]
781    #[cfg(feature = "geo")]
782    fn set_geometry_clears_bbox() {
783        use geojson::Geometry;
784        let mut item = Item::new("an-id");
785        item.set_geometry(Some(Geometry::new(geojson::GeometryValue::new_point(
786            vec![-105.1, 41.1],
787        ))))
788        .unwrap();
789        item.set_geometry(None).unwrap();
790        assert_eq!(item.bbox, None);
791    }
792
793    #[test]
794    #[cfg(feature = "geo")]
795    fn insersects() {
796        use geojson::Geometry;
797        let mut item = Item::new("an-id");
798        item.set_geometry(Some(Geometry::new(geojson::GeometryValue::new_point(
799            vec![-105.1, 41.1],
800        ))))
801        .unwrap();
802        assert!(
803            item.intersects(&crate::geo::bbox(&[-106.0, 41.0, -105.0, 42.0]).unwrap())
804                .unwrap()
805        );
806    }
807
808    #[test]
809    fn intersects_datetime() {
810        let mut item = Item::new("an-id");
811        item.properties.datetime = Some("2023-07-11T12:00:00Z".parse().unwrap());
812        for datetime in [
813            "2023-07-11T12:00:00Z",
814            "2023-07-11T00:00:00Z/2023-07-12T00:00:00Z",
815            "../2023-07-12T00:00:00Z",
816            "2023-07-11T00:00:00Z/..",
817        ] {
818            let (start, end) = crate::datetime::parse(datetime).unwrap();
819            assert!(item.intersects_datetimes(start, end).unwrap());
820        }
821        let (start, end) =
822            crate::datetime::parse("2023-07-12T00:00:00Z/2023-07-13T00:00:00Z").unwrap();
823        assert!(!item.intersects_datetimes(start, end).unwrap());
824        item.properties.datetime = None;
825        let _ = item
826            .properties
827            .additional_fields
828            .insert("start_datetime".to_string(), "2023-07-11T11:00:00Z".into());
829        let _ = item
830            .properties
831            .additional_fields
832            .insert("end_datetime".to_string(), "2023-07-11T13:00:00Z".into());
833        let (start, end) = crate::datetime::parse("2023-07-11T12:00:00Z").unwrap();
834        assert!(item.intersects_datetimes(start, end).unwrap());
835    }
836
837    mod roundtrip {
838        use super::Item;
839        use crate::tests::roundtrip;
840
841        roundtrip!(simple_item, "examples/simple-item.json", Item);
842        roundtrip!(extended_item, "examples/extended-item.json", Item);
843        roundtrip!(core_item, "examples/core-item.json", Item);
844        roundtrip!(
845            collectionless_item,
846            "examples/collectionless-item.json",
847            Item
848        );
849        roundtrip!(
850            proj_example_item,
851            "examples/extensions-collection/proj-example/proj-example.json",
852            Item
853        );
854    }
855
856    #[test]
857    fn builder() {
858        let builder = Builder::new("an-id").asset("data", "assets/dataset.tif");
859        let item = builder.build().unwrap();
860        assert_eq!(item.assets.len(), 1);
861        let asset = item.assets.get("data").unwrap();
862        assert!(
863            asset
864                .href
865                .to_string()
866                .ends_with(&format!("assets{}dataset.tif", std::path::MAIN_SEPARATOR))
867        );
868    }
869
870    #[test]
871    fn builder_relative_paths() {
872        let builder = Builder::new("an-id")
873            .canonicalize_paths(false)
874            .asset("data", "assets/dataset.tif");
875        let item = builder.build().unwrap();
876        let asset = item.assets.get("data").unwrap();
877        assert_eq!(asset.href, "assets/dataset.tif");
878    }
879
880    #[test]
881    fn builder_asset_roles() {
882        let item = Builder::new("an-id")
883            .asset("data", Asset::new("assets/dataset.tif").role("data"))
884            .build()
885            .unwrap();
886        let asset = item.assets.get("data").unwrap();
887        assert_eq!(asset.roles, vec!["data"]);
888    }
889
890    #[test]
891    fn try_from_geojson_feature() {
892        let mut feature = Feature {
893            bbox: None,
894            geometry: None,
895            id: None,
896            properties: None,
897            foreign_members: None,
898        };
899        let _ = Item::try_from(feature.clone()).unwrap_err();
900        feature.id = Some(Id::String("an-id".to_string()));
901        let _ = Item::try_from(feature).unwrap();
902    }
903
904    #[test]
905    fn try_into_geojson_feature() {
906        let item = Item::new("an-id");
907        let feature = Feature::try_from(item).unwrap();
908        assert_eq!(feature.id.unwrap(), Id::String("an-id".to_string()));
909    }
910
911    #[test]
912    fn item_into_flat_item() {
913        let mut item = Item::new("an-id");
914        let _ = item.clone().into_flat_item(true).unwrap();
915
916        let _ = item
917            .properties
918            .additional_fields
919            .insert("bbox".to_string(), vec![-105.1, 42.0, -105.0, 42.1].into());
920        let _ = item.clone().into_flat_item(true).unwrap();
921        let _ = item.clone().into_flat_item(false).unwrap_err();
922
923        item.properties.additional_fields = Default::default();
924        let _ = item
925            .additional_fields
926            .insert("foo".to_string(), "bar".to_string().into());
927        let _ = item.clone().into_flat_item(true).unwrap();
928        let _ = item.clone().into_flat_item(false).unwrap_err();
929    }
930
931    #[test]
932    fn flat_item_without_geometry() {
933        let mut item = Item::new("an-item");
934        item.bbox = Some(vec![-105., 42., -105., -42.].try_into().unwrap());
935        let mut value = serde_json::to_value(item).unwrap();
936        let _ = value.as_object_mut().unwrap().remove("geometry").unwrap();
937        let flat_item: FlatItem = serde_json::from_value(value).unwrap();
938        assert_eq!(flat_item.geometry, None);
939    }
940
941    #[test]
942    fn permissive_deserialization() {
943        let _: Item = serde_json::from_value(json!({})).unwrap();
944    }
945
946    #[test]
947    fn has_type() {
948        let value: serde_json::Value = serde_json::to_value(Item::new("an-id")).unwrap();
949        assert_eq!(value.as_object().unwrap()["type"], "Feature");
950    }
951
952    #[test]
953    fn read_invalid_item_datetime() {
954        let _: Item = crate::read("data/invalid-item-datetime.json").unwrap();
955    }
956
957    #[test]
958    fn read_invalid_item_datetimes() {
959        let _: Item = crate::read("data/invalid-datetimes.json").unwrap();
960    }
961
962    #[test]
963    fn matches_cql2() {
964        let item = Item::new("an-item");
965        assert!(
966            item.clone()
967                .matches_cql2("id = 'an-item'".parse().unwrap())
968                .unwrap()
969        );
970        assert!(
971            !item
972                .matches_cql2("id = 'another-item'".parse().unwrap())
973                .unwrap()
974        );
975    }
976}