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