Skip to main content

webx_core/route/
binding.rs

1//! Request model binding — the runtime half of `#[derive(WebxRequestMeta)]`.
2//!
3//! A request struct declares where each field comes from (`#[frompath]`,
4//! `#[fromquery]`, `#[frombody]`, `#[fromform]`, `#[fromheader]`,
5//! `#[fromclaim]`). The derive compiles that declaration into a
6//! [`RequestModel::bind`] implementation; this module provides the value
7//! providers and coercion helpers that implementation calls.
8//!
9//! # Why not `serde` for query/path/header/claim
10//!
11//! HTTP values are *strings*, often repeated (`?id=1&id=2`), and a failure in
12//! one field must not hide the others. `serde_json` models documents, not HTTP
13//! values, so it cannot coerce `"2"` into `u32`, cannot represent repeated keys,
14//! and fails at the first error. Here every scalar is parsed with [`FromStr`],
15//! lists keep every occurrence, and failures accumulate in a [`Binder`] so the
16//! response can report all of them at once.
17//!
18//! JSON bodies stay with `serde`: the derive generates a hidden body struct
19//! containing only the body-bound fields, so `rename`, `flatten`, `default` and
20//! custom deserializers keep working exactly as written.
21
22use 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/// Where a request field's value comes from.
34///
35/// [`Source::Auto`] is the absence of an explicit marker; the generated binder
36/// then resolves it from the request method:
37///
38/// * `GET` / `HEAD` / `DELETE` / `OPTIONS` — query string, then the route;
39/// * `POST` / `PUT` / `PATCH` — JSON body, then form, then query string;
40/// * `multipart/form-data` — form, then query string.
41///
42/// Headers and claims are never implicit: a header or token that happens to
43/// share a field name must not silently win.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum Source {
46    /// A `{name}` segment of the route template.
47    Path,
48    /// A query-string key (`?name=value`, repeatable).
49    Query,
50    /// A `multipart/form-data` part (text or file).
51    Form,
52    /// An HTTP header, looked up case-insensitively.
53    Header,
54    /// A claim from the authenticated principal.
55    Claim,
56    /// The JSON request body.
57    Body,
58    /// No marker: inferred from the request method.
59    Auto,
60}
61
62impl Source {
63    /// The OpenAPI `in` value (`""` for [`Source::Auto`], resolved per route).
64    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    /// Whether an explicitly marked field must be present.
77    pub const fn is_explicit(self) -> bool {
78        !matches!(self, Source::Auto)
79    }
80}
81
82/// Whether `method` carries a JSON body by default (ASP.NET Core's rule).
83pub fn is_body_method(method: &str) -> bool {
84    matches!(method, "POST" | "PUT" | "PATCH")
85}
86
87/// A type that can be constructed from an HTTP request.
88///
89/// Implemented by `#[derive(WebxRequestMeta)]`. The endpoint macros call
90/// [`RequestModel::bind`] before dispatching to the handler, so a request type
91/// without the derive does not compile.
92///
93/// ```ignore
94/// #[derive(Default, Deserialize, WebxRequestMeta)]
95/// pub struct GetUserRequest {
96///     #[frompath]
97///     pub id: String,
98///     #[fromquery]
99///     pub expand: Option<String>,
100/// }
101/// ```
102#[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    /// OpenAPI parameter metadata for this request type.
109    ///
110    /// Sources are `"path"`, `"query"`, `"header"`, `"form"`, `"body"`,
111    /// `"claim"`, or `""` for an unmarked field resolved by the route method.
112    fn param_meta() -> &'static [ParamMeta];
113
114    /// Bind an instance from the live request context.
115    fn bind<'a>(
116        ctx: &'a mut dyn IHttpContext,
117    ) -> Pin<Box<dyn Future<Output = Result<Self>> + Send + 'a>>;
118}
119
120/// The value providers a generated [`RequestModel::bind`] reads from.
121///
122/// Borrowed from the request context: binding never copies the route, query or
123/// header maps, and the form is shared through the request's cached parse.
124pub struct BindInput<'a> {
125    /// Route parameters captured by the router (`{}` segments).
126    pub path: &'a HashMap<String, String>,
127    /// Query string, last value per key (the compatibility view).
128    pub query: &'a HashMap<String, String>,
129    /// Query string, every value per key; `None` when the host does not
130    /// provide multi-value parsing.
131    pub query_multi: Option<&'a HashMap<String, Vec<String>>>,
132    /// The authenticated principal, when the request is authenticated.
133    pub claims: Option<&'a dyn IClaims>,
134    /// The parsed `multipart/form-data` body, when the request is multipart.
135    pub form: Option<&'a MultipartForm>,
136    /// Whether the route method is `POST`, `PUT` or `PATCH`.
137    pub is_body: bool,
138    /// Whether the request declares `multipart/form-data`.
139    pub is_multipart: bool,
140}
141
142impl<'a> BindInput<'a> {
143    /// The route value for `name`.
144    pub fn path_one(&self, name: &str) -> Option<&'a str> {
145        self.path.get(name).map(String::as_str)
146    }
147
148    /// Every route value for `name`, comma-separated lists expanded.
149    ///
150    /// A route segment is a single value, but `{ids}` accepting `1,2,3` is a
151    /// convenient way to bind `Vec<T>` from the path — the same rule query
152    /// values follow.
153    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    /// The last query value for `name`.
161    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    /// Every query value for `name`, comma-separated lists expanded.
171    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    /// The first text part named `name` in the form.
184    pub fn form_one(&self, name: &str) -> Option<&'a str> {
185        self.form?.text(name)
186    }
187
188    /// Every text part named `name`, comma-separated lists expanded.
189    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    /// The first file part named `name`.
197    pub fn file(&self, name: &str) -> Option<&'a FormFile> {
198        self.form?.file(name)
199    }
200
201    /// Every file part named `name`, in order.
202    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    /// A claim value by key.
210    ///
211    /// `sub` / `subject` resolve to the subject, `tenant_id` to the tenant
212    /// helper, and anything else to the raw claims map.
213    pub fn claim(&self, key: &str) -> Option<&'a str> {
214        let claims = self.claims?;
215        match key {
216            "sub" | "subject" | "user_id" => Some(claims.subject()),
217            "tenant_id" | "tenant" => claims.get_tenantid(),
218            other => claims.claims().get(other).map(String::as_str),
219        }
220    }
221
222    /// A claim as a list: `roles` / `permissions` use their dedicated slots,
223    /// anything else is comma-separated.
224    pub fn claim_list(&self, key: &str) -> Vec<&'a str> {
225        let Some(claims) = self.claims else {
226            return Vec::new();
227        };
228        match key {
229            "roles" => claims.roles().iter().map(String::as_str).collect(),
230            "permissions" => claims.permissions().iter().map(String::as_str).collect(),
231            other => match self.claim(other) {
232                Some(value) => split_one(value),
233                None => Vec::new(),
234            },
235        }
236    }
237}
238
239/// Convert a JSON body deserialization failure into a field-level 400.
240///
241/// `serde_json` reports the offending field for the common cases; those become
242/// `errors.<field>` entries, and everything else is reported at the body level
243/// under the empty key — the same convention ASP.NET Core uses for a body
244/// model that cannot be bound at all.
245pub fn body_error(error: serde_json::Error) -> Error {
246    let message = error.to_string();
247    let field = extract_error_field(&message)
248        .unwrap_or_default()
249        .to_string();
250    ProblemDetails::validation(vec![FieldError::new(field, message)]).to_error()
251}
252
253fn extract_error_field(message: &str) -> Option<&str> {
254    for marker in ["missing field `", "unknown field `", "duplicate field `"] {
255        if let Some((_, rest)) = message.split_once(marker) {
256            if let Some((name, _)) = rest.split_once('`') {
257                return Some(name);
258            }
259        }
260    }
261    None
262}
263
264/// A field-level body failure: the JSON key was present but its value did not
265/// match the field type.
266pub fn body_field_error(field: &str, error: serde_json::Error) -> Error {
267    ProblemDetails::validation(vec![FieldError::new(
268        field,
269        format!("The `{field}` field is invalid: {error}"),
270    )])
271    .to_error()
272}
273
274/// Remove `key` (or one of `aliases`) from a JSON object.
275///
276/// `serde`'s `rename`/`alias` are resolved by the derive; this looks the
277/// resolved keys up and takes ownership of the value, so the remaining map only
278/// holds keys no field claimed (which `deny_unknown_fields` can reject).
279pub fn take_body_field(
280    object: &mut serde_json::Map<String, serde_json::Value>,
281    key: &str,
282    aliases: &[&str],
283) -> Option<serde_json::Value> {
284    if let Some(value) = object.remove(key) {
285        return Some(value);
286    }
287    for alias in aliases {
288        if let Some(value) = object.remove(*alias) {
289            return Some(value);
290        }
291    }
292    None
293}
294
295/// Split one raw value on commas, dropping empty segments.
296fn split_one(value: &str) -> Vec<&str> {
297    if value.contains(',') {
298        value
299            .split(',')
300            .map(str::trim)
301            .filter(|part| !part.is_empty())
302            .collect()
303    } else {
304        vec![value]
305    }
306}
307
308/// Expand one optional raw value into a list, splitting on commas.
309///
310/// Public because the derive uses it for `Vec<T>` header fields, which carry a
311/// single comma-separated line.
312pub fn split_csv(value: Option<&str>) -> Vec<&str> {
313    match value {
314        Some(value) => split_one(value),
315        None => Vec::new(),
316    }
317}
318
319/// Split every raw value on commas, flattening the results.
320fn split_values(values: &[String]) -> Vec<&str> {
321    let mut out = Vec::with_capacity(values.len());
322    for value in values {
323        out.extend(split_one(value));
324    }
325    out
326}
327
328/// Accumulates field-level binding failures into one 400 response.
329///
330/// Binding is best-effort: a bad field records an error and yields `None`, the
331/// remaining fields still bind, and the request is rejected once at the end
332/// with every failure listed — matching ASP.NET Core's `ModelState`.
333#[derive(Default)]
334pub struct Binder {
335    errors: Vec<FieldError>,
336}
337
338impl Binder {
339    /// A binder with no recorded errors.
340    pub fn new() -> Self {
341        Self::default()
342    }
343
344    /// Whether any field failed.
345    pub fn has_errors(&self) -> bool {
346        !self.errors.is_empty()
347    }
348
349    /// The recorded failures, in binding order.
350    pub fn errors(&self) -> &[FieldError] {
351        &self.errors
352    }
353
354    /// Consume the binder, yielding the 400 `ProblemDetails` error.
355    ///
356    /// The response body carries `errors` as `{ "field": ["message"] }`.
357    pub fn into_error(self) -> Error {
358        ProblemDetails::validation(self.errors).to_error()
359    }
360
361    /// Record the canonical "required" failure for `field`.
362    pub fn missing(&mut self, field: &str) {
363        self.errors.push(FieldError::new(
364            field,
365            format!("The `{field}` field is required."),
366        ));
367    }
368
369    /// Record an unparsable value for `field`.
370    pub fn invalid(&mut self, field: &str, kind: &str, value: &str) {
371        self.errors.push(FieldError::new(
372            field,
373            format!("The `{field}` field must be a valid {kind} (got `{value}`)."),
374        ));
375    }
376
377    /// Record a JSON key that no field claimed (`deny_unknown_fields`).
378    pub fn unknown_field(&mut self, name: &str) {
379        self.errors.push(FieldError::new(
380            "",
381            format!("The body contains an unknown field `{name}`."),
382        ));
383    }
384
385    /// Parse a required scalar.
386    ///
387    /// Returns `None` (and records an error) when the value is absent or
388    /// unparsable; the caller checks [`Binder::has_errors`] before building the
389    /// request.
390    pub fn required<T: FromStr>(
391        &mut self,
392        raw: Option<&str>,
393        field: &str,
394        kind: &str,
395    ) -> Option<T> {
396        let Some(raw) = raw else {
397            self.missing(field);
398            return None;
399        };
400        match raw.parse::<T>() {
401            Ok(value) => Some(value),
402            Err(_) => {
403                self.invalid(field, kind, raw);
404                None
405            }
406        }
407    }
408
409    /// Parse an optional scalar: absent is `None`, present-but-invalid is an
410    /// error (never silently `None`).
411    pub fn optional<T: FromStr>(
412        &mut self,
413        raw: Option<&str>,
414        field: &str,
415        kind: &str,
416    ) -> Option<T> {
417        let raw = raw?;
418        match raw.parse::<T>() {
419            Ok(value) => Some(value),
420            Err(_) => {
421                self.invalid(field, kind, raw);
422                None
423            }
424        }
425    }
426
427    /// Parse every occurrence of a repeated value into a list.
428    ///
429    /// An empty list is valid (ASP.NET Core binds missing collections as
430    /// empty); unparsable elements are errors.
431    pub fn list<T: FromStr>(&mut self, raw: &[&str], field: &str, kind: &str) -> Vec<T> {
432        let mut out = Vec::with_capacity(raw.len());
433        for value in raw {
434            match value.parse::<T>() {
435                Ok(parsed) => out.push(parsed),
436                Err(_) => self.invalid(field, kind, value),
437            }
438        }
439        out
440    }
441
442    /// Record a "required" failure for an already-extracted optional value.
443    ///
444    /// Used for body fields: `serde` has already validated the body, so a
445    /// missing value here means the field was skipped by the shadow struct.
446    pub fn take<T>(&mut self, value: Option<T>, field: &str) -> Option<T> {
447        if value.is_none() {
448            self.missing(field);
449        }
450        value
451    }
452
453    /// The first uploaded file, required.
454    pub fn required_file(&mut self, file: Option<&FormFile>, field: &str) -> Option<FormFile> {
455        match file {
456            Some(file) => Some(file.clone()),
457            None => {
458                self.missing(field);
459                None
460            }
461        }
462    }
463
464    /// The first uploaded file, optional.
465    pub fn optional_file(&mut self, file: Option<&FormFile>) -> Option<FormFile> {
466        file.cloned()
467    }
468
469    /// Every uploaded file under `name`.
470    pub fn files(&mut self, files: Vec<&FormFile>) -> Vec<FormFile> {
471        files.into_iter().cloned().collect()
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use std::collections::HashMap;
479
480    #[test]
481    fn required_reports_missing_and_invalid() {
482        let mut binder = Binder::new();
483        assert_eq!(binder.required::<u32>(None, "page", "integer"), None);
484        assert_eq!(binder.required::<u32>(Some("abc"), "page", "integer"), None);
485        assert_eq!(
486            binder.required::<u32>(Some("2"), "page", "integer"),
487            Some(2)
488        );
489        assert_eq!(binder.errors().len(), 2);
490        assert!(binder.errors()[0].message.contains("required"));
491    }
492
493    #[test]
494    fn optional_keeps_absent_as_none_and_rejects_invalid() {
495        let mut binder = Binder::new();
496        assert_eq!(binder.optional::<u32>(None, "page", "integer"), None);
497        assert!(!binder.has_errors());
498        assert_eq!(binder.optional::<u32>(Some("x"), "page", "integer"), None);
499        assert!(binder.has_errors());
500    }
501
502    #[test]
503    fn list_binds_every_occurrence() {
504        let mut binder = Binder::new();
505        assert_eq!(
506            binder.list::<u32>(&["1", "2", "3"], "ids", "integer"),
507            vec![1, 2, 3]
508        );
509        assert!(!binder.has_errors());
510        assert_eq!(binder.list::<u32>(&["1", "x"], "ids", "integer"), vec![1]);
511        assert!(binder.has_errors());
512    }
513
514    #[test]
515    fn query_values_keep_every_occurrence_and_expand_comma_lists() {
516        let path = HashMap::new();
517        let query = HashMap::new();
518        let mut multi: HashMap<String, Vec<String>> = HashMap::new();
519        multi.insert("ids".into(), vec!["1".into(), "2,3".into()]);
520
521        let input = BindInput {
522            path: &path,
523            query: &query,
524            query_multi: Some(&multi),
525            claims: None,
526            form: None,
527            is_body: false,
528            is_multipart: false,
529        };
530        assert_eq!(input.query_list("ids"), vec!["1", "2", "3"]);
531        assert_eq!(input.query_one("ids"), Some("2,3"));
532    }
533
534    #[test]
535    fn error_body_groups_fields() {
536        let mut binder = Binder::new();
537        binder.missing("a");
538        binder.missing("a");
539        binder.missing("b");
540        let error = binder.into_error();
541        let Error::Problem(problem) = error else {
542            panic!("expected Problem, got {error:?}");
543        };
544        assert_eq!(problem.status, 400);
545        let errors = problem.extensions.as_ref().unwrap().get("errors").unwrap();
546        assert_eq!(
547            errors["a"],
548            serde_json::json!(["The `a` field is required.", "The `a` field is required."])
549        );
550        assert_eq!(
551            errors["b"],
552            serde_json::json!(["The `b` field is required."])
553        );
554    }
555}