1use std::fmt;
4
5use crate::error_codes::{InstanceErrorCode, SchemaErrorCode};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum Severity {
11 Error,
13 Warning,
15}
16
17impl fmt::Display for Severity {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 Severity::Error => write!(f, "error"),
21 Severity::Warning => write!(f, "warning"),
22 }
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
31pub struct JsonLocation {
32 pub line: usize,
34 pub column: usize,
36}
37
38impl JsonLocation {
39 #[must_use]
41 pub const fn new(line: usize, column: usize) -> Self {
42 Self { line, column }
43 }
44
45 #[must_use]
47 pub const fn unknown() -> Self {
48 Self { line: 0, column: 0 }
49 }
50
51 #[must_use]
53 pub const fn is_unknown(&self) -> bool {
54 self.line == 0 && self.column == 0
55 }
56}
57
58impl fmt::Display for JsonLocation {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 if self.is_unknown() {
61 write!(f, "(unknown)")
62 } else {
63 write!(f, "{}:{}", self.line, self.column)
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ValidationError {
74 pub code: String,
76 pub message: String,
78 pub path: String,
80 pub severity: Severity,
82 pub location: JsonLocation,
84}
85
86impl ValidationError {
87 pub fn new(
89 code: impl Into<String>,
90 message: impl Into<String>,
91 path: impl Into<String>,
92 severity: Severity,
93 location: JsonLocation,
94 ) -> Self {
95 Self {
96 code: code.into(),
97 message: message.into(),
98 path: path.into(),
99 severity,
100 location,
101 }
102 }
103
104 pub fn schema_error(
106 code: SchemaErrorCode,
107 message: impl Into<String>,
108 path: impl Into<String>,
109 location: JsonLocation,
110 ) -> Self {
111 Self::new(code.as_str(), message, path, Severity::Error, location)
112 }
113
114 pub fn schema_warning(
116 code: SchemaErrorCode,
117 message: impl Into<String>,
118 path: impl Into<String>,
119 location: JsonLocation,
120 ) -> Self {
121 Self::new(code.as_str(), message, path, Severity::Warning, location)
122 }
123
124 pub fn instance_error(
126 code: InstanceErrorCode,
127 message: impl Into<String>,
128 path: impl Into<String>,
129 location: JsonLocation,
130 ) -> Self {
131 Self::new(code.as_str(), message, path, Severity::Error, location)
132 }
133
134 pub fn instance_warning(
136 code: InstanceErrorCode,
137 message: impl Into<String>,
138 path: impl Into<String>,
139 location: JsonLocation,
140 ) -> Self {
141 Self::new(code.as_str(), message, path, Severity::Warning, location)
142 }
143
144 pub fn is_error(&self) -> bool {
146 self.severity == Severity::Error
147 }
148
149 pub fn is_warning(&self) -> bool {
151 self.severity == Severity::Warning
152 }
153
154 #[inline]
156 pub fn code(&self) -> &str {
157 &self.code
158 }
159
160 #[inline]
162 pub fn message(&self) -> &str {
163 &self.message
164 }
165
166 #[inline]
168 pub fn path(&self) -> &str {
169 &self.path
170 }
171
172 #[inline]
174 pub fn severity(&self) -> Severity {
175 self.severity
176 }
177
178 #[inline]
180 pub fn location(&self) -> JsonLocation {
181 self.location
182 }
183}
184
185impl std::error::Error for ValidationError {}
186
187impl fmt::Display for ValidationError {
188 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189 if self.location.is_unknown() {
190 write!(f, "[{}] {}: {} at {}", self.severity, self.code, self.message, self.path)
191 } else {
192 write!(
193 f,
194 "[{}] {}: {} at {} ({})",
195 self.severity, self.code, self.message, self.path, self.location
196 )
197 }
198 }
199}
200
201#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct ValidationResult {
207 errors: Vec<ValidationError>,
208}
209
210impl ValidationResult {
211 #[must_use]
213 pub fn new() -> Self {
214 Self { errors: Vec::new() }
215 }
216
217 pub fn add_error(&mut self, error: ValidationError) {
219 self.errors.push(error);
220 }
221
222 pub fn add_errors(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
224 self.errors.extend(errors);
225 }
226
227 #[must_use]
229 pub fn is_valid(&self) -> bool {
230 !self.errors.iter().any(|e| e.is_error())
231 }
232
233 #[must_use]
235 pub fn is_clean(&self) -> bool {
236 self.errors.is_empty()
237 }
238
239 #[must_use]
241 pub fn all_errors(&self) -> &[ValidationError] {
242 &self.errors
243 }
244
245 pub fn errors(&self) -> impl Iterator<Item = &ValidationError> {
247 self.errors.iter().filter(|e| e.is_error())
248 }
249
250 pub fn warnings(&self) -> impl Iterator<Item = &ValidationError> {
252 self.errors.iter().filter(|e| e.is_warning())
253 }
254
255 #[must_use]
257 pub fn error_count(&self) -> usize {
258 self.errors.iter().filter(|e| e.is_error()).count()
259 }
260
261 #[must_use]
263 pub fn warning_count(&self) -> usize {
264 self.errors.iter().filter(|e| e.is_warning()).count()
265 }
266
267 pub fn merge(&mut self, other: ValidationResult) {
269 self.errors.extend(other.errors);
270 }
271
272 #[must_use]
274 pub fn has_errors(&self) -> bool {
275 self.errors.iter().any(|e| e.is_error())
276 }
277
278 #[must_use]
280 pub fn has_warnings(&self) -> bool {
281 self.errors.iter().any(|e| e.is_warning())
282 }
283}
284
285pub const PRIMITIVE_TYPES: &[&str] = &[
287 "string", "boolean", "null", "number",
288 "int8", "int16", "int32", "int64", "int128",
289 "uint8", "uint16", "uint32", "uint64", "uint128",
290 "float", "float8", "double", "decimal",
291 "date", "time", "datetime", "duration",
292 "uuid", "uri", "binary", "jsonpointer",
293 "integer", ];
295
296pub const COMPOUND_TYPES: &[&str] = &[
298 "object", "array", "set", "map", "tuple", "choice", "any",
299];
300
301pub const NUMERIC_TYPES: &[&str] = &[
303 "number", "integer",
304 "int8", "int16", "int32", "int64", "int128",
305 "uint8", "uint16", "uint32", "uint64", "uint128",
306 "float", "float8", "double", "decimal",
307];
308
309pub const INTEGER_TYPES: &[&str] = &[
311 "integer",
312 "int8", "int16", "int32", "int64", "int128",
313 "uint8", "uint16", "uint32", "uint64", "uint128",
314];
315
316pub const SCHEMA_KEYWORDS: &[&str] = &[
318 "$schema", "$id", "$ref", "definitions", "$import", "$importdefs",
319 "$comment", "$extends", "$abstract", "$root", "$uses", "$offers",
320 "name", "abstract",
321 "type", "enum", "const", "default",
322 "title", "description", "examples",
323 "properties", "additionalProperties", "required", "propertyNames",
325 "minProperties", "maxProperties", "dependentRequired",
326 "items", "minItems", "maxItems", "uniqueItems", "contains",
328 "minContains", "maxContains",
329 "minLength", "maxLength", "pattern", "format", "contentEncoding", "contentMediaType",
331 "contentCompression",
332 "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
334 "precision", "scale",
335 "values",
337 "choices", "selector",
339 "tuple",
341 "allOf", "anyOf", "oneOf", "not", "if", "then", "else",
343 "altnames",
345 "unit",
347];
348
349pub const VALIDATION_EXTENSION_KEYWORDS: &[&str] = &[
351 "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
353 "minLength", "maxLength", "pattern", "format",
355 "minItems", "maxItems", "uniqueItems", "contains", "minContains", "maxContains",
357 "minProperties", "maxProperties", "dependentRequired", "propertyNames", "patternProperties",
359 "minEntries", "maxEntries", "keyNames",
361 "contentEncoding", "contentMediaType", "contentCompression",
363 "default",
365];
366
367pub const COMPOSITION_KEYWORDS: &[&str] = &[
369 "allOf", "anyOf", "oneOf", "not", "if", "then", "else",
370];
371
372pub const KNOWN_EXTENSIONS: &[&str] = &[
374 "JSONStructureImport",
375 "JSONStructureAlternateNames",
376 "JSONStructureUnits",
377 "JSONStructureConditionalComposition",
378 "JSONStructureValidation",
379];
380
381#[allow(dead_code)]
383pub const VALID_FORMATS: &[&str] = &[
384 "ipv4", "ipv6", "email", "idn-email", "hostname", "idn-hostname",
385 "iri", "iri-reference", "uri-template", "relative-json-pointer", "regex",
386];
387
388pub fn is_valid_type(type_name: &str) -> bool {
390 PRIMITIVE_TYPES.contains(&type_name) || COMPOUND_TYPES.contains(&type_name)
391}
392
393pub fn is_primitive_type(type_name: &str) -> bool {
395 PRIMITIVE_TYPES.contains(&type_name)
396}
397
398pub fn is_compound_type(type_name: &str) -> bool {
400 COMPOUND_TYPES.contains(&type_name)
401}
402
403pub fn is_numeric_type(type_name: &str) -> bool {
405 NUMERIC_TYPES.contains(&type_name)
406}
407
408pub fn is_integer_type(type_name: &str) -> bool {
410 INTEGER_TYPES.contains(&type_name)
411}
412
413#[derive(Debug, Clone)]
415pub struct SchemaValidatorOptions {
416 pub allow_import: bool,
418 pub max_validation_depth: usize,
420 pub warn_on_unused_extension_keywords: bool,
422 pub external_schemas: Vec<serde_json::Value>,
424}
425
426impl Default for SchemaValidatorOptions {
427 fn default() -> Self {
428 Self {
429 allow_import: false,
430 max_validation_depth: 64,
431 warn_on_unused_extension_keywords: true,
432 external_schemas: Vec::new(),
433 }
434 }
435}
436
437#[derive(Debug, Clone, Default)]
439pub struct InstanceValidatorOptions {
440 pub extended: bool,
442 pub allow_import: bool,
444}