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