Skip to main content

seam_server/validation/
compile.rs

1/* src/server/core/rust/src/validation/compile.rs */
2
3use 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	// Handle nullable wrapper
31	let nullable = obj.get("nullable").and_then(Value::as_bool).unwrap_or(false);
32
33	// Handle ref
34	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
90/// Compile a JTD schema JSON value into a `CompiledSchema` for fast validation.
91pub 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}