1use crate::{Backend, DEFAULT_DESCRIPTION, DEFAULT_ID, Error, Result};
2use http::Method;
3use serde::Serialize;
4use serde_json::{Map, Value, json};
5use stac::api::{Collections, Conformance, ItemCollection, Items, Root, Search};
6use stac::{Catalog, Collection, Fields, Item, Link, Links, mime::APPLICATION_OPENAPI_3_0};
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 && let Some(filter) = search.filter.take()
323 {
324 search.filter = Some(filter.into_cql2_text()?);
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 && let Some(collection_id) = item.get("collection").and_then(|id| id.as_str())
393 {
394 collection_url = Some(self.url(&format!("/collections/{collection_id}"))?);
395 item_link = Some(serde_json::to_value(
396 Link::self_(self.url(&format!("/collections/{collection_id}/items/{item_id}"))?)
397 .geojson(),
398 )?);
399 }
400 if item
401 .get("links")
402 .map(|links| !links.is_array())
403 .unwrap_or(true)
404 {
405 let _ = item.insert("links".to_string(), Value::Array(Vec::new()));
406 }
407 let links = item.get_mut("links").unwrap().as_array_mut().unwrap();
408 links.push(serde_json::to_value(Link::root(self.root.clone()).json())?);
409 if let Some(item_link) = item_link {
410 links.push(item_link);
411 }
412 if let Some(collection_url) = collection_url {
413 links.push(serde_json::to_value(
414 Link::collection(collection_url.clone()).json(),
415 )?);
416 links.push(serde_json::to_value(Link::parent(collection_url).json())?);
417 }
418 Ok(())
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::Api;
425 use crate::{Backend, MemoryBackend};
426 use http::Method;
427 use stac::api::{ITEM_SEARCH_URI, Items, Search};
428 use stac::{Catalog, Collection, Item, Links};
429 use std::collections::HashSet;
430
431 macro_rules! assert_link {
432 ($link:expr_2021, $href:expr_2021, $media_type:expr_2021) => {
433 let link = $link.unwrap();
434 assert_eq!(link.href, $href);
435 assert_eq!(link.r#type.as_ref().unwrap(), $media_type);
436 };
437 }
438
439 fn test_api(backend: MemoryBackend) -> Api<MemoryBackend> {
440 Api::new(backend, "http://stac.test/")
441 .unwrap()
442 .id("an-id")
443 .description("a description")
444 }
445
446 #[tokio::test]
447 async fn root() {
448 let mut backend = MemoryBackend::new();
449 backend
450 .add_collection(Collection::new("a-collection", "A description"))
451 .await
452 .unwrap();
453 let api = test_api(backend);
454 let root = api.root().await.unwrap();
455 assert!(!root.conformance.conforms_to.is_empty());
456 let catalog: Catalog = serde_json::from_value(serde_json::to_value(root).unwrap()).unwrap();
457 assert_eq!(catalog.id, "an-id");
459 assert_eq!(catalog.description, "a description");
460 assert_link!(
461 catalog.link("root"),
462 "http://stac.test/",
463 "application/json"
464 );
465 assert_link!(
466 catalog.link("self"),
467 "http://stac.test/",
468 "application/json"
469 );
470 assert_link!(
471 catalog.link("service-desc"),
472 "http://stac.test/api",
473 "application/vnd.oai.openapi+json;version=3.0"
474 );
475 assert_link!(
476 catalog.link("service-doc"),
477 "http://stac.test/api.html",
478 "text/html"
479 );
480 assert_link!(
481 catalog.link("conformance"),
482 "http://stac.test/conformance",
483 "application/json"
484 );
485 assert_link!(
486 catalog.link("data"),
487 "http://stac.test/collections",
488 "application/json"
489 );
490 let mut methods = HashSet::new();
491 let search_links = catalog.links.iter().filter(|link| link.rel == "search");
492 for link in search_links {
493 assert_eq!(link.href, "http://stac.test/search");
494 assert_eq!(link.r#type.as_deref().unwrap(), "application/geo+json");
495 let _ = methods.insert(link.method.as_deref().unwrap());
496 }
497 assert_eq!(methods.len(), 2);
498 assert!(methods.contains("GET"));
499 assert!(methods.contains("POST"));
500
501 let children: Vec<_> = catalog.iter_child_links().collect();
502 assert_eq!(children.len(), 1);
503 let child = children[0];
504 assert_eq!(child.href, "http://stac.test/collections/a-collection");
505 assert_eq!(child.r#type.as_ref().unwrap(), "application/json");
506 }
507
508 #[tokio::test]
509 async fn conformance() {
510 let api = test_api(MemoryBackend::new());
511 let conformance = api.conformance();
512 for conformance_class in [
513 "https://api.stacspec.org/v1.0.0/core",
514 "https://api.stacspec.org/v1.0.0/ogcapi-features",
515 "https://api.stacspec.org/v1.0.0/collections",
516 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
517 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
518 ] {
519 assert!(
520 conformance
521 .conforms_to
522 .contains(&conformance_class.to_string()),
523 "{conformance_class} not in the conforms_to list"
524 );
525 }
526 }
527
528 #[tokio::test]
529 async fn collections() {
530 let mut backend = MemoryBackend::new();
531 backend
532 .add_collection(Collection::new("a-collection", "A description"))
533 .await
534 .unwrap();
535 let api = test_api(backend);
536 let collections = api.collections().await.unwrap();
537 assert_link!(
538 collections.link("root"),
539 "http://stac.test/",
540 "application/json"
541 );
542 assert_link!(
543 collections.link("self"),
544 "http://stac.test/collections",
545 "application/json"
546 );
547 assert_eq!(collections.collections.len(), 1);
548 let collection = &collections.collections[0];
549 assert_link!(
551 collection.link("root"),
552 "http://stac.test/",
553 "application/json"
554 );
555 assert_link!(
556 collection.link("self"),
557 "http://stac.test/collections/a-collection",
558 "application/json"
559 );
560 assert_link!(
561 collection.link("parent"),
562 "http://stac.test/",
563 "application/json"
564 );
565 assert_link!(
566 collection.link("items"),
567 "http://stac.test/collections/a-collection/items",
568 "application/geo+json"
569 );
570 }
571
572 #[tokio::test]
573 async fn collection() {
574 let mut backend = MemoryBackend::new();
575 backend
576 .add_collection(Collection::new("a-collection", "A description"))
577 .await
578 .unwrap();
579 let api = test_api(backend);
580 let collection = api.collection("a-collection").await.unwrap().unwrap();
581 assert_link!(
583 collection.link("root"),
584 "http://stac.test/",
585 "application/json"
586 );
587 assert_link!(
588 collection.link("self"),
589 "http://stac.test/collections/a-collection",
590 "application/json"
591 );
592 assert_link!(
593 collection.link("parent"),
594 "http://stac.test/",
595 "application/json"
596 );
597 assert_link!(
598 collection.link("items"),
599 "http://stac.test/collections/a-collection/items",
600 "application/geo+json"
601 );
602 }
603
604 #[tokio::test]
605 async fn items() {
606 let mut backend = MemoryBackend::new();
607 let api = test_api(backend.clone());
608 assert!(
609 api.items("collection-id", Items::default())
610 .await
611 .unwrap()
612 .is_none()
613 );
614
615 backend
616 .add_collection(Collection::new("collection-id", "a description"))
617 .await
618 .unwrap();
619 backend
620 .add_item(Item::new("item-a").collection("collection-id"))
621 .await
622 .unwrap();
623 let items = api
624 .items("collection-id", Items::default())
625 .await
626 .unwrap()
627 .unwrap();
628 assert_link!(items.link("root"), "http://stac.test/", "application/json");
629 assert_link!(
630 items.link("self"),
631 "http://stac.test/collections/collection-id/items",
632 "application/geo+json"
633 );
634 assert_link!(
635 items.link("collection"),
636 "http://stac.test/collections/collection-id",
637 "application/json"
638 );
639 assert_eq!(items.items.len(), 1);
640 let item: Item = items.items[0].clone().try_into().unwrap();
641 assert_link!(item.link("root"), "http://stac.test/", "application/json");
642 assert_link!(
643 item.link("self"),
644 "http://stac.test/collections/collection-id/items/item-a",
645 "application/geo+json"
646 );
647 assert_link!(
648 item.link("collection"),
649 "http://stac.test/collections/collection-id",
650 "application/json"
651 );
652 assert_link!(
653 item.link("parent"),
654 "http://stac.test/collections/collection-id",
655 "application/json"
656 );
657 }
658
659 #[tokio::test]
660 async fn items_pagination() {
661 let mut backend = MemoryBackend::new();
662 backend
663 .add_collection(Collection::new("collection-id", "a description"))
664 .await
665 .unwrap();
666 backend
667 .add_item(Item::new("item-a").collection("collection-id"))
668 .await
669 .unwrap();
670 backend
671 .add_item(Item::new("item-b").collection("collection-id"))
672 .await
673 .unwrap();
674 let api = test_api(backend);
675 let items = Items {
676 limit: Some(1),
677 ..Default::default()
678 };
679 let items = api.items("collection-id", items).await.unwrap().unwrap();
680 assert_eq!(items.items.len(), 1);
681 assert_link!(
682 items.link("next"),
683 "http://stac.test/collections/collection-id/items?limit=1&skip=1",
684 "application/geo+json"
685 );
686
687 let mut items = Items {
688 limit: Some(1),
689 ..Default::default()
690 };
691 let _ = items
692 .additional_fields
693 .insert("skip".to_string(), "1".into());
694 let items = api.items("collection-id", items).await.unwrap().unwrap();
695 assert_eq!(items.items.len(), 1);
696 assert_link!(
697 items.link("prev"),
698 "http://stac.test/collections/collection-id/items?limit=1&skip=0",
699 "application/geo+json"
700 );
701 }
702
703 #[tokio::test]
704 async fn item() {
705 let mut backend = MemoryBackend::new();
706 let api = test_api(backend.clone());
707 assert!(
708 api.item("collection-id", "item-id")
709 .await
710 .unwrap()
711 .is_none()
712 );
713
714 backend
715 .add_collection(Collection::new("collection-id", "a description"))
716 .await
717 .unwrap();
718 backend
719 .add_item(Item::new("item-id").collection("collection-id"))
720 .await
721 .unwrap();
722 let item = api.item("collection-id", "item-id").await.unwrap().unwrap();
723 assert_link!(item.link("root"), "http://stac.test/", "application/json");
724 assert_link!(
725 item.link("self"),
726 "http://stac.test/collections/collection-id/items/item-id",
727 "application/geo+json"
728 );
729 assert_link!(
730 item.link("collection"),
731 "http://stac.test/collections/collection-id",
732 "application/json"
733 );
734 assert_link!(
735 item.link("parent"),
736 "http://stac.test/collections/collection-id",
737 "application/json"
738 );
739 }
740
741 #[tokio::test]
742 async fn search() {
743 let api = test_api(MemoryBackend::new());
744 let item_collection = api.search(Search::default(), Method::GET).await.unwrap();
745 assert!(item_collection.items.is_empty());
746 assert_link!(
747 item_collection.link("root"),
748 "http://stac.test/",
749 "application/json"
750 );
751 }
752
753 #[test]
754 fn memory_item_search_conformance() {
755 let api = test_api(MemoryBackend::new());
756 let conformance = api.conformance();
757 assert!(
758 conformance
759 .conforms_to
760 .contains(&ITEM_SEARCH_URI.to_string())
761 );
762 }
763}