Skip to main content

rama_http/layer/
header_option_value.rs

1//! Similar to [`super::header_config::HeaderConfigLayer`],
2//! but storing the [`Default`] value of type `T` in case
3//! the header with the given [`HeaderName`] is present
4//! and has a bool-like value.
5
6use crate::{HeaderName, Request, utils::HeaderValueGetter};
7use rama_core::error::BoxErrorExt as _;
8use rama_core::{
9    Layer, Service,
10    error::{BoxError, ErrorContext as _, ErrorExt},
11    extensions::{Extension, ExtensionsRef},
12    telemetry::tracing,
13};
14use rama_utils::macros::define_inner_service_accessors;
15use std::{fmt, marker::PhantomData};
16
17/// A [`Service`] which stores the [`Default`] value of type `T` in case
18/// the header with the given [`HeaderName`] is present
19/// and has a bool-like value.
20pub struct HeaderOptionValueService<T, S> {
21    inner: S,
22    header_name: HeaderName,
23    optional: bool,
24    _marker: PhantomData<fn() -> T>,
25}
26
27impl<T, S> HeaderOptionValueService<T, S> {
28    /// Create a new [`HeaderOptionValueService`].
29    ///
30    /// Alias for [`HeaderOptionValueService::required`] if `!optional`
31    /// and [`HeaderOptionValueService::optional`] if `optional`.
32    pub const fn new(inner: S, header_name: HeaderName, optional: bool) -> Self {
33        Self {
34            inner,
35            header_name,
36            optional,
37            _marker: PhantomData,
38        }
39    }
40
41    define_inner_service_accessors!();
42
43    /// Create a new [`HeaderOptionValueService`] with the given inner service
44    /// and header name, on which optionally create the value,
45    /// and which will fail if the header is missing.
46    pub const fn required(inner: S, header_name: HeaderName) -> Self {
47        Self::new(inner, header_name, false)
48    }
49
50    /// Create a new [`HeaderOptionValueService`] with the given inner service
51    /// and header name, on which optionally create the value,
52    /// and which will gracefully accept if the header is missing.
53    pub const fn optional(inner: S, header_name: HeaderName) -> Self {
54        Self::new(inner, header_name, true)
55    }
56}
57
58impl<T, S: fmt::Debug> fmt::Debug for HeaderOptionValueService<T, S> {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        f.debug_struct("HeaderOptionValueService")
61            .field("inner", &self.inner)
62            .field("header_name", &self.header_name)
63            .field("optional", &self.optional)
64            .field(
65                "_marker",
66                &format_args!("{}", std::any::type_name::<fn() -> T>()),
67            )
68            .finish()
69    }
70}
71
72impl<T, S> Clone for HeaderOptionValueService<T, S>
73where
74    S: Clone,
75{
76    fn clone(&self) -> Self {
77        Self {
78            inner: self.inner.clone(),
79            header_name: self.header_name.clone(),
80            optional: self.optional,
81            _marker: PhantomData,
82        }
83    }
84}
85
86impl<T, S, Body, E> Service<Request<Body>> for HeaderOptionValueService<T, S>
87where
88    S: Service<Request<Body>, Error = E>,
89    T: Default + Extension,
90    Body: Send + Sync + 'static,
91    E: Into<BoxError> + Send + Sync + 'static,
92{
93    type Output = S::Output;
94    type Error = BoxError;
95
96    async fn serve(&self, request: Request<Body>) -> Result<Self::Output, Self::Error> {
97        match request.header_str(&self.header_name) {
98            Ok(str_value) => {
99                let str_value = str_value.trim();
100                if str_value == "1" || str_value.eq_ignore_ascii_case("true") {
101                    request.extensions().insert(T::default());
102                } else if str_value != "0" && !str_value.eq_ignore_ascii_case("false") {
103                    return Err(BoxError::from_static_str("invalid header option")
104                        .context_field("header_name", self.header_name.clone())
105                        .context_str_field("header_value", str_value));
106                }
107            }
108            Err(err) => {
109                if self.optional && matches!(err, crate::utils::HeaderValueErr::HeaderMissing(_)) {
110                    tracing::debug!(
111                        http.header.name  = %self.header_name,
112                        "failed to determine header option: {err:?}",
113                    );
114                    return self.inner.serve(request).await.into_box_error();
115                } else {
116                    return Err(err
117                        .context("determine header option")
118                        .context_field("header_name", self.header_name.clone()));
119                }
120            }
121        };
122        self.inner.serve(request).await.into_box_error()
123    }
124}
125
126/// Layer which stores the [`Default`] value of type `T` in case
127/// the header with the given [`HeaderName`] is present
128/// and has a bool-like value.
129pub struct HeaderOptionValueLayer<T> {
130    header_name: HeaderName,
131    optional: bool,
132    _marker: PhantomData<fn() -> T>,
133}
134
135impl<T> fmt::Debug for HeaderOptionValueLayer<T> {
136    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137        f.debug_struct("HeaderOptionValueLayer")
138            .field("header_name", &self.header_name)
139            .field("optional", &self.optional)
140            .field(
141                "_marker",
142                &format_args!("{}", std::any::type_name::<fn() -> T>()),
143            )
144            .finish()
145    }
146}
147
148impl<T> Clone for HeaderOptionValueLayer<T> {
149    fn clone(&self) -> Self {
150        Self {
151            header_name: self.header_name.clone(),
152            optional: self.optional,
153            _marker: PhantomData,
154        }
155    }
156}
157
158impl<T> HeaderOptionValueLayer<T> {
159    /// Create a new [`HeaderOptionValueLayer`] with the given header name,
160    /// on which optionally create the valu,
161    /// and which will fail if the header is missing.
162    pub fn required(header_name: HeaderName) -> Self {
163        Self {
164            header_name,
165            optional: false,
166            _marker: PhantomData,
167        }
168    }
169
170    /// Create a new [`HeaderOptionValueLayer`] with the given header name,
171    /// on which optionally create the valu,
172    /// and which will gracefully accept if the header is missing.
173    pub fn optional(header_name: HeaderName) -> Self {
174        Self {
175            header_name,
176            optional: true,
177            _marker: PhantomData,
178        }
179    }
180}
181
182impl<T, S> Layer<S> for HeaderOptionValueLayer<T> {
183    type Service = HeaderOptionValueService<T, S>;
184
185    fn layer(&self, inner: S) -> Self::Service {
186        HeaderOptionValueService::new(inner, self.header_name.clone(), self.optional)
187    }
188
189    fn into_layer(self, inner: S) -> Self::Service {
190        HeaderOptionValueService::new(inner, self.header_name, self.optional)
191    }
192}
193
194#[cfg(test)]
195mod test {
196    use rama_core::extensions::{Extension, ExtensionsRef};
197
198    use super::*;
199    use crate::Method;
200
201    #[derive(Debug, Clone, Default, Extension)]
202    struct UnitValue;
203
204    #[tokio::test]
205    async fn test_header_option_value_required_happy_path() {
206        let test_cases = [
207            ("1", true),
208            ("true", true),
209            ("True", true),
210            ("TrUE", true),
211            ("TRUE", true),
212            ("0", false),
213            ("false", false),
214            ("False", false),
215            ("FaLsE", false),
216            ("FALSE", false),
217        ];
218        for (str_value, expected_output) in test_cases {
219            let request = Request::builder()
220                .method(Method::GET)
221                .uri("https://www.example.com")
222                .header("x-unit-value", str_value)
223                .body(())
224                .unwrap();
225
226            let inner_service =
227                rama_core::service::service_fn(move |req: Request<()>| async move {
228                    assert_eq!(expected_output, req.extensions().contains::<UnitValue>());
229                    Ok::<_, std::convert::Infallible>(())
230                });
231
232            let service = HeaderOptionValueService::<UnitValue, _>::required(
233                inner_service,
234                HeaderName::from_static("x-unit-value"),
235            );
236
237            service.serve(request).await.unwrap();
238        }
239    }
240
241    #[tokio::test]
242    async fn test_header_option_value_optional_found() {
243        let test_cases = [
244            ("1", true),
245            ("true", true),
246            ("True", true),
247            ("TrUE", true),
248            ("TRUE", true),
249            ("0", false),
250            ("false", false),
251            ("False", false),
252            ("FaLsE", false),
253            ("FALSE", false),
254        ];
255        for (str_value, expected_output) in test_cases {
256            let request = Request::builder()
257                .method(Method::GET)
258                .uri("https://www.example.com")
259                .header("x-unit-value", str_value)
260                .body(())
261                .unwrap();
262
263            let inner_service =
264                rama_core::service::service_fn(move |req: Request<()>| async move {
265                    assert_eq!(expected_output, req.extensions().contains::<UnitValue>());
266                    Ok::<_, std::convert::Infallible>(())
267                });
268
269            let service = HeaderOptionValueService::<UnitValue, _>::optional(
270                inner_service,
271                HeaderName::from_static("x-unit-value"),
272            );
273
274            service.serve(request).await.unwrap();
275        }
276    }
277
278    #[tokio::test]
279    async fn test_header_option_value_optional_missing() {
280        let request = Request::builder()
281            .method(Method::GET)
282            .uri("https://www.example.com")
283            .body(())
284            .unwrap();
285
286        let inner_service = rama_core::service::service_fn(async |req: Request<()>| {
287            assert!(!req.extensions().contains::<UnitValue>());
288
289            Ok::<_, std::convert::Infallible>(())
290        });
291
292        let service = HeaderOptionValueService::<UnitValue, _>::optional(
293            inner_service,
294            HeaderName::from_static("x-unit-value"),
295        );
296
297        service.serve(request).await.unwrap();
298    }
299
300    #[tokio::test]
301    async fn test_header_option_value_required_missing_header() {
302        let request = Request::builder()
303            .method(Method::GET)
304            .uri("https://www.example.com")
305            .body(())
306            .unwrap();
307
308        let inner_service = rama_core::service::service_fn(async |_: Request<()>| {
309            Ok::<_, std::convert::Infallible>(())
310        });
311
312        let service = HeaderOptionValueService::<UnitValue, _>::required(
313            inner_service,
314            HeaderName::from_static("x-unit-value"),
315        );
316
317        let result = service.serve(request).await;
318        assert!(result.is_err());
319    }
320
321    #[tokio::test]
322    async fn test_header_option_value_required_invalid_value() {
323        let test_cases = ["", "foo", "yes"];
324
325        for test_case in test_cases {
326            let request = Request::builder()
327                .method(Method::GET)
328                .uri("https://www.example.com")
329                .header("x-unit-value", test_case)
330                .body(())
331                .unwrap();
332
333            let inner_service = rama_core::service::service_fn(async |_: Request<()>| {
334                Ok::<_, std::convert::Infallible>(())
335            });
336
337            let service = HeaderOptionValueService::<UnitValue, _>::required(
338                inner_service,
339                HeaderName::from_static("x-unit-value"),
340            );
341
342            let result = service.serve(request).await;
343            assert!(result.is_err());
344        }
345    }
346
347    #[tokio::test]
348    async fn test_header_option_value_optional_invalid_value() {
349        let test_cases = ["", "foo", "yes"];
350
351        for test_case in test_cases {
352            let request = Request::builder()
353                .method(Method::GET)
354                .uri("https://www.example.com")
355                .header("x-unit-value", test_case)
356                .body(())
357                .unwrap();
358
359            let inner_service = rama_core::service::service_fn(async |_: Request<()>| {
360                Ok::<_, std::convert::Infallible>(())
361            });
362
363            let service = HeaderOptionValueService::<UnitValue, _>::optional(
364                inner_service,
365                HeaderName::from_static("x-unit-value"),
366            );
367
368            let result = service.serve(request).await;
369            assert!(result.is_err());
370        }
371    }
372}