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}