Skip to main content

seam_server/validation/
mod.rs

1/* src/server/core/rust/src/validation/mod.rs */
2
3mod check;
4mod compile;
5mod walk;
6
7use serde_json::Value;
8use std::env;
9
10use check::ValidateCtx;
11pub use compile::compile_schema;
12use walk::validate_walk;
13
14/// Controls when input validation runs.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ValidationMode {
17	/// Validate only in dev mode (default).
18	Dev,
19	/// Always validate.
20	Always,
21	/// Never validate.
22	Never,
23}
24
25/// Check whether validation should run for the given mode.
26pub fn should_validate(mode: &ValidationMode) -> bool {
27	match mode {
28		ValidationMode::Never => false,
29		ValidationMode::Always => true,
30		ValidationMode::Dev => {
31			if let Ok(v) = env::var("SEAM_ENV") {
32				return v != "production";
33			}
34			if let Ok(v) = env::var("NODE_ENV") {
35				return v != "production";
36			}
37			true
38		}
39	}
40}
41
42/// JTD primitive type identifiers.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum JtdType {
45	Boolean,
46	String,
47	Timestamp,
48	Int8,
49	Int16,
50	Int32,
51	Uint8,
52	Uint16,
53	Uint32,
54	Float32,
55	Float64,
56}
57
58/// Pre-compiled JTD schema for fast repeated validation.
59#[derive(Debug, Clone)]
60pub enum CompiledSchema {
61	Empty,
62	Type(JtdType),
63	Enum(Vec<String>),
64	Elements(Box<CompiledSchema>),
65	Values(Box<CompiledSchema>),
66	Properties {
67		required: Vec<(String, CompiledSchema)>,
68		optional: Vec<(String, CompiledSchema)>,
69		allow_extra: bool,
70	},
71	Discriminator {
72		tag: String,
73		mapping: Vec<(String, CompiledSchema)>,
74	},
75	Nullable(Box<CompiledSchema>),
76}
77
78/// A single validation error with path, expected type, and actual value description.
79#[derive(Debug, Clone)]
80pub struct ValidationDetail {
81	pub path: String,
82	pub expected: String,
83	pub actual: String,
84}
85
86impl ValidationDetail {
87	pub fn to_json(&self) -> Value {
88		serde_json::json!({
89			"path": self.path,
90			"expected": self.expected,
91			"actual": self.actual,
92		})
93	}
94}
95
96const MAX_ERRORS: usize = 10;
97const MAX_DEPTH: usize = 32;
98
99/// Validate `data` against a JTD schema (compiles on each call).
100/// Returns `Ok(())` if valid, or `Err((summary, details))` on failure.
101pub fn validate_input(schema: &Value, data: &Value) -> Result<(), (String, Vec<ValidationDetail>)> {
102	let compiled = compile_schema(schema).map_err(|e| (e, vec![]))?;
103	validate_compiled(&compiled, data)
104}
105
106/// Validate `data` against a pre-compiled schema.
107/// Returns `Ok(())` if valid, or `Err((summary, details))` on failure.
108pub fn validate_compiled(
109	schema: &CompiledSchema,
110	data: &Value,
111) -> Result<(), (String, Vec<ValidationDetail>)> {
112	let mut errors = Vec::new();
113	let mut ctx = ValidateCtx { errors: &mut errors, max_errors: MAX_ERRORS, max_depth: MAX_DEPTH };
114	validate_walk(schema, data, "", &mut ctx, 0, None);
115	if errors.is_empty() {
116		Ok(())
117	} else {
118		let count = errors.len();
119		let summary = if count == 1 {
120			"validation failed: 1 error".into()
121		} else {
122			format!("validation failed: {count} errors")
123		};
124		Err((summary, errors))
125	}
126}
127
128#[cfg(test)]
129mod tests;