Skip to main content

json_structure/
types.rs

1//! Core types for JSON Structure validation.
2
3use std::fmt;
4
5use crate::error_codes::{InstanceErrorCode, SchemaErrorCode};
6
7/// Severity level for validation messages.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum Severity {
11    /// An error that causes validation to fail.
12    Error,
13    /// A warning that does not cause validation to fail.
14    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/// Location in the source JSON document.
27///
28/// Line and column numbers are 1-indexed. An unknown location
29/// is represented as (0, 0).
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
31pub struct JsonLocation {
32    /// Line number (1-indexed).
33    pub line: usize,
34    /// Column number (1-indexed).
35    pub column: usize,
36}
37
38impl JsonLocation {
39    /// Creates a new location.
40    #[must_use]
41    pub const fn new(line: usize, column: usize) -> Self {
42        Self { line, column }
43    }
44
45    /// Returns an unknown location (0, 0).
46    #[must_use]
47    pub const fn unknown() -> Self {
48        Self { line: 0, column: 0 }
49    }
50
51    /// Returns true if this is an unknown location.
52    #[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/// A validation error with code, message, path, and location.
69///
70/// This struct implements [`std::error::Error`] for integration with
71/// Rust's standard error handling patterns.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ValidationError {
74    /// The error code.
75    pub code: String,
76    /// The error message.
77    pub message: String,
78    /// The JSON Pointer path to the error location.
79    pub path: String,
80    /// The severity of the error.
81    pub severity: Severity,
82    /// The source location in the JSON document.
83    pub location: JsonLocation,
84}
85
86impl ValidationError {
87    /// Creates a new validation error.
88    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    /// Creates a new schema error.
105    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    /// Creates a new schema warning.
115    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    /// Creates a new instance error.
125    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    /// Creates a new instance warning.
135    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    /// Returns true if this is an error (not a warning).
145    pub fn is_error(&self) -> bool {
146        self.severity == Severity::Error
147    }
148
149    /// Returns true if this is a warning (not an error).
150    pub fn is_warning(&self) -> bool {
151        self.severity == Severity::Warning
152    }
153
154    /// Returns the error code.
155    #[inline]
156    pub fn code(&self) -> &str {
157        &self.code
158    }
159
160    /// Returns the error message.
161    #[inline]
162    pub fn message(&self) -> &str {
163        &self.message
164    }
165
166    /// Returns the JSON Pointer path.
167    #[inline]
168    pub fn path(&self) -> &str {
169        &self.path
170    }
171
172    /// Returns the severity.
173    #[inline]
174    pub fn severity(&self) -> Severity {
175        self.severity
176    }
177
178    /// Returns the source location.
179    #[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/// Result of validation containing errors and warnings.
202///
203/// Use [`is_valid()`](Self::is_valid) to check if validation passed.
204/// Use [`errors()`](Self::errors) and [`warnings()`](Self::warnings) to iterate over issues.
205#[derive(Debug, Clone, Default, PartialEq, Eq)]
206pub struct ValidationResult {
207    errors: Vec<ValidationError>,
208}
209
210impl ValidationResult {
211    /// Creates a new empty validation result.
212    #[must_use]
213    pub fn new() -> Self {
214        Self { errors: Vec::new() }
215    }
216
217    /// Adds an error to the result.
218    pub fn add_error(&mut self, error: ValidationError) {
219        self.errors.push(error);
220    }
221
222    /// Adds multiple errors to the result.
223    pub fn add_errors(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
224        self.errors.extend(errors);
225    }
226
227    /// Returns true if validation passed (no errors, warnings are OK).
228    #[must_use]
229    pub fn is_valid(&self) -> bool {
230        !self.errors.iter().any(|e| e.is_error())
231    }
232
233    /// Returns true if there are no errors or warnings.
234    #[must_use]
235    pub fn is_clean(&self) -> bool {
236        self.errors.is_empty()
237    }
238
239    /// Returns all errors and warnings.
240    #[must_use]
241    pub fn all_errors(&self) -> &[ValidationError] {
242        &self.errors
243    }
244
245    /// Returns only errors (not warnings).
246    pub fn errors(&self) -> impl Iterator<Item = &ValidationError> {
247        self.errors.iter().filter(|e| e.is_error())
248    }
249
250    /// Returns only warnings (not errors).
251    pub fn warnings(&self) -> impl Iterator<Item = &ValidationError> {
252        self.errors.iter().filter(|e| e.is_warning())
253    }
254
255    /// Returns the count of errors (not warnings).
256    #[must_use]
257    pub fn error_count(&self) -> usize {
258        self.errors.iter().filter(|e| e.is_error()).count()
259    }
260
261    /// Returns the count of warnings (not errors).
262    #[must_use]
263    pub fn warning_count(&self) -> usize {
264        self.errors.iter().filter(|e| e.is_warning()).count()
265    }
266
267    /// Merges another result into this one.
268    pub fn merge(&mut self, other: ValidationResult) {
269        self.errors.extend(other.errors);
270    }
271
272    /// Returns true if there are any errors (not warnings).
273    #[must_use]
274    pub fn has_errors(&self) -> bool {
275        self.errors.iter().any(|e| e.is_error())
276    }
277
278    /// Returns true if there are any warnings.
279    #[must_use]
280    pub fn has_warnings(&self) -> bool {
281        self.errors.iter().any(|e| e.is_warning())
282    }
283}
284
285/// Primitive types in JSON Structure.
286pub 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", // alias for int32
294];
295
296/// Compound types in JSON Structure.
297pub const COMPOUND_TYPES: &[&str] = &[
298    "object", "array", "set", "map", "tuple", "choice", "any",
299];
300
301/// Numeric types in JSON Structure.
302pub 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
309/// Integer types in JSON Structure.
310pub const INTEGER_TYPES: &[&str] = &[
311    "integer",
312    "int8", "int16", "int32", "int64", "int128",
313    "uint8", "uint16", "uint32", "uint64", "uint128",
314];
315
316/// Core schema keywords.
317pub 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    // Object keywords
324    "properties", "additionalProperties", "required", "propertyNames",
325    "minProperties", "maxProperties", "dependentRequired",
326    // Array/Set/Tuple keywords
327    "items", "minItems", "maxItems", "uniqueItems", "contains",
328    "minContains", "maxContains",
329    // String keywords
330    "minLength", "maxLength", "pattern", "format", "contentEncoding", "contentMediaType",
331    "contentCompression",
332    // Number keywords
333    "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
334    "precision", "scale",
335    // Map keywords
336    "values",
337    // Choice keywords
338    "choices", "selector",
339    // Tuple keywords
340    "tuple",
341    // Conditional composition
342    "allOf", "anyOf", "oneOf", "not", "if", "then", "else",
343    // Alternate names
344    "altnames",
345    // Units
346    "unit",
347];
348
349/// Validation extension keywords that require JSONStructureValidation.
350pub const VALIDATION_EXTENSION_KEYWORDS: &[&str] = &[
351    // Numeric validation
352    "minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
353    // String validation
354    "minLength", "maxLength", "pattern", "format",
355    // Array/Set validation
356    "minItems", "maxItems", "uniqueItems", "contains", "minContains", "maxContains",
357    // Object/Map validation
358    "minProperties", "maxProperties", "dependentRequired", "propertyNames", "patternProperties",
359    // Map-specific validation
360    "minEntries", "maxEntries", "keyNames",
361    // Content validation
362    "contentEncoding", "contentMediaType", "contentCompression",
363    // Default value
364    "default",
365];
366
367/// Conditional composition keywords that require JSONStructureConditionalComposition.
368pub const COMPOSITION_KEYWORDS: &[&str] = &[
369    "allOf", "anyOf", "oneOf", "not", "if", "then", "else",
370];
371
372/// Known extension names.
373pub const KNOWN_EXTENSIONS: &[&str] = &[
374    "JSONStructureImport",
375    "JSONStructureAlternateNames",
376    "JSONStructureUnits",
377    "JSONStructureConditionalComposition",
378    "JSONStructureValidation",
379];
380
381/// Valid format values for the "format" keyword.
382#[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
388/// Returns true if the given type name is a valid JSON Structure type.
389pub fn is_valid_type(type_name: &str) -> bool {
390    PRIMITIVE_TYPES.contains(&type_name) || COMPOUND_TYPES.contains(&type_name)
391}
392
393/// Returns true if the given type name is a primitive type.
394pub fn is_primitive_type(type_name: &str) -> bool {
395    PRIMITIVE_TYPES.contains(&type_name)
396}
397
398/// Returns true if the given type name is a compound type.
399pub fn is_compound_type(type_name: &str) -> bool {
400    COMPOUND_TYPES.contains(&type_name)
401}
402
403/// Returns true if the given type name is a numeric type.
404pub fn is_numeric_type(type_name: &str) -> bool {
405    NUMERIC_TYPES.contains(&type_name)
406}
407
408/// Returns true if the given type name is an integer type.
409pub fn is_integer_type(type_name: &str) -> bool {
410    INTEGER_TYPES.contains(&type_name)
411}
412
413/// Options for schema validation.
414#[derive(Debug, Clone)]
415pub struct SchemaValidatorOptions {
416    /// Whether to allow $import/$importdefs keywords.
417    pub allow_import: bool,
418    /// Maximum depth for recursive validation.
419    pub max_validation_depth: usize,
420    /// Whether to warn on unused extension keywords.
421    pub warn_on_unused_extension_keywords: bool,
422    /// External schemas for resolving imports.
423    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/// Options for instance validation.
438#[derive(Debug, Clone, Default)]
439pub struct InstanceValidatorOptions {
440    /// Whether to enable extended validation features.
441    pub extended: bool,
442    /// Whether to allow $import/$importdefs keywords.
443    pub allow_import: bool,
444}