Skip to main content

mii_http/
value.rs

1//! Value validation against type expressions.
2
3use crate::spec::{JsonField, JsonFieldType, JsonSchema, TypeExpr};
4use regex::Regex;
5use serde_json::Value;
6
7#[derive(Debug, Clone)]
8pub struct ValidationError {
9    pub message: String,
10}
11
12impl ValidationError {
13    fn new(s: impl Into<String>) -> Self {
14        Self { message: s.into() }
15    }
16}
17
18/// Validate a textual value (e.g. query/header/path/form field) against a type.
19pub fn validate_text(value: &str, ty: &TypeExpr) -> Result<(), ValidationError> {
20    match ty {
21        TypeExpr::Int => value
22            .parse::<i64>()
23            .map(|_| ())
24            .map_err(|_| ValidationError::new("expected integer")),
25        TypeExpr::Float => value
26            .parse::<f64>()
27            .map(|_| ())
28            .map_err(|_| ValidationError::new("expected float")),
29        TypeExpr::Boolean => match value {
30            "true" | "false" => Ok(()),
31            _ => Err(ValidationError::new("expected boolean (true/false)")),
32        },
33        TypeExpr::Uuid => uuid::Uuid::parse_str(value)
34            .map(|_| ())
35            .map_err(|_| ValidationError::new("expected uuid")),
36        TypeExpr::IntRange { min, max, .. } => {
37            let n: i64 = value
38                .parse()
39                .map_err(|_| ValidationError::new("expected integer"))?;
40            if n < *min || n > *max {
41                Err(ValidationError::new(format!(
42                    "value {} out of range [{}..{}]",
43                    n, min, max
44                )))
45            } else {
46                Ok(())
47            }
48        }
49        TypeExpr::FloatRange { min, max, .. } => {
50            let n: f64 = value
51                .parse()
52                .map_err(|_| ValidationError::new("expected float"))?;
53            if n < *min || n > *max {
54                Err(ValidationError::new(format!(
55                    "value {} out of range [{}..{}]",
56                    n, min, max
57                )))
58            } else {
59                Ok(())
60            }
61        }
62        TypeExpr::Union { variants, .. } => {
63            if variants.iter().any(|v| v == value) {
64                Ok(())
65            } else {
66                Err(ValidationError::new(format!(
67                    "expected one of {}",
68                    variants.join(", ")
69                )))
70            }
71        }
72        TypeExpr::Regex { pattern, .. } => {
73            let re = Regex::new(&format!("^(?:{})$", pattern))
74                .map_err(|e| ValidationError::new(format!("invalid regex: {}", e)))?;
75            if re.is_match(value) {
76                Ok(())
77            } else {
78                Err(ValidationError::new(format!(
79                    "value does not match pattern /{}/",
80                    pattern
81                )))
82            }
83        }
84        TypeExpr::String | TypeExpr::Json | TypeExpr::Binary => Ok(()),
85    }
86}
87
88pub fn validate_json(value: &Value, schema: &JsonSchema) -> Result<(), ValidationError> {
89    let obj = value
90        .as_object()
91        .ok_or_else(|| ValidationError::new("expected JSON object"))?;
92    for f in &schema.fields {
93        match obj.get(&f.name) {
94            None => {
95                if !f.optional {
96                    return Err(ValidationError::new(format!(
97                        "missing required field `{}`",
98                        f.name
99                    )));
100                }
101            }
102            Some(v) => validate_json_field(v, f)?,
103        }
104    }
105    Ok(())
106}
107
108fn validate_json_field(v: &Value, f: &JsonField) -> Result<(), ValidationError> {
109    match &f.ty {
110        JsonFieldType::Scalar(t) => validate_json_value(v, t).map_err(|e| {
111            ValidationError::new(format!("field `{}`: {}", f.name, e.message))
112        }),
113        JsonFieldType::Array(t) => {
114            let arr = v
115                .as_array()
116                .ok_or_else(|| ValidationError::new(format!("field `{}` expected array", f.name)))?;
117            for item in arr {
118                validate_json_value(item, t).map_err(|e| {
119                    ValidationError::new(format!("field `{}`: {}", f.name, e.message))
120                })?;
121            }
122            Ok(())
123        }
124    }
125}
126
127fn validate_json_value(v: &Value, ty: &TypeExpr) -> Result<(), ValidationError> {
128    match ty {
129        TypeExpr::Int => v
130            .as_i64()
131            .map(|_| ())
132            .ok_or_else(|| ValidationError::new("expected integer")),
133        TypeExpr::Float => v
134            .as_f64()
135            .map(|_| ())
136            .ok_or_else(|| ValidationError::new("expected float")),
137        TypeExpr::Boolean => v
138            .as_bool()
139            .map(|_| ())
140            .ok_or_else(|| ValidationError::new("expected boolean")),
141        TypeExpr::Uuid => v
142            .as_str()
143            .ok_or_else(|| ValidationError::new("expected string"))
144            .and_then(|s| validate_text(s, ty)),
145        TypeExpr::String | TypeExpr::Json => Ok(()),
146        TypeExpr::Binary => Err(ValidationError::new("binary not allowed in JSON schema")),
147        TypeExpr::IntRange { .. } => v
148            .as_i64()
149            .map(|n| n.to_string())
150            .ok_or_else(|| ValidationError::new("expected integer"))
151            .and_then(|s| validate_text(&s, ty)),
152        TypeExpr::FloatRange { .. } => v
153            .as_f64()
154            .map(|n| n.to_string())
155            .ok_or_else(|| ValidationError::new("expected float"))
156            .and_then(|s| validate_text(&s, ty)),
157        TypeExpr::Union { .. } | TypeExpr::Regex { .. } => v
158            .as_str()
159            .ok_or_else(|| ValidationError::new("expected string"))
160            .and_then(|s| validate_text(s, ty)),
161    }
162}