trillium_client/
client.rs

1use crate::{Conn, IntoUrl, Pool, USER_AGENT};
2use std::{fmt::Debug, sync::Arc};
3use trillium_http::{
4    transport::BoxedTransport, HeaderName, HeaderValues, Headers, KnownHeaderName, Method,
5    ReceivedBodyState,
6};
7use trillium_server_common::{
8    url::{Origin, Url},
9    Connector, ObjectSafeConnector,
10};
11
12/**
13A client contains a Config and an optional connection pool and builds
14conns.
15
16*/
17#[derive(Clone, Debug)]
18pub struct Client {
19    config: Arc<dyn ObjectSafeConnector>,
20    pool: Option<Pool<Origin, BoxedTransport>>,
21    base: Option<Arc<Url>>,
22    default_headers: Arc<Headers>,
23}
24
25macro_rules! method {
26    ($fn_name:ident, $method:ident) => {
27        method!(
28            $fn_name,
29            $method,
30            concat!(
31                // yep, macro-generated doctests
32                "Builds a new client conn with the ",
33                stringify!($fn_name),
34                " http method and the provided url.
35
36```
37# use trillium_testing::prelude::*;
38# use trillium_smol::ClientConfig;
39# use trillium_client::Client;
40let client = Client::new(ClientConfig::default());
41let conn = client.",
42                stringify!($fn_name),
43                "(\"http://localhost:8080/some/route\"); //<-
44
45assert_eq!(conn.method(), Method::",
46                stringify!($method),
47                ");
48assert_eq!(conn.url().to_string(), \"http://localhost:8080/some/route\");
49```
50"
51            )
52        );
53    };
54
55    ($fn_name:ident, $method:ident, $doc_comment:expr) => {
56        #[doc = $doc_comment]
57        pub fn $fn_name(&self, url: impl IntoUrl) -> Conn {
58            self.build_conn(Method::$method, url)
59        }
60    };
61}
62
63pub(crate) fn default_request_headers() -> Headers {
64    Headers::new()
65        .with_inserted_header(KnownHeaderName::UserAgent, USER_AGENT)
66        .with_inserted_header(KnownHeaderName::Accept, "*/*")
67}
68
69impl Client {
70    /// builds a new client from this `Connector`
71    pub fn new(config: impl Connector) -> Self {
72        Self {
73            config: config.arced(),
74            pool: None,
75            base: None,
76            default_headers: Arc::new(default_request_headers()),
77        }
78    }
79
80    /// chainable method to remove a header from default request headers
81    pub fn without_default_header(mut self, name: impl Into<HeaderName<'static>>) -> Self {
82        self.default_headers_mut().remove(name);
83        self
84    }
85
86    /// chainable method to insert a new default request header, replacing any existing value
87    pub fn with_default_header(
88        mut self,
89        name: impl Into<HeaderName<'static>>,
90        value: impl Into<HeaderValues>,
91    ) -> Self {
92        self.default_headers_mut().insert(name, value);
93        self
94    }
95
96    /// borrow the default headers
97    pub fn default_headers(&self) -> &Headers {
98        &self.default_headers
99    }
100
101    /// borrow the default headers mutably
102    ///
103    /// calling this will copy-on-write if the default headers are shared with another client clone
104    pub fn default_headers_mut(&mut self) -> &mut Headers {
105        Arc::make_mut(&mut self.default_headers)
106    }
107
108    /**
109    chainable constructor to enable connection pooling. this can be
110    combined with [`Client::with_config`]
111
112
113    ```
114    use trillium_smol::ClientConfig;
115    use trillium_client::Client;
116
117    let client = Client::new(ClientConfig::default())
118        .with_default_pool(); //<-
119    ```
120    */
121    pub fn with_default_pool(mut self) -> Self {
122        self.pool = Some(Pool::default());
123        self
124    }
125
126    /**
127    builds a new conn.
128
129    if the client has pooling enabled and there is
130    an available connection to the dns-resolved socket (ip and port),
131    the new conn will reuse that when it is sent.
132
133    ```
134    use trillium_smol::ClientConfig;
135    use trillium_client::Client;
136    use trillium_testing::prelude::*;
137    let client = Client::new(ClientConfig::default());
138
139    let conn = client.build_conn("get", "http://trillium.rs"); //<-
140
141    assert_eq!(conn.method(), Method::Get);
142    assert_eq!(conn.url().host_str().unwrap(), "trillium.rs");
143    ```
144    */
145    pub fn build_conn<M>(&self, method: M, url: impl IntoUrl) -> Conn
146    where
147        M: TryInto<Method>,
148        <M as TryInto<Method>>::Error: Debug,
149    {
150        Conn {
151            url: self.build_url(url).unwrap(),
152            method: method.try_into().unwrap(),
153            request_headers: Headers::clone(&self.default_headers),
154            response_headers: Headers::new(),
155            transport: None,
156            status: None,
157            request_body: None,
158            pool: self.pool.clone(),
159            buffer: Vec::with_capacity(128).into(),
160            response_body_state: ReceivedBodyState::Start,
161            config: Arc::clone(&self.config),
162            headers_finalized: false,
163        }
164    }
165
166    /// borrow the connector for this client
167    pub fn connector(&self) -> &Arc<dyn ObjectSafeConnector> {
168        &self.config
169    }
170
171    /**
172    The pool implementation currently accumulates a small memory
173    footprint for each new host. If your application is reusing a pool
174    against a large number of unique hosts, call this method
175    intermittently.
176    */
177    pub fn clean_up_pool(&self) {
178        if let Some(pool) = &self.pool {
179            pool.cleanup();
180        }
181    }
182
183    /// chainable method to set the base for this client
184    pub fn with_base(mut self, base: impl IntoUrl) -> Self {
185        self.set_base(base).unwrap();
186        self
187    }
188
189    /// retrieve the base for this client, if any
190    pub fn base(&self) -> Option<&Url> {
191        self.base.as_deref()
192    }
193
194    /// attempt to build a url from this IntoUrl and the [`Client::base`], if set
195    pub fn build_url(&self, url: impl IntoUrl) -> crate::Result<Url> {
196        url.into_url(self.base())
197    }
198
199    /// set the base for this client
200    pub fn set_base(&mut self, base: impl IntoUrl) -> crate::Result<()> {
201        let mut base = base.into_url(None)?;
202
203        if !base.path().ends_with('/') {
204            log::warn!("appending a trailing / to {base}");
205            base.set_path(&format!("{}/", base.path()));
206        }
207
208        self.base = Some(Arc::new(base));
209        Ok(())
210    }
211
212    method!(get, Get);
213    method!(post, Post);
214    method!(put, Put);
215    method!(delete, Delete);
216    method!(patch, Patch);
217}
218
219impl<T: Connector> From<T> for Client {
220    fn from(connector: T) -> Self {
221        Self::new(connector)
222    }
223}