Skip to main content

dmc_schema/
primitives.rs

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}