odata_simple_client/
lib.rs

1//! This crate provides a Rust-interface to an [OData 3.0](https://www.odata.org/documentation/odata-version-3-0/) API over HTTP(S)
2//!
3//! To get started, construct a [`DataSource`] and then create either a [`ListRequest`] or [`GetRequest`] and
4//! [`fetch`](`DataSource::fetch`)/[`fetch_paged`](`DataSource::fetch_paged`) it using your [`DataSource`]
5//!
6//! Here's a complete example which fetches a single `Dokument` from the [Danish Parliament's](https://oda.ft.dk) OData API:
7//!
8//!  ```rust
9//! use hyper::{Client, client::HttpConnector};
10//! use hyper_openssl::{HttpsConnector};
11//! use odata_simple_client::{DataSource, GetRequest};
12//! use serde::Deserialize;
13//!
14//! #[derive(Deserialize)]
15//! struct Dokument {
16//!     titel: String,
17//! }
18//!
19//! // Construct a Hyper client for communicating over HTTPS
20//! let client: Client<HttpsConnector<HttpConnector>> =
21//!     Client::builder().build(HttpsConnector::<HttpConnector>::new().unwrap());
22//!
23//! // Set up our DataSource. The API is reachable on https://oda.ft.dk/api/
24//! let datasource = DataSource::new(client, "oda.ft.dk", Some(String::from("/api"))).unwrap();
25//!
26//! // The tokio_test::block_on call is just to make this example work in a rustdoc example.
27//! // Normally you would just write the enclosed code in an async function.
28//! tokio_test::block_on(async {
29//!     let dokument: Dokument = datasource.fetch(
30//!         GetRequest::new("Dokument", 24)
31//!      ).await.unwrap();
32//!
33//!     assert_eq!(dokument.titel, "Grund- og nærhedsnotat vedr. sanktioner på toldområdet");
34//! });
35//!  ```
36//! The example above has requirements on a number of crates. See the `Cargo.toml`-file for a list.
37
38#![deny(
39    bad_style,
40    dead_code,
41    improper_ctypes,
42    non_shorthand_field_patterns,
43    no_mangle_generic_items,
44    overflowing_literals,
45    path_statements,
46    patterns_in_fns_without_body,
47    private_in_public,
48    unconditional_recursion,
49    unused,
50    unused_allocation,
51    unused_comparisons,
52    unused_parens,
53    while_true,
54    missing_debug_implementations,
55    missing_docs,
56    trivial_casts,
57    trivial_numeric_casts,
58    unused_extern_crates,
59    unused_import_braces,
60    unused_qualifications,
61    unused_results
62)]
63#![forbid(unsafe_code)]
64
65#[cfg(feature = "rate-limiting")]
66mod ratelimiting;
67#[cfg(feature = "rate-limiting")]
68pub use ratelimiting::RateLimitedDataSource;
69
70mod path;
71use path::PathBuilder;
72pub use path::{Comparison, Direction, Format, InlineCount};
73
74use hyper::{
75    body::Buf,
76    client::{connect::Connect, Client},
77    http::uri::{Authority, InvalidUri, Scheme},
78    Body, Response, Uri,
79};
80use log::debug;
81use serde::{de::DeserializeOwned, Deserialize};
82use std::{convert::TryFrom, io::Read};
83use thiserror::Error;
84
85/// Umbrella trait covering all the traits required of a [`Client`] for a [`DataSource`] to work.
86pub trait Connector: Connect + Clone + Send + Sync + 'static {}
87impl<T: Connect + Clone + Send + Sync + 'static> Connector for T {}
88
89/// Represents a target OData API.
90#[derive(Clone, Debug)]
91pub struct DataSource<C> {
92    client: Client<C>,
93    authority: Authority,
94    base_path: String,
95    scheme: Scheme,
96}
97
98/// Generalized Error type encompassing all the possible errors that can be generated by this crate.
99#[derive(Error, Debug)]
100pub enum Error {
101    /// The provided URI was not valid.
102    #[error("invalid URI")]
103    Uri(#[from] InvalidUri),
104    /// HTTP error occurred while executing a request.
105    #[error("http error")]
106    Http(#[from] hyper::http::Error),
107    /// A Hyper error occurred while executing the request, or constructing the Client.
108    #[error("hyper error")]
109    Hyper(#[from] hyper::Error),
110    /// An error occurred while serializing or deserializing data during a request or response.
111    #[error("serde error")]
112    Serde(serde_json::Error, String),
113    /// An IO error occurred.
114    #[error("io error")]
115    Io(#[from] std::io::Error),
116}
117
118/// Wraps lists of Resources returned by the API. Used for deserializing ListRequest responses.
119#[derive(Debug, Deserialize)]
120pub struct Page<T> {
121    /// List of returned values in the page.
122    pub value: Vec<T>,
123    #[serde(rename = "odata.count")]
124    /// Inline count of remanining objects to be fetched, excluding the ones in this page.
125    pub count: Option<String>,
126    /// URL Request to send, to fetch the next page in this sequence.
127    #[serde(rename = "odata.nextLink")]
128    pub next_link: Option<String>,
129    /// Url to the schema describing the data returned
130    #[serde(rename = "odata.metadata")]
131    pub metadata: Option<String>,
132}
133
134async fn deserialize_as<T: DeserializeOwned>(response: Response<Body>) -> Result<T, Error> {
135    let body = hyper::body::aggregate(response).await?;
136
137    let mut content = String::new();
138    // We don't care about the read number of bytes,
139    // we just read until EOF.
140    let _ = body.reader().read_to_string(&mut content)?;
141
142    serde_json::from_str(&content).map_err(|e| Error::Serde(e, content))
143}
144
145impl<C> DataSource<C>
146where
147    C: Connector,
148{
149    /// Construct a new DataSource using a [`Client`], [`Authority`] and a base_domain.
150    /// ```rust
151    /// # use hyper::{Client, client::HttpConnector};
152    /// # use hyper_openssl::{HttpsConnector};
153    /// # use odata_simple_client::DataSource;
154    /// # let client: Client<HttpsConnector<HttpConnector>> =
155    /// #   Client::builder().build(HttpsConnector::<HttpConnector>::new().unwrap());
156    /// #
157    /// let datasource = DataSource::new(
158    ///     client,
159    ///     "oda.ft.dk",
160    ///     Some(String::from("/api"))
161    /// ).unwrap();
162    /// ```
163    pub fn new<A>(
164        client: Client<C>,
165        domain: A,
166        base_path: Option<String>,
167    ) -> Result<DataSource<C>, Error>
168    where
169        Authority: TryFrom<A>,
170        Error: From<<Authority as TryFrom<A>>::Error>,
171    {
172        Ok(DataSource {
173            client,
174            authority: Authority::try_from(domain)?,
175            base_path: base_path.unwrap_or_default(),
176            scheme: Scheme::HTTPS,
177        })
178    }
179
180    async fn execute<R>(&self, request: R) -> Result<Response<Body>, Error>
181    where
182        R: Into<PathBuilder>,
183    {
184        let builder: PathBuilder = request.into().base_path(self.base_path.clone());
185
186        let uri = Uri::builder()
187            .scheme(self.scheme.as_ref())
188            .authority(self.authority.as_ref())
189            .path_and_query(builder.build()?)
190            .build()?;
191
192        debug!("fetching {}", uri);
193        Ok(self.client.get(uri).await?)
194    }
195
196    /// Fetch a single resource using a [`GetRequest`]
197    /// ```rust
198    /// # use hyper::{Client, client::HttpConnector};
199    /// # use hyper_openssl::{HttpsConnector};
200    /// # use odata_simple_client::{DataSource, GetRequest};
201    /// # use serde::Deserialize;
202    /// #
203    /// # let client: Client<HttpsConnector<HttpConnector>> =
204    /// #   Client::builder().build(HttpsConnector::<HttpConnector>::new().unwrap());
205    /// #
206    /// # let datasource = DataSource::new(client, "oda.ft.dk", Some(String::from("/api"))).unwrap();
207    /// #
208    /// #[derive(Deserialize)]
209    /// struct Dokument {
210    ///     titel: String,
211    /// }
212    ///
213    /// # tokio_test::block_on(async {
214    /// let dokument: Dokument = datasource.fetch(
215    ///         GetRequest::new("Dokument", 24)
216    ///     ).await.unwrap();
217    ///
218    /// assert_eq!(dokument.titel, "Grund- og nærhedsnotat vedr. sanktioner på toldområdet");
219    /// # });
220    /// ```
221    pub async fn fetch<T>(&self, request: GetRequest) -> Result<T, Error>
222    where
223        T: DeserializeOwned,
224    {
225        let response = self
226            .execute(Into::<PathBuilder>::into(request).format(Format::Json))
227            .await?;
228        deserialize_as::<T>(response).await
229    }
230
231    /// Fetch a [`Page`]d list of resources using a [`ListRequest`]
232    /// ```rust
233    /// # use hyper::{Client, client::HttpConnector};
234    /// # use hyper_openssl::{HttpsConnector};
235    /// # use odata_simple_client::{DataSource, ListRequest, Page, InlineCount};
236    /// # use serde::Deserialize;
237    /// #
238    /// # let client: Client<HttpsConnector<HttpConnector>> =
239    /// #   Client::builder().build(HttpsConnector::<HttpConnector>::new().unwrap());
240    /// #
241    /// # let datasource = DataSource::new(client, "oda.ft.dk", Some(String::from("/api"))).unwrap();
242    /// #
243    /// #[derive(Deserialize)]
244    /// struct Dokument {
245    ///     titel: String,
246    /// }
247    ///
248    /// # tokio_test::block_on(async {
249    /// let page: Page<Dokument> = datasource
250    ///     .fetch_paged(ListRequest::new("Dokument")
251    ///         .inline_count(InlineCount::AllPages)
252    ///     ).await.unwrap();
253    /// assert!(page.count.unwrap().parse::<u32>().unwrap() > 0)
254    /// # });
255    /// ```
256    pub async fn fetch_paged<T>(&self, request: ListRequest) -> Result<Page<T>, Error>
257    where
258        T: DeserializeOwned,
259    {
260        let response = self
261            .execute(Into::<PathBuilder>::into(request).format(Format::Json))
262            .await?;
263        deserialize_as::<Page<T>>(response).await
264    }
265}
266
267/// Request a single resource by ID
268#[derive(Debug, Clone)]
269pub struct GetRequest {
270    builder: PathBuilder,
271}
272
273impl GetRequest {
274    /// Constructs a GET request for `<DataSource Path>/resource_type(id)`
275    ///
276    /// Must be [`DataSource::fetch`]ed using a [`DataSource`] to retrieve data.
277    pub fn new(resource_type: &str, id: usize) -> Self {
278        GetRequest {
279            builder: PathBuilder::new(resource_type.to_string()).id(id),
280        }
281    }
282
283    /// Change format of the returned data.
284    ///
285    /// Can be either [`Format::Json`] or [`Format::Xml`]
286    pub fn format(mut self, format: Format) -> Self {
287        self.builder = self.builder.format(format);
288        self
289    }
290
291    /// Expand specific relations of the returned object, if possible.
292    ///
293    /// For the [Folketinget API](https://oda.ft.dk) for example, you can expand the `DokumentAktør` field of a `Dokument`, to simultaneously retrieve information about the document authors, instead of having to do two separate lookups for the `DokumentAktør` relation and then the actual `Aktør`.
294    pub fn expand<'f, F>(mut self, field: F) -> Self
295    where
296        F: IntoIterator<Item = &'f str>,
297    {
298        self.builder = self.builder.expand(field);
299        self
300    }
301}
302
303impl From<GetRequest> for PathBuilder {
304    fn from(request: GetRequest) -> Self {
305        request.builder
306    }
307}
308
309/// Request a list of resources.
310#[derive(Debug, Clone)]
311pub struct ListRequest {
312    builder: PathBuilder,
313}
314
315impl ListRequest {
316    /// Create a new ListRequest, fetching all resources of type `resource_type`.
317    ///
318    /// Use a [`DataSource`] to execute the `ListRequest`
319    pub fn new(resource_type: &str) -> Self {
320        ListRequest {
321            builder: PathBuilder::new(resource_type.to_string()),
322        }
323    }
324
325    /// Change format of the returned data.
326    ///
327    /// Can be either [`Format::Json`] or [`Format::Xml`]
328    pub fn format(mut self, format: Format) -> Self {
329        self.builder = self.builder.format(format);
330        self
331    }
332
333    /// Order the returned resources by `field`, in specified `direction`. [`Direction::Ascending`] by default.
334    pub fn order_by(mut self, field: &str, direction: Direction) -> Self {
335        self.builder = self.builder.order_by(field, direction);
336        self
337    }
338
339    /// Only retrieve the top `count` items.
340    pub fn top(mut self, count: u32) -> Self {
341        self.builder = self.builder.top(count);
342        self
343    }
344
345    /// Skip the first `count` items.
346    pub fn skip(mut self, count: u32) -> Self {
347        self.builder = self.builder.skip(count);
348        self
349    }
350
351    /// Include an inline count field in the odata page metadata.
352    /// Useful for gauging how many results/pages are left. By default this is not specified, which implies [`InlineCount::None`]
353    pub fn inline_count(mut self, value: InlineCount) -> Self {
354        self.builder = self.builder.inline_count(value);
355        self
356    }
357
358    /// Filter the returned results using an OData conditional expression.
359    ///
360    /// See [the OData 2.0 documentation (section 4.5)](https://www.odata.org/documentation/odata-version-2-0/uri-conventions/) for more information.
361    /// ```rust
362    /// # use hyper::{Client, client::HttpConnector};
363    /// # use hyper_openssl::{HttpsConnector};
364    /// # use odata_simple_client::{DataSource, ListRequest, Page, Comparison};
365    /// # use serde::Deserialize;
366    /// #
367    /// # let client: Client<HttpsConnector<HttpConnector>> =
368    /// #   Client::builder().build(HttpsConnector::<HttpConnector>::new().unwrap());
369    /// #
370    /// # let datasource = DataSource::new(client, "oda.ft.dk", Some(String::from("/api"))).unwrap();
371    /// #
372    /// #[derive(Deserialize, Debug)]
373    /// struct Dokument {
374    ///     titel: String,
375    /// }
376    ///
377    /// # tokio_test::block_on(async {
378    /// let page: Page<Dokument> = datasource
379    ///     .fetch_paged(ListRequest::new("Dokument")
380    ///         .filter("id", Comparison::Equal, "24")
381    ///     ).await.unwrap();
382    /// assert_eq!(page.value[0].titel, "Grund- og nærhedsnotat vedr. sanktioner på toldområdet")
383    /// # });
384    /// ```
385    pub fn filter(mut self, field: &str, comparison: Comparison, value: &str) -> Self {
386        self.builder = self.builder.filter(field, comparison, value);
387        self
388    }
389
390    /// Expand specific relations of the returned object, if possible.
391    ///
392    /// For the [Folketinget API](https://oda.ft.dk) for example, you can expand the `DokumentAktør` field of a `Dokument`, to simultaneously retrieve information about the document authors, instead of having to do two separate lookups for the `DokumentAktør` relation and then the actual `Aktør`.
393    pub fn expand<'f, F>(mut self, field: F) -> Self
394    where
395        F: IntoIterator<Item = &'f str>,
396    {
397        self.builder = self.builder.expand(field);
398        self
399    }
400}
401
402impl From<ListRequest> for PathBuilder {
403    fn from(request: ListRequest) -> Self {
404        request.builder
405    }
406}