Skip to main content

better_fetch/
endpoint.rs

1//! Typed API routes via the [`Endpoint`] trait.
2//!
3//! Define routes as types, then use [`Client::call`](crate::Client::call) for a typed
4//! [`EndpointRequestBuilder`]. See also the [`endpoint!`] macro for simple definitions.
5
6use std::marker::PhantomData;
7
8use http::Method;
9use indexmap::IndexMap;
10
11use crate::request::RequestBuilder;
12use crate::url_build::QueryValue;
13
14#[cfg(feature = "json")]
15use serde::de::DeserializeOwned;
16
17/// Describes a typed API route.
18///
19/// Implement this trait (or use [`endpoint!`]) and call [`Client::call`](crate::Client::call).
20///
21/// # Examples
22///
23/// ```no_run
24/// # use better_fetch::{Client, Endpoint, Result};
25/// # use http::Method;
26/// # use serde::Deserialize;
27/// struct GetTodo;
28///
29/// impl Endpoint for GetTodo {
30///     const METHOD: Method = Method::GET;
31///     const PATH: &'static str = "/todos/:id";
32///     type Response = Todo;
33///     type Params = ();
34///     type Query = ();
35/// }
36///
37/// #[derive(Deserialize)]
38/// struct Todo { id: u64, title: String }
39///
40/// # #[tokio::main]
41/// # async fn main() -> Result<()> {
42/// let client = Client::new("https://jsonplaceholder.typicode.com")?;
43/// let todo: Todo = client.call::<GetTodo>().param("id", 1).send_json().await?;
44/// # Ok(())
45/// # }
46/// ```
47pub trait Endpoint {
48    /// HTTP method for this route.
49    const METHOD: Method;
50    /// Path template (may include `:param` segments).
51    const PATH: &'static str;
52
53    #[cfg(feature = "json")]
54    /// JSON response type for [`EndpointRequestBuilder::send_json`].
55    type Response: DeserializeOwned;
56
57    #[cfg(not(feature = "json"))]
58    /// Response type when the `json` feature is disabled.
59    type Response;
60
61    /// Path parameters applied via [`EndpointRequestBuilder::params`].
62    type Params: EndpointParams + Default;
63    /// Query parameters applied via [`EndpointRequestBuilder::query`].
64    type Query: EndpointQuery + Default;
65}
66
67/// Applies path parameters to a [`RequestBuilder`].
68pub trait EndpointParams {
69    /// Applies this type's parameters to `builder`.
70    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_>;
71}
72
73impl EndpointParams for () {
74    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
75        builder
76    }
77}
78
79impl EndpointParams for std::collections::HashMap<String, String> {
80    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
81        builder.params(self)
82    }
83}
84
85impl EndpointParams for Vec<(String, String)> {
86    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
87        builder.params_iter(self)
88    }
89}
90
91/// Applies query parameters to a [`RequestBuilder`].
92pub trait EndpointQuery {
93    /// Applies this type's query map to `builder`.
94    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_>;
95}
96
97impl EndpointQuery for () {
98    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
99        builder
100    }
101}
102
103impl EndpointQuery for IndexMap<String, QueryValue> {
104    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
105        builder.queries(self)
106    }
107}
108
109/// Fluent builder for a typed [`Endpoint`].
110pub struct EndpointRequestBuilder<'a, E: Endpoint> {
111    pub(crate) inner: RequestBuilder<'a>,
112    _marker: PhantomData<E>,
113}
114
115impl<'a, E: Endpoint> EndpointRequestBuilder<'a, E> {
116    pub(crate) fn new(inner: RequestBuilder<'a>) -> Self {
117        Self {
118            inner,
119            _marker: PhantomData,
120        }
121    }
122
123    /// Applies typed path parameters for `E::Params`.
124    pub fn params(self, params: E::Params) -> Self {
125        Self {
126            inner: params.apply_params(self.inner),
127            _marker: PhantomData,
128        }
129    }
130
131    /// Applies typed query parameters for `E::Query`.
132    pub fn query(self, query: E::Query) -> Self {
133        Self {
134            inner: query.apply_query(self.inner),
135            _marker: PhantomData,
136        }
137    }
138
139    /// Sets a single path parameter.
140    pub fn param(self, key: impl Into<String>, value: impl ToString) -> Self {
141        Self {
142            inner: self.inner.param(key, value),
143            _marker: PhantomData,
144        }
145    }
146
147    /// Adds a query parameter.
148    pub fn query_pair(self, key: impl Into<String>, value: impl ToString) -> Self {
149        Self {
150            inner: self.inner.query(key, value),
151            _marker: PhantomData,
152        }
153    }
154
155    /// Adds a request header.
156    pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> crate::Result<Self> {
157        Ok(Self {
158            inner: self.inner.header(key, value)?,
159            _marker: PhantomData,
160        })
161    }
162
163    /// Sets bearer authentication.
164    pub fn bearer_token(self, token: impl Into<String>) -> Self {
165        Self {
166            inner: self.inner.bearer_token(token),
167            _marker: PhantomData,
168        }
169    }
170
171    /// Attaches a cancellation token.
172    pub fn cancellation_token(self, token: crate::CancellationToken) -> Self {
173        Self {
174            inner: self.inner.cancellation_token(token),
175            _marker: PhantomData,
176        }
177    }
178
179    /// When `true`, [`send`](Self::send) returns `Err` on non-2xx.
180    pub fn throw_on_error(self, throw: bool) -> Self {
181        Self {
182            inner: self.inner.throw_on_error(throw),
183            _marker: PhantomData,
184        }
185    }
186
187    /// Executes the request and returns [`Response`](crate::Response).
188    pub async fn send(self) -> crate::Result<crate::Response> {
189        self.inner.send().await
190    }
191
192    /// Executes and deserializes `E::Response` (feature `json`).
193    #[cfg(feature = "json")]
194    pub async fn send_json(self) -> crate::Result<E::Response> {
195        self.inner.send().await?.json::<E::Response>().await
196    }
197
198    /// Returns the underlying [`RequestBuilder`] for advanced options.
199    pub fn into_inner(self) -> RequestBuilder<'a> {
200        self.inner
201    }
202}
203
204/// Defines a simple [`Endpoint`] with no params or query.
205///
206/// # Examples
207///
208/// ```
209/// use better_fetch::endpoint;
210/// use serde::Deserialize;
211///
212/// #[derive(Deserialize)]
213/// pub struct Health {
214///     ok: bool,
215/// }
216///
217/// endpoint!(HealthCheck, GET, "/health", Response = Health);
218/// ```
219#[macro_export]
220macro_rules! endpoint {
221    (
222        $name:ident,
223        $method:ident,
224        $path:literal,
225        Response = $response:ty
226    ) => {
227        pub struct $name;
228        impl $crate::Endpoint for $name {
229            const METHOD: http::Method = http::Method::$method;
230            const PATH: &'static str = $path;
231            type Response = $response;
232            type Params = ();
233            type Query = ();
234        }
235    };
236}