use std::fmt;
use crate::error_codes::{InstanceErrorCode, SchemaErrorCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Severity {
Error,
Warning,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct JsonLocation {
pub line: usize,
pub column: usize,
}
impl JsonLocation {
#[must_use]
pub const fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
#[must_use]
pub const fn unknown() -> Self {
Self { line: 0, column: 0 }
}
#[must_use]
pub const fn is_unknown(&self) -> bool {
self.line == 0 && self.column == 0
}
}
impl fmt::Display for JsonLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_unknown() {
write!(f, "(unknown)")
} else {
write!(f, "{}:{}", self.line, self.column)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationError {
pub code: String,
pub message: String,
pub path: String,
pub severity: Severity,
pub location: JsonLocation,
}
impl ValidationError {
pub fn new(
code: impl Into<String>,
message: impl Into<String>,
path: impl Into<String>,
severity: Severity,
location: JsonLocation,
) -> Self {
Self {
code: code.into(),
message: message.into(),
path: path.into(),
severity,
location,
}
}
pub fn schema_error(
code: SchemaErrorCode,
message: impl Into<String>,
path: impl Into<String>,
location: JsonLocation,
) -> Self {
Self::new(code.as_str(), message, path, Severity::Error, location)
}
pub fn schema_warning(
code: SchemaErrorCode,
message: impl Into<String>,
path: impl Into<String>,
location: JsonLocation,
) -> Self {
Self::new(code.as_str(), message, path, Severity::Warning, location)
}
pub fn instance_error(
code: InstanceErrorCode,
message: impl Into<String>,
path: impl Into<String>,
location: JsonLocation,
) -> Self {
Self::new(code.as_str(), message, path, Severity::Error, location)
}
pub fn instance_warning(
code: InstanceErrorCode,
message: impl Into<String>,
path: impl Into<String>,
location: JsonLocation,
) -> Self {
Self::new(code.as_str(), message, path, Severity::Warning, location)
}
pub fn is_error(&self) -> bool {
self.severity == Severity::Error
}
pub fn is_warning(&self) -> bool {
self.severity == Severity::Warning
}
#[inline]
pub fn code(&self) -> &str {
&self.code
}
#[inline]
pub fn message(&self) -> &str {
&self.message
}
#[inline]
pub fn path(&self) -> &str {
&self.path
}
#[inline]
pub fn severity(&self) -> Severity {
self.severity
}
#[inline]
pub fn location(&self) -> JsonLocation {
self.location
}
}
impl std::error::Error for ValidationError {}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.location.is_unknown() {
write!(f, "[{}] {}: {} at {}", self.severity, self.code, self.message, self.path)
} else {
write!(
f,
"[{}] {}: {} at {} ({})",
self.severity, self.code, self.message, self.path, self.location
)
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ValidationResult {
errors: Vec<ValidationError>,
}
impl ValidationResult {
#[must_use]
pub fn new() -> Self {
Self { errors: Vec::new() }
}
pub fn add_error(&mut self, error: ValidationError) {
self.errors.push(error);
}
pub fn add_errors(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
self.errors.extend(errors);
}
#[must_use]
pub fn is_valid(&self) -> bool {
!self.errors.iter().any(|e| e.is_error())
}
#[must_use]
pub fn is_clean(&self) -> bool {
self.errors.is_empty()
}
#[must_use]
pub fn all_errors(&self) -> &[ValidationError] {
&self.errors
}
pub fn errors(&self) -> impl Iterator<Item = &ValidationError> {
self.errors.iter().filter(|e| e.is_error())
}
pub fn warnings(&self) -> impl Iterator<Item = &ValidationError> {
self.errors.iter().filter(|e| e.is_warning())
}
#[must_use]
pub fn error_count(&self) -> usize {
self.errors.iter().filter(|e| e.is_error()).count()
}
#[must_use]
pub fn warning_count(&self) -> usize {
self.errors.iter().filter(|e| e.is_warning()).count()
}
pub fn merge(&mut self, other: ValidationResult) {
self.errors.extend(other.errors);
}
#[must_use]
pub fn has_errors(&self) -> bool {
self.errors.iter().any(|e| e.is_error())
}
#[must_use]
pub fn has_warnings(&self) -> bool {
self.errors.iter().any(|e| e.is_warning())
}
}
pub const PRIMITIVE_TYPES: &[&str] = &[
"string", "boolean", "null", "number",
"int8", "int16", "int32", "int64", "int128",
"uint8", "uint16", "uint32", "uint64", "uint128",
"float", "float8", "double", "decimal",
"date", "time", "datetime", "duration",
"uuid", "uri", "binary", "jsonpointer",
"integer", ];
pub const COMPOUND_TYPES: &[&str] = &[
"object", "array", "set", "map", "tuple", "choice", "any",
];
pub const NUMERIC_TYPES: &[&str] = &[
"number", "integer",
"int8", "int16", "int32", "int64", "int128",
"uint8", "uint16", "uint32", "uint64", "uint128",
"float", "float8", "double", "decimal",
];
pub const INTEGER_TYPES: &[&str] = &[
"integer",
"int8", "int16", "int32", "int64", "int128",
"uint8", "uint16", "uint32", "uint64", "uint128",
];
pub const SCHEMA_KEYWORDS: &[&str] = &[
"$schema", "$id", "$ref", "definitions", "$import", "$importdefs",
"$comment", "$extends", "$abstract", "$root", "$uses", "$offers",
"name", "abstract",
"type", "enum", "const", "default",
"title", "description", "examples",
"properties", "additionalProperties", "required", "propertyNames",
"minProperties", "maxProperties", "dependentRequired",
"items", "minItems", "maxItems", "uniqueItems", "contains",
"minContains", "maxContains",
"minLength", "maxLength", "pattern", "format", "contentEncoding", "contentMediaType",
"contentCompression",
"minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
"precision", "scale",
"values",
"choices", "selector",
"tuple",
"allOf", "anyOf", "oneOf", "not", "if", "then", "else",
"altnames",
"unit",
];
pub const VALIDATION_EXTENSION_KEYWORDS: &[&str] = &[
"minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
"minLength", "maxLength", "pattern", "format",
"minItems", "maxItems", "uniqueItems", "contains", "minContains", "maxContains",
"minProperties", "maxProperties", "dependentRequired", "propertyNames", "patternProperties",
"minEntries", "maxEntries", "keyNames",
"contentEncoding", "contentMediaType", "contentCompression",
"default",
];
pub const COMPOSITION_KEYWORDS: &[&str] = &[
"allOf", "anyOf", "oneOf", "not", "if", "then", "else",
];
pub const KNOWN_EXTENSIONS: &[&str] = &[
"JSONStructureImport",
"JSONStructureAlternateNames",
"JSONStructureUnits",
"JSONStructureConditionalComposition",
"JSONStructureValidation",
];
#[allow(dead_code)]
pub const VALID_FORMATS: &[&str] = &[
"ipv4", "ipv6", "email", "idn-email", "hostname", "idn-hostname",
"iri", "iri-reference", "uri-template", "relative-json-pointer", "regex",
];
pub fn is_valid_type(type_name: &str) -> bool {
PRIMITIVE_TYPES.contains(&type_name) || COMPOUND_TYPES.contains(&type_name)
}
pub fn is_primitive_type(type_name: &str) -> bool {
PRIMITIVE_TYPES.contains(&type_name)
}
pub fn is_compound_type(type_name: &str) -> bool {
COMPOUND_TYPES.contains(&type_name)
}
pub fn is_numeric_type(type_name: &str) -> bool {
NUMERIC_TYPES.contains(&type_name)
}
pub fn is_integer_type(type_name: &str) -> bool {
INTEGER_TYPES.contains(&type_name)
}
#[derive(Debug, Clone)]
pub struct SchemaValidatorOptions {
pub allow_import: bool,
pub max_validation_depth: usize,
pub warn_on_unused_extension_keywords: bool,
pub external_schemas: Vec<serde_json::Value>,
}
impl Default for SchemaValidatorOptions {
fn default() -> Self {
Self {
allow_import: false,
max_validation_depth: 64,
warn_on_unused_extension_keywords: true,
external_schemas: Vec::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct InstanceValidatorOptions {
pub extended: bool,
pub allow_import: bool,
}