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)
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}