1use 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
18pub 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}