1use crate::{Backend, Error, Result, DEFAULT_DESCRIPTION, DEFAULT_ID};
2use http::Method;
3use serde::Serialize;
4use serde_json::{json, Map, Value};
5use stac::{mime::APPLICATION_OPENAPI_3_0, Catalog, Collection, Fields, Item, Link, Links};
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 if let Some(mut collection) = self.backend.collection(id).await? {
208 self.set_collection_links(&mut collection)?;
209 Ok(Some(collection))
210 } else {
211 Ok(None)
212 }
213 }
214
215 pub async fn items(&self, collection_id: &str, items: Items) -> Result<Option<ItemCollection>> {
234 if let Some(mut item_collection) = self.backend.items(collection_id, items.clone()).await? {
235 let collection_url = self.url(&format!("/collections/{}", collection_id))?;
236 let items_url = self.url(&format!("/collections/{}/items", collection_id))?;
237 item_collection.set_link(Link::root(self.root.clone()).json());
238 item_collection.set_link(Link::self_(items_url.clone()).geojson());
239 item_collection.set_link(Link::collection(collection_url).json());
240 if let Some(next) = item_collection.next.take() {
241 item_collection.set_link(self.pagination_link(
242 items_url.clone(),
243 items.clone(),
244 next,
245 "next",
246 &Method::GET,
247 )?);
248 }
249 if let Some(prev) = item_collection.prev.take() {
250 item_collection.set_link(self.pagination_link(
251 items_url,
252 items,
253 prev,
254 "prev",
255 &Method::GET,
256 )?);
257 }
258 for item in item_collection.items.iter_mut() {
259 self.set_item_links(item)?;
260 }
261 Ok(Some(item_collection))
262 } else {
263 Ok(None)
264 }
265 }
266
267 pub async fn item(&self, collection_id: &str, item_id: &str) -> Result<Option<Item>> {
285 if let Some(mut item) = self.backend.item(collection_id, item_id).await? {
286 item.set_link(Link::root(self.root.clone()).json());
287 item.set_link(
288 Link::self_(
289 self.url(&format!("/collections/{}/items/{}", collection_id, item_id))?,
290 )
291 .geojson(),
292 );
293 let collection_url = self.url(&format!("/collections/{}", collection_id))?;
294 item.set_link(Link::collection(collection_url.clone()).json());
295 item.set_link(Link::parent(collection_url).json());
296 Ok(Some(item))
297 } else {
298 Ok(None)
299 }
300 }
301
302 pub async fn search(&self, mut search: Search, method: Method) -> Result<ItemCollection> {
317 let mut item_collection = self.backend.search(search.clone()).await?;
318 if method == Method::GET {
319 if let Some(filter) = search.filter.take() {
320 search.filter = Some(filter.into_cql2_text()?);
321 }
322 }
323 item_collection.set_link(Link::root(self.root.clone()).json());
324 let search_url = self.url("/search")?;
325 if let Some(next) = item_collection.next.take() {
326 tracing::debug!("adding next pagination link");
327 item_collection.set_link(self.pagination_link(
328 search_url.clone(),
329 search.clone(),
330 next,
331 "next",
332 &method,
333 )?);
334 }
335 if let Some(prev) = item_collection.prev.take() {
336 tracing::debug!("adding prev pagination link");
337 item_collection
338 .set_link(self.pagination_link(search_url, search, prev, "prev", &method)?);
339 }
340 for item in item_collection.items.iter_mut() {
341 self.set_item_links(item)?;
342 }
343 Ok(item_collection)
344 }
345
346 fn set_collection_links(&self, collection: &mut Collection) -> Result<()> {
347 collection.set_link(Link::root(self.root.clone()).json());
348 collection
349 .set_link(Link::self_(self.url(&format!("/collections/{}", collection.id))?).json());
350 collection.set_link(Link::parent(self.root.clone()).json());
351 collection.set_link(
352 Link::new(
353 self.url(&format!("/collections/{}/items", collection.id))?,
354 "items",
355 )
356 .geojson(),
357 );
358 Ok(())
359 }
360
361 fn pagination_link<D>(
362 &self,
363 mut url: Url,
364 mut data: D,
365 pagination: Map<String, Value>,
366 rel: &str,
367 method: &Method,
368 ) -> Result<Link>
369 where
370 D: Fields + Serialize,
371 {
372 for (key, value) in pagination {
373 let _ = data.set_field(key, value)?;
374 }
375 match *method {
376 Method::GET => {
377 url.set_query(Some(&serde_urlencoded::to_string(data)?));
378 Ok(Link::new(url, rel).geojson().method("GET"))
379 }
380 Method::POST => Ok(Link::new(url, rel).geojson().method("POST").body(data)?),
381 _ => unimplemented!(),
382 }
383 }
384
385 fn set_item_links(&self, item: &mut stac_api::Item) -> Result<()> {
386 let mut collection_url = None;
387 let mut item_link = None;
388 if let Some(item_id) = item.get("id").and_then(|id| id.as_str()) {
389 if let Some(collection_id) = item.get("collection").and_then(|id| id.as_str()) {
390 collection_url = Some(self.url(&format!("/collections/{}", collection_id))?);
391 item_link = Some(serde_json::to_value(
392 Link::self_(
393 self.url(&format!("/collections/{}/items/{}", collection_id, item_id))?,
394 )
395 .geojson(),
396 )?);
397 }
398 }
399 if item
400 .get("links")
401 .map(|links| !links.is_array())
402 .unwrap_or(true)
403 {
404 let _ = item.insert("links".to_string(), Value::Array(Vec::new()));
405 }
406 let links = item.get_mut("links").unwrap().as_array_mut().unwrap();
407 links.push(serde_json::to_value(Link::root(self.root.clone()).json())?);
408 if let Some(item_link) = item_link {
409 links.push(item_link);
410 }
411 if let Some(collection_url) = collection_url {
412 links.push(serde_json::to_value(
413 Link::collection(collection_url.clone()).json(),
414 )?);
415 links.push(serde_json::to_value(Link::parent(collection_url).json())?);
416 }
417 Ok(())
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::Api;
424 use crate::{Backend, MemoryBackend};
425 use http::Method;
426 use stac::{Catalog, Collection, Item, Links};
427 use stac_api::{Items, Search, ITEM_SEARCH_URI};
428 use std::collections::HashSet;
429
430 macro_rules! assert_link {
431 ($link:expr, $href:expr, $media_type:expr) => {
432 let link = $link.unwrap();
433 assert_eq!(link.href, $href);
434 assert_eq!(link.r#type.as_ref().unwrap(), $media_type);
435 };
436 }
437
438 fn test_api(backend: MemoryBackend) -> Api<MemoryBackend> {
439 Api::new(backend, "http://stac.test/")
440 .unwrap()
441 .id("an-id")
442 .description("a description")
443 }
444
445 #[tokio::test]
446 async fn root() {
447 let mut backend = MemoryBackend::new();
448 backend
449 .add_collection(Collection::new("a-collection", "A description"))
450 .await
451 .unwrap();
452 let api = test_api(backend);
453 let root = api.root().await.unwrap();
454 assert!(!root.conformance.conforms_to.is_empty());
455 let catalog: Catalog = serde_json::from_value(serde_json::to_value(root).unwrap()).unwrap();
456 assert_eq!(catalog.id, "an-id");
458 assert_eq!(catalog.description, "a description");
459 assert_link!(
460 catalog.link("root"),
461 "http://stac.test/",
462 "application/json"
463 );
464 assert_link!(
465 catalog.link("self"),
466 "http://stac.test/",
467 "application/json"
468 );
469 assert_link!(
470 catalog.link("service-desc"),
471 "http://stac.test/api",
472 "application/vnd.oai.openapi+json;version=3.0"
473 );
474 assert_link!(
475 catalog.link("service-doc"),
476 "http://stac.test/api.html",
477 "text/html"
478 );
479 assert_link!(
480 catalog.link("conformance"),
481 "http://stac.test/conformance",
482 "application/json"
483 );
484 assert_link!(
485 catalog.link("data"),
486 "http://stac.test/collections",
487 "application/json"
488 );
489 let mut methods = HashSet::new();
490 let search_links = catalog.links.iter().filter(|link| link.rel == "search");
491 for link in search_links {
492 assert_eq!(link.href, "http://stac.test/search");
493 assert_eq!(link.r#type.as_deref().unwrap(), "application/geo+json");
494 let _ = methods.insert(link.method.as_deref().unwrap());
495 }
496 assert_eq!(methods.len(), 2);
497 assert!(methods.contains("GET"));
498 assert!(methods.contains("POST"));
499
500 let children: Vec<_> = catalog.iter_child_links().collect();
501 assert_eq!(children.len(), 1);
502 let child = children[0];
503 assert_eq!(child.href, "http://stac.test/collections/a-collection");
504 assert_eq!(child.r#type.as_ref().unwrap(), "application/json");
505 }
506
507 #[tokio::test]
508 async fn conformance() {
509 let api = test_api(MemoryBackend::new());
510 let conformance = api.conformance();
511 for conformance_class in [
512 "https://api.stacspec.org/v1.0.0/core",
513 "https://api.stacspec.org/v1.0.0/ogcapi-features",
514 "https://api.stacspec.org/v1.0.0/collections",
515 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson",
516 "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core",
517 ] {
518 assert!(
519 conformance
520 .conforms_to
521 .contains(&conformance_class.to_string()),
522 "{} not in the conforms_to list",
523 conformance_class
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!(api
609 .items("collection-id", Items::default())
610 .await
611 .unwrap()
612 .is_none());
613
614 backend
615 .add_collection(Collection::new("collection-id", "a description"))
616 .await
617 .unwrap();
618 backend
619 .add_item(Item::new("item-a").collection("collection-id"))
620 .await
621 .unwrap();
622 let items = api
623 .items("collection-id", Items::default())
624 .await
625 .unwrap()
626 .unwrap();
627 assert_link!(items.link("root"), "http://stac.test/", "application/json");
628 assert_link!(
629 items.link("self"),
630 "http://stac.test/collections/collection-id/items",
631 "application/geo+json"
632 );
633 assert_link!(
634 items.link("collection"),
635 "http://stac.test/collections/collection-id",
636 "application/json"
637 );
638 assert_eq!(items.items.len(), 1);
639 let item: Item = items.items[0].clone().try_into().unwrap();
640 assert_link!(item.link("root"), "http://stac.test/", "application/json");
641 assert_link!(
642 item.link("self"),
643 "http://stac.test/collections/collection-id/items/item-a",
644 "application/geo+json"
645 );
646 assert_link!(
647 item.link("collection"),
648 "http://stac.test/collections/collection-id",
649 "application/json"
650 );
651 assert_link!(
652 item.link("parent"),
653 "http://stac.test/collections/collection-id",
654 "application/json"
655 );
656 }
657
658 #[tokio::test]
659 async fn items_pagination() {
660 let mut backend = MemoryBackend::new();
661 backend
662 .add_collection(Collection::new("collection-id", "a description"))
663 .await
664 .unwrap();
665 backend
666 .add_item(Item::new("item-a").collection("collection-id"))
667 .await
668 .unwrap();
669 backend
670 .add_item(Item::new("item-b").collection("collection-id"))
671 .await
672 .unwrap();
673 let api = test_api(backend);
674 let items = Items {
675 limit: Some(1),
676 ..Default::default()
677 };
678 let items = api.items("collection-id", items).await.unwrap().unwrap();
679 assert_eq!(items.items.len(), 1);
680 assert_link!(
681 items.link("next"),
682 "http://stac.test/collections/collection-id/items?limit=1&skip=1",
683 "application/geo+json"
684 );
685
686 let mut items = Items {
687 limit: Some(1),
688 ..Default::default()
689 };
690 let _ = items
691 .additional_fields
692 .insert("skip".to_string(), "1".into());
693 let items = api.items("collection-id", items).await.unwrap().unwrap();
694 assert_eq!(items.items.len(), 1);
695 assert_link!(
696 items.link("prev"),
697 "http://stac.test/collections/collection-id/items?limit=1&skip=0",
698 "application/geo+json"
699 );
700 }
701
702 #[tokio::test]
703 async fn item() {
704 let mut backend = MemoryBackend::new();
705 let api = test_api(backend.clone());
706 assert!(api
707 .item("collection-id", "item-id")
708 .await
709 .unwrap()
710 .is_none());
711
712 backend
713 .add_collection(Collection::new("collection-id", "a description"))
714 .await
715 .unwrap();
716 backend
717 .add_item(Item::new("item-id").collection("collection-id"))
718 .await
719 .unwrap();
720 let item = api.item("collection-id", "item-id").await.unwrap().unwrap();
721 assert_link!(item.link("root"), "http://stac.test/", "application/json");
722 assert_link!(
723 item.link("self"),
724 "http://stac.test/collections/collection-id/items/item-id",
725 "application/geo+json"
726 );
727 assert_link!(
728 item.link("collection"),
729 "http://stac.test/collections/collection-id",
730 "application/json"
731 );
732 assert_link!(
733 item.link("parent"),
734 "http://stac.test/collections/collection-id",
735 "application/json"
736 );
737 }
738
739 #[tokio::test]
740 async fn search() {
741 let api = test_api(MemoryBackend::new());
742 let item_collection = api.search(Search::default(), Method::GET).await.unwrap();
743 assert!(item_collection.items.is_empty());
744 assert_link!(
745 item_collection.link("root"),
746 "http://stac.test/",
747 "application/json"
748 );
749 }
750
751 #[test]
752 fn memory_item_search_conformance() {
753 let api = test_api(MemoryBackend::new());
754 let conformance = api.conformance();
755 assert!(conformance
756 .conforms_to
757 .contains(&ITEM_SEARCH_URI.to_string()));
758 }
759}