Skip to main content

a3s_boot/http/
extractor.rs

1use super::request::BootRequest;
2use crate::{BootError, Result};
3use std::any::type_name;
4use std::fmt;
5use std::str::FromStr;
6
7/// Custom request value extractor used by Nest-style controller argument binding.
8pub trait RequestExtractor<T>: Send + Sync + 'static {
9    fn extract(&self, request: &BootRequest) -> Result<T>;
10}
11
12impl<T, F> RequestExtractor<T> for F
13where
14    F: Fn(&BootRequest) -> Result<T> + Send + Sync + 'static,
15{
16    fn extract(&self, request: &BootRequest) -> Result<T> {
17        self(request)
18    }
19}
20
21pub fn extract_request_value<T, E>(request: &BootRequest, extractor: E) -> Result<T>
22where
23    E: RequestExtractor<T>,
24{
25    extractor.extract(request)
26}
27
28/// Transforms a single request value extracted from a path, query, header, or host parameter.
29pub trait RequestValuePipe<I, O>: Send + Sync + 'static {
30    fn transform(&self, value: I) -> Result<O>;
31}
32
33impl<I, O, F> RequestValuePipe<I, O> for F
34where
35    F: Fn(I) -> Result<O> + Send + Sync + 'static,
36{
37    fn transform(&self, value: I) -> Result<O> {
38        self(value)
39    }
40}
41
42pub fn transform_request_value<I, O, P>(value: I, pipe: P) -> Result<O>
43where
44    P: RequestValuePipe<I, O>,
45{
46    pipe.transform(value)
47}
48
49/// Built-in Nest-style pipe that parses integer request values.
50#[derive(Debug, Clone, Copy, Default)]
51pub struct ParseIntPipe;
52
53impl<T> RequestValuePipe<String, T> for ParseIntPipe
54where
55    T: ParseIntTarget,
56{
57    fn transform(&self, value: String) -> Result<T> {
58        T::parse_int(value.trim()).map_err(|error| {
59            BootError::BadRequest(format!(
60                "validation failed: numeric string is expected for {}: {error}",
61                T::target_name()
62            ))
63        })
64    }
65}
66
67/// Built-in Nest-style pipe that parses boolean request values.
68#[derive(Debug, Clone, Copy, Default)]
69pub struct ParseBoolPipe;
70
71impl RequestValuePipe<String, bool> for ParseBoolPipe {
72    fn transform(&self, value: String) -> Result<bool> {
73        match value.trim().to_ascii_lowercase().as_str() {
74            "true" | "1" => Ok(true),
75            "false" | "0" => Ok(false),
76            _ => Err(BootError::BadRequest(
77                "validation failed: boolean string is expected".to_string(),
78            )),
79        }
80    }
81}
82
83/// Built-in Nest-style pipe that parses floating point request values.
84#[derive(Debug, Clone, Copy, Default)]
85pub struct ParseFloatPipe;
86
87impl<T> RequestValuePipe<String, T> for ParseFloatPipe
88where
89    T: ParseFloatTarget,
90{
91    fn transform(&self, value: String) -> Result<T> {
92        T::parse_float(value.trim()).map_err(|error| {
93            BootError::BadRequest(format!(
94                "validation failed: numeric string is expected for {}: {error}",
95                T::target_name()
96            ))
97        })
98    }
99}
100
101/// Built-in Nest-style pipe that parses separated request values into arrays.
102#[derive(Debug, Clone, Copy, Default)]
103pub struct ParseArrayPipe;
104
105impl ParseArrayPipe {
106    pub fn separator(separator: impl Into<String>) -> ParseArraySeparatorPipe {
107        ParseArraySeparatorPipe::new(separator)
108    }
109}
110
111impl<T> RequestValuePipe<String, Vec<T>> for ParseArrayPipe
112where
113    T: FromStr + Send + Sync + 'static,
114    T::Err: fmt::Display,
115{
116    fn transform(&self, value: String) -> Result<Vec<T>> {
117        parse_array_value(value, ",")
118    }
119}
120
121/// Built-in Nest-style pipe that parses request values with a custom separator.
122#[derive(Debug, Clone)]
123pub struct ParseArraySeparatorPipe {
124    separator: String,
125}
126
127impl ParseArraySeparatorPipe {
128    pub fn new(separator: impl Into<String>) -> Self {
129        Self {
130            separator: separator.into(),
131        }
132    }
133
134    pub fn separator(&self) -> &str {
135        &self.separator
136    }
137}
138
139impl<T> RequestValuePipe<String, Vec<T>> for ParseArraySeparatorPipe
140where
141    T: FromStr + Send + Sync + 'static,
142    T::Err: fmt::Display,
143{
144    fn transform(&self, value: String) -> Result<Vec<T>> {
145        parse_array_value(value, &self.separator)
146    }
147}
148
149/// Built-in Nest-style pipe that parses enum-like request values through `FromStr`.
150#[derive(Debug, Clone, Copy, Default)]
151pub struct ParseEnumPipe;
152
153impl<T> RequestValuePipe<String, T> for ParseEnumPipe
154where
155    T: FromStr + Send + Sync + 'static,
156    T::Err: fmt::Display,
157{
158    fn transform(&self, value: String) -> Result<T> {
159        value.trim().parse::<T>().map_err(|error| {
160            BootError::BadRequest(format!(
161                "validation failed: enum value is expected for {}: {error}",
162                type_name::<T>()
163            ))
164        })
165    }
166}
167
168/// UUID versions accepted by [`ParseUuidVersionPipe`].
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum UuidVersion {
171    All,
172    V1,
173    V3,
174    V4,
175    V5,
176    V6,
177    V7,
178    V8,
179}
180
181impl UuidVersion {
182    fn expected_nibble(self) -> Option<char> {
183        match self {
184            Self::All => None,
185            Self::V1 => Some('1'),
186            Self::V3 => Some('3'),
187            Self::V4 => Some('4'),
188            Self::V5 => Some('5'),
189            Self::V6 => Some('6'),
190            Self::V7 => Some('7'),
191            Self::V8 => Some('8'),
192        }
193    }
194
195    fn label(self) -> &'static str {
196        match self {
197            Self::All => "UUID",
198            Self::V1 => "UUID v1",
199            Self::V3 => "UUID v3",
200            Self::V4 => "UUID v4",
201            Self::V5 => "UUID v5",
202            Self::V6 => "UUID v6",
203            Self::V7 => "UUID v7",
204            Self::V8 => "UUID v8",
205        }
206    }
207}
208
209/// Built-in Nest-style pipe that validates UUID request values.
210#[derive(Debug, Clone, Copy, Default)]
211pub struct ParseUuidPipe;
212
213impl ParseUuidPipe {
214    pub fn version(version: UuidVersion) -> ParseUuidVersionPipe {
215        ParseUuidVersionPipe::new(version)
216    }
217
218    pub fn v4() -> ParseUuidVersionPipe {
219        Self::version(UuidVersion::V4)
220    }
221}
222
223impl RequestValuePipe<String, String> for ParseUuidPipe {
224    fn transform(&self, value: String) -> Result<String> {
225        parse_uuid_value(value, UuidVersion::All)
226    }
227}
228
229/// Built-in Nest-style pipe that validates UUID request values with a version constraint.
230#[derive(Debug, Clone, Copy)]
231pub struct ParseUuidVersionPipe {
232    version: UuidVersion,
233}
234
235impl ParseUuidVersionPipe {
236    pub fn new(version: UuidVersion) -> Self {
237        Self { version }
238    }
239
240    pub fn version(&self) -> UuidVersion {
241        self.version
242    }
243}
244
245impl RequestValuePipe<String, String> for ParseUuidVersionPipe {
246    fn transform(&self, value: String) -> Result<String> {
247        parse_uuid_value(value, self.version)
248    }
249}
250
251/// Built-in Nest-style pipe that replaces missing optional values with a default.
252#[derive(Debug, Clone)]
253pub struct DefaultValuePipe<T> {
254    value: T,
255}
256
257impl<T> DefaultValuePipe<T> {
258    pub fn new(value: T) -> Self {
259        Self { value }
260    }
261
262    pub fn value(&self) -> &T {
263        &self.value
264    }
265
266    pub fn into_value(self) -> T {
267        self.value
268    }
269}
270
271impl<T> RequestValuePipe<Option<T>, T> for DefaultValuePipe<T>
272where
273    T: Clone + Send + Sync + 'static,
274{
275    fn transform(&self, value: Option<T>) -> Result<T> {
276        Ok(value.unwrap_or_else(|| self.value.clone()))
277    }
278}
279
280pub trait ParseIntTarget: Sized + Send + Sync + 'static {
281    fn parse_int(value: &str) -> std::result::Result<Self, String>;
282    fn target_name() -> &'static str;
283}
284
285pub trait ParseFloatTarget: Sized + Send + Sync + 'static {
286    fn parse_float(value: &str) -> std::result::Result<Self, String>;
287    fn target_name() -> &'static str;
288}
289
290macro_rules! impl_parse_int_target {
291    ($($ty:ty),* $(,)?) => {
292        $(
293            impl ParseIntTarget for $ty {
294                fn parse_int(value: &str) -> std::result::Result<Self, String> {
295                    value.parse::<$ty>().map_err(display_error)
296                }
297
298                fn target_name() -> &'static str {
299                    stringify!($ty)
300                }
301            }
302        )*
303    };
304}
305
306macro_rules! impl_parse_float_target {
307    ($($ty:ty),* $(,)?) => {
308        $(
309            impl ParseFloatTarget for $ty {
310                fn parse_float(value: &str) -> std::result::Result<Self, String> {
311                    value.parse::<$ty>().map_err(display_error)
312                }
313
314                fn target_name() -> &'static str {
315                    stringify!($ty)
316                }
317            }
318        )*
319    };
320}
321
322impl_parse_int_target!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize,);
323impl_parse_float_target!(f32, f64);
324
325fn parse_array_value<T>(value: String, separator: &str) -> Result<Vec<T>>
326where
327    T: FromStr,
328    T::Err: fmt::Display,
329{
330    if separator.is_empty() {
331        return Err(BootError::BadRequest(
332            "validation failed: array separator cannot be empty".to_string(),
333        ));
334    }
335
336    let value = value.trim();
337    if value.is_empty() {
338        return Ok(Vec::new());
339    }
340
341    value
342        .split(separator)
343        .map(str::trim)
344        .map(|item| {
345            item.parse::<T>().map_err(|error| {
346                BootError::BadRequest(format!(
347                    "validation failed: array item is invalid for {}: {error}",
348                    type_name::<T>()
349                ))
350            })
351        })
352        .collect()
353}
354
355fn parse_uuid_value(value: String, version: UuidVersion) -> Result<String> {
356    let value = value.trim();
357    if is_uuid(value, version) {
358        return Ok(value.to_string());
359    }
360
361    Err(BootError::BadRequest(format!(
362        "validation failed: {} string is expected",
363        version.label()
364    )))
365}
366
367fn is_uuid(value: &str, version: UuidVersion) -> bool {
368    let bytes = value.as_bytes();
369    if bytes.len() != 36 {
370        return false;
371    }
372
373    for index in [8, 13, 18, 23] {
374        if bytes[index] != b'-' {
375            return false;
376        }
377    }
378
379    for (index, byte) in bytes.iter().enumerate() {
380        if matches!(index, 8 | 13 | 18 | 23) {
381            continue;
382        }
383        if !byte.is_ascii_hexdigit() {
384            return false;
385        }
386    }
387
388    let Some(expected) = version.expected_nibble() else {
389        return true;
390    };
391
392    let actual = (bytes[14] as char).to_ascii_lowercase();
393    if actual != expected {
394        return false;
395    }
396
397    matches!(
398        (bytes[19] as char).to_ascii_lowercase(),
399        '8' | '9' | 'a' | 'b'
400    )
401}
402
403fn display_error(error: impl fmt::Display) -> String {
404    error.to_string()
405}