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