Skip to main content

better_fetch/
endpoint.rs

1use std::marker::PhantomData;
2
3use http::Method;
4use indexmap::IndexMap;
5
6use crate::request::RequestBuilder;
7use crate::url_build::QueryValue;
8
9#[cfg(feature = "json")]
10use serde::de::DeserializeOwned;
11
12/// Describes a typed API route.
13pub trait Endpoint {
14    const METHOD: Method;
15    const PATH: &'static str;
16
17    #[cfg(feature = "json")]
18    type Response: DeserializeOwned;
19
20    #[cfg(not(feature = "json"))]
21    type Response;
22
23    type Params: EndpointParams + Default;
24    type Query: EndpointQuery + Default;
25}
26
27/// Applies path parameters to a [`RequestBuilder`].
28pub trait EndpointParams {
29    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_>;
30}
31
32impl EndpointParams for () {
33    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
34        builder
35    }
36}
37
38impl EndpointParams for std::collections::HashMap<String, String> {
39    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
40        builder.params(self)
41    }
42}
43
44impl EndpointParams for Vec<(String, String)> {
45    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
46        builder.params_iter(self)
47    }
48}
49
50/// Applies query parameters to a [`RequestBuilder`].
51pub trait EndpointQuery {
52    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_>;
53}
54
55impl EndpointQuery for () {
56    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
57        builder
58    }
59}
60
61impl EndpointQuery for IndexMap<String, QueryValue> {
62    fn apply_query(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
63        builder.queries(self)
64    }
65}
66
67/// Fluent builder for a typed [`Endpoint`].
68pub struct EndpointRequestBuilder<'a, E: Endpoint> {
69    pub(crate) inner: RequestBuilder<'a>,
70    _marker: PhantomData<E>,
71}
72
73impl<'a, E: Endpoint> EndpointRequestBuilder<'a, E> {
74    pub(crate) fn new(inner: RequestBuilder<'a>) -> Self {
75        Self {
76            inner,
77            _marker: PhantomData,
78        }
79    }
80
81    pub fn params(self, params: E::Params) -> Self {
82        Self {
83            inner: params.apply_params(self.inner),
84            _marker: PhantomData,
85        }
86    }
87
88    pub fn query(self, query: E::Query) -> Self {
89        Self {
90            inner: query.apply_query(self.inner),
91            _marker: PhantomData,
92        }
93    }
94
95    pub fn param(self, key: impl Into<String>, value: impl ToString) -> Self {
96        Self {
97            inner: self.inner.param(key, value),
98            _marker: PhantomData,
99        }
100    }
101
102    pub fn query_pair(self, key: impl Into<String>, value: impl ToString) -> Self {
103        Self {
104            inner: self.inner.query(key, value),
105            _marker: PhantomData,
106        }
107    }
108
109    pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> crate::Result<Self> {
110        Ok(Self {
111            inner: self.inner.header(key, value)?,
112            _marker: PhantomData,
113        })
114    }
115
116    pub fn bearer_token(self, token: impl Into<String>) -> Self {
117        Self {
118            inner: self.inner.bearer_token(token),
119            _marker: PhantomData,
120        }
121    }
122
123    pub fn cancellation_token(self, token: crate::CancellationToken) -> Self {
124        Self {
125            inner: self.inner.cancellation_token(token),
126            _marker: PhantomData,
127        }
128    }
129
130    pub fn throw_on_error(self, throw: bool) -> Self {
131        Self {
132            inner: self.inner.throw_on_error(throw),
133            _marker: PhantomData,
134        }
135    }
136
137    pub async fn send(self) -> crate::Result<crate::Response> {
138        self.inner.send().await
139    }
140
141    #[cfg(feature = "json")]
142    pub async fn send_json(self) -> crate::Result<E::Response> {
143        self.inner.send().await?.json::<E::Response>().await
144    }
145
146    pub fn into_inner(self) -> RequestBuilder<'a> {
147        self.inner
148    }
149}
150
151/// Helper macro for simple endpoint definitions.
152#[macro_export]
153macro_rules! endpoint {
154    (
155        $name:ident,
156        $method:ident,
157        $path:literal,
158        Response = $response:ty
159    ) => {
160        pub struct $name;
161        impl $crate::Endpoint for $name {
162            const METHOD: http::Method = http::Method::$method;
163            const PATH: &'static str = $path;
164            type Response = $response;
165            type Params = ();
166            type Query = ();
167        }
168    };
169}