Skip to main content

trillium_conn_id/
lib.rs

1/*!
2Trillium crate to add identifiers to conns.
3
4This crate provides the following utilities:
5* [`ConnId`] a handler which must be called for the rest of this crate to function
6* [`log_formatter::conn_id`] a formatter to use with trillium_logger
7  (note that this does not depend on the trillium_logger crate and is very lightweight
8  if you do not use that crate)
9* [`ConnIdExt`] an extension trait for retrieving the id from a conn
10
11*/
12#![forbid(unsafe_code)]
13#![deny(
14    missing_copy_implementations,
15    rustdoc::missing_crate_level_docs,
16    missing_debug_implementations,
17    missing_docs,
18    nonstandard_style,
19    unused_qualifications
20)]
21
22use fastrand::Rng;
23use std::{
24    fmt::{Debug, Formatter, Result},
25    iter::repeat_with,
26    ops::Deref,
27    sync::{Arc, Mutex},
28};
29use trillium::{async_trait, Conn, Handler, HeaderName, KnownHeaderName, StateSet};
30
31#[derive(Default)]
32enum IdGenerator {
33    #[default]
34    Default,
35    SeededFastrand(Arc<Mutex<Rng>>),
36    Fn(Box<dyn Fn() -> String + Send + Sync + 'static>),
37}
38
39impl IdGenerator {
40    fn generate(&self) -> Id {
41        match self {
42            IdGenerator::Default => Id::default(),
43            IdGenerator::SeededFastrand(rng) => Id::with_rng(&mut rng.lock().unwrap()),
44            IdGenerator::Fn(gen_fun) => Id(gen_fun()),
45        }
46    }
47}
48
49/**
50Trillium handler to set a identifier for every Conn.
51
52By default, it will use an inbound `x-request-id` request header or if
53that is missing, populate a ten character random id. This handler will
54set an outbound `x-request-id` header as well by default. All of this
55behavior can be customized with [`ConnId::with_request_header`],
56[`ConnId::with_response_header`] and [`ConnId::with_id_generator`]
57*/
58pub struct ConnId {
59    request_header: Option<HeaderName<'static>>,
60    response_header: Option<HeaderName<'static>>,
61    id_generator: IdGenerator,
62}
63
64impl Debug for ConnId {
65    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
66        f.debug_struct("ConnId")
67            .field("request_header", &self.request_header)
68            .field("response_header", &self.response_header)
69            .field("id_generator", &self.id_generator)
70            .finish()
71    }
72}
73
74impl Default for ConnId {
75    fn default() -> Self {
76        Self {
77            request_header: Some(KnownHeaderName::XrequestId.into()),
78            response_header: Some(KnownHeaderName::XrequestId.into()),
79            id_generator: Default::default(),
80        }
81    }
82}
83
84impl ConnId {
85    /**
86    Constructs a new ConnId handler
87    ```
88    # use trillium_testing::prelude::*;
89    # use trillium_conn_id::ConnId;
90    let app = (ConnId::new().with_seed(1000), "ok"); // seeded for testing
91    assert_ok!(
92        get("/").on(&app),
93        "ok",
94        "x-request-id" => "J4lzoPXcT5"
95    );
96
97    assert_headers!(
98        get("/")
99            .with_request_header("x-request-id", "inbound")
100            .on(&app),
101        "x-request-id" => "inbound"
102    );
103    ```
104    */
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /**
110    Specifies a request header to use. If this header is provided on
111    the inbound request, the id will be used unmodified. To disable
112    this behavior, see [`ConnId::without_request_header`]
113
114    ```
115    # use trillium_testing::prelude::*;
116    # use trillium_conn_id::ConnId;
117
118    let app = (
119        ConnId::new().with_request_header("x-custom-id"),
120        "ok"
121    );
122
123    assert_headers!(
124        get("/")
125            .with_request_header("x-custom-id", "inbound")
126            .on(&app),
127        "x-request-id" => "inbound"
128    );
129    ```
130
131    */
132    pub fn with_request_header(mut self, request_header: impl Into<HeaderName<'static>>) -> Self {
133        self.request_header = Some(request_header.into());
134        self
135    }
136
137    /**
138    disables the default behavior of reusing an inbound header for
139    the request id. If a ConnId is configured
140    `without_request_header`, a new id will always be generated
141    */
142    pub fn without_request_header(mut self) -> Self {
143        self.request_header = None;
144        self
145    }
146
147    /**
148    Specifies a response header to set. To disable this behavior, see
149    [`ConnId::without_response_header`]
150
151    ```
152    # use trillium_testing::prelude::*;
153    # use trillium_conn_id::ConnId;
154    let app = (
155        ConnId::new()
156            .with_seed(1000) // for testing
157            .with_response_header("x-custom-header"),
158        "ok"
159    );
160
161    assert_headers!(
162        get("/").on(&app),
163        "x-custom-header" => "J4lzoPXcT5"
164    );
165    ```
166    */
167    pub fn with_response_header(mut self, response_header: impl Into<HeaderName<'static>>) -> Self {
168        self.response_header = Some(response_header.into());
169        self
170    }
171
172    /**
173    Disables the default behavior of sending the conn id as a response
174    header. A request id will be available within the application
175    through use of [`ConnIdExt`] but will not be sent as part of the
176    response.
177    */
178    pub fn without_response_header(mut self) -> Self {
179        self.response_header = None;
180        self
181    }
182
183    /**
184    Provide an alternative generator function for ids. The default
185    is a ten-character alphanumeric random sequence.
186
187    ```
188    # use trillium_testing::prelude::*;
189    # use trillium_conn_id::ConnId;
190    # use uuid::Uuid;
191    let app = (
192        ConnId::new().with_id_generator(|| Uuid::new_v4().to_string()),
193        "ok"
194    );
195
196    // assert that the id is a valid uuid, even if we can't assert a specific value
197    assert!(Uuid::parse_str(get("/").on(&app).response_headers().get_str("x-request-id").unwrap()).is_ok());
198    ```
199    */
200    pub fn with_id_generator<F>(mut self, id_generator: F) -> Self
201    where
202        F: Fn() -> String + Send + Sync + 'static,
203    {
204        self.id_generator = IdGenerator::Fn(Box::new(id_generator));
205        self
206    }
207
208    /// seed a shared rng
209    ///
210    /// this is primarily useful for tests
211    pub fn with_seed(mut self, seed: u64) -> Self {
212        self.id_generator = IdGenerator::SeededFastrand(Arc::new(Mutex::new(Rng::with_seed(seed))));
213        self
214    }
215
216    fn generate_id(&self) -> Id {
217        self.id_generator.generate()
218    }
219}
220
221#[derive(Clone, Debug)]
222struct Id(String);
223
224impl Deref for Id {
225    type Target = str;
226
227    fn deref(&self) -> &Self::Target {
228        &self.0
229    }
230}
231
232impl std::fmt::Display for Id {
233    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
234        f.write_str(self)
235    }
236}
237
238impl Default for Id {
239    fn default() -> Self {
240        Self(repeat_with(fastrand::alphanumeric).take(10).collect())
241    }
242}
243
244impl Id {
245    fn with_rng(rng: &mut Rng) -> Self {
246        Self(repeat_with(|| rng.alphanumeric()).take(10).collect())
247    }
248}
249
250#[async_trait]
251impl Handler for ConnId {
252    async fn run(&self, mut conn: Conn) -> Conn {
253        let id = self
254            .request_header
255            .as_ref()
256            .and_then(|request_header| conn.request_headers().get_str(request_header.clone()))
257            .map(|request_header| Id(request_header.to_string()))
258            .unwrap_or_else(|| self.generate_id());
259
260        if let Some(ref response_header) = self.response_header {
261            conn.response_headers_mut()
262                .insert(response_header.clone(), id.to_string());
263        }
264
265        conn.with_state(id)
266    }
267}
268
269/// Extension trait to retrieve an id generated by the [`ConnId`] handler
270pub trait ConnIdExt {
271    /// Retrieves the id for this conn. This method will panic if it
272    /// is run before the [`ConnId`] handler.
273    fn id(&self) -> &str;
274}
275
276impl<ConnLike> ConnIdExt for ConnLike
277where
278    ConnLike: AsRef<StateSet>,
279{
280    fn id(&self) -> &str {
281        self.as_ref()
282            .get::<Id>()
283            .expect("ConnId handler must be run before calling IdConnExt::id")
284    }
285}
286
287/// Formatter for the trillium_log crate
288pub mod log_formatter {
289    use std::borrow::Cow;
290
291    use super::*;
292    /// Formatter for the trillium_log crate. This will be `-` if
293    /// there is no id on the conn.
294    pub fn conn_id(conn: &Conn, _color: bool) -> Cow<'static, str> {
295        conn.state::<Id>()
296            .map(|id| Cow::Owned(id.0.clone()))
297            .unwrap_or_else(|| Cow::Borrowed("-"))
298    }
299}
300
301/// Alias for ConnId::new()
302pub fn conn_id() -> ConnId {
303    ConnId::new()
304}
305
306impl Debug for IdGenerator {
307    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
308        f.write_str(match self {
309            IdGenerator::Default => "IdGenerator::Default",
310            IdGenerator::SeededFastrand(_) => "IdGenerator::SeededFastrand",
311            IdGenerator::Fn(_) => "IdGenerator::Fn",
312        })
313    }
314}