Skip to main content

typeway_client/
request_builder.rs

1//! Per-call [`RequestBuilder`] for overriding headers, query params, and timeouts.
2//!
3//! Obtained via [`Client::request`](crate::Client::request). This allows
4//! per-call customization without changing the client-wide configuration.
5//!
6//! # Example
7//!
8//! ```ignore
9//! let user = client
10//!     .request::<GetUserEndpoint>((42u32,))
11//!     .header("X-Request-Id", "abc123")
12//!     .timeout(Duration::from_secs(5))
13//!     .send()
14//!     .await?;
15//! ```
16
17use std::marker::PhantomData;
18use std::time::Duration;
19
20use http::header::{HeaderMap, HeaderName, HeaderValue};
21
22use crate::call::CallEndpoint;
23use crate::client::Client;
24use crate::error::ClientError;
25use crate::typed_response::TypedResponse;
26
27/// A builder for a single request with per-call overrides.
28///
29/// Created by [`Client::request`](crate::Client::request). The builder allows
30/// adding extra headers, query parameters, and a per-request timeout before
31/// sending. Retries are **not** applied on the builder path — use
32/// [`Client::call`](crate::Client::call) for automatic retries.
33pub struct RequestBuilder<'a, E: CallEndpoint> {
34    client: &'a Client,
35    args: E::Args,
36    extra_headers: HeaderMap,
37    query_params: Vec<(String, String)>,
38    timeout: Option<Duration>,
39    _endpoint: PhantomData<E>,
40}
41
42impl<'a, E: CallEndpoint> RequestBuilder<'a, E> {
43    pub(crate) fn new(client: &'a Client, args: E::Args) -> Self {
44        Self {
45            client,
46            args,
47            extra_headers: HeaderMap::new(),
48            query_params: Vec::new(),
49            timeout: None,
50            _endpoint: PhantomData,
51        }
52    }
53
54    /// Add a header to this specific request.
55    pub fn header(mut self, name: impl Into<HeaderName>, value: impl Into<HeaderValue>) -> Self {
56        self.extra_headers.insert(name.into(), value.into());
57        self
58    }
59
60    /// Add a query parameter to the URL.
61    pub fn query(mut self, key: &str, value: &str) -> Self {
62        self.query_params.push((key.to_string(), value.to_string()));
63        self
64    }
65
66    /// Override the timeout for this specific request.
67    pub fn timeout(mut self, duration: Duration) -> Self {
68        self.timeout = Some(duration);
69        self
70    }
71
72    /// Send the request and deserialize the response body.
73    ///
74    /// Retries are **not** applied — this executes a single attempt.
75    pub async fn send(self) -> Result<E::Response, ClientError> {
76        let overrides = CallOverrides {
77            extra_headers: if self.extra_headers.is_empty() {
78                None
79            } else {
80                Some(self.extra_headers)
81            },
82            query_string: None,
83            query_params: if self.query_params.is_empty() {
84                None
85            } else {
86                Some(self.query_params)
87            },
88            timeout: self.timeout,
89        };
90        let (_, body) = self
91            .client
92            .call_inner::<E>(&self.args, Some(&overrides))
93            .await?;
94        Ok(body)
95    }
96
97    /// Send the request and return the full response metadata alongside the body.
98    ///
99    /// Retries are **not** applied — this executes a single attempt.
100    pub async fn send_full(self) -> Result<TypedResponse<E::Response>, ClientError> {
101        let overrides = CallOverrides {
102            extra_headers: if self.extra_headers.is_empty() {
103                None
104            } else {
105                Some(self.extra_headers)
106            },
107            query_string: None,
108            query_params: if self.query_params.is_empty() {
109                None
110            } else {
111                Some(self.query_params)
112            },
113            timeout: self.timeout,
114        };
115        self.client
116            .call_inner::<E>(&self.args, Some(&overrides))
117            .await
118            .map(|(meta, body)| TypedResponse {
119                body,
120                status: meta.status,
121                headers: meta.headers,
122            })
123    }
124}
125
126/// Per-call overrides applied by `RequestBuilder` and `call_inner`.
127pub(crate) struct CallOverrides {
128    pub extra_headers: Option<HeaderMap>,
129    /// Pre-encoded query string (from `serde_urlencoded`), appended as-is.
130    pub query_string: Option<String>,
131    /// Individual key-value query parameters.
132    pub query_params: Option<Vec<(String, String)>>,
133    pub timeout: Option<Duration>,
134}
135
136/// Response metadata returned alongside the deserialized body from `call_inner`.
137pub(crate) struct ResponseMeta {
138    pub status: http::StatusCode,
139    pub headers: HeaderMap,
140}