Skip to main content

sova_cookies/
lib.rs

1//! Cookie parsing middleware and `Response::cookie` extension.
2
3use cookie::Cookie;
4use http::{header, HeaderMap, HeaderValue};
5use sova_core::extend::named;
6use sova_core::{App, Next, Plugin, Request, Response};
7use std::collections::HashMap;
8
9/// Parsed request cookies stored in extensions.
10#[derive(Debug, Default, Clone)]
11pub struct Cookies {
12    map: HashMap<String, String>,
13}
14
15impl Cookies {
16    pub fn get(&self, name: &str) -> Option<&str> {
17        self.map.get(name).map(|s| s.as_str())
18    }
19
20    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
21        self.map.iter().map(|(k, v)| (k.as_str(), v.as_str()))
22    }
23}
24
25/// Parse `Cookie` header into [`Cookies`] on each request.
26pub struct CookieLayer;
27
28impl Plugin for CookieLayer {
29    fn id(&self) -> &'static str {
30        "cookies"
31    }
32
33    fn meta(&self) -> sova_core::PluginMeta {
34        sova_core::PluginMeta::new("Cookies")
35            .description("Parse Cookie header into request-local Cookies")
36            .version(env!("CARGO_PKG_VERSION"))
37    }
38
39    fn install(self, app: &mut App) {
40        app.state(CookieLayerPresent);
41        app.use_middleware(named("cookies", |mut req: Request, next: Next| async move {
42            let parsed = parse_cookies(&req.headers);
43            req.set(parsed);
44            next(req).await
45        }));
46    }
47}
48
49/// Marker inserted by [`CookieLayer`] so other plugins can require cookies at startup.
50#[derive(Clone, Copy, Debug)]
51pub struct CookieLayerPresent;
52
53fn parse_cookies(headers: &HeaderMap) -> Cookies {
54    let mut cookies = Cookies::default();
55    if let Some(raw) = headers
56        .get(header::COOKIE)
57        .and_then(|v| v.to_str().ok())
58    {
59        for part in raw.split(';') {
60            if let Ok(c) = Cookie::parse(part.trim()) {
61                cookies
62                    .map
63                    .insert(c.name().to_string(), c.value().to_string());
64            }
65        }
66    }
67    cookies
68}
69
70/// Attach `Set-Cookie` on a [`Response`] (lives outside core on purpose).
71pub trait ResponseCookieExt {
72    fn cookie(self, cookie: Cookie<'_>) -> Self;
73}
74
75impl ResponseCookieExt for Response {
76    fn cookie(mut self, cookie: Cookie<'_>) -> Self {
77        let value = cookie.encoded().to_string();
78        if let Ok(v) = HeaderValue::from_str(&value) {
79            self.headers_mut().append(header::SET_COOKIE, v);
80        }
81        self
82    }
83}
84
85/// Re-export cookie builder for responses.
86pub use cookie::Cookie as CookieBuilder;