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#[derive(Clone, Default, Debug, Serialize, Deserialize)]
10pub struct Items {
11 #[serde(skip_serializing_if = "Option::is_none")]
13 pub limit: Option<u64>,
14
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub bbox: Option<Bbox>,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
24 pub datetime: Option<String>,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub fields: Option<Fields>,
29
30 #[serde(skip_serializing_if = "Vec::is_empty", default)]
32 pub sortby: Vec<Sortby>,
33
34 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
38 pub filter_crs: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none", flatten)]
42 pub filter: Option<Filter>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
48 pub query: Option<Map<String, Value>>,
49
50 #[serde(flatten)]
52 pub additional_fields: Map<String, Value>,
53}
54
55#[derive(Clone, Default, Debug, Serialize, Deserialize)]
60pub struct GetItems {
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub limit: Option<String>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub bbox: Option<String>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
74 pub datetime: Option<String>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub fields: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub sortby: Option<String>,
83
84 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
88 pub filter_crs: Option<String>,
89
90 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-lang")]
92 pub filter_lang: Option<String>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub filter: Option<String>,
97
98 #[serde(flatten)]
100 pub additional_fields: HashMap<String, String>,
101}
102
103impl Items {
104 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 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 #[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 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 pub fn query_matches(&self, _: &Item) -> Result<bool> {
240 if self.query.as_ref().is_some() {
241 Err(Error::Unimplemented("query"))
243 } else {
244 Ok(true)
245 }
246 }
247
248 pub fn filter_matches(&self, _: &Item) -> Result<bool> {
265 if self.filter.as_ref().is_some() {
266 Err(Error::Unimplemented("filter"))
268 } else {
269 Ok(true)
270 }
271 }
272
273 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 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}