1use 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
13pub trait NamedHeader: Sized + Send {
15 const NAME: &'static str;
17
18 fn decode(value: &str) -> Result<Self, String>;
23
24 fn schema(_registry: &mut SchemaRegistry) -> SchemaObject {
26 SchemaObject::of_type("string")
27 }
28}
29
30#[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#[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#[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#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct Cookies {
101 pairs: Vec<(String, String)>,
102}
103
104impl Cookies {
105 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub enum SameSite {
138 Strict,
140 Lax,
142 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#[derive(Debug, Clone)]
158pub struct SetCookie {
159 inner: Cookie<'static>,
160}
161
162impl SetCookie {
163 #[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 #[must_use]
173 pub fn http_only(mut self, value: bool) -> Self {
174 self.inner.set_http_only(value);
175 self
176 }
177
178 #[must_use]
180 pub fn secure(mut self, value: bool) -> Self {
181 self.inner.set_secure(value);
182 self
183 }
184
185 #[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 #[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 #[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 #[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#[derive(Debug)]
218pub struct WithCookies<R> {
219 inner: R,
220 cookies: Vec<SetCookie>,
221}
222
223impl<R> WithCookies<R> {
224 #[must_use]
226 pub fn new(inner: R) -> Self {
227 Self {
228 inner,
229 cookies: Vec::new(),
230 }
231 }
232
233 #[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}