seam_server/validation/
compile.rs1use serde_json::{Map, Value};
4
5use super::{CompiledSchema, JtdType};
6
7pub(super) fn parse_jtd_type(s: &str) -> Result<JtdType, String> {
8 match s {
9 "boolean" => Ok(JtdType::Boolean),
10 "string" => Ok(JtdType::String),
11 "timestamp" => Ok(JtdType::Timestamp),
12 "int8" => Ok(JtdType::Int8),
13 "int16" => Ok(JtdType::Int16),
14 "int32" => Ok(JtdType::Int32),
15 "uint8" => Ok(JtdType::Uint8),
16 "uint16" => Ok(JtdType::Uint16),
17 "uint32" => Ok(JtdType::Uint32),
18 "float32" => Ok(JtdType::Float32),
19 "float64" => Ok(JtdType::Float64),
20 other => Err(format!("unknown JTD type: {other}")),
21 }
22}
23
24pub(super) fn compile_inner(
25 schema: &Value,
26 defs: &Map<String, Value>,
27) -> Result<CompiledSchema, String> {
28 let obj = schema.as_object().ok_or_else(|| "schema must be an object".to_string())?;
29
30 let nullable = obj.get("nullable").and_then(Value::as_bool).unwrap_or(false);
32
33 if let Some(ref_name) = obj.get("ref").and_then(Value::as_str) {
35 let def = defs.get(ref_name).ok_or_else(|| format!("undefined ref: {ref_name}"))?;
36 let inner = compile_inner(def, defs)?;
37 return if nullable { Ok(CompiledSchema::Nullable(Box::new(inner))) } else { Ok(inner) };
38 }
39
40 let inner = if let Some(type_val) = obj.get("type").and_then(Value::as_str) {
41 CompiledSchema::Type(parse_jtd_type(type_val)?)
42 } else if let Some(enum_val) = obj.get("enum") {
43 let arr = enum_val.as_array().ok_or_else(|| "enum must be an array".to_string())?;
44 let variants = arr
45 .iter()
46 .map(|v| {
47 v.as_str().map(String::from).ok_or_else(|| "enum values must be strings".to_string())
48 })
49 .collect::<Result<Vec<_>, _>>()?;
50 CompiledSchema::Enum(variants)
51 } else if let Some(elements_val) = obj.get("elements") {
52 CompiledSchema::Elements(Box::new(compile_inner(elements_val, defs)?))
53 } else if let Some(values_val) = obj.get("values") {
54 CompiledSchema::Values(Box::new(compile_inner(values_val, defs)?))
55 } else if obj.contains_key("properties") || obj.contains_key("optionalProperties") {
56 let mut required = Vec::new();
57 let mut optional = Vec::new();
58
59 if let Some(props) = obj.get("properties").and_then(Value::as_object) {
60 for (key, val) in props {
61 required.push((key.clone(), compile_inner(val, defs)?));
62 }
63 }
64 if let Some(props) = obj.get("optionalProperties").and_then(Value::as_object) {
65 for (key, val) in props {
66 optional.push((key.clone(), compile_inner(val, defs)?));
67 }
68 }
69
70 let allow_extra = obj.get("additionalProperties").and_then(Value::as_bool).unwrap_or(false);
71
72 CompiledSchema::Properties { required, optional, allow_extra }
73 } else if let Some(disc_val) = obj.get("discriminator").and_then(Value::as_str) {
74 let mapping_obj = obj
75 .get("mapping")
76 .and_then(Value::as_object)
77 .ok_or_else(|| "discriminator requires mapping".to_string())?;
78 let mut mapping = Vec::new();
79 for (key, val) in mapping_obj {
80 mapping.push((key.clone(), compile_inner(val, defs)?));
81 }
82 CompiledSchema::Discriminator { tag: disc_val.to_string(), mapping }
83 } else {
84 CompiledSchema::Empty
85 };
86
87 if nullable { Ok(CompiledSchema::Nullable(Box::new(inner))) } else { Ok(inner) }
88}
89
90pub fn compile_schema(schema: &Value) -> Result<CompiledSchema, String> {
92 let defs = schema.get("definitions").and_then(Value::as_object).cloned().unwrap_or_default();
93 compile_inner(schema, &defs)
94}