use crate::auth::IClaims;
use crate::error::{Error, Result};
use crate::form::{FormFile, MultipartForm};
use crate::http::IHttpContext;
use crate::problem::{FieldError, ProblemDetails};
use crate::route::scan::ParamMeta;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Source {
Path,
Query,
Form,
Header,
Claim,
Body,
Auto,
}
impl Source {
pub const fn as_str(self) -> &'static str {
match self {
Source::Path => "path",
Source::Query => "query",
Source::Form => "form",
Source::Header => "header",
Source::Claim => "claim",
Source::Body => "body",
Source::Auto => "",
}
}
pub const fn is_explicit(self) -> bool {
!matches!(self, Source::Auto)
}
}
pub fn is_body_method(method: &str) -> bool {
matches!(method, "POST" | "PUT" | "PATCH")
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be bound from an HTTP request",
label = "add `#[derive(WebxRequestMeta)]` to this request struct",
note = "the endpoint macros call `RequestModel::bind`; the derive is what implements it"
)]
pub trait RequestModel: Sized + Send + 'static {
fn param_meta() -> &'static [ParamMeta];
fn bind<'a>(
ctx: &'a mut dyn IHttpContext,
) -> Pin<Box<dyn Future<Output = Result<Self>> + Send + 'a>>;
}
pub struct BindInput<'a> {
pub path: &'a HashMap<String, String>,
pub query: &'a HashMap<String, String>,
pub query_multi: Option<&'a HashMap<String, Vec<String>>>,
pub claims: Option<&'a dyn IClaims>,
pub form: Option<&'a MultipartForm>,
pub is_body: bool,
pub is_multipart: bool,
}
impl<'a> BindInput<'a> {
pub fn path_one(&self, name: &str) -> Option<&'a str> {
self.path.get(name).map(String::as_str)
}
pub fn path_list(&self, name: &str) -> Vec<&'a str> {
match self.path.get(name) {
Some(value) => split_one(value),
None => Vec::new(),
}
}
pub fn query_one(&self, name: &str) -> Option<&'a str> {
if let Some(multi) = self.query_multi {
if let Some(values) = multi.get(name) {
return values.last().map(String::as_str);
}
}
self.query.get(name).map(String::as_str)
}
pub fn query_list(&self, name: &str) -> Vec<&'a str> {
if let Some(multi) = self.query_multi {
if let Some(values) = multi.get(name) {
return split_values(values);
}
}
match self.query.get(name) {
Some(value) => split_one(value),
None => Vec::new(),
}
}
pub fn form_one(&self, name: &str) -> Option<&'a str> {
self.form?.text(name)
}
pub fn form_list(&self, name: &str) -> Vec<&'a str> {
match self.form {
Some(form) => form.texts(name).flat_map(split_one).collect(),
None => Vec::new(),
}
}
pub fn file(&self, name: &str) -> Option<&'a FormFile> {
self.form?.file(name)
}
pub fn files(&self, name: &str) -> Vec<&'a FormFile> {
match self.form {
Some(form) => form.files_named(name).collect(),
None => Vec::new(),
}
}
pub fn claim(&self, key: &str) -> Option<&'a str> {
let claims = self.claims?;
match key {
"sub" | "subject" | "user_id" => Some(claims.subject()),
"tenant_id" | "tenant" => claims.get_tenantid(),
other => claims.claims().get(other).map(String::as_str),
}
}
pub fn claim_list(&self, key: &str) -> Vec<&'a str> {
let Some(claims) = self.claims else {
return Vec::new();
};
match key {
"roles" => claims.roles().iter().map(String::as_str).collect(),
"permissions" => claims.permissions().iter().map(String::as_str).collect(),
other => match self.claim(other) {
Some(value) => split_one(value),
None => Vec::new(),
},
}
}
}
pub fn body_error(error: serde_json::Error) -> Error {
let message = error.to_string();
let field = extract_error_field(&message)
.unwrap_or_default()
.to_string();
ProblemDetails::validation(vec![FieldError::new(field, message)]).to_error()
}
fn extract_error_field(message: &str) -> Option<&str> {
for marker in ["missing field `", "unknown field `", "duplicate field `"] {
if let Some((_, rest)) = message.split_once(marker) {
if let Some((name, _)) = rest.split_once('`') {
return Some(name);
}
}
}
None
}
pub fn body_field_error(field: &str, error: serde_json::Error) -> Error {
ProblemDetails::validation(vec![FieldError::new(
field,
format!("The `{field}` field is invalid: {error}"),
)])
.to_error()
}
pub fn take_body_field(
object: &mut serde_json::Map<String, serde_json::Value>,
key: &str,
aliases: &[&str],
) -> Option<serde_json::Value> {
if let Some(value) = object.remove(key) {
return Some(value);
}
for alias in aliases {
if let Some(value) = object.remove(*alias) {
return Some(value);
}
}
None
}
fn split_one(value: &str) -> Vec<&str> {
if value.contains(',') {
value
.split(',')
.map(str::trim)
.filter(|part| !part.is_empty())
.collect()
} else {
vec![value]
}
}
pub fn split_csv(value: Option<&str>) -> Vec<&str> {
match value {
Some(value) => split_one(value),
None => Vec::new(),
}
}
fn split_values(values: &[String]) -> Vec<&str> {
let mut out = Vec::with_capacity(values.len());
for value in values {
out.extend(split_one(value));
}
out
}
#[derive(Default)]
pub struct Binder {
errors: Vec<FieldError>,
}
impl Binder {
pub fn new() -> Self {
Self::default()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn errors(&self) -> &[FieldError] {
&self.errors
}
pub fn into_error(self) -> Error {
ProblemDetails::validation(self.errors).to_error()
}
pub fn missing(&mut self, field: &str) {
self.errors.push(FieldError::new(
field,
format!("The `{field}` field is required."),
));
}
pub fn invalid(&mut self, field: &str, kind: &str, value: &str) {
self.errors.push(FieldError::new(
field,
format!("The `{field}` field must be a valid {kind} (got `{value}`)."),
));
}
pub fn unknown_field(&mut self, name: &str) {
self.errors.push(FieldError::new(
"",
format!("The body contains an unknown field `{name}`."),
));
}
pub fn required<T: FromStr>(
&mut self,
raw: Option<&str>,
field: &str,
kind: &str,
) -> Option<T> {
let Some(raw) = raw else {
self.missing(field);
return None;
};
match raw.parse::<T>() {
Ok(value) => Some(value),
Err(_) => {
self.invalid(field, kind, raw);
None
}
}
}
pub fn optional<T: FromStr>(
&mut self,
raw: Option<&str>,
field: &str,
kind: &str,
) -> Option<T> {
let raw = raw?;
match raw.parse::<T>() {
Ok(value) => Some(value),
Err(_) => {
self.invalid(field, kind, raw);
None
}
}
}
pub fn list<T: FromStr>(&mut self, raw: &[&str], field: &str, kind: &str) -> Vec<T> {
let mut out = Vec::with_capacity(raw.len());
for value in raw {
match value.parse::<T>() {
Ok(parsed) => out.push(parsed),
Err(_) => self.invalid(field, kind, value),
}
}
out
}
pub fn take<T>(&mut self, value: Option<T>, field: &str) -> Option<T> {
if value.is_none() {
self.missing(field);
}
value
}
pub fn required_file(&mut self, file: Option<&FormFile>, field: &str) -> Option<FormFile> {
match file {
Some(file) => Some(file.clone()),
None => {
self.missing(field);
None
}
}
}
pub fn optional_file(&mut self, file: Option<&FormFile>) -> Option<FormFile> {
file.cloned()
}
pub fn files(&mut self, files: Vec<&FormFile>) -> Vec<FormFile> {
files.into_iter().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn required_reports_missing_and_invalid() {
let mut binder = Binder::new();
assert_eq!(binder.required::<u32>(None, "page", "integer"), None);
assert_eq!(binder.required::<u32>(Some("abc"), "page", "integer"), None);
assert_eq!(
binder.required::<u32>(Some("2"), "page", "integer"),
Some(2)
);
assert_eq!(binder.errors().len(), 2);
assert!(binder.errors()[0].message.contains("required"));
}
#[test]
fn optional_keeps_absent_as_none_and_rejects_invalid() {
let mut binder = Binder::new();
assert_eq!(binder.optional::<u32>(None, "page", "integer"), None);
assert!(!binder.has_errors());
assert_eq!(binder.optional::<u32>(Some("x"), "page", "integer"), None);
assert!(binder.has_errors());
}
#[test]
fn list_binds_every_occurrence() {
let mut binder = Binder::new();
assert_eq!(
binder.list::<u32>(&["1", "2", "3"], "ids", "integer"),
vec![1, 2, 3]
);
assert!(!binder.has_errors());
assert_eq!(binder.list::<u32>(&["1", "x"], "ids", "integer"), vec![1]);
assert!(binder.has_errors());
}
#[test]
fn query_values_keep_every_occurrence_and_expand_comma_lists() {
let path = HashMap::new();
let query = HashMap::new();
let mut multi: HashMap<String, Vec<String>> = HashMap::new();
multi.insert("ids".into(), vec!["1".into(), "2,3".into()]);
let input = BindInput {
path: &path,
query: &query,
query_multi: Some(&multi),
claims: None,
form: None,
is_body: false,
is_multipart: false,
};
assert_eq!(input.query_list("ids"), vec!["1", "2", "3"]);
assert_eq!(input.query_one("ids"), Some("2,3"));
}
#[test]
fn error_body_groups_fields() {
let mut binder = Binder::new();
binder.missing("a");
binder.missing("a");
binder.missing("b");
let error = binder.into_error();
let Error::Problem(problem) = error else {
panic!("expected Problem, got {error:?}");
};
assert_eq!(problem.status, 400);
let errors = problem.extensions.as_ref().unwrap().get("errors").unwrap();
assert_eq!(
errors["a"],
serde_json::json!(["The `a` field is required.", "The `a` field is required."])
);
assert_eq!(
errors["b"],
serde_json::json!(["The `b` field is required."])
);
}
}