cargo_api/
api.rs

1pub mod crates;
2
3use bytes::Bytes;
4use http::StatusCode;
5use std::borrow::Cow;
6use std::collections::HashMap;
7use std::fmt;
8use url::Url;
9
10#[derive(fmt::Debug, Default)]
11pub struct QueryParams {
12    inner: HashMap<Cow<'static, str>, Cow<'static, str>>,
13}
14
15impl QueryParams {
16    pub fn append_to_url(&self, url: &mut Url) {
17        url.query_pairs_mut().extend_pairs(self.iter_tuples());
18    }
19
20    fn iter_tuples(&self) -> impl Iterator<Item = (&str, &str)> {
21        self.inner.iter().map(|(k, v)| (k.as_ref(), v.as_ref()))
22    }
23}
24
25#[derive(fmt::Debug, thiserror::Error)]
26#[error(transparent)]
27#[non_exhaustive]
28pub struct BodyError {
29    pub error: serde_json::Error,
30}
31
32#[derive(fmt::Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum JsonResult {
35    #[error("{0}")]
36    Json(serde_json::Value),
37    #[error(transparent)]
38    Error(serde_json::Error),
39}
40
41impl From<Result<serde_json::Value, serde_json::Error>> for JsonResult {
42    fn from(value: Result<serde_json::Value, serde_json::Error>) -> Self {
43        match value {
44            Ok(v) => JsonResult::Json(v),
45            Err(e) => JsonResult::Error(e),
46        }
47    }
48}
49
50#[derive(fmt::Debug, thiserror::Error)]
51#[non_exhaustive]
52pub enum ApiError<C: std::error::Error + Send + Sync + 'static> {
53    #[error("Body error: {}", error)]
54    Body {
55        #[from]
56        error: BodyError,
57    },
58    #[error("Client error: {}", error)]
59    Client { error: C },
60    #[error("Unable to build HTTP request: {}", error)]
61    HttpRequest { error: http::Error },
62    #[error("HTTP request failed with status code '{}': {}", status_code, body)]
63    HttpResponse {
64        status_code: StatusCode,
65        body: JsonResult,
66    },
67    #[error("Unable to parse JSON response into type '{}': {}", r#type, error)]
68    ParseType {
69        error: serde_json::Error,
70        r#type: &'static str,
71    },
72    #[error("Unable to parse url '{}' (path '{}'): {}", url, path, error)]
73    Url {
74        error: url::ParseError,
75        url: Cow<'static, str>,
76        path: Cow<'static, str>,
77    },
78}
79
80impl<C: std::error::Error + Send + Sync + 'static> ApiError<C> {
81    pub fn parse_type_error<T>(error: serde_json::Error) -> Self {
82        ApiError::ParseType {
83            error,
84            r#type: std::any::type_name::<T>(),
85        }
86    }
87}
88
89/// Http endpoint trait for cargo-api.
90///
91/// # Credits
92///
93/// Inspired by Ben Boeckel's blog [post](https://plume.benboeckel.net/~/JustAnotherBlog/designing-rust-bindings-for-rest-ap-is)
94/// titled "Designing Rust bindings for REST APIs".
95pub trait Endpoint {
96    fn method(&self) -> http::Method;
97
98    fn endpoint(&self) -> Cow<'static, str>;
99
100    fn parameters(&self) -> QueryParams {
101        QueryParams::default()
102    }
103
104    fn body(&self) -> Result<Vec<u8>, BodyError> {
105        Ok(Vec::with_capacity(0))
106    }
107}
108
109/// Http api trait for cargo-api.
110///
111/// # Credits
112///
113/// Inspired by Ben Boeckel's blog [post](https://plume.benboeckel.net/~/JustAnotherBlog/designing-rust-bindings-for-rest-ap-is)
114/// titled "Designing Rust bindings for REST APIs".
115pub trait Client {
116    type Error: std::error::Error + Send + Sync + 'static;
117
118    fn base_endpoint(&self, path: &str) -> Result<Url, ApiError<Self::Error>>;
119
120    // By separating the request builder and the body, additional items may be added
121    // to the request, such as authentication.
122    fn send(
123        &self,
124        request_builder: http::request::Builder,
125        body: Vec<u8>,
126    ) -> Result<http::Response<Bytes>, ApiError<Self::Error>>;
127}
128
129/// Query trait for 'cargo-api'
130///
131/// # Credits
132///
133/// Inspired by Ben Boeckel's blog [post](https://plume.benboeckel.net/~/JustAnotherBlog/designing-rust-bindings-for-rest-ap-is)
134/// titled "Designing Rust bindings for REST APIs".
135pub trait Query<T, C: Client> {
136    fn query(&self, client: &C) -> Result<T, ApiError<C::Error>>;
137}
138
139impl<E> Endpoint for &E
140where
141    E: Endpoint,
142{
143    fn method(&self) -> http::Method {
144        (*self).method()
145    }
146
147    fn endpoint(&self) -> Cow<'static, str> {
148        (*self).endpoint()
149    }
150
151    fn parameters(&self) -> QueryParams {
152        (*self).parameters()
153    }
154
155    fn body(&self) -> Result<Vec<u8>, BodyError> {
156        (*self).body()
157    }
158}
159
160impl<E, T, C> Query<T, C> for E
161where
162    E: Endpoint,
163    T: serde::de::DeserializeOwned,
164    C: Client,
165{
166    fn query(&self, client: &C) -> Result<T, ApiError<C::Error>> {
167        // -- compute the URL
168        // this is the base url with the path, but excluding any query parameters
169        let mut url = client.base_endpoint(self.endpoint().as_ref())?;
170        // add query parameters to the url
171        self.parameters().append_to_url(&mut url);
172
173        // -- build the request
174        let body = self.body()?;
175        let request = http::Request::builder()
176            .method(self.method())
177            .uri(url.as_ref());
178
179        // -- send
180        let response = client.send(request, body)?;
181
182        // -- handle response errors
183        if !response.status().is_success() {
184            // request failed, can be any non-2xx for now
185            return Err(ApiError::HttpResponse {
186                status_code: response.status(),
187                body: serde_json::from_slice(response.body()).into(),
188            });
189        }
190
191        // -- parse type
192        serde_json::from_slice::<T>(response.body()).map_err(ApiError::parse_type_error::<T>)
193    }
194}
195
196pub struct Json<E> {
197    endpoint: E,
198}
199
200impl<E> Json<E> {
201    pub fn new(endpoint: E) -> Self {
202        Self { endpoint }
203    }
204}
205
206impl<E, C> Query<serde_json::Value, C> for Json<E>
207where
208    E: Endpoint,
209    C: Client,
210{
211    fn query(&self, client: &C) -> Result<serde_json::Value, ApiError<C::Error>> {
212        self.endpoint.query(client)
213    }
214}