1use crate::{Conn, IntoUrl, Pool, USER_AGENT, h3::H3ClientState};
2use std::{fmt::Debug, sync::Arc, time::Duration};
3use trillium_http::{
4 HeaderName, HeaderValues, Headers, HttpContext, KnownHeaderName, Method, ReceivedBodyState,
5 TypeSet, Version::Http1_1,
6};
7use trillium_server_common::{
8 ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Transport,
9 url::{Origin, Url},
10};
11
12#[derive(Clone, Debug, fieldwork::Fieldwork)]
15pub struct Client {
16 config: ArcedConnector,
17 h3: Option<H3ClientState>,
18 pool: Option<Pool<Origin, Box<dyn Transport>>>,
19
20 #[field(get)]
22 base: Option<Arc<Url>>,
23
24 #[field(get)]
26 default_headers: Arc<Headers>,
27
28 #[field(get, set, with, copy, option_set_some)]
30 timeout: Option<Duration>,
31
32 #[field(get, get_mut, set, with, into)]
34 context: Arc<HttpContext>,
35}
36
37macro_rules! method {
38 ($fn_name:ident, $method:ident) => {
39 method!(
40 $fn_name,
41 $method,
42 concat!(
43 "Builds a new client conn with the ",
45 stringify!($fn_name),
46 " http method and the provided url.
47
48```
49use trillium_client::{Client, Method};
50use trillium_testing::client_config;
51
52let client = Client::new(client_config());
53let conn = client.",
54 stringify!($fn_name),
55 "(\"http://localhost:8080/some/route\"); //<-
56
57assert_eq!(conn.method(), Method::",
58 stringify!($method),
59 ");
60assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
61```
62"
63 )
64 );
65 };
66
67 ($fn_name:ident, $method:ident, $doc_comment:expr_2021) => {
68 #[doc = $doc_comment]
69 pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
70 self.build_conn(Method::$method, url)
71 }
72 };
73}
74
75pub(crate) fn default_request_headers() -> Headers {
76 Headers::new()
77 .with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
78 .with_inserted_header(KnownHeaderName::Accept, "*/*")
79}
80
81impl Client {
82 method!(get, Get);
83
84 method!(post, Post);
85
86 method!(put, Put);
87
88 method!(delete, Delete);
89
90 method!(patch, Patch);
91
92 pub fn new(connector: impl Connector) -> Self {
94 Self {
95 config: ArcedConnector::new(connector),
96 h3: None,
97 pool: Some(Pool::default()),
98 base: None,
99 default_headers: Arc::new(default_request_headers()),
100 timeout: None,
101 context: Default::default(),
102 }
103 }
104
105 pub fn new_with_quic<C: Connector, Q: QuicClientConfig<C>>(connector: C, quic: Q) -> Self {
115 let arced_quic = ArcedQuicClientConfig::new(&connector, quic);
117 Self {
118 config: ArcedConnector::new(connector),
119 h3: Some(H3ClientState::new(arced_quic)),
120 pool: Some(Pool::default()),
121 base: None,
122 default_headers: Arc::new(default_request_headers()),
123 timeout: None,
124 context: Default::default(),
125 }
126 }
127
128 pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
130 self.default_headers_mut().remove(name);
131 self
132 }
133
134 pub fn with_default_header(
136 mut self,
137 name: impl Into<HeaderName<'static>>,
138 value: impl Into<HeaderValues>,
139 ) -> Self {
140 self.default_headers_mut().insert(name, value);
141 self
142 }
143
144 pub fn default_headers_mut(&mut self) -> &mut Headers {
148 Arc::make_mut(&mut self.default_headers)
149 }
150
151 pub fn without_keepalive(mut self) -> Self {
160 self.pool = None;
161 self
162 }
163
164 pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
181 where
182 M: TryInto<Method>,
183 <M as TryInto<Method>>::Error: Debug,
184 {
185 let method = method.try_into().unwrap();
186 let (url, request_target) = if let Some(base) = &self.base
187 && let Some(request_target) = url.request_target(method)
188 {
189 ((**base).clone(), Some(request_target))
190 } else {
191 (self.build_url(url).unwrap(), None)
192 };
193
194 Conn {
195 url,
196 method,
197 request_headers: Headers::clone(&self.default_headers),
198 response_headers: Headers::new(),
199 transport: None,
200 status: None,
201 request_body: None,
202 pool: self.pool.clone(),
203 h3: self.h3.clone(),
204 buffer: Vec::with_capacity(128).into(),
205 response_body_state: ReceivedBodyState::Start,
206 config: self.config.clone(),
207 headers_finalized: false,
208 timeout: self.timeout,
209 http_version: Http1_1,
210 max_head_length: 8 * 1024,
211 state: TypeSet::new(),
212 context: self.context.clone(),
213 authority: None,
214 scheme: None,
215 path: None,
216 request_target,
217 request_trailers: None,
218 response_trailers: None,
219 }
220 }
221
222 pub fn connector(&self) -> &ArcedConnector {
224 &self.config
225 }
226
227 pub fn clean_up_pool(&self) {
232 if let Some(pool) = &self.pool {
233 pool.cleanup();
234 }
235 }
236
237 pub fn with_base(mut self, base: impl IntoUrl) -> Self {
239 self.set_base(base).unwrap();
240 self
241 }
242
243 pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
245 url.into_url(self.base())
246 }
247
248 pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
250 let mut base = base.into_url(None)?;
251
252 if !base.path().ends_with('/') {
253 log::warn!("appending a trailing / to {base}");
254 base.set_path(&format!("{}/", base.path()));
255 }
256
257 self.base = Some(Arc::new(base));
258 Ok(())
259 }
260
261 pub fn base_mut(&mut self) -> Option<&mut Url> {
266 let base = self.base.as_mut()?;
267 Some(Arc::make_mut(base))
268 }
269}
270
271impl<T: Connector> From<T> for Client {
272 fn from(connector: T) -> Self {
273 Self::new(connector)
274 }
275}