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)
111            .map_err(|e| ValidationError::new(format!("field `{}`: {}", f.name, e.message))),
112        JsonFieldType::Array(t) => {
113            let arr = v.as_array().ok_or_else(|| {
114                ValidationError::new(format!("field `{}` expected array", f.name))
115            })?;
116            for item in arr {
117                validate_json_value(item, t).map_err(|e| {
118                    ValidationError::new(format!("field `{}`: {}", f.name, e.message))
119                })?;
120            }
121            Ok(())
122        }
123    }
124}
125
126fn validate_json_value(v: &Value, ty: &TypeExpr) -> Result<(), ValidationError> {
127    match ty {
128        TypeExpr::Int => v
129            .as_i64()
130            .map(|_| ())
131            .ok_or_else(|| ValidationError::new("expected integer")),
132        TypeExpr::Float => v
133            .as_f64()
134            .map(|_| ())
135            .ok_or_else(|| ValidationError::new("expected float")),
136        TypeExpr::Boolean => v
137            .as_bool()
138            .map(|_| ())
139            .ok_or_else(|| ValidationError::new("expected boolean")),
140        TypeExpr::Uuid => v
141            .as_str()
142            .ok_or_else(|| ValidationError::new("expected string"))
143            .and_then(|s| validate_text(s, ty)),
144        TypeExpr::String | TypeExpr::Json => Ok(()),
145        TypeExpr::Binary => Err(ValidationError::new("binary not allowed in JSON schema")),
146        TypeExpr::IntRange { .. } => v
147            .as_i64()
148            .map(|n| n.to_string())
149            .ok_or_else(|| ValidationError::new("expected integer"))
150            .and_then(|s| validate_text(&s, ty)),
151        TypeExpr::FloatRange { .. } => v
152            .as_f64()
153            .map(|n| n.to_string())
154            .ok_or_else(|| ValidationError::new("expected float"))
155            .and_then(|s| validate_text(&s, ty)),
156        TypeExpr::Union { .. } | TypeExpr::Regex { .. } => v
157            .as_str()
158            .ok_or_else(|| ValidationError::new("expected string"))
159            .and_then(|s| validate_text(s, ty)),
160    }
161}