1use super::request::BootRequest;
2use crate::{BootError, Result};
3use std::any::type_name;
4use std::fmt;
5use std::str::FromStr;
6
7pub 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
28pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}