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#[derive(Clone, Default, Debug, Serialize, Deserialize)]
11pub struct Items {
12 #[serde(skip_serializing_if = "Option::is_none")]
14 pub limit: Option<u64>,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub bbox: Option<Bbox>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
25 pub datetime: Option<String>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
29 pub fields: Option<Fields>,
30
31 #[serde(skip_serializing_if = "Vec::is_empty", default)]
33 pub sortby: Vec<Sortby>,
34
35 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
39 pub filter_crs: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none", flatten)]
43 pub filter: Option<Filter>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
49 pub query: Option<Map<String, Value>>,
50
51 #[serde(flatten)]
53 pub additional_fields: Map<String, Value>,
54}
55
56#[derive(Clone, Default, Debug, Serialize, Deserialize)]
61pub struct GetItems {
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub limit: Option<String>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub bbox: Option<String>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
75 pub datetime: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub fields: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub sortby: Option<String>,
84
85 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-crs")]
89 pub filter_crs: Option<String>,
90
91 #[serde(skip_serializing_if = "Option::is_none", rename = "filter-lang")]
93 pub filter_lang: Option<String>,
94
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub filter: Option<String>,
98
99 #[serde(flatten)]
101 pub additional_fields: IndexMap<String, String>,
102}
103
104impl Items {
105 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 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 #[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 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 pub fn query_matches(&self, _: &Item) -> Result<bool> {
241 if self.query.as_ref().is_some() {
242 Err(Error::Unimplemented("query"))
244 } else {
245 Ok(true)
246 }
247 }
248
249 pub fn filter_matches(&self, _: &Item) -> Result<bool> {
266 if self.filter.as_ref().is_some() {
267 Err(Error::Unimplemented("filter"))
269 } else {
270 Ok(true)
271 }
272 }
273
274 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 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}