1use 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#[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 #[serde(rename = "stac_version", default)]
62 pub version: Version,
63
64 #[serde(
66 rename = "stac_extensions",
67 skip_serializing_if = "Vec::is_empty",
68 default
69 )]
70 pub extensions: Vec<String>,
71
72 #[serde(default)]
76 pub id: String,
77
78 pub geometry: Option<Geometry>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
93 pub bbox: Option<Bbox>,
94
95 #[serde(default)]
97 pub properties: Properties,
98
99 #[serde(default)]
101 pub links: Vec<Link>,
102
103 #[serde(default)]
105 pub assets: IndexMap<String, Asset>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
115 pub collection: Option<String>,
116
117 #[serde(flatten)]
119 pub additional_fields: Map<String, Value>,
120
121 #[serde(skip)]
122 self_href: Option<String>,
123}
124
125#[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 #[serde(
140 rename = "stac_extensions",
141 skip_serializing_if = "Vec::is_empty",
142 default
143 )]
144 pub extensions: Vec<String>,
145
146 pub id: String,
148
149 pub geometry: Option<Geometry>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub bbox: Option<Bbox>,
162
163 pub links: Vec<Link>,
165
166 #[serde(skip_serializing_if = "IndexMap::is_empty")]
168 pub assets: IndexMap<String, Asset>,
169
170 pub collection: Option<String>,
172
173 #[serde(flatten)]
176 pub properties: Map<String, Value>,
177}
178
179#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
181pub struct Properties {
182 #[serde(default, deserialize_with = "deserialize_datetime_permissively")]
187 pub datetime: Option<DateTime<Utc>>,
188
189 #[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 #[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 #[serde(skip_serializing_if = "Option::is_none")]
223 pub title: Option<String>,
224
225 #[serde(skip_serializing_if = "Option::is_none")]
233 pub description: Option<String>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
243 pub created: Option<String>,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
253 pub updated: Option<String>,
254
255 #[serde(flatten)]
257 pub additional_fields: Map<String, Value>,
258}
259
260#[derive(Debug)]
262pub struct Builder {
263 id: String,
264 canonicalize_paths: bool,
265 assets: IndexMap<String, Asset>,
266}
267
268impl Builder {
269 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 pub fn canonicalize_paths(mut self, canonicalize_paths: bool) -> Builder {
296 self.canonicalize_paths = canonicalize_paths;
297 self
298 }
299
300 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 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 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 pub fn collection(mut self, id: impl ToString) -> Item {
392 self.collection = Some(id.to_string());
393 self
394 }
395
396 pub fn collection_link(&self) -> Option<&Link> {
408 self.links.iter().find(|link| link.is_collection())
409 }
410
411 #[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 #[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 #[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 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 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 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 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 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}