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