Skip to main content

stac/api/
items.rs

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