stac_api/
item_collection.rs

1use crate::{Item, Result};
2use serde::{Deserialize, Deserializer, Serialize};
3use serde_json::{Map, Value};
4use stac::Link;
5use stac_derive::{Links, SelfHref};
6
7const ITEM_COLLECTION_TYPE: &str = "FeatureCollection";
8
9fn item_collection_type() -> String {
10    ITEM_COLLECTION_TYPE.to_string()
11}
12
13fn deserialize_item_collection_type<'de, D>(
14    deserializer: D,
15) -> std::result::Result<String, D::Error>
16where
17    D: Deserializer<'de>,
18{
19    let r#type = String::deserialize(deserializer)?;
20    if r#type != ITEM_COLLECTION_TYPE {
21        Err(serde::de::Error::invalid_value(
22            serde::de::Unexpected::Str(&r#type),
23            &ITEM_COLLECTION_TYPE,
24        ))
25    } else {
26        Ok(r#type)
27    }
28}
29
30/// The return value of the `/items` and `/search` endpoints.
31///
32/// This might be a [stac::ItemCollection], but if the [fields
33/// extension](https://github.com/stac-api-extensions/fields) is used, it might
34/// not be. Defined by the [itemcollection
35/// fragment](https://github.com/radiantearth/stac-api-spec/blob/main/fragments/itemcollection/README.md).
36#[derive(Debug, Serialize, Deserialize, Default, Links, SelfHref)]
37pub struct ItemCollection {
38    #[serde(
39        default = "item_collection_type",
40        deserialize_with = "deserialize_item_collection_type"
41    )]
42    r#type: String,
43
44    /// A possibly-empty array of Item objects.
45    #[serde(rename = "features")]
46    pub items: Vec<Item>,
47
48    /// An array of Links related to this ItemCollection.
49    pub links: Vec<Link>,
50
51    /// The number of Items that meet the selection parameters, possibly estimated.
52    #[serde(skip_serializing_if = "Option::is_none", rename = "numberMatched")]
53    pub number_matched: Option<u64>,
54
55    /// The number of Items in the features array.
56    #[serde(skip_serializing_if = "Option::is_none", rename = "numberReturned")]
57    pub number_returned: Option<u64>,
58
59    /// The search-related metadata for the [ItemCollection].
60    ///
61    /// Part of the [context extension](https://github.com/stac-api-extensions/context).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub context: Option<Context>,
64
65    /// Additional fields.
66    #[serde(flatten)]
67    pub additional_fields: Map<String, Value>,
68
69    /// Optional pagination information for the next page.
70    ///
71    /// This is not part of the specification, but can be used to hold arbitrary
72    /// pagination information (tokens) to later be turned into links.
73    #[serde(skip)]
74    pub next: Option<Map<String, Value>>,
75
76    /// Optional pagination information for the previous page.
77    ///
78    /// This is not part of the specification, but can be used to hold arbitrary
79    /// pagination information (tokens) to later be turned into links.
80    #[serde(skip)]
81    pub prev: Option<Map<String, Value>>,
82
83    /// Optional pagination information for the first page.
84    ///
85    /// This is not part of the specification, but can be used to hold arbitrary
86    /// pagination information (tokens) to later be turned into links.
87    #[serde(skip)]
88    pub first: Option<Map<String, Value>>,
89
90    /// Optional pagination information for the last page.
91    ///
92    /// This is not part of the specification, but can be used to hold arbitrary
93    /// pagination information (tokens) to later be turned into links.
94    #[serde(skip)]
95    pub last: Option<Map<String, Value>>,
96
97    #[serde(skip)]
98    self_href: Option<String>,
99}
100
101/// The search-related metadata for the [ItemCollection].
102///
103/// Part of the [context extension](https://github.com/stac-api-extensions/context).
104#[derive(Debug, Serialize, Deserialize)]
105pub struct Context {
106    /// The count of results returned by this response. Equal to the cardinality
107    /// of features array.
108    pub returned: u64,
109
110    /// The maximum number of results to which the result was limited.
111    pub limit: Option<u64>,
112
113    /// The count of total number of results that match for this query, possibly
114    /// estimated, particularly in the context of NoSQL data stores.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub matched: Option<u64>,
117
118    /// Additional fields.
119    #[serde(flatten)]
120    pub additional_fields: Map<String, Value>,
121}
122
123impl ItemCollection {
124    /// Creates a new [ItemCollection] from a vector of items.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// let item: stac_api::Item = stac::Item::new("an-id").try_into().unwrap();
130    /// let item_collection = stac_api::ItemCollection::new(vec![item]).unwrap();
131    /// ```
132    pub fn new(items: Vec<Item>) -> Result<ItemCollection> {
133        let number_returned = items.len();
134        Ok(ItemCollection {
135            r#type: item_collection_type(),
136            items,
137            links: Vec::new(),
138            number_matched: None,
139            number_returned: Some(number_returned.try_into()?),
140            context: None,
141            additional_fields: Map::new(),
142            next: None,
143            prev: None,
144            first: None,
145            last: None,
146            self_href: None,
147        })
148    }
149}
150
151impl From<Vec<Item>> for ItemCollection {
152    fn from(items: Vec<Item>) -> Self {
153        ItemCollection {
154            r#type: item_collection_type(),
155            items,
156            ..Default::default()
157        }
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::ItemCollection;
164
165    #[test]
166    fn serialize_type_field() {
167        let item_collection = ItemCollection::from(vec![]);
168        let value = serde_json::to_value(item_collection).unwrap();
169        assert_eq!(value.as_object().unwrap()["type"], "FeatureCollection");
170    }
171}