Skip to main content

rskit_schema/
validation.rs

1//! JSON Schema compilation and validation.
2
3use crate::ValidationLimits;
4#[cfg(feature = "validation")]
5use crate::limits::check_json_limits;
6#[cfg(feature = "validation")]
7use rskit_errors::{AppError, AppResult, ErrorCode};
8#[cfg(feature = "validation")]
9use serde_json::Value;
10
11/// Options for schema compilation and value validation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct ValidationOptions {
14    /// Structural limits applied to schemas and values before validation.
15    pub limits: ValidationLimits,
16}
17
18/// A single validation error with a JSON-pointer path.
19#[derive(Debug, Clone)]
20pub struct ValidationError {
21    /// JSON Pointer path to the failing value.
22    pub path: String,
23    /// Human-readable validation failure message.
24    pub message: String,
25}
26
27impl std::fmt::Display for ValidationError {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        if self.path.is_empty() {
30            write!(f, "{}", self.message)
31        } else {
32            write!(f, "{}: {}", self.path, self.message)
33        }
34    }
35}
36
37/// Outcome of validating a value against a schema.
38#[derive(Debug, Clone)]
39pub struct ValidationResult {
40    /// Whether the value satisfied the schema.
41    pub valid: bool,
42    /// Validation failures. Empty when `valid` is true.
43    pub errors: Vec<ValidationError>,
44}
45
46impl ValidationResult {
47    /// Return a valid result with no errors.
48    #[must_use]
49    pub const fn valid() -> Self {
50        Self {
51            valid: true,
52            errors: Vec::new(),
53        }
54    }
55
56    /// Return an invalid result containing one root-level error.
57    #[must_use]
58    pub fn invalid(message: impl Into<String>) -> Self {
59        Self {
60            valid: false,
61            errors: vec![ValidationError {
62                path: String::new(),
63                message: message.into(),
64            }],
65        }
66    }
67}
68
69/// Reusable compiled JSON Schema validator.
70#[cfg(feature = "validation")]
71pub struct CompiledSchema {
72    validator: jsonschema::Validator,
73    options: ValidationOptions,
74}
75
76#[cfg(feature = "validation")]
77impl CompiledSchema {
78    /// Validate a JSON value against this compiled schema.
79    pub fn validate(&self, value: &Value) -> ValidationResult {
80        match self.try_validate(value) {
81            Ok(result) => result,
82            Err(err) => ValidationResult::invalid(err.message().to_owned()),
83        }
84    }
85
86    /// Validate a JSON value and surface structural-limit failures as [`AppError`].
87    pub fn try_validate(&self, value: &Value) -> AppResult<ValidationResult> {
88        check_json_limits("value", value, self.options.limits)?;
89        Ok(validation_result(&self.validator, value))
90    }
91}
92
93/// Compile a JSON Schema once for repeated validation.
94#[cfg(feature = "validation")]
95pub fn compile(schema: &Value) -> AppResult<CompiledSchema> {
96    compile_with_options(schema, ValidationOptions::default())
97}
98
99/// Compile a JSON Schema once with explicit validation options.
100#[cfg(feature = "validation")]
101pub fn compile_with_options(
102    schema: &Value,
103    options: ValidationOptions,
104) -> AppResult<CompiledSchema> {
105    check_json_limits("schema", schema, options.limits)?;
106    jsonschema::validator_for(schema)
107        .map(|validator| CompiledSchema { validator, options })
108        .map_err(|err| {
109            AppError::new(
110                ErrorCode::InvalidInput,
111                format!("invalid JSON Schema: {err}"),
112            )
113            .with_cause(err)
114        })
115}
116
117/// Validate a JSON value against a JSON Schema.
118///
119/// Invalid schemas
120/// and structural-limit violations are converted to a `ValidationResult` for compatibility with callers that expect validation failures rather than hard errors.
121#[cfg(feature = "validation")]
122pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
123    match validate_with_options(schema, value, ValidationOptions::default()) {
124        Ok(result) => result,
125        Err(err) => ValidationResult::invalid(err.message().to_owned()),
126    }
127}
128
129/// Validate a JSON value against a JSON Schema and return hard setup errors.
130#[cfg(feature = "validation")]
131pub fn validate_with_options(
132    schema: &Value,
133    value: &Value,
134    options: ValidationOptions,
135) -> AppResult<ValidationResult> {
136    let validator = compile_with_options(schema, options)?;
137    validator.try_validate(value)
138}
139
140#[cfg(feature = "validation")]
141fn validation_result(validator: &jsonschema::Validator, value: &Value) -> ValidationResult {
142    let errors = validator
143        .iter_errors(value)
144        .map(|err| ValidationError {
145            path: err.instance_path().to_string(),
146            message: err.to_string(),
147        })
148        .collect::<Vec<_>>();
149
150    ValidationResult {
151        valid: errors.is_empty(),
152        errors,
153    }
154}
155
156/// Validate structured model output against a JSON Schema 2020-12-compatible schema subset.
157#[cfg(feature = "validation")]
158pub fn validate_structured_output(schema: &Value, value: &Value) -> ValidationResult {
159    validate(schema, value)
160}