stac_server/
api.rs

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/// 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        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    /// Returns all items for a given collection.
216    ///
217    /// # Examples
218    ///
219    /// ```
220    /// use stac_server::{Api, MemoryBackend, Backend};
221    /// use stac::{Collection, Item};
222    /// use stac_api::Items;
223    ///
224    /// let mut backend = MemoryBackend::new();
225    /// # tokio_test::block_on(async {
226    /// backend.add_collection(Collection::new("collection-id", "a description")).await.unwrap();
227    /// backend.add_item(Item::new("item-id").collection("collection-id")).await.unwrap();
228    /// let api = Api::new(backend, "http://stac.test").unwrap();
229    /// let items = api.items("collection-id", Items::default()).await.unwrap().unwrap();
230    /// assert_eq!(items.items.len(), 1);
231    /// # })
232    /// ```
233    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    /// Returns an item.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// use stac_server::{Api, MemoryBackend, Backend};
273    /// use stac::{Collection, Item};
274    /// use stac_api::Items;
275    ///
276    /// let mut backend = MemoryBackend::new();
277    /// # tokio_test::block_on(async {
278    /// backend.add_collection(Collection::new("collection-id", "a description")).await.unwrap();
279    /// backend.add_item(Item::new("item-id").collection("collection-id")).await.unwrap();
280    /// let api = Api::new(backend, "http://stac.test").unwrap();
281    /// let item = api.item("collection-id", "item-id").await.unwrap().unwrap();
282    /// # })
283    /// ```
284    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    /// Searches the API.
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use stac_api::Search;
308    /// use stac_server::{Api, MemoryBackend, Backend};
309    /// use http::Method;
310    ///
311    /// let api = Api::new(MemoryBackend::new(), "http://stac.test").unwrap();
312    /// # tokio_test::block_on(async {
313    /// let item_collection = api.search(Search::default(), Method::GET).await.unwrap();
314    /// # })
315    /// ```
316    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        // catalog.validate().await.unwrap();
457        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        // collection.validate().await.unwrap();
550        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        // collection.validate().await.unwrap();
582        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}