1use crate::{Backend, DEFAULT_DESCRIPTION, DEFAULT_ID, Error, Result};
2use http::Method;
3use serde::Serialize;
4use serde_json::{Map, Value, json};
5use stac::api::{
6 CollectionSearchClient, Collections, Conformance, ItemCollection, Items, Root, Search,
7 SearchClient,
8};
9use stac::{Catalog, Collection, Fields, Item, Link, Links, mime::APPLICATION_OPENAPI_3_0};
10use url::Url;
11
12#[derive(Clone, Debug)]
14pub struct Api<B: Backend> {
15 pub backend: B,
17
18 pub description: String,
20
21 pub id: String,
23
24 pub root: Url,
26}
27
28impl<B: Backend> Api<B> {
29 pub fn new(backend: B, root: &str) -> Result<Api<B>> {
40 Ok(Api {
41 backend,
42 id: DEFAULT_ID.to_string(),
43 description: DEFAULT_DESCRIPTION.to_string(),
44 root: root.parse()?,
45 })
46 }
47
48 pub fn id(mut self, id: impl ToString) -> Api<B> {
59 self.id = id.to_string();
60 self
61 }
62
63 pub fn description(mut self, description: impl ToString) -> Api<B> {
74 self.description = description.to_string();
75 self
76 }
77
78 fn url(&self, path: &str) -> Result<Url> {
79 self.root.join(path).map_err(Error::from)
80 }
81
82 pub async fn root(&self) -> Result<Root> {
95 let mut catalog = Catalog::new(&self.id, &self.description);
96 catalog.set_link(Link::root(self.root.clone()).json());
97 catalog.set_link(Link::self_(self.root.clone()).json());
98 catalog.set_link(
99 Link::new(self.url("/api")?, "service-desc")
100 .r#type(APPLICATION_OPENAPI_3_0.to_string()),
101 );
102 catalog.set_link(
103 Link::new(self.url("/api.html")?, "service-doc").r#type("text/html".to_string()),
104 );
105 catalog.set_link(Link::new(self.url("/conformance")?, "conformance").json());
106 catalog.set_link(Link::new(self.url("/collections")?, "data").json());
107 for collection in self.backend.collections().await? {
108 catalog
109 .links
110 .push(Link::child(self.url(&format!("/collections/{}", collection.id))?).json());
111 }
112 let search_url = self.url("/search")?;
113 catalog.links.push(
114 Link::new(search_url.clone(), "search")
115 .geojson()
116 .method("GET"),
117 );
118 catalog
119 .links
120 .push(Link::new(search_url, "search").geojson().method("POST"));
121 if self.backend.has_filter() {
122 catalog.links.push(
123 Link::new(
124 self.url("/queryables")?,
125 "http://www.opengis.net/def/rel/ogc/1.0/queryables",
126 )
127 .r#type("application/schema+json".to_string()),
128 );
129 }
130 Ok(Root {
131 catalog,
132 conformance: self.conformance(),
133 })
134 }
135
136 pub fn conformance(&self) -> Conformance {
147 let mut conformance = Conformance::new().ogcapi_features();
148 if self.backend.has_item_search() {
149 conformance = conformance.item_search();
150 }
151 if self.backend.has_filter() {
152 conformance = conformance.filter();
153 }
154 conformance
155 }
156
157 pub fn queryables(&self) -> Value {
159 json!({
161 "$schema" : "https://json-schema.org/draft/2019-09/schema",
162 "$id" : "https://stac-api.example.com/queryables",
163 "type" : "object",
164 "title" : "Queryables for Example STAC API",
165 "description" : "Queryable names for the example STAC API Item Search filter.",
166 "properties" : {
167 },
168 "additionalProperties": true
169 })
170 }
171
172 pub async fn collections(&self) -> Result<Collections> {
185 let mut collections: Collections = self.backend.collections().await?.into();
186 collections.set_link(Link::root(self.root.clone()).json());
187 collections.set_link(Link::self_(self.url("/collections")?).json());
188 for collection in collections.collections.iter_mut() {
189 self.set_collection_links(collection)?;
190 }
191 Ok(collections)
192 }
193
194 pub async fn collection(&self, id: &str) -> Result<Option<Collection>> {
211 match self.backend.collection(id).await? {
212 Some(mut collection) => {
213 self.set_collection_links(&mut collection)?;
214 Ok(Some(collection))
215 }
216 _ => Ok(None),
217 }
218 }
219
220 pub async fn items(&self, collection_id: &str, items: Items) -> Result<Option<ItemCollection>> {
239 if CollectionSearchClient::collection(&self.backend, collection_id)
240 .await?
241 .is_none()
242 {
243 return Ok(None);
244 }
245 let mut item_collection =
246 SearchClient::items(&self.backend, collection_id, items.clone()).await?;
247 let collection_url = self.url(&format!("/collections/{collection_id}"))?;
248 let items_url = self.url(&format!("/collections/{collection_id}/items"))?;
249 item_collection.set_link(Link::root(self.root.clone()).json());
250 item_collection.set_link(Link::self_(items_url.clone()).geojson());
251 item_collection.set_link(Link::collection(collection_url).json());
252 if let Some(next) = item_collection.next.take() {
253 item_collection.set_link(self.pagination_link(
254 items_url.clone(),
255 items.clone(),
256 next,
257 "next",
258 &Method::GET,
259 )?);
260 }
261 if let Some(prev) = item_collection.prev.take() {
262 item_collection.set_link(self.pagination_link(
263 items_url,
264 items,
265 prev,
266 "prev",
267 &Method::GET,
268 )?);
269 }
270 for item in item_collection.items.iter_mut() {
271 self.set_item_links(item)?;
272 }
273 Ok(Some(item_collection))
274 }
275
276 pub async fn item(&self, collection_id: &str, item_id: &str) -> Result<Option<Item>> {
294 match self.backend.item(collection_id, item_id).await? {
295 Some(mut item) => {
296 item.set_link(Link::root(self.root.clone()).json());
297 item.set_link(
298 Link::self_(
299 self.url(&format!("/collections/{collection_id}/items/{item_id}"))?,
300 )
301 .geojson(),
302 );
303 let collection_url = self.url(&format!("/collections/{collection_id}"))?;
304 item.set_link(Link::collection(collection_url.clone()).json());
305 item.set_link(Link::parent(collection_url).json());
306 Ok(Some(item))
307 }
308 _ => Ok(None),
309 }
310 }
311
312 pub async fn search(&self, mut search: Search, method: Method) -> Result<ItemCollection> {
327 let mut item_collection = self.backend.search(search.clone()).await?;
328 if method == Method::GET
329 && let Some(filter) = search.filter.take()
330 {
331 search.filter = Some(filter.into_cql2_text()?);
332 }
333 item_collection.set_link(Link::root(self.root.clone()).json());
334 let search_url = self.url("/search")?;
335 if let Some(next) = item_collection.next.take() {
336 tracing::debug!("adding next pagination link");
337 item_collection.set_link(self.pagination_link(
338 search_url.clone(),
339 search.clone(),
340 next,
341 "next",
342 &method,
343 )?);
344 }
345 if let Some(prev) = item_collection.prev.take() {
346 tracing::debug!("adding prev pagination link");
347 item_collection
348 .set_link(self.pagination_link(search_url, search, prev, "prev", &method)?);
349 }
350 for item in item_collection.items.iter_mut() {
351 self.set_item_links(item)?;
352 }
353 Ok(item_collection)
354 }
355
356 fn set_collection_links(&self, collection: &mut Collection) -> Result<()> {
357 collection.set_link(Link::root(self.root.clone()).json());
358 collection
359 .set_link(Link::self_(self.url(&format!("/collections/{}", collection.id))?).json());
360 collection.set_link(Link::parent(self.root.clone()).json());
361 collection.set_link(
362 Link::new(
363 self.url(&format!("/collections/{}/items", collection.id))?,
364 "items",
365 )
366 .geojson(),
367 );
368 Ok(())
369 }
370
371 fn pagination_link<D>(
372 &self,
373 mut url: Url,
374 mut data: D,
375 pagination: Map<String, Value>,
376 rel: &str,
377 method: &Method,
378 ) -> Result<Link>
379 where
380 D: Fields + Serialize,
381 {
382 for (key, value) in pagination {
383 let _ = data.set_field(key, value)?;
384 }
385 match *method {
386 Method::GET => {
387 url.set_query(Some(&serde_urlencoded::to_string(data)?));
388 Ok(Link::new(url, rel).geojson().method("GET"))
389 }
390 Method::POST => Ok(Link::new(url, rel).geojson().method("POST").body(data)?),
391 _ => unimplemented!(),
392 }
393 }
394
395 fn set_item_links(&self, item: &mut stac::api::Item) -> Result<()> {
396 let mut collection_url = None;
397 let mut item_link = None;
398 if let Some(item_id) = item.get("id").and_then(|id| id.as_str())
399 && let Some(collection_id) = item.get("collection").and_then(|id| id.as_str())
400 {
401 collection_url = Some(self.url(&format!("/collections/{collection_id}"))?);
402 item_link = Some(serde_json::to_value(
403 Link::self_(self.url(&format!("/collections/{collection_id}/items/{item_id}"))?)
404 .geojson(),
405 )?);
406 }
407 if item
408 .get("links")
409 .map(|links| !links.is_array())
410 .unwrap_or(true)
411 {
412 let _ = item.insert("links".to_string(), Value::Array(Vec::new()));
413 }
414 let links = item.get_mut("links").unwrap().as_array_mut().unwrap();
415 links.push(serde_json::to_value(Link::root(self.root.clone()).json())?);
416 if let Some(item_link) = item_link {
417 links.push(item_link);
418 }
419 if let Some(collection_url) = collection_url {
420 links.push(serde_json::to_value(
421 Link::collection(collection_url.clone()).json(),
422 )?);
423 links.push(serde_json::to_value(Link::parent(collection_url).json())?);
424 }
425 Ok(())
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::Api;
432 use crate::MemoryBackend;
433 use http::Method;
434 use stac::api::TransactionClient;
435 use stac::api::{ITEM_SEARCH_URI, Items, Search};
436 use stac::{Catalog, Collection, Item, Links};
437 use std::collections::HashSet;
438
439 macro_rules! assert_link {
440 ($link:expr_2021, $href:expr_2021, $media_type:expr_2021) => {
441 let link = $link.unwrap();
442 assert_eq!(link.href, $href);
443 assert_eq!(link.r#type.as_ref().unwrap(), $media_type);
444 };
445 }
446
447 fn test_api(backend: MemoryBackend) -> Api<MemoryBackend> {
448 Api::new(backend, "http://stac.test/")
449 .unwrap()
450 .id("an-id")
451 .description("a description")
452 }
453
454 #[tokio::test]
455 async fn root() {
456 let mut backend = MemoryBackend::new();
457 backend
458 .add_collection(Collection::new("a-collection", "A description"))
459 .await
460 .unwrap();
461 let api = test_api(backend);
462 let root = api.root().await.unwrap();
463 assert!(!root.conformance.conforms_to.is_empty());
464 let catalog: Catalog = serde_json::from_value(serde_json::to_value(root).unwrap()).unwrap();
465 assert_eq!(catalog.id, "an-id");
467 assert_eq!(catalog.description, "a description");
468 assert_link!(
469 catalog.link("root"),
470 "http://stac.test/",
471 "application/json"
472 );
473 assert_link!(
474 catalog.link("self"),
475 "http://stac.test/",
476 "application/json"
477 );
478 assert_link!(
479 catalog.link("service-desc"),
480 "http://stac.test/api",
481 "application/vnd.oai.openapi+json;version=3.0"
482 );
483 assert_link!(
484 catalog.link("service-doc"),
485 "http://stac.test/api.html",
486 "text/html"
487 );
488 assert_link!(
489 catalog.link("conformance"),
490 "http://stac.test/conformance",
491 "application/json"
492 );
493 assert_link!(
494 catalog.link("data"),
495 "http://stac.test/collections",
496 "application/json"
497 );
498 let mut methods = HashSet::new();
499 let search_links = catalog.links.iter().filter(|link| link.rel == "search");
500 for link in search_links {
501 assert_eq!(link.href, "http://stac.test/search");
502 assert_eq!(link.r#type.as_deref().unwrap(), "application/geo+json");
503 let _ = methods.insert(link.method.as_deref().unwrap());
504 }
505 assert_eq!(methods.len(), 2);
506 assert!(methods.contains("GET"));
507 assert!(methods.contains("POST"));
508
509 let children: Vec<_> = catalog.iter_child_links().collect();
510 assert_eq!(children.len(), 1);
511 let child = children[0];
512 assert_eq!(child.href, "http://stac.test/collections/a-collection");
513 assert_eq!(child.r#type.as_ref().unwrap(), "application/json");
514 }
515
516 #[tokio::test]
517 async fn conformance() {
518 let api = test_api(MemoryBackend::new());
519 let conformance = api.conformance();
520 for conformance_class in [
521 "https://api.stacspec.org/v1.0.0/core",
522 "https://api.stacspec.org/v1.0.0/ogcapi-features",
523 "https://api.stacspec.org/v1.0.0/collections",
524 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
525 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
526 ] {
527 assert!(
528 conformance
529 .conforms_to
530 .contains(&conformance_class.to_string()),
531 "{conformance_class} not in the conforms_to list"
532 );
533 }
534 }
535
536 #[tokio::test]
537 async fn collections() {
538 let mut backend = MemoryBackend::new();
539 backend
540 .add_collection(Collection::new("a-collection", "A description"))
541 .await
542 .unwrap();
543 let api = test_api(backend);
544 let collections = api.collections().await.unwrap();
545 assert_link!(
546 collections.link("root"),
547 "http://stac.test/",
548 "application/json"
549 );
550 assert_link!(
551 collections.link("self"),
552 "http://stac.test/collections",
553 "application/json"
554 );
555 assert_eq!(collections.collections.len(), 1);
556 let collection = &collections.collections[0];
557 assert_link!(
559 collection.link("root"),
560 "http://stac.test/",
561 "application/json"
562 );
563 assert_link!(
564 collection.link("self"),
565 "http://stac.test/collections/a-collection",
566 "application/json"
567 );
568 assert_link!(
569 collection.link("parent"),
570 "http://stac.test/",
571 "application/json"
572 );
573 assert_link!(
574 collection.link("items"),
575 "http://stac.test/collections/a-collection/items",
576 "application/geo+json"
577 );
578 }
579
580 #[tokio::test]
581 async fn collection() {
582 let mut backend = MemoryBackend::new();
583 backend
584 .add_collection(Collection::new("a-collection", "A description"))
585 .await
586 .unwrap();
587 let api = test_api(backend);
588 let collection = api.collection("a-collection").await.unwrap().unwrap();
589 assert_link!(
591 collection.link("root"),
592 "http://stac.test/",
593 "application/json"
594 );
595 assert_link!(
596 collection.link("self"),
597 "http://stac.test/collections/a-collection",
598 "application/json"
599 );
600 assert_link!(
601 collection.link("parent"),
602 "http://stac.test/",
603 "application/json"
604 );
605 assert_link!(
606 collection.link("items"),
607 "http://stac.test/collections/a-collection/items",
608 "application/geo+json"
609 );
610 }
611
612 #[tokio::test]
613 async fn items() {
614 let mut backend = MemoryBackend::new();
615 let api = test_api(backend.clone());
616 assert!(
617 api.items("collection-id", Items::default())
618 .await
619 .unwrap()
620 .is_none()
621 );
622
623 backend
624 .add_collection(Collection::new("collection-id", "a description"))
625 .await
626 .unwrap();
627 backend
628 .add_item(Item::new("item-a").collection("collection-id"))
629 .await
630 .unwrap();
631 let items = api
632 .items("collection-id", Items::default())
633 .await
634 .unwrap()
635 .unwrap();
636 assert_link!(items.link("root"), "http://stac.test/", "application/json");
637 assert_link!(
638 items.link("self"),
639 "http://stac.test/collections/collection-id/items",
640 "application/geo+json"
641 );
642 assert_link!(
643 items.link("collection"),
644 "http://stac.test/collections/collection-id",
645 "application/json"
646 );
647 assert_eq!(items.items.len(), 1);
648 let item: Item = items.items[0].clone().try_into().unwrap();
649 assert_link!(item.link("root"), "http://stac.test/", "application/json");
650 assert_link!(
651 item.link("self"),
652 "http://stac.test/collections/collection-id/items/item-a",
653 "application/geo+json"
654 );
655 assert_link!(
656 item.link("collection"),
657 "http://stac.test/collections/collection-id",
658 "application/json"
659 );
660 assert_link!(
661 item.link("parent"),
662 "http://stac.test/collections/collection-id",
663 "application/json"
664 );
665 }
666
667 #[tokio::test]
668 async fn items_pagination() {
669 let mut backend = MemoryBackend::new();
670 backend
671 .add_collection(Collection::new("collection-id", "a description"))
672 .await
673 .unwrap();
674 backend
675 .add_item(Item::new("item-a").collection("collection-id"))
676 .await
677 .unwrap();
678 backend
679 .add_item(Item::new("item-b").collection("collection-id"))
680 .await
681 .unwrap();
682 let api = test_api(backend);
683 let items = Items {
684 limit: Some(1),
685 ..Default::default()
686 };
687 let items = api.items("collection-id", items).await.unwrap().unwrap();
688 assert_eq!(items.items.len(), 1);
689 assert_link!(
690 items.link("next"),
691 "http://stac.test/collections/collection-id/items?limit=1&skip=1",
692 "application/geo+json"
693 );
694
695 let mut items = Items {
696 limit: Some(1),
697 ..Default::default()
698 };
699 let _ = items
700 .additional_fields
701 .insert("skip".to_string(), "1".into());
702 let items = api.items("collection-id", items).await.unwrap().unwrap();
703 assert_eq!(items.items.len(), 1);
704 assert_link!(
705 items.link("prev"),
706 "http://stac.test/collections/collection-id/items?limit=1&skip=0",
707 "application/geo+json"
708 );
709 }
710
711 #[tokio::test]
712 async fn item() {
713 let mut backend = MemoryBackend::new();
714 let api = test_api(backend.clone());
715 assert!(
716 api.item("collection-id", "item-id")
717 .await
718 .unwrap()
719 .is_none()
720 );
721
722 backend
723 .add_collection(Collection::new("collection-id", "a description"))
724 .await
725 .unwrap();
726 backend
727 .add_item(Item::new("item-id").collection("collection-id"))
728 .await
729 .unwrap();
730 let item = api.item("collection-id", "item-id").await.unwrap().unwrap();
731 assert_link!(item.link("root"), "http://stac.test/", "application/json");
732 assert_link!(
733 item.link("self"),
734 "http://stac.test/collections/collection-id/items/item-id",
735 "application/geo+json"
736 );
737 assert_link!(
738 item.link("collection"),
739 "http://stac.test/collections/collection-id",
740 "application/json"
741 );
742 assert_link!(
743 item.link("parent"),
744 "http://stac.test/collections/collection-id",
745 "application/json"
746 );
747 }
748
749 #[tokio::test]
750 async fn search() {
751 let api = test_api(MemoryBackend::new());
752 let item_collection = api.search(Search::default(), Method::GET).await.unwrap();
753 assert!(item_collection.items.is_empty());
754 assert_link!(
755 item_collection.link("root"),
756 "http://stac.test/",
757 "application/json"
758 );
759 }
760
761 #[test]
762 fn memory_item_search_conformance() {
763 let api = test_api(MemoryBackend::new());
764 let conformance = api.conformance();
765 assert!(
766 conformance
767 .conforms_to
768 .contains(&ITEM_SEARCH_URI.to_string())
769 );
770 }
771}