1use crate::{Ctx, Schema, ValidationError};
2use serde_json::Value;
3
4#[derive(Default, Clone)]
5pub struct StringSchema {
6 pub min: Option<usize>,
7 pub max: Option<usize>,
8 pub regex: Option<String>,
9 pub length: Option<usize>,
10}
11
12impl StringSchema {
13 pub fn min(mut self, n: usize) -> Self {
14 self.min = Some(n);
15 self
16 }
17 pub fn max(mut self, n: usize) -> Self {
18 self.max = Some(n);
19 self
20 }
21 pub fn length(mut self, n: usize) -> Self {
22 self.length = Some(n);
23 self
24 }
25 pub fn regex(mut self, pat: impl Into<String>) -> Self {
26 self.regex = Some(pat.into());
27 self
28 }
29}
30
31impl Schema for StringSchema {
32 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
33 let s =
34 value.as_str().ok_or_else(|| ValidationError::root(format!("expected string, got {}", json_kind(value),)))?;
35 let len = s.chars().count();
36 if let Some(m) = self.min
37 && len < m
38 {
39 return Err(ValidationError::root(format!("too short (min {m}, got {len})")));
40 }
41 if let Some(m) = self.max
42 && len > m
43 {
44 return Err(ValidationError::root(format!("too long (max {m}, got {len})")));
45 }
46 if let Some(l) = self.length
47 && len != l
48 {
49 return Err(ValidationError::root(format!("length {l} required (got {len})")));
50 }
51 if let Some(pat) = &self.regex {
52 let re = fancy_regex::Regex::new(pat).map_err(|e| ValidationError::root(format!("invalid regex {pat}: {e}")))?;
53 let matched = re.is_match(s).map_err(|e| ValidationError::root(format!("regex match error: {e}")))?;
54 if !matched {
55 return Err(ValidationError::root(format!("does not match /{pat}/")));
56 }
57 }
58 Ok(Value::String(s.to_string()))
59 }
60}
61
62pub struct RecordSchema {
63 pub value: Box<dyn Schema>,
64}
65
66impl Schema for RecordSchema {
67 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
68 let obj =
69 value.as_object().ok_or_else(|| ValidationError::root(format!("expected object, got {}", json_kind(value),)))?;
70 let mut out = serde_json::Map::new();
71 for (k, v) in obj {
72 let parsed = self.value.parse(v, ctx).map_err(|e| e.at(k))?;
73 out.insert(k.clone(), parsed);
74 }
75 Ok(Value::Object(out))
76 }
77}
78
79pub struct TupleSchema {
80 pub items: Vec<Box<dyn Schema>>,
81}
82
83impl Schema for TupleSchema {
84 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
85 let arr =
86 value.as_array().ok_or_else(|| ValidationError::root(format!("expected tuple, got {}", json_kind(value),)))?;
87 if arr.len() != self.items.len() {
88 return Err(ValidationError::root(format!(
89 "tuple length mismatch: expected {}, got {}",
90 self.items.len(),
91 arr.len(),
92 )));
93 }
94 let mut out = Vec::with_capacity(arr.len());
95 for (i, (schema, v)) in self.items.iter().zip(arr.iter()).enumerate() {
96 out.push(schema.parse(v, ctx).map_err(|e| e.at_index(i))?);
97 }
98 Ok(Value::Array(out))
99 }
100}
101
102pub struct IntersectionSchema {
103 pub left: Box<dyn Schema>,
104 pub right: Box<dyn Schema>,
105}
106
107impl Schema for IntersectionSchema {
108 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
109 let a = self.left.parse(value, ctx)?;
110 let b = self.right.parse(value, ctx)?;
111 match (a, b) {
112 (Value::Object(mut ma), Value::Object(mb)) => {
113 for (k, v) in mb {
114 ma.insert(k, v);
115 }
116 Ok(Value::Object(ma))
117 },
118 (a, _) => Ok(a),
119 }
120 }
121}
122
123pub struct DiscriminatedUnionSchema {
124 pub discriminator: String,
125 pub variants: Vec<Box<dyn Schema>>,
126}
127
128impl Schema for DiscriminatedUnionSchema {
129 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
130 let obj = value.as_object().ok_or_else(|| ValidationError::root("discriminatedUnion expects object"))?;
131 let tag = obj
132 .get(&self.discriminator)
133 .ok_or_else(|| ValidationError::root(format!("missing discriminator field '{}'", self.discriminator,)))?;
134 for v in &self.variants {
135 if let Ok(parsed) = v.parse(value, ctx) {
136 return Ok(parsed);
137 }
138 }
139 Err(ValidationError::root(format!("no discriminatedUnion variant matched for {}={}", self.discriminator, tag,)))
140 }
141}
142
143pub struct CoerceSchema {
144 pub target: CoerceTarget,
145}
146
147#[derive(Clone, Copy)]
148pub enum CoerceTarget {
149 String,
150 Number,
151 Boolean,
152 Date,
153}
154
155impl Schema for CoerceSchema {
156 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
157 match self.target {
158 CoerceTarget::String => match value {
159 Value::String(s) => Ok(Value::String(s.clone())),
160 Value::Number(n) => Ok(Value::String(n.to_string())),
161 Value::Bool(b) => Ok(Value::String(b.to_string())),
162 Value::Null => Ok(Value::String(String::new())),
163 _ => Err(ValidationError::root(format!("cannot coerce {} to string", json_kind(value)))),
164 },
165 CoerceTarget::Number => match value {
166 Value::Number(_) => Ok(value.clone()),
167 Value::String(s) => s
168 .parse::<f64>()
169 .map(|n| serde_json::json!(n))
170 .map_err(|_| ValidationError::root(format!("cannot coerce '{s}' to number"))),
171 Value::Bool(b) => Ok(serde_json::json!(if *b { 1 } else { 0 })),
172 _ => Err(ValidationError::root(format!("cannot coerce {} to number", json_kind(value)))),
173 },
174 CoerceTarget::Boolean => match value {
175 Value::Bool(_) => Ok(value.clone()),
176 Value::String(s) => Ok(Value::Bool(!s.is_empty() && s != "false" && s != "0")),
177 Value::Number(n) => Ok(Value::Bool(n.as_f64().is_some_and(|f| f != 0.0))),
178 Value::Null => Ok(Value::Bool(false)),
179 _ => Ok(Value::Bool(true)),
180 },
181 CoerceTarget::Date => match value {
182 Value::String(s) => Ok(Value::String(s.clone())),
183 _ => Err(ValidationError::root("date coerce requires string")),
184 },
185 }
186 }
187}
188
189#[derive(Default, Clone)]
190pub struct NumberSchema {
191 pub min: Option<f64>,
192 pub max: Option<f64>,
193 pub int: bool,
194}
195
196impl NumberSchema {
197 pub fn min(mut self, n: f64) -> Self {
198 self.min = Some(n);
199 self
200 }
201 pub fn max(mut self, n: f64) -> Self {
202 self.max = Some(n);
203 self
204 }
205 pub fn int(mut self) -> Self {
206 self.int = true;
207 self
208 }
209}
210
211impl Schema for NumberSchema {
212 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
213 let n =
214 value.as_f64().ok_or_else(|| ValidationError::root(format!("expected number, got {}", json_kind(value),)))?;
215 if self.int && n.fract() != 0.0 {
216 return Err(ValidationError::root(format!("expected integer, got {n}")));
217 }
218 if let Some(m) = self.min
219 && n < m
220 {
221 return Err(ValidationError::root(format!("below min {m} (got {n})")));
222 }
223 if let Some(m) = self.max
224 && n > m
225 {
226 return Err(ValidationError::root(format!("above max {m} (got {n})")));
227 }
228 Ok(value.clone())
229 }
230}
231
232#[derive(Default, Clone)]
233pub struct BooleanSchema;
234
235impl Schema for BooleanSchema {
236 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
237 if value.is_boolean() {
238 Ok(value.clone())
239 } else {
240 Err(ValidationError::root(format!("expected boolean, got {}", json_kind(value),)))
241 }
242 }
243}
244
245pub struct ArraySchema {
246 pub item: Box<dyn Schema>,
247 pub min: Option<usize>,
248 pub max: Option<usize>,
249}
250
251impl ArraySchema {
252 pub fn min(mut self, n: usize) -> Self {
253 self.min = Some(n);
254 self
255 }
256 pub fn max(mut self, n: usize) -> Self {
257 self.max = Some(n);
258 self
259 }
260}
261
262impl Schema for ArraySchema {
263 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
264 let arr =
265 value.as_array().ok_or_else(|| ValidationError::root(format!("expected array, got {}", json_kind(value),)))?;
266 if let Some(m) = self.min
267 && arr.len() < m
268 {
269 return Err(ValidationError::root(format!("too few items (min {m}, got {})", arr.len())));
270 }
271 if let Some(m) = self.max
272 && arr.len() > m
273 {
274 return Err(ValidationError::root(format!("too many items (max {m}, got {})", arr.len())));
275 }
276 let mut out = Vec::with_capacity(arr.len());
277 for (idx, item) in arr.iter().enumerate() {
278 out.push(self.item.parse(item, ctx).map_err(|e| e.at_index(idx))?);
279 }
280 Ok(Value::Array(out))
281 }
282}
283
284pub struct ObjectSchema {
285 pub fields: Vec<(String, Box<dyn Schema>)>,
286 pub passthrough: bool,
287}
288
289impl ObjectSchema {
290 pub fn passthrough(mut self) -> Self {
291 self.passthrough = true;
292 self
293 }
294}
295
296impl Schema for ObjectSchema {
297 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
298 let obj =
299 value.as_object().ok_or_else(|| ValidationError::root(format!("expected object, got {}", json_kind(value),)))?;
300 let mut out = serde_json::Map::new();
301 for (key, schema) in &self.fields {
302 let v = obj.get(key).cloned().unwrap_or(Value::Null);
303 let parsed = schema.parse(&v, ctx).map_err(|e| e.at(key))?;
304 if !parsed.is_null() {
305 out.insert(key.clone(), parsed);
306 }
307 }
308 if self.passthrough {
309 for (k, v) in obj {
310 if !out.contains_key(k) {
311 out.insert(k.clone(), v.clone());
312 }
313 }
314 }
315 Ok(Value::Object(out))
316 }
317}
318
319pub struct EnumSchema {
320 pub variants: Vec<Value>,
321}
322
323impl Schema for EnumSchema {
324 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
325 if self.variants.contains(value) {
326 Ok(value.clone())
327 } else {
328 let allowed: Vec<String> = self.variants.iter().map(|v| v.to_string()).collect();
329 Err(ValidationError::root(format!("must be one of [{}], got {}", allowed.join(", "), value,)))
330 }
331 }
332}
333
334pub struct LiteralSchema {
335 pub expected: Value,
336}
337
338impl Schema for LiteralSchema {
339 fn parse(&self, value: &Value, _ctx: &Ctx) -> Result<Value, ValidationError> {
340 if value == &self.expected {
341 Ok(value.clone())
342 } else {
343 Err(ValidationError::root(format!("must equal {}, got {}", self.expected, value,)))
344 }
345 }
346}
347
348pub struct UnionSchema {
349 pub variants: Vec<Box<dyn Schema>>,
350}
351
352impl Schema for UnionSchema {
353 fn parse(&self, value: &Value, ctx: &Ctx) -> Result<Value, ValidationError> {
354 let mut errors = Vec::new();
355 for variant in &self.variants {
356 match variant.parse(value, ctx) {
357 Ok(v) => return Ok(v),
358 Err(e) => errors.push(e),
359 }
360 }
361 Err(ValidationError::root(format!(
362 "no union variant matched ({} attempts: {})",
363 errors.len(),
364 errors.iter().map(|e| e.message.clone()).collect::<Vec<_>>().join("; "),
365 )))
366 }
367}
368
369fn json_kind(v: &Value) -> &'static str {
370 match v {
371 Value::Null => "null",
372 Value::Bool(_) => "boolean",
373 Value::Number(_) => "number",
374 Value::String(_) => "string",
375 Value::Array(_) => "array",
376 Value::Object(_) => "object",
377 }
378}