stac_api/
items.rs

1use crate::{Error, Fields, Filter, Result, Search, Sortby};
2use chrono::{DateTime, FixedOffset};
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use stac::{Bbox, Item};
6use std::collections::HashMap;
7
8/// Parameters for the items endpoint from STAC API - Features.
9#[derive(Clone, Default, Debug, Serialize, Deserialize)]
10pub struct Items {
11    /// The maximum number of results to return (page size).
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub limit: Option<u64>,
14
15    /// Requested bounding box.
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub bbox: Option<Bbox>,
18
19    /// Single date+time, or a range ('/' separator), formatted to [RFC 3339,
20    /// section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6).
21    ///
22    /// Use double dots `..` for open date ranges.
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub datetime: Option<String>,
25
26    /// Include/exclude fields from item collections.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub fields: Option<Fields>,
29
30    /// Fields by which to sort results.
31    #[serde(skip_serializing_if = "Vec::is_empty", default)]
32    pub sortby: Vec<Sortby>,
33
34    /// Recommended to not be passed, but server must only accept
35    /// <http://www.opengis.net/def/crs/OGC/1.3/CRS84> as a valid value, may
36    /// reject any others
37    #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
38    pub filter_crs: Option<String>,
39
40    /// CQL2 filter expression.
41    #[serde(skip_serializing_if = "Option::is_none", flatten)]
42    pub filter: Option<Filter>,
43
44    /// Additional filtering based on properties.
45    ///
46    /// It is recommended to use the filter extension instead.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub query: Option<Map<String, Value>>,
49
50    /// Additional fields.
51    #[serde(flatten)]
52    pub additional_fields: Map<String, Value>,
53}
54
55/// GET parameters for the items endpoint from STAC API - Features.
56///
57/// This is a lot like [Search](crate::Search), but without intersects, ids, and
58/// collections.
59#[derive(Clone, Default, Debug, Serialize, Deserialize)]
60pub struct GetItems {
61    /// The maximum number of results to return (page size).
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub limit: Option<String>,
64
65    /// Requested bounding box.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub bbox: Option<String>,
68
69    /// Single date+time, or a range ('/' separator), formatted to [RFC 3339,
70    /// section 5.6](https://tools.ietf.org/html/rfc3339#section-5.6).
71    ///
72    /// Use double dots `..` for open date ranges.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub datetime: Option<String>,
75
76    /// Include/exclude fields from item collections.
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub fields: Option<String>,
79
80    /// Fields by which to sort results.
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub sortby: Option<String>,
83
84    /// Recommended to not be passed, but server must only accept
85    /// <http://www.opengis.net/def/crs/OGC/1.3/CRS84> as a valid value, may
86    /// reject any others
87    #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
88    pub filter_crs: Option<String>,
89
90    /// This should always be cql2-text if present.
91    #[serde(skip_serializing_if = "Option::is_none", rename = "filter-lang")]
92    pub filter_lang: Option<String>,
93
94    /// CQL2 filter expression.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub filter: Option<String>,
97
98    /// Additional fields.
99    #[serde(flatten)]
100    pub additional_fields: HashMap<String, String>,
101}
102
103impl Items {
104    /// Runs a set of validity checks on this query and returns an error if it is invalid.
105    ///
106    /// Returns the items, unchanged, if it is valid.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use stac_api::Items;
112    ///
113    /// let items = Items::default().valid().unwrap();
114    /// ```
115    pub fn valid(self) -> Result<Items> {
116        if let Some(bbox) = self.bbox.as_ref() {
117            if !bbox.is_valid() {
118                return Err(Error::from(stac::Error::InvalidBbox((*bbox).into())));
119            }
120        }
121        if let Some(datetime) = self.datetime.as_deref() {
122            if let Some((start, end)) = datetime.split_once('/') {
123                let (start, end) = (
124                    maybe_parse_from_rfc3339(start)?,
125                    maybe_parse_from_rfc3339(end)?,
126                );
127                if let Some(start) = start {
128                    if let Some(end) = end {
129                        if end < start {
130                            return Err(Error::StartIsAfterEnd(start, end));
131                        }
132                    }
133                } else if end.is_none() {
134                    return Err(Error::EmptyDatetimeInterval);
135                }
136            } else {
137                let _ = maybe_parse_from_rfc3339(datetime)?;
138            }
139        }
140        Ok(self)
141    }
142
143    /// Returns true if this items structure matches the given item.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use stac_api::Items;
149    /// use stac::Item;
150    ///
151    /// assert!(Items::default().matches(&Item::new("an-id")).unwrap());
152    /// ```
153    pub fn matches(&self, item: &Item) -> Result<bool> {
154        Ok(self.bbox_matches(item)?
155            & self.datetime_matches(item)?
156            & self.query_matches(item)?
157            & self.filter_matches(item)?)
158    }
159
160    /// Returns true if this item's geometry matches this search's bbox.
161    ///
162    /// If **stac-api** is not built with the `geo` feature, this will return an error.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// # #[cfg(feature = "geo")]
168    /// # {
169    /// use stac_api::Search;
170    /// use stac::Item;
171    /// use geojson::{Geometry, Value};
172    ///
173    /// let mut search = Search::new();
174    /// let mut item = Item::new("item-id");
175    /// assert!(search.bbox_matches(&item).unwrap());
176    /// search.bbox = Some(vec![-110.0, 40.0, -100.0, 50.0].try_into().unwrap());
177    /// assert!(!search.bbox_matches(&item).unwrap());
178    /// item.set_geometry(Geometry::new(Value::Point(vec![-105.1, 41.1])));
179    /// assert!(search.bbox_matches(&item).unwrap());
180    /// # }
181    /// ```
182    #[allow(unused_variables)]
183    pub fn bbox_matches(&self, item: &Item) -> Result<bool> {
184        if let Some(bbox) = self.bbox.as_ref() {
185            #[cfg(feature = "geo")]
186            {
187                let bbox: geo::Rect = (*bbox).into();
188                item.intersects(&bbox).map_err(Error::from)
189            }
190            #[cfg(not(feature = "geo"))]
191            {
192                Err(Error::FeatureNotEnabled("geo"))
193            }
194        } else {
195            Ok(true)
196        }
197    }
198
199    /// Returns true if this item's datetime matches this items structure.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use stac_api::Search;
205    /// use stac::Item;
206    ///
207    /// let mut search = Search::new();
208    /// let mut item = Item::new("item-id");  // default datetime is now
209    /// assert!(search.datetime_matches(&item).unwrap());
210    /// search.datetime = Some("../2023-10-09T00:00:00Z".to_string());
211    /// assert!(!search.datetime_matches(&item).unwrap());
212    /// item.properties.datetime = Some("2023-10-08T00:00:00Z".parse().unwrap());
213    /// assert!(search.datetime_matches(&item).unwrap());
214    /// ```
215    pub fn datetime_matches(&self, item: &Item) -> Result<bool> {
216        if let Some(datetime) = self.datetime.as_ref() {
217            item.intersects_datetime_str(datetime).map_err(Error::from)
218        } else {
219            Ok(true)
220        }
221    }
222
223    /// Returns true if this item's matches this search query.
224    ///
225    /// Currently unsupported, always raises an error if query is set.
226    ///
227    /// # Examples
228    ///
229    /// ```
230    /// use stac_api::Search;
231    /// use stac::Item;
232    ///
233    /// let mut search = Search::new();
234    /// let mut item = Item::new("item-id");
235    /// assert!(search.query_matches(&item).unwrap());
236    /// search.query = Some(Default::default());
237    /// assert!(search.query_matches(&item).is_err());
238    /// ```
239    pub fn query_matches(&self, _: &Item) -> Result<bool> {
240        if self.query.as_ref().is_some() {
241            // TODO implement
242            Err(Error::Unimplemented("query"))
243        } else {
244            Ok(true)
245        }
246    }
247
248    /// Returns true if this item matches this search's filter.
249    ///
250    /// Currently unsupported, always raises an error if filter is set.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use stac_api::Search;
256    /// use stac::Item;
257    ///
258    /// let mut search = Search::new();
259    /// let mut item = Item::new("item-id");
260    /// assert!(search.filter_matches(&item).unwrap());
261    /// search.filter = Some(Default::default());
262    /// assert!(search.filter_matches(&item).is_err());
263    /// ```
264    pub fn filter_matches(&self, _: &Item) -> Result<bool> {
265        if self.filter.as_ref().is_some() {
266            // TODO implement
267            Err(Error::Unimplemented("filter"))
268        } else {
269            Ok(true)
270        }
271    }
272
273    /// Converts this items object to a search in the given collection.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use stac_api::Items;
279    /// let items = Items {
280    ///     datetime: Some("2023".to_string()),
281    ///     ..Default::default()
282    /// };
283    /// let search = items.search_collection("collection-id");
284    /// assert_eq!(search.collections, vec!["collection-id"]);
285    /// ```
286    pub fn search_collection(self, collection_id: impl ToString) -> Search {
287        Search {
288            items: self,
289            intersects: None,
290            ids: Vec::new(),
291            collections: vec![collection_id.to_string()],
292        }
293    }
294
295    /// Converts the filter to cql2-json, if it is set.
296    pub fn into_cql2_json(mut self) -> Result<Items> {
297        if let Some(filter) = self.filter {
298            self.filter = Some(filter.into_cql2_json()?);
299        }
300        Ok(self)
301    }
302}
303
304impl TryFrom<Items> for GetItems {
305    type Error = Error;
306
307    fn try_from(items: Items) -> Result<GetItems> {
308        if let Some(query) = items.query {
309            return Err(Error::CannotConvertQueryToString(query));
310        }
311        let filter = if let Some(filter) = items.filter {
312            match filter {
313                Filter::Cql2Json(json) => return Err(Error::CannotConvertCql2JsonToString(json)),
314                Filter::Cql2Text(text) => Some(text),
315            }
316        } else {
317            None
318        };
319        Ok(GetItems {
320            limit: items.limit.map(|n| n.to_string()),
321            bbox: items.bbox.map(|bbox| {
322                Vec::from(bbox)
323                    .into_iter()
324                    .map(|n| n.to_string())
325                    .collect::<Vec<_>>()
326                    .join(",")
327            }),
328            datetime: items.datetime,
329            fields: items.fields.map(|fields| fields.to_string()),
330            sortby: if items.sortby.is_empty() {
331                None
332            } else {
333                Some(
334                    items
335                        .sortby
336                        .into_iter()
337                        .map(|s| s.to_string())
338                        .collect::<Vec<_>>()
339                        .join(","),
340                )
341            },
342            filter_crs: items.filter_crs,
343            filter_lang: if filter.is_some() {
344                Some("cql2-text".to_string())
345            } else {
346                None
347            },
348            filter,
349            additional_fields: items
350                .additional_fields
351                .into_iter()
352                .map(|(key, value)| (key, value.to_string()))
353                .collect(),
354        })
355    }
356}
357
358impl TryFrom<GetItems> for Items {
359    type Error = Error;
360
361    fn try_from(get_items: GetItems) -> Result<Items> {
362        let bbox = if let Some(value) = get_items.bbox {
363            let mut bbox = Vec::new();
364            for s in value.split(',') {
365                bbox.push(s.parse()?)
366            }
367            Some(bbox.try_into()?)
368        } else {
369            None
370        };
371
372        let sortby = get_items
373            .sortby
374            .map(|s| {
375                let mut sortby = Vec::new();
376                for s in s.split(',') {
377                    sortby.push(s.parse().expect("infallible"));
378                }
379                sortby
380            })
381            .unwrap_or_default();
382
383        Ok(Items {
384            limit: get_items.limit.map(|limit| limit.parse()).transpose()?,
385            bbox,
386            datetime: get_items.datetime,
387            fields: get_items
388                .fields
389                .map(|fields| fields.parse().expect("infallible")),
390            sortby,
391            filter_crs: get_items.filter_crs,
392            filter: get_items.filter.map(Filter::Cql2Text),
393            query: None,
394            additional_fields: get_items
395                .additional_fields
396                .into_iter()
397                .map(|(key, value)| (key, Value::String(value)))
398                .collect(),
399        })
400    }
401}
402
403impl stac::Fields for Items {
404    fn fields(&self) -> &Map<String, Value> {
405        &self.additional_fields
406    }
407    fn fields_mut(&mut self) -> &mut Map<String, Value> {
408        &mut self.additional_fields
409    }
410}
411
412fn maybe_parse_from_rfc3339(s: &str) -> Result<Option<DateTime<FixedOffset>>> {
413    if s.is_empty() || s == ".." {
414        Ok(None)
415    } else {
416        DateTime::parse_from_rfc3339(s)
417            .map(Some)
418            .map_err(Error::from)
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::{GetItems, Items};
425    use crate::{sort::Direction, Fields, Filter, Sortby};
426    use serde_json::{json, Map, Value};
427    use std::collections::HashMap;
428
429    #[test]
430    fn get_items_try_from_items() {
431        let mut additional_fields = HashMap::new();
432        let _ = additional_fields.insert("token".to_string(), "foobar".to_string());
433
434        let get_items = GetItems {
435            limit: Some("42".to_string()),
436            bbox: Some("-1,-2,1,2".to_string()),
437            datetime: Some("2023".to_string()),
438            fields: Some("+foo,-bar".to_string()),
439            sortby: Some("-foo".to_string()),
440            filter_crs: None,
441            filter_lang: Some("cql2-text".to_string()),
442            filter: Some("dummy text".to_string()),
443            additional_fields,
444        };
445
446        let items: Items = get_items.try_into().unwrap();
447        assert_eq!(items.limit.unwrap(), 42);
448        assert_eq!(
449            items.bbox.unwrap(),
450            vec![-1.0, -2.0, 1.0, 2.0].try_into().unwrap()
451        );
452        assert_eq!(items.datetime.unwrap(), "2023");
453        assert_eq!(
454            items.fields.unwrap(),
455            Fields {
456                include: vec!["foo".to_string()],
457                exclude: vec!["bar".to_string()],
458            }
459        );
460        assert_eq!(
461            items.sortby,
462            vec![Sortby {
463                field: "foo".to_string(),
464                direction: Direction::Descending,
465            }]
466        );
467        assert_eq!(
468            items.filter.unwrap(),
469            Filter::Cql2Text("dummy text".to_string())
470        );
471        assert_eq!(items.additional_fields["token"], "foobar");
472    }
473
474    #[test]
475    fn items_try_from_get_items() {
476        let mut additional_fields = Map::new();
477        let _ = additional_fields.insert("token".to_string(), Value::String("foobar".to_string()));
478
479        let items = Items {
480            limit: Some(42),
481            bbox: Some(vec![-1.0, -2.0, 1.0, 2.0].try_into().unwrap()),
482            datetime: Some("2023".to_string()),
483            fields: Some(Fields {
484                include: vec!["foo".to_string()],
485                exclude: vec!["bar".to_string()],
486            }),
487            sortby: vec![Sortby {
488                field: "foo".to_string(),
489                direction: Direction::Descending,
490            }],
491            filter_crs: None,
492            filter: Some(Filter::Cql2Text("dummy text".to_string())),
493            query: None,
494            additional_fields,
495        };
496
497        let get_items: GetItems = items.try_into().unwrap();
498        assert_eq!(get_items.limit.unwrap(), "42");
499        assert_eq!(get_items.bbox.unwrap(), "-1,-2,1,2");
500        assert_eq!(get_items.datetime.unwrap(), "2023");
501        assert_eq!(get_items.fields.unwrap(), "foo,-bar");
502        assert_eq!(get_items.sortby.unwrap(), "-foo");
503        assert_eq!(get_items.filter.unwrap(), "dummy text");
504        assert_eq!(get_items.additional_fields["token"], "\"foobar\"");
505    }
506
507    #[test]
508    fn filter() {
509        let value = json!({
510            "filter": "eo:cloud_cover >= 5 AND eo:cloud_cover < 10",
511            "filter-lang": "cql2-text",
512        });
513        let items: Items = serde_json::from_value(value).unwrap();
514        assert!(items.filter.is_some());
515    }
516}