rskit_schema/
validation.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct ValidationOptions {
14 pub limits: ValidationLimits,
16}
17
18#[derive(Debug, Clone)]
20pub struct ValidationError {
21 pub path: String,
23 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#[derive(Debug, Clone)]
39pub struct ValidationResult {
40 pub valid: bool,
42 pub errors: Vec<ValidationError>,
44}
45
46impl ValidationResult {
47 #[must_use]
49 pub const fn valid() -> Self {
50 Self {
51 valid: true,
52 errors: Vec::new(),
53 }
54 }
55
56 #[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#[cfg(feature = "validation")]
71pub struct CompiledSchema {
72 validator: jsonschema::Validator,
73 options: ValidationOptions,
74}
75
76#[cfg(feature = "validation")]
77impl CompiledSchema {
78 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 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#[cfg(feature = "validation")]
95pub fn compile(schema: &Value) -> AppResult<CompiledSchema> {
96 compile_with_options(schema, ValidationOptions::default())
97}
98
99#[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#[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#[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#[cfg(feature = "validation")]
158pub fn validate_structured_output(schema: &Value, value: &Value) -> ValidationResult {
159 validate(schema, value)
160}