use crate::zod::{Issues, Path, Schema};
use serde_json::Value;
use std::marker::PhantomData;
use std::sync::OnceLock;
pub struct FromSchemars<T: ?Sized> {
_type: PhantomData<fn() -> T>,
document: OnceLock<Value>,
validator_with_formats: OnceLock<Validator>,
}
enum Validator {
Compiled(::jsonschema::Validator),
Broken(String),
}
#[must_use]
pub fn from_schemars<T: ::schemars::JsonSchema + 'static>() -> FromSchemars<T> {
FromSchemars::new()
}
impl<T: ::schemars::JsonSchema + 'static> FromSchemars<T> {
#[must_use]
pub fn new() -> Self {
Self {
_type: PhantomData,
document: OnceLock::new(),
validator_with_formats: OnceLock::new(),
}
}
fn document(&self) -> &Value {
self.document.get_or_init(|| {
serde_json::to_value(::schemars::schema_for!(T))
.unwrap_or_else(|_| serde_json::json!({ "type": "object" }))
})
}
fn validator_with_formats(&self) -> &Validator {
self.validator_with_formats.get_or_init(|| {
let document = self.document();
match ::jsonschema::options()
.should_validate_formats(true)
.build(document)
{
Ok(validator) => Validator::Compiled(validator),
Err(error) => Validator::Broken(error.to_string()),
}
})
}
}
impl<T: ::schemars::JsonSchema + 'static> Default for FromSchemars<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: ::schemars::JsonSchema + 'static> Schema for FromSchemars<T> {
fn json(&self) -> Value {
self.document().clone()
}
fn validate(&self, path: &mut Path, value: &Value, issues: &mut Issues) {
match self.validator_with_formats() {
Validator::Broken(message) => issues.push(format!(
"`{}` : schéma interne incompilable ({message})",
path.as_str()
)),
Validator::Compiled(validator) => {
for error in validator.iter_errors(value) {
let pointer = error.instance_path().to_string();
let location = if pointer.is_empty() {
path.as_str().to_string()
} else {
format!("{}{}", path.as_str(), pointer_to_suffix(&pointer))
};
issues.push(format!("`{location}` : {error}"));
}
}
}
}
}
fn pointer_to_suffix(pointer: &str) -> String {
let mut suffix = String::new();
for segment in pointer.split('/').skip(1) {
if segment.parse::<usize>().is_ok() && !segment.is_empty() {
suffix.push('[');
suffix.push_str(segment);
suffix.push(']');
} else {
suffix.push('.');
suffix.push_str(&segment.replace("~1", "/").replace("~0", "~"));
}
}
suffix
}