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`]. Path params use [`.params()`](EndpointRequestBuilder::params)
5//! with type-state ([`NeedsParams`]); query uses [`.query()`](EndpointRequestBuilder::query)
6//! with structs when you want a query string — **not** enforced by type-state (see [`EndpointQuery`]).
7//!
8//! For ad-hoc string paths, use [`Client::get`](crate::Client::get) instead (see [`RequestBuilder`](crate::RequestBuilder)).
9//!
10//! Helpers: [`endpoint!`], [`define_params!`], and (feature `macros`) `EndpointParamsDerive` /
11//! `EndpointQueryDerive`.
12
13use std::marker::PhantomData;
14use std::ops::Deref;
15
16use http::Method;
17use indexmap::IndexMap;
18
19use crate::error::Error;
20use crate::request::RequestBuilder;
21use crate::url_build::QueryValue;
22
23#[cfg(feature = "json")]
24use serde::de::DeserializeOwned;
25
26/// Type-state: path parameters still required before send.
27#[derive(Debug, Clone, Copy, Default)]
28pub struct NeedsParams;
29
30/// Type-state: request body required before send (POST with typed body via `#[derive(Endpoint)]`).
31#[derive(Debug, Clone, Copy, Default)]
32pub struct NeedsBody;
33
34/// Type-state: ready to configure query/headers and send.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct Ready;
37
38/// Describes a typed API route.
39///
40/// Implement this trait (or use [`endpoint!`]) and call [`Client::call`](crate::Client::call).
41///
42/// **Compile-time enforcement:** [`EndpointParams`] can require [`.params()`](EndpointRequestBuilder::params)
43/// via [`NeedsParams`]. [`EndpointBody`] can require a body via [`NeedsBody`] on POST routes.
44/// [`EndpointQuery`] only types query values — [`.query()`](EndpointRequestBuilder::query) is optional
45/// even when `Query` is not [`()`]; the `Default` bound is for struct construction, not auto-apply on send.
46///
47/// # Examples
48///
49/// ```no_run
50/// # use better_fetch::{Client, Endpoint, EndpointParams, Result, define_params};
51/// # use http::Method;
52/// # use serde::Deserialize;
53/// define_params!(GetTodoParams for "/todos/:id" { id: u64 });
54///
55/// struct GetTodo;
56///
57/// impl Endpoint for GetTodo {
58///     const METHOD: Method = Method::GET;
59///     const PATH: &'static str = "/todos/:id";
60///     type Response = Todo;
61///     type Params = GetTodoParams;
62///     type Query = ();
63///     type Body = ();
64///     type Headers = ();
65/// }
66///
67/// #[derive(Deserialize)]
68/// struct Todo { id: u64, title: String }
69///
70/// # #[tokio::main]
71/// # async fn main() -> Result<()> {
72/// let client = Client::new("https://jsonplaceholder.typicode.com")?;
73/// let todo: Todo = client
74///     .call::<GetTodo>()
75///     .params(GetTodoParams { id: 1 })
76///     .send_json()
77///     .await?;
78/// # Ok(())
79/// # }
80/// ```
81pub trait Endpoint {
82    /// HTTP method for this route.
83    const METHOD: Method;
84    /// Path template (may include `:param` segments).
85    const PATH: &'static str;
86
87    #[cfg(feature = "json")]
88    /// JSON response type for [`EndpointRequestBuilder::send_json`].
89    type Response: DeserializeOwned;
90
91    #[cfg(not(feature = "json"))]
92    /// Response type when the `json` feature is disabled.
93    type Response;
94
95    /// Path parameters applied via [`EndpointRequestBuilder::params`].
96    type Params: EndpointParams + Default;
97    /// Query parameters serialized by [`EndpointRequestBuilder::query`] when you call it.
98    ///
99    /// Not required at compile time: omitting [`.query()`](EndpointRequestBuilder::query) sends no
100    /// typed query string. Use [`()`] when the route has no query struct.
101    type Query: EndpointQuery + Default;
102
103    /// Optional typed request body ([`()`] = none).
104    type Body: EndpointBody + Default;
105
106    /// Optional typed request headers ([`()`] = none).
107    type Headers: EndpointHeaders + Default;
108}
109
110/// Applies a typed request body before send.
111pub trait EndpointBody: Default + Sized {
112    /// Builder state after [`.params()`](EndpointRequestBuilder::params) when path params were required.
113    type ParamsNext: Default;
114    /// Whether [`Client::call`](crate::Client::call) starts in [`NeedsBody`] (POST + required body).
115    type CallInitial: Default;
116
117    /// Applies this body to the builder.
118    fn apply_body(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>>;
119}
120
121impl EndpointBody for () {
122    type ParamsNext = Ready;
123    type CallInitial = Ready;
124
125    fn apply_body(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>> {
126        Ok(builder)
127    }
128}
129
130/// Default `()` params initial state when `E::Params` is [`()`].
131pub trait DefaultParamsInitial<E: Endpoint> {
132    fn initial(
133        client: &crate::Client,
134    ) -> EndpointRequestBuilder<'_, E, <E::Body as EndpointBody>::CallInitial>;
135}
136
137impl<E: Endpoint> DefaultParamsInitial<E> for ()
138where
139    E::Params: EndpointParams<BuilderState = Ready>,
140    E::Body: EndpointBody<CallInitial = Ready>,
141{
142    fn initial(client: &crate::Client) -> EndpointRequestBuilder<'_, E, Ready> {
143        EndpointRequestBuilder::new_ready(client.request(E::METHOD, E::PATH))
144    }
145}
146
147/// Applies typed default headers before send.
148pub trait EndpointHeaders: Default + Sized {
149    /// Applies headers to the builder.
150    fn apply_headers(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>>;
151}
152
153impl EndpointHeaders for () {
154    fn apply_headers(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>> {
155        Ok(builder)
156    }
157}
158
159/// Initial builder state for an endpoint's path parameters.
160pub type ParamsBuilderState<P> = <P as EndpointParams>::BuilderState;
161
162/// Creates the initial [`EndpointRequestBuilder`] for `client.call::<E>()`.
163pub trait EndpointParamsInitial<E: Endpoint>: EndpointParams {
164    /// Type-state after [`Client::call`](crate::Client::call).
165    type State;
166    fn initial(client: &crate::Client) -> EndpointRequestBuilder<'_, E, Self::State>;
167}
168
169impl<E: Endpoint> EndpointParamsInitial<E> for ()
170where
171    (): DefaultParamsInitial<E>,
172{
173    type State = <E::Body as EndpointBody>::CallInitial;
174
175    fn initial(client: &crate::Client) -> EndpointRequestBuilder<'_, E, Self::State> {
176        <() as DefaultParamsInitial<E>>::initial(client)
177    }
178}
179
180impl<E: Endpoint, P: EndpointParams<BuilderState = NeedsParams>> EndpointParamsInitial<E> for P {
181    type State = NeedsParams;
182
183    fn initial(client: &crate::Client) -> EndpointRequestBuilder<'_, E, NeedsParams> {
184        EndpointRequestBuilder::new_needs_params(client.request(E::METHOD, E::PATH))
185    }
186}
187
188/// Applies path parameters to a [`RequestBuilder`].
189///
190/// Unlike [`EndpointQuery`], this trait participates in type-state: non-unit params use
191/// [`NeedsParams`] so [`.params()`](EndpointRequestBuilder::params) is required before send.
192pub trait EndpointParams: Default + Sized {
193    /// When [`NeedsParams`], [`.params()`](EndpointRequestBuilder::params) is required before send.
194    type BuilderState;
195    /// Applies this type's parameters to `builder`.
196    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_>;
197}
198
199impl EndpointParams for () {
200    type BuilderState = Ready;
201
202    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
203        builder
204    }
205}
206
207impl EndpointParams for std::collections::HashMap<String, String> {
208    type BuilderState = NeedsParams;
209
210    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
211        builder.params(self)
212    }
213}
214
215impl EndpointParams for Vec<(String, String)> {
216    type BuilderState = NeedsParams;
217
218    fn apply_params(self, builder: RequestBuilder<'_>) -> RequestBuilder<'_> {
219        builder.params_iter(self)
220    }
221}
222
223/// Applies query parameters to a [`RequestBuilder`].
224///
225/// This trait does **not** use type-state: [`Client::call`](crate::Client::call) does not require
226/// [`.query()`](EndpointRequestBuilder::query) before [`.send_json()`](EndpointRequestBuilder::send_json),
227/// even when `E::Query` is a custom struct. Call [`.query()`](EndpointRequestBuilder::query) explicitly
228/// to serialize `self` onto the request.
229pub trait EndpointQuery {
230    /// Applies this type's query map to `builder`.
231    fn apply_query(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>>;
232}
233
234impl EndpointQuery for () {
235    fn apply_query(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>> {
236        Ok(builder)
237    }
238}
239
240impl EndpointQuery for IndexMap<String, QueryValue> {
241    fn apply_query(self, builder: RequestBuilder<'_>) -> crate::Result<RequestBuilder<'_>> {
242        Ok(builder.queries(self))
243    }
244}
245
246/// Applies a serde-serializable query struct to a request builder (feature `json`).
247#[cfg(feature = "json")]
248pub fn apply_serialized_query<T: serde::Serialize>(
249    query: T,
250    builder: RequestBuilder<'_>,
251) -> crate::Result<RequestBuilder<'_>> {
252    let map = crate::url_build::serialize_to_query_map(&query).map_err(|e| match e {
253        Error::Other(msg) => Error::query_serialize(msg),
254        other => other,
255    })?;
256    Ok(builder.queries(map))
257}
258
259/// Serializes and validates a query struct before applying it (feature `validate`).
260#[cfg(all(feature = "json", feature = "validate"))]
261pub fn apply_serialized_query_validated<T>(
262    query: T,
263    builder: RequestBuilder<'_>,
264) -> crate::Result<RequestBuilder<'_>>
265where
266    T: serde::Serialize + garde::Validate,
267    T::Context: Default,
268{
269    garde::Validate::validate(&query).map_err(|report: garde::Report| {
270        Error::RequestValidation {
271            message: report.to_string(),
272        }
273    })?;
274    apply_serialized_query(query, builder)
275}
276
277/// Fluent builder for a typed [`Endpoint`].
278///
279/// When `E::Params` is not [`()`], the builder starts in [`NeedsParams`] and requires
280/// [`.params()`](Self::params) before [`.send_json()`](Self::send_json).
281/// When `E::Query` is not [`()`], [`.query()`](Self::query) is still **optional** — it only runs when called.
282///
283/// In [`Ready`] state, use [`.query(E::Query)`](Self::query) for typed query structs, or the forwarded
284/// methods on this type (`.header`, `.json`, etc.). Prefer typed `.query()` over string keys on [`Deref`].
285#[must_use = "endpoint builders do nothing until you call `.send().await`, `.send_json().await`, or similar"]
286pub struct EndpointRequestBuilder<'a, E: Endpoint, S> {
287    pub(crate) inner: RequestBuilder<'a>,
288    _marker: PhantomData<(E, S)>,
289}
290
291impl<'a, E: Endpoint> EndpointRequestBuilder<'a, E, NeedsParams> {
292    pub(crate) fn new_needs_params(inner: RequestBuilder<'a>) -> Self {
293        Self {
294            inner,
295            _marker: PhantomData,
296        }
297    }
298
299    /// Applies typed path parameters and transitions to the next builder state.
300    pub fn params(
301        self,
302        params: E::Params,
303    ) -> EndpointRequestBuilder<'a, E, ParamsBuilderStateAfter<E>>
304    where
305        E::Body: EndpointBody,
306    {
307        EndpointRequestBuilder {
308            inner: params.apply_params(self.inner),
309            _marker: PhantomData,
310        }
311    }
312}
313
314/// Builder state after path params when `E::Body` may require a body.
315pub type ParamsBuilderStateAfter<E> = <<E as Endpoint>::Body as EndpointBody>::ParamsNext;
316
317impl<'a, E: Endpoint> EndpointRequestBuilder<'a, E, NeedsBody> {
318    pub fn new_needs_body(inner: RequestBuilder<'a>) -> Self {
319        Self {
320            inner,
321            _marker: PhantomData,
322        }
323    }
324
325    /// JSON request body (transitions to [`Ready`]).
326    #[cfg(feature = "json")]
327    pub fn json<T: serde::Serialize>(
328        self,
329        body: &T,
330    ) -> crate::Result<EndpointRequestBuilder<'a, E, Ready>> {
331        Ok(EndpointRequestBuilder {
332            inner: self.inner.json(body)?,
333            _marker: PhantomData,
334        })
335    }
336
337    /// Validated JSON request body (feature `validate`).
338    #[cfg(feature = "validate")]
339    pub fn json_validated<T>(self, body: &T) -> crate::Result<EndpointRequestBuilder<'a, E, Ready>>
340    where
341        T: serde::Serialize + garde::Validate,
342        T::Context: Default,
343    {
344        Ok(EndpointRequestBuilder {
345            inner: self.inner.json_validated(body)?,
346            _marker: PhantomData,
347        })
348    }
349
350    /// Applies typed request body for `E::Body` (transitions to [`Ready`]).
351    pub fn with_body(self, body: E::Body) -> crate::Result<EndpointRequestBuilder<'a, E, Ready>> {
352        Ok(EndpointRequestBuilder {
353            inner: body.apply_body(self.inner)?,
354            _marker: PhantomData,
355        })
356    }
357
358    /// Raw request body (transitions to [`Ready`]).
359    pub fn body(self, body: impl Into<bytes::Bytes>) -> EndpointRequestBuilder<'a, E, Ready> {
360        EndpointRequestBuilder {
361            inner: self.inner.body(body),
362            _marker: PhantomData,
363        }
364    }
365}
366
367impl<'a, E: Endpoint> EndpointRequestBuilder<'a, E, Ready> {
368    pub(crate) fn new_ready(inner: RequestBuilder<'a>) -> Self {
369        Self {
370            inner,
371            _marker: PhantomData,
372        }
373    }
374
375    /// Applies typed query parameters for `E::Query`.
376    ///
377    /// Optional at compile time: you can call [`.send_json()`](Self::send_json) without this method;
378    /// no query string from `E::Query` is sent unless you call `.query(...)`.
379    ///
380    /// Returns [`Error::QuerySerialize`](crate::Error::QuerySerialize) when serde serialization fails
381    /// (since 0.4.0 — failures are no longer ignored).
382    ///
383    /// # Examples
384    ///
385    /// ```no_run
386    /// # use better_fetch::{Client, Endpoint, Result, define_params};
387    /// # use http::Method;
388    /// # use serde::{Deserialize, Serialize};
389    /// define_params!(ItemParams for "/items/:id" { id: u64 });
390    ///
391    /// #[derive(Default, Serialize)]
392    /// struct ItemQuery { tag: Option<String> }
393    /// better_fetch::impl_serde_endpoint_query!(ItemQuery);
394    ///
395    /// struct GetItem;
396    /// impl Endpoint for GetItem {
397    ///     const METHOD: Method = Method::GET;
398    ///     const PATH: &'static str = "/items/:id";
399    ///     type Response = serde_json::Value;
400    ///     type Params = ItemParams;
401    ///     type Query = ItemQuery;
402    ///     type Body = ();
403    ///     type Headers = ();
404    /// }
405    ///
406    /// # #[tokio::main]
407    /// # async fn main() -> Result<()> {
408    /// let client = Client::new("https://api.example.com")?;
409    /// let _ = client
410    ///     .call::<GetItem>()
411    ///     .params(ItemParams { id: 1 })
412    ///     .query(ItemQuery { tag: Some("news".into()) })?
413    ///     .send_json()
414    ///     .await?;
415    /// # Ok(())
416    /// # }
417    /// ```
418    pub fn query(self, query: E::Query) -> crate::Result<Self> {
419        Ok(Self {
420            inner: query.apply_query(self.inner)?,
421            _marker: PhantomData,
422        })
423    }
424
425    /// Like [`Self::query`] but runs [`garde::Validate`] on the query value first (feature `validate`).
426    ///
427    /// Intended for serde query structs (including those using [`EndpointQueryDerive`](crate::EndpointQueryDerive)).
428    #[cfg(all(feature = "json", feature = "validate"))]
429    pub fn query_validated(self, query: E::Query) -> crate::Result<Self>
430    where
431        E::Query: serde::Serialize + garde::Validate,
432        <E::Query as garde::Validate>::Context: Default,
433    {
434        Ok(Self {
435            inner: apply_serialized_query_validated(query, self.inner)?,
436            _marker: PhantomData,
437        })
438    }
439
440    /// Adds a request header.
441    pub fn header(self, key: impl AsRef<str>, value: impl AsRef<str>) -> crate::Result<Self> {
442        Ok(Self {
443            inner: self.inner.header(key, value)?,
444            _marker: PhantomData,
445        })
446    }
447
448    /// Sets bearer authentication.
449    pub fn bearer_token(self, token: impl Into<String>) -> Self {
450        Self {
451            inner: self.inner.bearer_token(token),
452            _marker: PhantomData,
453        }
454    }
455
456    /// Attaches a cancellation token.
457    pub fn cancellation_token(self, token: crate::CancellationToken) -> Self {
458        Self {
459            inner: self.inner.cancellation_token(token),
460            _marker: PhantomData,
461        }
462    }
463
464    /// When `true`, [`send`](Self::send) returns `Err` on non-2xx.
465    pub fn throw_on_error(self, throw: bool) -> Self {
466        Self {
467            inner: self.inner.throw_on_error(throw),
468            _marker: PhantomData,
469        }
470    }
471
472    /// Overrides the client base URL for this request ([`RequestBuilder::base_url`](crate::RequestBuilder::base_url)).
473    pub fn base_url(self, base_url: impl AsRef<str>) -> crate::Result<Self> {
474        Ok(Self {
475            inner: self.inner.base_url(base_url)?,
476            _marker: PhantomData,
477        })
478    }
479
480    /// Overrides retry policy ([`RequestBuilder::retry`](crate::RequestBuilder::retry)).
481    pub fn retry(self, policy: crate::RetryPolicy) -> Self {
482        Self {
483            inner: self.inner.retry(policy),
484            _marker: PhantomData,
485        }
486    }
487
488    /// Overrides timeout ([`RequestBuilder::timeout`](crate::RequestBuilder::timeout)).
489    pub fn timeout(self, timeout: std::time::Duration) -> Self {
490        Self {
491            inner: self.inner.timeout(timeout),
492            _marker: PhantomData,
493        }
494    }
495
496    /// Streaming execution ([`RequestBuilder::send_stream`](crate::RequestBuilder::send_stream)).
497    pub async fn send_stream(self) -> crate::Result<crate::StreamingResponse> {
498        self.inner.send_stream().await
499    }
500
501    /// Caps response body size ([`RequestBuilder::max_response_bytes`](crate::RequestBuilder::max_response_bytes)).
502    pub fn max_response_bytes(self, limit: u64) -> Self {
503        Self {
504            inner: self.inner.max_response_bytes(limit),
505            _marker: PhantomData,
506        }
507    }
508
509    /// Skips registry JSON Schema validation for this request ([`RequestBuilder::disable_validation`](crate::RequestBuilder::disable_validation)).
510    #[cfg(feature = "schema-validate")]
511    pub fn disable_validation(self, disable: bool) -> Self {
512        Self {
513            inner: self.inner.disable_validation(disable),
514            _marker: PhantomData,
515        }
516    }
517
518    /// JSON request body ([`RequestBuilder::json`](crate::RequestBuilder::json)).
519    #[cfg(feature = "json")]
520    pub fn json<T: serde::Serialize>(self, body: &T) -> crate::Result<Self> {
521        Ok(Self {
522            inner: self.inner.json(body)?,
523            _marker: PhantomData,
524        })
525    }
526
527    /// Validated JSON request body (feature `validate`).
528    #[cfg(feature = "validate")]
529    pub fn json_validated<T>(self, body: &T) -> crate::Result<Self>
530    where
531        T: serde::Serialize + garde::Validate,
532        T::Context: Default,
533    {
534        Ok(Self {
535            inner: self.inner.json_validated(body)?,
536            _marker: PhantomData,
537        })
538    }
539
540    /// Raw request body ([`RequestBuilder::body`](crate::RequestBuilder::body)).
541    pub fn body(self, body: impl Into<bytes::Bytes>) -> Self {
542        Self {
543            inner: self.inner.body(body),
544            _marker: PhantomData,
545        }
546    }
547
548    /// Executes the request and returns [`Response`](crate::Response).
549    pub async fn send(self) -> crate::Result<crate::Response> {
550        self.inner.send().await
551    }
552
553    /// Applies typed request body for `E::Body`.
554    pub fn with_body(self, body: E::Body) -> crate::Result<Self> {
555        Ok(Self {
556            inner: body.apply_body(self.inner)?,
557            _marker: PhantomData,
558        })
559    }
560
561    /// Applies typed headers for `E::Headers`.
562    pub fn with_headers(self, headers: E::Headers) -> crate::Result<Self> {
563        Ok(Self {
564            inner: headers.apply_headers(self.inner)?,
565            _marker: PhantomData,
566        })
567    }
568
569    /// Like [`Self::with_headers`] but runs [`garde::Validate`] on the headers value first (feature `validate`).
570    #[cfg(feature = "validate")]
571    pub fn with_headers_validated(self, headers: E::Headers) -> crate::Result<Self>
572    where
573        E::Headers: garde::Validate,
574        <E::Headers as garde::Validate>::Context: Default,
575    {
576        garde::Validate::validate(&headers).map_err(|report: garde::Report| {
577            Error::RequestValidation {
578                message: report.to_string(),
579            }
580        })?;
581        self.with_headers(headers)
582    }
583
584    /// Executes and deserializes `E::Response` (feature `json`).
585    #[cfg(feature = "json")]
586    pub async fn send_json(self) -> crate::Result<E::Response> {
587        self.inner.send().await?.json::<E::Response>().await
588    }
589
590    /// Success/error deserialization by status (feature `json`).
591    #[cfg(feature = "json")]
592    pub async fn send_api<T, ErrBody>(self) -> crate::Result<std::result::Result<T, ErrBody>>
593    where
594        T: serde::de::DeserializeOwned,
595        ErrBody: serde::de::DeserializeOwned,
596    {
597        crate::api_response::into_api_result(self.inner.send().await?)
598    }
599}
600
601impl<'a, E: Endpoint> Deref for EndpointRequestBuilder<'a, E, Ready> {
602    type Target = RequestBuilder<'a>;
603
604    fn deref(&self) -> &Self::Target {
605        &self.inner
606    }
607}
608
609/// Defines path parameters for a route and implements [`EndpointParams`].
610///
611/// Each struct field maps to a `:field` segment in `path` (by field name).
612/// For compile-time path validation, use `#[derive(EndpointParamsDerive)]` (feature `macros`).
613///
614/// # Examples
615///
616/// ```
617/// use better_fetch::{define_params, EndpointParams, NeedsParams};
618///
619/// define_params!(GetTodoParams for "/todos/:id" { id: u64 });
620///
621/// fn assert_needs_params<T: EndpointParams<BuilderState = NeedsParams>>() {}
622/// assert_needs_params::<GetTodoParams>();
623/// ```
624#[macro_export]
625macro_rules! define_params {
626    (
627        $name:ident for $path:literal {
628            $( $field:ident : $ty:ty ),* $(,)?
629        }
630    ) => {
631        #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
632        pub struct $name {
633            $( pub $field: $ty, )*
634        }
635
636        impl $crate::EndpointParams for $name {
637            type BuilderState = $crate::NeedsParams;
638
639            fn apply_params(self, builder: $crate::RequestBuilder<'_>) -> $crate::RequestBuilder<'_> {
640                let builder = builder;
641                $(
642                    let builder = builder.param(stringify!($field), self.$field);
643                )*
644                builder
645            }
646        }
647    };
648}
649
650/// Implements [`EndpointQuery`] for a serde-serializable query struct (feature `json`).
651#[cfg(feature = "json")]
652#[macro_export]
653macro_rules! impl_serde_endpoint_query {
654    ($ty:ty) => {
655        impl $crate::EndpointQuery for $ty {
656            fn apply_query(
657                self,
658                builder: $crate::RequestBuilder<'_>,
659            ) -> $crate::Result<$crate::RequestBuilder<'_>> {
660                $crate::endpoint::apply_serialized_query(self, builder)
661            }
662        }
663    };
664}
665
666/// Defines a simple [`Endpoint`] with optional typed params and query.
667///
668/// # Examples
669///
670/// ```
671/// use better_fetch::{endpoint, define_params};
672/// use serde::Deserialize;
673///
674/// #[derive(Deserialize)]
675/// pub struct Health {
676///     ok: bool,
677/// }
678///
679/// endpoint!(HealthCheck, GET, "/health", Response = Health);
680///
681/// define_params!(GetTodoParams for "/todos/:id" { id: u64 });
682/// endpoint!(GetTodo, GET, "/todos/:id", Response = Health, Params = GetTodoParams);
683/// ```
684#[macro_export]
685macro_rules! endpoint {
686    (
687        $name:ident,
688        $method:ident,
689        $path:literal,
690        Response = $response:ty
691    ) => {
692        $crate::endpoint!(
693            $name,
694            $method,
695            $path,
696            Response = $response,
697            Params = (),
698            Query = ()
699        );
700    };
701    (
702        $name:ident,
703        $method:ident,
704        $path:literal,
705        Response = $response:ty,
706        Params = $params:ty
707    ) => {
708        $crate::endpoint!(
709            $name,
710            $method,
711            $path,
712            Response = $response,
713            Params = $params,
714            Query = ()
715        );
716    };
717    (
718        $name:ident,
719        $method:ident,
720        $path:literal,
721        Response = $response:ty,
722        Query = $query:ty
723    ) => {
724        $crate::endpoint!(
725            $name,
726            $method,
727            $path,
728            Response = $response,
729            Params = (),
730            Query = $query
731        );
732    };
733    (
734        $name:ident,
735        $method:ident,
736        $path:literal,
737        Response = $response:ty,
738        Params = $params:ty,
739        Query = $query:ty
740    ) => {
741        pub struct $name;
742        impl $crate::Endpoint for $name {
743            const METHOD: http::Method = http::Method::$method;
744            const PATH: &'static str = $path;
745            type Response = $response;
746            type Params = $params;
747            type Query = $query;
748            type Body = ();
749            type Headers = ();
750        }
751    };
752}
753
754#[cfg(test)]
755mod tests {
756    use super::*;
757
758    define_params!(TestParams for "/items/:id" { id: u64 });
759
760    #[test]
761    fn params_builder_state_is_needs_params() {
762        fn assert_needs<T: EndpointParams<BuilderState = NeedsParams>>() {}
763        assert_needs::<TestParams>();
764    }
765
766    #[test]
767    fn unit_params_builder_state_is_ready() {
768        fn assert_ready<T: EndpointParams<BuilderState = Ready>>() {}
769        assert_ready::<()>();
770    }
771}