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