use crate::ValidationLimits;
#[cfg(feature = "validation")]
use crate::limits::check_json_limits;
#[cfg(feature = "validation")]
use rskit_errors::{AppError, AppResult, ErrorCode};
#[cfg(feature = "validation")]
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ValidationOptions {
pub limits: ValidationLimits,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.path.is_empty() {
write!(f, "{}", self.message)
} else {
write!(f, "{}: {}", self.path, self.message)
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
impl ValidationResult {
#[must_use]
pub const fn valid() -> Self {
Self {
valid: true,
errors: Vec::new(),
}
}
#[must_use]
pub fn invalid(message: impl Into<String>) -> Self {
Self {
valid: false,
errors: vec![ValidationError {
path: String::new(),
message: message.into(),
}],
}
}
}
#[cfg(feature = "validation")]
pub struct CompiledSchema {
validator: jsonschema::Validator,
options: ValidationOptions,
}
#[cfg(feature = "validation")]
impl CompiledSchema {
pub fn validate(&self, value: &Value) -> ValidationResult {
match self.try_validate(value) {
Ok(result) => result,
Err(err) => ValidationResult::invalid(err.message().to_owned()),
}
}
pub fn try_validate(&self, value: &Value) -> AppResult<ValidationResult> {
check_json_limits("value", value, self.options.limits)?;
Ok(validation_result(&self.validator, value))
}
}
#[cfg(feature = "validation")]
pub fn compile(schema: &Value) -> AppResult<CompiledSchema> {
compile_with_options(schema, ValidationOptions::default())
}
#[cfg(feature = "validation")]
pub fn compile_with_options(
schema: &Value,
options: ValidationOptions,
) -> AppResult<CompiledSchema> {
check_json_limits("schema", schema, options.limits)?;
jsonschema::validator_for(schema)
.map(|validator| CompiledSchema { validator, options })
.map_err(|err| {
AppError::new(
ErrorCode::InvalidInput,
format!("invalid JSON Schema: {err}"),
)
.with_cause(err)
})
}
#[cfg(feature = "validation")]
pub fn validate(schema: &Value, value: &Value) -> ValidationResult {
match validate_with_options(schema, value, ValidationOptions::default()) {
Ok(result) => result,
Err(err) => ValidationResult::invalid(err.message().to_owned()),
}
}
#[cfg(feature = "validation")]
pub fn validate_with_options(
schema: &Value,
value: &Value,
options: ValidationOptions,
) -> AppResult<ValidationResult> {
let validator = compile_with_options(schema, options)?;
validator.try_validate(value)
}
#[cfg(feature = "validation")]
fn validation_result(validator: &jsonschema::Validator, value: &Value) -> ValidationResult {
let errors = validator
.iter_errors(value)
.map(|err| ValidationError {
path: err.instance_path().to_string(),
message: err.to_string(),
})
.collect::<Vec<_>>();
ValidationResult {
valid: errors.is_empty(),
errors,
}
}
#[cfg(feature = "validation")]
pub fn validate_structured_output(schema: &Value, value: &Value) -> ValidationResult {
validate(schema, value)
}