Skip to main content

stac_server/
api.rs

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/// A STAC server API.
12#[derive(Clone, Debug)]
13pub struct Api<B: Backend> {
14    /// The backend storage for this API.
15    pub backend: B,
16
17    /// The text description of this API.
18    pub description: String,
19
20    /// The catalog id of this API.
21    pub id: String,
22
23    /// The root url of this API.
24    pub root: Url,
25}
26
27impl<B: Backend> Api<B> {
28    /// Creates a new API with the given backend.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use stac_server::{Api, MemoryBackend};
34    ///
35    /// let backend = MemoryBackend::new();
36    /// let api = Api::new(backend, "http://stac.test").unwrap();
37    /// ```
38    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    /// Sets this API's id.
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// use stac_server::{Api, MemoryBackend};
53    ///
54    /// let backend = MemoryBackend::new();
55    /// let api = Api::new(backend, "http://stac.test").unwrap().id("an-id");
56    /// ```
57    pub fn id(mut self, id: impl ToString) -> Api<B> {
58        self.id = id.to_string();
59        self
60    }
61
62    /// Sets this API's description.
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use stac_server::{Api, MemoryBackend};
68    ///
69    /// let backend = MemoryBackend::new();
70    /// let api = Api::new(backend, "http://stac.test").unwrap().description("a description");
71    /// ```
72    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    /// Returns the root of the API.
82    ///
83    /// # Examples
84    ///
85    /// ```
86    /// use stac_server::{Api, MemoryBackend};
87    ///
88    /// let api = Api::new(MemoryBackend::new(), "http://stac.test").unwrap();
89    /// # tokio_test::block_on(async {
90    /// let root = api.root().await.unwrap();
91    /// # })
92    /// ```
93    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    /// Returns the conformance classes.
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use stac_server::{Api, MemoryBackend};
141    ///
142    /// let api = Api::new(MemoryBackend::new(), "http://stac.test").unwrap();
143    /// let conformance = api.conformance();
144    /// ```
145    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    /// Returns queryables.
157    pub fn queryables(&self) -> Value {
158        // This is a pure punt from https://github.com/stac-api-extensions/filter?tab=readme-ov-file#queryables
159        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    /// Returns the collections from the backend.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use stac_server::{Api, MemoryBackend};
177    ///
178    /// let api = Api::new(MemoryBackend::new(), "http://stac.test").unwrap();
179    /// # tokio_test::block_on(async {
180    /// let collections = api.collections().await.unwrap();
181    /// # })
182    /// ```
183    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    /// Returns the collections from the backend.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use stac_server::{Api, MemoryBackend};
199    /// use stac::Collection;
200    /// use stac::api::TransactionClient;
201    ///
202    /// let mut backend = MemoryBackend::new();
203    /// # tokio_test::block_on(async {
204    /// backend.add_collection(Collection::new("an-id", "a description")).await.unwrap();
205    /// let api = Api::new(backend, "http://stac.test").unwrap();
206    /// let collection = api.collection("an-id").await.unwrap().unwrap();
207    /// # })
208    /// ```
209    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    /// Returns all items for a given collection.
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use stac_server::{Api, MemoryBackend};
225    /// use stac::{Collection, Item};
226    /// use stac::api::{Items, TransactionClient};
227    ///
228    /// let mut backend = MemoryBackend::new();
229    /// # tokio_test::block_on(async {
230    /// backend.add_collection(Collection::new("collection-id", "a description")).await.unwrap();
231    /// backend.add_item(Item::new("item-id").collection("collection-id")).await.unwrap();
232    /// let api = Api::new(backend, "http://stac.test").unwrap();
233    /// let items = api.items("collection-id", Items::default()).await.unwrap().unwrap();
234    /// assert_eq!(items.items.len(), 1);
235    /// # })
236    /// ```
237    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    /// Returns an item.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use stac_server::{Api, MemoryBackend};
281    /// use stac::{Collection, Item};
282    /// use stac::api::{Items, TransactionClient};
283    ///
284    /// let mut backend = MemoryBackend::new();
285    /// # tokio_test::block_on(async {
286    /// backend.add_collection(Collection::new("collection-id", "a description")).await.unwrap();
287    /// backend.add_item(Item::new("item-id").collection("collection-id")).await.unwrap();
288    /// let api = Api::new(backend, "http://stac.test").unwrap();
289    /// let item = api.item("collection-id", "item-id").await.unwrap().unwrap();
290    /// # })
291    /// ```
292    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    /// Searches the API.
312    ///
313    /// # Examples
314    ///
315    /// ```
316    /// use stac::api::Search;
317    /// use stac_server::{Api, MemoryBackend};
318    /// use http::Method;
319    ///
320    /// let api = Api::new(MemoryBackend::new(), "http://stac.test").unwrap();
321    /// # tokio_test::block_on(async {
322    /// let item_collection = api.search(Search::default(), Method::GET).await.unwrap();
323    /// # })
324    /// ```
325    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        // catalog.validate().await.unwrap();
465        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        // collection.validate().await.unwrap();
557        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        // collection.validate().await.unwrap();
589        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}