Skip to main content

caelix_core/
context.rs

1use crate::Result;
2use std::{
3    any::{Any, TypeId},
4    collections::HashMap,
5    sync::{Arc, RwLock},
6};
7
8/// Public Caelix type `RequestContext`.
9pub struct RequestContext {
10    method: String,
11    path: String,
12    headers: HashMap<String, String>,
13    cookies: HashMap<String, String>,
14    extensions: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
15}
16
17impl RequestContext {
18    /// Runs the `new` public API operation.
19    pub fn new(
20        method: impl Into<String>,
21        path: impl Into<String>,
22        headers: HashMap<String, String>,
23    ) -> Self {
24        let headers = headers
25            .into_iter()
26            .map(|(name, value)| (name.to_ascii_lowercase(), value))
27            .collect::<HashMap<_, _>>();
28        let cookies = parse_cookies(headers.get("cookie").map(String::as_str));
29        Self {
30            method: method.into(),
31            path: path.into(),
32            headers,
33            cookies,
34            extensions: RwLock::new(HashMap::new()),
35        }
36    }
37
38    /// Runs the `method` public API operation.
39    pub fn method(&self) -> &str {
40        &self.method
41    }
42
43    /// Runs the `path` public API operation.
44    pub fn path(&self) -> &str {
45        &self.path
46    }
47
48    /// Runs the `header` public API operation.
49    pub fn header(&self, name: &str) -> Option<&str> {
50        self.headers
51            .get(&name.to_ascii_lowercase())
52            .map(String::as_str)
53    }
54
55    /// Runs the `bearer_token` public API operation.
56    pub fn bearer_token(&self) -> Option<&str> {
57        self.header("authorization")?.strip_prefix("Bearer ")
58    }
59
60    /// Returns the first cookie with `name`.
61    pub fn cookie(&self, name: &str) -> Option<&str> {
62        self.cookies.get(name).map(String::as_str)
63    }
64
65    /// Runs the `set` public API operation.
66    pub fn set<T: Send + Sync + 'static>(&self, value: T) -> Result<()> {
67        self.extensions
68            .write()
69            .map_err(|_| {
70                crate::exception::startup_error("request context extensions lock poisoned")
71            })?
72            .insert(TypeId::of::<T>(), Arc::new(value));
73        Ok(())
74    }
75
76    /// Runs the `get` public API operation.
77    pub fn get<T: Send + Sync + 'static>(&self) -> Result<Option<Arc<T>>> {
78        let value = self
79            .extensions
80            .read()
81            .map_err(|_| {
82                crate::exception::startup_error("request context extensions lock poisoned")
83            })?
84            .get(&TypeId::of::<T>())
85            .cloned();
86
87        let Some(value) = value else {
88            return Ok(None);
89        };
90
91        value
92            .clone()
93            .downcast::<T>()
94            .map(Some)
95            .map_err(|_| crate::exception::startup_error("request context extension type mismatch"))
96    }
97}
98
99fn parse_cookies(header: Option<&str>) -> HashMap<String, String> {
100    let mut cookies = HashMap::new();
101    for pair in header.into_iter().flat_map(|value| value.split(';')) {
102        let pair = pair.trim();
103        let Some((name, value)) = pair.split_once('=') else {
104            continue;
105        };
106        let name = name.trim();
107        if name.is_empty() {
108            continue;
109        }
110        let Ok(name) = cookie::Cookie::parse_encoded(format!("{name}=")) else {
111            continue;
112        };
113        let Ok(value) = cookie::Cookie::parse_encoded(format!("value={}", value.trim())) else {
114            continue;
115        };
116        cookies
117            .entry(name.name().to_string())
118            .or_insert_with(|| value.value().to_string());
119    }
120    cookies
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn parses_cookie_edge_cases_once() {
129        let ctx = RequestContext::new(
130            "GET",
131            "/",
132            HashMap::from([(
133                "Cookie".into(),
134                " theme = dark%20mode ; token=a=b=c; broken; =bad; dup=first; dup=second; na%6De=value"
135                    .into(),
136            )]),
137        );
138        assert_eq!(ctx.cookie("theme"), Some("dark mode"));
139        assert_eq!(ctx.cookie("token"), Some("a=b=c"));
140        assert_eq!(ctx.cookie("dup"), Some("first"));
141        assert_eq!(ctx.cookie("name"), Some("value"));
142        assert_eq!(ctx.cookie("broken"), None);
143    }
144}