Skip to main content

siderite_core/
header.rs

1//! Typed header and cookie extractors and `Set-Cookie` responses.
2
3use crate::error::ApiError;
4use crate::extract::FromRequestParts;
5use crate::response::{IntoResponse, Response};
6use cookie::Cookie;
7use http::HeaderValue;
8use http::header::{COOKIE, SET_COOKIE};
9use http::request::Parts;
10use serde_json::json;
11use siderite_openapi::{Operation, Parameter, ParameterLocation, SchemaObject, SchemaRegistry};
12
13/// A typed HTTP header identified by a static name.
14pub trait NamedHeader: Sized + Send {
15    /// Header name as sent on the wire (matched case-insensitively).
16    const NAME: &'static str;
17
18    /// Decode the header value.
19    ///
20    /// # Errors
21    /// Returns a message describing why the value is invalid.
22    fn decode(value: &str) -> Result<Self, String>;
23
24    /// JSON Schema for this header (defaults to a string).
25    fn schema(_registry: &mut SchemaRegistry) -> SchemaObject {
26        SchemaObject::of_type("string")
27    }
28}
29
30/// Extractor for a required typed header `H`.
31///
32/// Missing headers yield `422` with code `missing` and location
33/// `["header", H::NAME]`. Invalid values yield code `header_invalid`.
34/// Wrap in [`Option`] to make the header optional: a missing header becomes
35/// `None`, an invalid one is still `422`.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Header<H: NamedHeader>(pub H);
38
39impl<H: NamedHeader> FromRequestParts for Header<H> {
40    async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
41        let value = parts.headers.get(H::NAME).ok_or_else(|| {
42            header_error(H::NAME, "missing", format!("missing header `{}`", H::NAME)).absent()
43        })?;
44        let text = value.to_str().map_err(|_| {
45            header_error(
46                H::NAME,
47                "header_invalid",
48                format!("header `{}` is not valid UTF-8", H::NAME),
49            )
50        })?;
51        H::decode(text)
52            .map(Header)
53            .map_err(|message| header_error(H::NAME, "header_invalid", message))
54    }
55
56    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
57        op.add_parameter(Parameter::new(
58            H::NAME,
59            ParameterLocation::Header,
60            true,
61            H::schema(registry),
62        ));
63    }
64}
65
66fn header_error(name: &str, code: &str, message: impl Into<String>) -> ApiError {
67    ApiError::unprocessable(json!([{
68        "location": ["header", name],
69        "code": code,
70        "message": message.into(),
71    }]))
72}
73
74/// The `User-Agent` request header.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct UserAgent(pub String);
77
78impl NamedHeader for UserAgent {
79    const NAME: &'static str = "User-Agent";
80
81    fn decode(value: &str) -> Result<Self, String> {
82        Ok(Self(value.to_owned()))
83    }
84}
85
86/// The `Accept` request header.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Accept(pub String);
89
90impl NamedHeader for Accept {
91    const NAME: &'static str = "Accept";
92
93    fn decode(value: &str) -> Result<Self, String> {
94        Ok(Self(value.to_owned()))
95    }
96}
97
98/// Cookies parsed from the request `Cookie` header.
99#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct Cookies {
101    pairs: Vec<(String, String)>,
102}
103
104impl Cookies {
105    /// The first cookie value for `name`, if present.
106    #[must_use]
107    pub fn get(&self, name: &str) -> Option<&str> {
108        self.pairs
109            .iter()
110            .find(|(n, _)| n == name)
111            .map(|(_, v)| v.as_str())
112    }
113
114    /// Iterate over `(name, value)` pairs in header order.
115    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
116        self.pairs.iter().map(|(n, v)| (n.as_str(), v.as_str()))
117    }
118}
119
120impl FromRequestParts for Cookies {
121    async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
122        let mut pairs = Vec::new();
123        for header in parts.headers.get_all(COOKIE) {
124            let Ok(text) = header.to_str() else {
125                continue;
126            };
127            for cookie in Cookie::split_parse(text).flatten() {
128                pairs.push((cookie.name().to_owned(), cookie.value().to_owned()));
129            }
130        }
131        Ok(Self { pairs })
132    }
133}
134
135/// `SameSite` attribute for a [`SetCookie`].
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum SameSite {
138    /// Never send the cookie on cross-site requests.
139    Strict,
140    /// Send the cookie on top-level cross-site GET navigations.
141    Lax,
142    /// Send the cookie on all cross-site requests (requires `Secure`).
143    None,
144}
145
146impl From<SameSite> for cookie::SameSite {
147    fn from(value: SameSite) -> Self {
148        match value {
149            SameSite::Strict => Self::Strict,
150            SameSite::Lax => Self::Lax,
151            SameSite::None => Self::None,
152        }
153    }
154}
155
156/// Builder for a `Set-Cookie` header.
157#[derive(Debug, Clone)]
158pub struct SetCookie {
159    inner: Cookie<'static>,
160}
161
162impl SetCookie {
163    /// Start a cookie with `name` and `value`.
164    #[must_use]
165    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
166        Self {
167            inner: Cookie::new(name.into(), value.into()),
168        }
169    }
170
171    /// Set the `HttpOnly` flag.
172    #[must_use]
173    pub fn http_only(mut self, value: bool) -> Self {
174        self.inner.set_http_only(value);
175        self
176    }
177
178    /// Set the `Secure` flag.
179    #[must_use]
180    pub fn secure(mut self, value: bool) -> Self {
181        self.inner.set_secure(value);
182        self
183    }
184
185    /// Set the `SameSite` attribute.
186    #[must_use]
187    pub fn same_site(mut self, value: SameSite) -> Self {
188        self.inner.set_same_site(cookie::SameSite::from(value));
189        self
190    }
191
192    /// Set the cookie `Path`.
193    #[must_use]
194    pub fn path(mut self, value: impl Into<String>) -> Self {
195        self.inner.set_path(value.into());
196        self
197    }
198
199    /// Set the cookie `Domain`.
200    #[must_use]
201    pub fn domain(mut self, value: impl Into<String>) -> Self {
202        self.inner.set_domain(value.into());
203        self
204    }
205
206    /// Set `Max-Age` from a [`std::time::Duration`].
207    #[must_use]
208    pub fn max_age(mut self, value: std::time::Duration) -> Self {
209        if let Ok(duration) = cookie::time::Duration::try_from(value) {
210            self.inner.set_max_age(duration);
211        }
212        self
213    }
214}
215
216/// Wraps a response and appends `Set-Cookie` headers.
217#[derive(Debug)]
218pub struct WithCookies<R> {
219    inner: R,
220    cookies: Vec<SetCookie>,
221}
222
223impl<R> WithCookies<R> {
224    /// Wrap `inner` with no cookies.
225    #[must_use]
226    pub fn new(inner: R) -> Self {
227        Self {
228            inner,
229            cookies: Vec::new(),
230        }
231    }
232
233    /// Append a `Set-Cookie` header built from `cookie`.
234    #[must_use]
235    pub fn cookie(mut self, cookie: SetCookie) -> Self {
236        self.cookies.push(cookie);
237        self
238    }
239}
240
241impl<R: IntoResponse> IntoResponse for WithCookies<R> {
242    fn into_response(self) -> Response {
243        let mut headers = Vec::new();
244        for cookie in self.cookies {
245            match HeaderValue::from_str(&cookie.inner.to_string()) {
246                Ok(value) => headers.push(value),
247                Err(_) => {
248                    return ApiError::internal("invalid Set-Cookie header value").into_response();
249                }
250            }
251        }
252        let mut response = self.inner.into_response();
253        for value in headers {
254            response.headers_mut().append(SET_COOKIE, value);
255        }
256        response
257    }
258
259    fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
260        R::describe(op, registry);
261    }
262}