stac_async/
client.rs

1use crate::Error;
2use http::header::HeaderName;
3use reqwest::{header::HeaderMap, IntoUrl, Method, StatusCode};
4use serde::{de::DeserializeOwned, Serialize};
5use serde_json::{Map, Value};
6use stac::{Href, Link};
7
8/// A thin wrapper around [reqwest::Client].
9#[derive(Clone, Debug)]
10pub struct Client(pub reqwest::Client);
11
12impl Client {
13    /// Creates a new client.
14    ///
15    /// # Examples
16    ///
17    /// ```
18    /// let client = stac_async::Client::new();
19    /// ```
20    ///
21    /// ## Custom client
22    ///
23    /// You can construct the client directly using a pre-built
24    /// [reqwest::Client], e.g. to do authorization:
25    ///
26    /// ```
27    /// use reqwest::header;
28    /// let mut headers = header::HeaderMap::new();
29    /// let mut auth_value = header::HeaderValue::from_static("secret");
30    /// auth_value.set_sensitive(true);
31    /// headers.insert(header::AUTHORIZATION, auth_value);
32    /// let client = reqwest::Client::builder().default_headers(headers).build().unwrap();
33    /// let client = stac_async::Client(client);
34    /// ```
35    pub fn new() -> Client {
36        Client(reqwest::Client::new())
37    }
38
39    /// Gets a STAC value from a url.
40    ///
41    /// Also sets that [Values](Value) href. Returns Ok(None) if a 404 is
42    /// returned from the server.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// let client = stac_async::Client::new();
48    /// let href = "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/simple-item.json";
49    /// # tokio_test::block_on(async {
50    /// let item: stac::Item = client.get(href).await.unwrap().unwrap();
51    /// # })
52    /// ```
53    pub async fn get<V>(&self, url: impl IntoUrl) -> Result<Option<V>, Error>
54    where
55        V: DeserializeOwned + Href,
56    {
57        let url = url.into_url()?;
58        if let Some(mut value) = self
59            .request::<(), V>(Method::GET, url.clone(), None, None)
60            .await?
61        {
62            value.set_href(url);
63            Ok(Some(value))
64        } else {
65            Ok(None)
66        }
67    }
68
69    /// Posts data to a url.
70    ///
71    /// # Examples
72    ///
73    /// ```no_run
74    /// use stac_api::Search;
75    /// let client = stac_async::Client::new();
76    /// let href = "https://planetarycomputer.microsoft.com/api/stac/v1/search";
77    /// let mut search = Search::default();
78    /// search.items.limit = Some(1);
79    /// # tokio_test::block_on(async {
80    /// let items: stac_api::ItemCollection = client.post(href, &search).await.unwrap().unwrap();
81    /// # })
82    /// ```
83    pub async fn post<S, R>(&self, url: impl IntoUrl, data: &S) -> Result<Option<R>, Error>
84    where
85        S: Serialize + 'static,
86        R: DeserializeOwned,
87    {
88        self.request(Method::POST, url, Some(data), None).await
89    }
90
91    /// Sends a request to a url.
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use stac::Item;
97    /// use reqwest::Method;
98    ///
99    /// let client = stac_async::Client::new();
100    /// let href = "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/simple-item.json";
101    /// # tokio_test::block_on(async {
102    /// let item = client.request::<(), Item>(Method::GET, href, None, None).await.unwrap().unwrap();
103    /// # })
104    /// ```
105    pub async fn request<S, R>(
106        &self,
107        method: Method,
108        url: impl IntoUrl,
109        params: impl Into<Option<&S>>,
110        headers: impl Into<Option<HeaderMap>>,
111    ) -> Result<Option<R>, Error>
112    where
113        S: Serialize + 'static,
114        R: DeserializeOwned,
115    {
116        let url = url.into_url()?;
117        let mut request = match method {
118            Method::GET => {
119                let mut request = self.0.get(url);
120                if let Some(query) = params.into() {
121                    request = request.query(query);
122                }
123                request
124            }
125            Method::POST => {
126                let mut request = self.0.post(url);
127                if let Some(data) = params.into() {
128                    request = request.json(&data);
129                }
130                request
131            }
132            _ => unimplemented!(),
133        };
134        if let Some(headers) = headers.into() {
135            request = request.headers(headers);
136        }
137        let response = request.send().await?;
138        if response.status() == StatusCode::NOT_FOUND {
139            return Ok(None);
140        }
141        let response = response.error_for_status()?;
142        response.json().await.map_err(Error::from)
143    }
144
145    /// Builds and sends a request, as defined in a link.
146    ///
147    /// Used mostly for "next" links in pagination.
148    ///
149    /// # Examples
150    ///
151    /// ```no_run
152    /// use stac::Link;
153    /// let link = Link::new("http://stac-async-rs.test/search?foo=bar", "next");
154    /// let client = stac_async::Client::new();
155    /// # tokio_test::block_on(async {
156    /// let page: stac_api::ItemCollection = client.request_from_link(link).await.unwrap().unwrap();
157    /// # })
158    /// ```
159    pub async fn request_from_link<R>(&self, link: Link) -> Result<Option<R>, Error>
160    where
161        R: DeserializeOwned,
162    {
163        let method = if let Some(method) = link.method {
164            method.parse()?
165        } else {
166            Method::GET
167        };
168        let headers = if let Some(headers) = link.headers {
169            let mut header_map = HeaderMap::new();
170            for (key, value) in headers.into_iter() {
171                let header_name: HeaderName = key.parse()?;
172                let _ = header_map.insert(header_name, value.to_string().parse()?);
173            }
174            Some(header_map)
175        } else {
176            None
177        };
178        self.request::<Map<String, Value>, R>(method, link.href, &link.body, headers)
179            .await
180    }
181}
182
183impl Default for Client {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::Client;
192    use mockito::Server;
193    use stac::{Href, Item};
194    use stac_api::Search;
195
196    #[tokio::test]
197    async fn client_get() {
198        let client = Client::new();
199        let href = "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/simple-item.json";
200        let item: Item = client.get(href).await.unwrap().unwrap();
201        assert_eq!(item.href().unwrap(), href);
202    }
203
204    #[tokio::test]
205    async fn client_get_404() {
206        let client = Client::new();
207        let href = "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/not-an-item.json";
208        assert!(client.get::<Item>(href).await.unwrap().is_none());
209    }
210
211    #[tokio::test]
212    async fn client_post() {
213        let mut server = Server::new_async().await;
214        let page = server
215            .mock("POST", "/search")
216            .with_body(include_str!("../mocks/search-page-1.json"))
217            .with_header("content-type", "application/geo+json")
218            .create_async()
219            .await;
220        let client = Client::new();
221        let href = format!("{}/search", server.url());
222        let mut search = Search::default();
223        search.items.limit = Some(1);
224        let _: stac_api::ItemCollection = client.post(href, &search).await.unwrap().unwrap();
225        page.assert_async().await;
226    }
227}