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    /// JSON request body ([`RequestBuilder::json`](crate::RequestBuilder::json)).
510    #[cfg(feature = "json")]
511    pub fn json<T: serde::Serialize>(self, body: &T) -> crate::Result<Self> {
512        Ok(Self {
513            inner: self.inner.json(body)?,
514            _marker: PhantomData,
515        })
516    }
517
518    /// Validated JSON request body (feature `validate`).
519    #[cfg(feature = "validate")]
520    pub fn json_validated<T>(self, body: &T) -> crate::Result<Self>
521    where
522        T: serde::Serialize + garde::Validate,
523        T::Context: Default,
524    {
525        Ok(Self {
526            inner: self.inner.json_validated(body)?,
527            _marker: PhantomData,
528        })
529    }
530
531    /// Raw request body ([`RequestBuilder::body`](crate::RequestBuilder::body)).
532    pub fn body(self, body: impl Into<bytes::Bytes>) -> Self {
533        Self {
534            inner: self.inner.body(body),
535            _marker: PhantomData,
536        }
537    }
538
539    /// Executes the request and returns [`Response`](crate::Response).
540    pub async fn send(self) -> crate::Result<crate::Response> {
541        self.inner.send().await
542    }
543
544    /// Applies typed request body for `E::Body`.
545    pub fn with_body(self, body: E::Body) -> crate::Result<Self> {
546        Ok(Self {
547            inner: body.apply_body(self.inner)?,
548            _marker: PhantomData,
549        })
550    }
551
552    /// Applies typed headers for `E::Headers`.
553    pub fn with_headers(self, headers: E::Headers) -> crate::Result<Self> {
554        Ok(Self {
555            inner: headers.apply_headers(self.inner)?,
556            _marker: PhantomData,
557        })
558    }
559
560    /// Like [`Self::with_headers`] but runs [`garde::Validate`] on the headers value first (feature `validate`).
561    #[cfg(feature = "validate")]
562    pub fn with_headers_validated(self, headers: E::Headers) -> crate::Result<Self>
563    where
564        E::Headers: garde::Validate,
565        <E::Headers as garde::Validate>::Context: Default,
566    {
567        garde::Validate::validate(&headers).map_err(|report: garde::Report| {
568            Error::RequestValidation {
569                message: report.to_string(),
570            }
571        })?;
572        self.with_headers(headers)
573    }
574
575    /// Executes and deserializes `E::Response` (feature `json`).
576    #[cfg(feature = "json")]
577    pub async fn send_json(self) -> crate::Result<E::Response> {
578        self.inner.send().await?.json::<E::Response>().await
579    }
580
581    /// Success/error deserialization by status (feature `json`).
582    #[cfg(feature = "json")]
583    pub async fn send_api<T, ErrBody>(self) -> crate::Result<std::result::Result<T, ErrBody>>
584    where
585        T: serde::de::DeserializeOwned,
586        ErrBody: serde::de::DeserializeOwned,
587    {
588        crate::api_response::into_api_result(self.inner.send().await?)
589    }
590}
591
592impl<'a, E: Endpoint> Deref for EndpointRequestBuilder<'a, E, Ready> {
593    type Target = RequestBuilder<'a>;
594
595    fn deref(&self) -> &Self::Target {
596        &self.inner
597    }
598}
599
600/// Defines path parameters for a route and implements [`EndpointParams`].
601///
602/// Each struct field maps to a `:field` segment in `path` (by field name).
603/// For compile-time path validation, use `#[derive(EndpointParamsDerive)]` (feature `macros`).
604///
605/// # Examples
606///
607/// ```
608/// use better_fetch::{define_params, EndpointParams, NeedsParams};
609///
610/// define_params!(GetTodoParams for "/todos/:id" { id: u64 });
611///
612/// fn assert_needs_params<T: EndpointParams<BuilderState = NeedsParams>>() {}
613/// assert_needs_params::<GetTodoParams>();
614/// ```
615#[macro_export]
616macro_rules! define_params {
617    (
618        $name:ident for $path:literal {
619            $( $field:ident : $ty:ty ),* $(,)?
620        }
621    ) => {
622        #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
623        pub struct $name {
624            $( pub $field: $ty, )*
625        }
626
627        impl $crate::EndpointParams for $name {
628            type BuilderState = $crate::NeedsParams;
629
630            fn apply_params(self, builder: $crate::RequestBuilder<'_>) -> $crate::RequestBuilder<'_> {
631                let builder = builder;
632                $(
633                    let builder = builder.param(stringify!($field), self.$field);
634                )*
635                builder
636            }
637        }
638    };
639}
640
641/// Implements [`EndpointQuery`] for a serde-serializable query struct (feature `json`).
642#[cfg(feature = "json")]
643#[macro_export]
644macro_rules! impl_serde_endpoint_query {
645    ($ty:ty) => {
646        impl $crate::EndpointQuery for $ty {
647            fn apply_query(
648                self,
649                builder: $crate::RequestBuilder<'_>,
650            ) -> $crate::Result<$crate::RequestBuilder<'_>> {
651                $crate::endpoint::apply_serialized_query(self, builder)
652            }
653        }
654    };
655}
656
657/// Defines a simple [`Endpoint`] with optional typed params and query.
658///
659/// # Examples
660///
661/// ```
662/// use better_fetch::{endpoint, define_params};
663/// use serde::Deserialize;
664///
665/// #[derive(Deserialize)]
666/// pub struct Health {
667///     ok: bool,
668/// }
669///
670/// endpoint!(HealthCheck, GET, "/health", Response = Health);
671///
672/// define_params!(GetTodoParams for "/todos/:id" { id: u64 });
673/// endpoint!(GetTodo, GET, "/todos/:id", Response = Health, Params = GetTodoParams);
674/// ```
675#[macro_export]
676macro_rules! endpoint {
677    (
678        $name:ident,
679        $method:ident,
680        $path:literal,
681        Response = $response:ty
682    ) => {
683        $crate::endpoint!(
684            $name,
685            $method,
686            $path,
687            Response = $response,
688            Params = (),
689            Query = ()
690        );
691    };
692    (
693        $name:ident,
694        $method:ident,
695        $path:literal,
696        Response = $response:ty,
697        Params = $params:ty
698    ) => {
699        $crate::endpoint!(
700            $name,
701            $method,
702            $path,
703            Response = $response,
704            Params = $params,
705            Query = ()
706        );
707    };
708    (
709        $name:ident,
710        $method:ident,
711        $path:literal,
712        Response = $response:ty,
713        Query = $query:ty
714    ) => {
715        $crate::endpoint!(
716            $name,
717            $method,
718            $path,
719            Response = $response,
720            Params = (),
721            Query = $query
722        );
723    };
724    (
725        $name:ident,
726        $method:ident,
727        $path:literal,
728        Response = $response:ty,
729        Params = $params:ty,
730        Query = $query:ty
731    ) => {
732        pub struct $name;
733        impl $crate::Endpoint for $name {
734            const METHOD: http::Method = http::Method::$method;
735            const PATH: &'static str = $path;
736            type Response = $response;
737            type Params = $params;
738            type Query = $query;
739            type Body = ();
740            type Headers = ();
741        }
742    };
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748
749    define_params!(TestParams for "/items/:id" { id: u64 });
750
751    #[test]
752    fn params_builder_state_is_needs_params() {
753        fn assert_needs<T: EndpointParams<BuilderState = NeedsParams>>() {}
754        assert_needs::<TestParams>();
755    }
756
757    #[test]
758    fn unit_params_builder_state_is_ready() {
759        fn assert_ready<T: EndpointParams<BuilderState = Ready>>() {}
760        assert_ready::<()>();
761    }
762}