1use crate::auth::IClaims;
23use crate::error::{Error, Result};
24use crate::form::{FormFile, MultipartForm};
25use crate::http::IHttpContext;
26use crate::problem::{FieldError, ProblemDetails};
27use crate::route::scan::ParamMeta;
28use std::collections::HashMap;
29use std::future::Future;
30use std::pin::Pin;
31use std::str::FromStr;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum Source {
46 Path,
48 Query,
50 Form,
52 Header,
54 Claim,
56 Body,
58 Auto,
60}
61
62impl Source {
63 pub const fn as_str(self) -> &'static str {
65 match self {
66 Source::Path => "path",
67 Source::Query => "query",
68 Source::Form => "form",
69 Source::Header => "header",
70 Source::Claim => "claim",
71 Source::Body => "body",
72 Source::Auto => "",
73 }
74 }
75
76 pub const fn is_explicit(self) -> bool {
78 !matches!(self, Source::Auto)
79 }
80}
81
82pub fn is_body_method(method: &str) -> bool {
84 matches!(method, "POST" | "PUT" | "PATCH")
85}
86
87#[diagnostic::on_unimplemented(
103 message = "`{Self}` cannot be bound from an HTTP request",
104 label = "add `#[derive(WebxRequestMeta)]` to this request struct",
105 note = "the endpoint macros call `RequestModel::bind`; the derive is what implements it"
106)]
107pub trait RequestModel: Sized + Send + 'static {
108 fn param_meta() -> &'static [ParamMeta];
113
114 fn bind<'a>(
116 ctx: &'a mut dyn IHttpContext,
117 ) -> Pin<Box<dyn Future<Output = Result<Self>> + Send + 'a>>;
118}
119
120pub struct BindInput<'a> {
125 pub path: &'a HashMap<String, String>,
127 pub query: &'a HashMap<String, String>,
129 pub query_multi: Option<&'a HashMap<String, Vec<String>>>,
132 pub claims: Option<&'a dyn IClaims>,
134 pub form: Option<&'a MultipartForm>,
136 pub is_body: bool,
138 pub is_multipart: bool,
140}
141
142impl<'a> BindInput<'a> {
143 pub fn path_one(&self, name: &str) -> Option<&'a str> {
145 self.path.get(name).map(String::as_str)
146 }
147
148 pub fn path_list(&self, name: &str) -> Vec<&'a str> {
154 match self.path.get(name) {
155 Some(value) => split_one(value),
156 None => Vec::new(),
157 }
158 }
159
160 pub fn query_one(&self, name: &str) -> Option<&'a str> {
162 if let Some(multi) = self.query_multi {
163 if let Some(values) = multi.get(name) {
164 return values.last().map(String::as_str);
165 }
166 }
167 self.query.get(name).map(String::as_str)
168 }
169
170 pub fn query_list(&self, name: &str) -> Vec<&'a str> {
172 if let Some(multi) = self.query_multi {
173 if let Some(values) = multi.get(name) {
174 return split_values(values);
175 }
176 }
177 match self.query.get(name) {
178 Some(value) => split_one(value),
179 None => Vec::new(),
180 }
181 }
182
183 pub fn form_one(&self, name: &str) -> Option<&'a str> {
185 self.form?.text(name)
186 }
187
188 pub fn form_list(&self, name: &str) -> Vec<&'a str> {
190 match self.form {
191 Some(form) => form.texts(name).flat_map(split_one).collect(),
192 None => Vec::new(),
193 }
194 }
195
196 pub fn file(&self, name: &str) -> Option<&'a FormFile> {
198 self.form?.file(name)
199 }
200
201 pub fn files(&self, name: &str) -> Vec<&'a FormFile> {
203 match self.form {
204 Some(form) => form.files_named(name).collect(),
205 None => Vec::new(),
206 }
207 }
208
209 pub fn claim(&self, key: &str) -> Option<&'a str> {
216 let claims = self.claims?;
217 if let Some(value) = claims.claims().get(key) {
218 return Some(value.as_str());
219 }
220 match key {
221 "sub" | "subject" | "user_id" => Some(claims.subject()),
222 "tenant_id" | "tenant" => claims.get_tenantid(),
223 _ => None,
224 }
225 }
226
227 pub fn claim_list(&self, key: &str) -> Vec<&'a str> {
231 let Some(claims) = self.claims else {
232 return Vec::new();
233 };
234 if let Some(value) = claims.claims().get(key) {
235 return split_one(value);
236 }
237 match key {
238 "roles" => claims.roles().iter().map(String::as_str).collect(),
239 "permissions" => claims.permissions().iter().map(String::as_str).collect(),
240 _ => Vec::new(),
241 }
242 }
243}
244
245pub fn body_error(error: serde_json::Error) -> Error {
252 let message = error.to_string();
253 let field = extract_error_field(&message)
254 .unwrap_or_default()
255 .to_string();
256 ProblemDetails::validation(vec![FieldError::new(field, message)]).to_error()
257}
258
259fn extract_error_field(message: &str) -> Option<&str> {
260 for marker in ["missing field `", "unknown field `", "duplicate field `"] {
261 if let Some((_, rest)) = message.split_once(marker) {
262 if let Some((name, _)) = rest.split_once('`') {
263 return Some(name);
264 }
265 }
266 }
267 None
268}
269
270pub fn body_field_error(field: &str, error: serde_json::Error) -> Error {
273 ProblemDetails::validation(vec![FieldError::new(
274 field,
275 format!("The `{field}` field is invalid: {error}"),
276 )])
277 .to_error()
278}
279
280pub fn take_body_field(
286 object: &mut serde_json::Map<String, serde_json::Value>,
287 key: &str,
288 aliases: &[&str],
289) -> Option<serde_json::Value> {
290 if let Some(value) = object.remove(key) {
291 return Some(value);
292 }
293 for alias in aliases {
294 if let Some(value) = object.remove(*alias) {
295 return Some(value);
296 }
297 }
298 None
299}
300
301fn split_one(value: &str) -> Vec<&str> {
306 if value.is_empty() {
307 Vec::new()
308 } else if value.contains(',') {
309 value
310 .split(',')
311 .map(str::trim)
312 .filter(|part| !part.is_empty())
313 .collect()
314 } else {
315 vec![value]
316 }
317}
318
319pub fn split_csv(value: Option<&str>) -> Vec<&str> {
324 match value {
325 Some(value) => split_one(value),
326 None => Vec::new(),
327 }
328}
329
330fn split_values(values: &[String]) -> Vec<&str> {
332 let mut out = Vec::with_capacity(values.len());
333 for value in values {
334 out.extend(split_one(value));
335 }
336 out
337}
338
339#[derive(Default)]
345pub struct Binder {
346 errors: Vec<FieldError>,
347}
348
349impl Binder {
350 pub fn new() -> Self {
352 Self::default()
353 }
354
355 pub fn has_errors(&self) -> bool {
357 !self.errors.is_empty()
358 }
359
360 pub fn errors(&self) -> &[FieldError] {
362 &self.errors
363 }
364
365 pub fn into_error(self) -> Error {
369 ProblemDetails::validation(self.errors).to_error()
370 }
371
372 pub fn missing(&mut self, field: &str) {
374 self.errors.push(FieldError::new(
375 field,
376 format!("The `{field}` field is required."),
377 ));
378 }
379
380 pub fn invalid(&mut self, field: &str, kind: &str, value: &str) {
382 self.errors.push(FieldError::new(
383 field,
384 format!("The `{field}` field must be a valid {kind} (got `{value}`)."),
385 ));
386 }
387
388 pub fn unknown_field(&mut self, name: &str) {
390 self.errors.push(FieldError::new(
391 "",
392 format!("The body contains an unknown field `{name}`."),
393 ));
394 }
395
396 pub fn required<T: FromStr>(
402 &mut self,
403 raw: Option<&str>,
404 field: &str,
405 kind: &str,
406 ) -> Option<T> {
407 let Some(raw) = raw else {
408 self.missing(field);
409 return None;
410 };
411 match raw.parse::<T>() {
412 Ok(value) => Some(value),
413 Err(_) => {
414 self.invalid(field, kind, raw);
415 None
416 }
417 }
418 }
419
420 pub fn optional<T: FromStr>(
423 &mut self,
424 raw: Option<&str>,
425 field: &str,
426 kind: &str,
427 ) -> Option<T> {
428 let raw = raw?;
429 match raw.parse::<T>() {
430 Ok(value) => Some(value),
431 Err(_) => {
432 self.invalid(field, kind, raw);
433 None
434 }
435 }
436 }
437
438 pub fn list<T: FromStr>(&mut self, raw: &[&str], field: &str, kind: &str) -> Vec<T> {
443 let mut out = Vec::with_capacity(raw.len());
444 for value in raw {
445 match value.parse::<T>() {
446 Ok(parsed) => out.push(parsed),
447 Err(_) => self.invalid(field, kind, value),
448 }
449 }
450 out
451 }
452
453 pub fn take<T>(&mut self, value: Option<T>, field: &str) -> Option<T> {
458 if value.is_none() {
459 self.missing(field);
460 }
461 value
462 }
463
464 pub fn required_file(&mut self, file: Option<&FormFile>, field: &str) -> Option<FormFile> {
466 match file {
467 Some(file) => Some(file.clone()),
468 None => {
469 self.missing(field);
470 None
471 }
472 }
473 }
474
475 pub fn optional_file(&mut self, file: Option<&FormFile>) -> Option<FormFile> {
477 file.cloned()
478 }
479
480 pub fn files(&mut self, files: Vec<&FormFile>) -> Vec<FormFile> {
482 files.into_iter().cloned().collect()
483 }
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use std::collections::HashMap;
490
491 #[test]
492 fn required_reports_missing_and_invalid() {
493 let mut binder = Binder::new();
494 assert_eq!(binder.required::<u32>(None, "page", "integer"), None);
495 assert_eq!(binder.required::<u32>(Some("abc"), "page", "integer"), None);
496 assert_eq!(
497 binder.required::<u32>(Some("2"), "page", "integer"),
498 Some(2)
499 );
500 assert_eq!(binder.errors().len(), 2);
501 assert!(binder.errors()[0].message.contains("required"));
502 }
503
504 #[test]
505 fn optional_keeps_absent_as_none_and_rejects_invalid() {
506 let mut binder = Binder::new();
507 assert_eq!(binder.optional::<u32>(None, "page", "integer"), None);
508 assert!(!binder.has_errors());
509 assert_eq!(binder.optional::<u32>(Some("x"), "page", "integer"), None);
510 assert!(binder.has_errors());
511 }
512
513 #[test]
514 fn list_binds_every_occurrence() {
515 let mut binder = Binder::new();
516 assert_eq!(
517 binder.list::<u32>(&["1", "2", "3"], "ids", "integer"),
518 vec![1, 2, 3]
519 );
520 assert!(!binder.has_errors());
521 assert_eq!(binder.list::<u32>(&["1", "x"], "ids", "integer"), vec![1]);
522 assert!(binder.has_errors());
523 }
524
525 #[test]
526 fn query_values_keep_every_occurrence_and_expand_comma_lists() {
527 let path = HashMap::new();
528 let query = HashMap::new();
529 let mut multi: HashMap<String, Vec<String>> = HashMap::new();
530 multi.insert("ids".into(), vec!["1".into(), "2,3".into()]);
531
532 let input = BindInput {
533 path: &path,
534 query: &query,
535 query_multi: Some(&multi),
536 claims: None,
537 form: None,
538 is_body: false,
539 is_multipart: false,
540 };
541 assert_eq!(input.query_list("ids"), vec!["1", "2", "3"]);
542 assert_eq!(input.query_one("ids"), Some("2,3"));
543 }
544
545 #[test]
546 fn error_body_groups_fields() {
547 let mut binder = Binder::new();
548 binder.missing("a");
549 binder.missing("a");
550 binder.missing("b");
551 let error = binder.into_error();
552 let Error::Problem(problem) = error else {
553 panic!("expected Problem, got {error:?}");
554 };
555 assert_eq!(problem.status, 400);
556 let errors = problem.extensions.as_ref().unwrap().get("errors").unwrap();
557 assert_eq!(
558 errors["a"],
559 serde_json::json!(["The `a` field is required.", "The `a` field is required."])
560 );
561 assert_eq!(
562 errors["b"],
563 serde_json::json!(["The `b` field is required."])
564 );
565 }
566
567 struct FakeClaims {
570 subject: String,
571 roles: Vec<String>,
572 permissions: Vec<String>,
573 tenant: Option<String>,
574 raw: HashMap<String, String>,
575 }
576
577 impl FakeClaims {
578 fn new(raw: &[(&str, &str)]) -> Self {
579 Self {
580 subject: "subject-from-helper".to_string(),
581 roles: vec!["slot-role".to_string()],
582 permissions: vec!["slot-permission".to_string()],
583 tenant: Some("tenant-from-helper".to_string()),
584 raw: raw
585 .iter()
586 .map(|(key, value)| ((*key).to_string(), (*value).to_string()))
587 .collect(),
588 }
589 }
590 }
591
592 impl IClaims for FakeClaims {
593 fn subject(&self) -> &str {
594 &self.subject
595 }
596
597 fn roles(&self) -> &[String] {
598 &self.roles
599 }
600
601 fn permissions(&self) -> &[String] {
602 &self.permissions
603 }
604
605 fn claims(&self) -> &HashMap<String, String> {
606 &self.raw
607 }
608
609 fn get_tenantid(&self) -> Option<&str> {
610 self.tenant.as_deref()
611 }
612
613 fn clone_box(&self) -> Box<dyn IClaims> {
614 Box::new(Self {
615 subject: self.subject.clone(),
616 roles: self.roles.clone(),
617 permissions: self.permissions.clone(),
618 tenant: self.tenant.clone(),
619 raw: self.raw.clone(),
620 })
621 }
622 }
623
624 fn claims_input<'a>(
625 path: &'a HashMap<String, String>,
626 claims: &'a FakeClaims,
627 ) -> BindInput<'a> {
628 BindInput {
629 path,
630 query: path,
631 query_multi: None,
632 claims: Some(claims),
633 form: None,
634 is_body: false,
635 is_multipart: false,
636 }
637 }
638
639 #[test]
640 fn claim_prefers_the_raw_map_over_the_dedicated_accessors() {
641 let path = HashMap::new();
642 let claims = FakeClaims::new(&[
643 ("sub", "raw-sub"),
644 ("user_id", "raw-user-id"),
645 ("tenant", "raw-tenant"),
646 ]);
647 let input = claims_input(&path, &claims);
648
649 assert_eq!(input.claim("sub"), Some("raw-sub"));
652 assert_eq!(input.claim("user_id"), Some("raw-user-id"));
653 assert_eq!(input.claim("tenant"), Some("raw-tenant"));
654 }
655
656 #[test]
657 fn claim_falls_back_to_the_dedicated_accessors() {
658 let path = HashMap::new();
659 let claims = FakeClaims::new(&[]);
660 let input = claims_input(&path, &claims);
661
662 assert_eq!(input.claim("sub"), Some("subject-from-helper"));
663 assert_eq!(input.claim("subject"), Some("subject-from-helper"));
664 assert_eq!(input.claim("user_id"), Some("subject-from-helper"));
665 assert_eq!(input.claim("tenant_id"), Some("tenant-from-helper"));
666 assert_eq!(input.claim("tenant"), Some("tenant-from-helper"));
667 assert_eq!(input.claim("email"), None);
668 }
669
670 #[test]
671 fn claim_list_prefers_the_raw_map_then_the_dedicated_slots() {
672 let path = HashMap::new();
673 let claims = FakeClaims::new(&[("roles", "raw-admin, raw-auditor")]);
674 let input = claims_input(&path, &claims);
675
676 assert_eq!(input.claim_list("roles"), vec!["raw-admin", "raw-auditor"]);
677 assert_eq!(input.claim_list("permissions"), vec!["slot-permission"]);
679 assert_eq!(input.claim_list("missing"), Vec::<&str>::new());
680 }
681
682 #[test]
683 fn an_empty_value_is_an_empty_list_not_one_empty_element() {
684 assert_eq!(split_one(""), Vec::<&str>::new());
685 assert_eq!(split_one("a"), vec!["a"]);
686 assert_eq!(split_one("a,, b"), vec!["a", "b"]);
687 assert_eq!(split_csv(Some("")), Vec::<&str>::new());
688 assert_eq!(split_csv(None), Vec::<&str>::new());
689
690 let path = HashMap::new();
693 let query = HashMap::new();
694 let mut multi: HashMap<String, Vec<String>> = HashMap::new();
695 multi.insert("ids".into(), vec![String::new()]);
696 let input = BindInput {
697 path: &path,
698 query: &query,
699 query_multi: Some(&multi),
700 claims: None,
701 form: None,
702 is_body: false,
703 is_multipart: false,
704 };
705 assert_eq!(input.query_list("ids"), Vec::<&str>::new());
706 }
707}