use crate::zod::{Issues, Path, Schema};
use serde_json::Value;
use std::marker::PhantomData;
use std::sync::OnceLock;
pub struct FromJsonSchema {
document: Value,
validator: OnceLock<Validator>,
}
enum Validator {
Compiled(::jsonschema::Validator),
Broken(String),
}
#[must_use]
pub fn from_json_schema(document: Value) -> FromJsonSchema {
assert!(
document.is_object() || document.is_boolean(),
"litemcp/schemars : document JSON Schema invalide — un objet ou un \
booléen est attendu (reçu : {})",
if document.is_null() {
"null".to_string()
} else {
document.to_string()
}
);
FromJsonSchema {
document,
validator: OnceLock::new(),
}
}
impl FromJsonSchema {
#[must_use]
pub fn document(&self) -> &Value {
&self.document
}
fn validator(&self) -> &Validator {
self.validator.get_or_init(|| {
match ::jsonschema::options()
.should_validate_formats(true)
.build(&self.document)
{
Ok(validator) => Validator::Compiled(validator),
Err(error) => Validator::Broken(error.to_string()),
}
})
}
}
impl Schema for FromJsonSchema {
fn json(&self) -> Value {
self.document.clone()
}
fn validate(&self, path: &mut Path, value: &Value, issues: &mut Issues) {
match self.validator() {
Validator::Broken(message) => issues.push(format!(
"`{}`: invalid internal schema ({message})",
path.as_str()
)),
Validator::Compiled(validator) => {
for error in validator.iter_errors(value) {
if issues.is_full() {
issues.record_overflow();
continue;
}
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}"));
}
}
}
}
}
pub struct FromSchemars<T: ?Sized> {
_type: PhantomData<fn() -> T>,
document: OnceLock<Value>,
validator_with_formats: OnceLock<Validator>,
}
#[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!(
"`{}`: invalid internal schema ({message})",
path.as_str()
)),
Validator::Compiled(validator) => {
for error in validator.iter_errors(value) {
if issues.is_full() {
issues.record_overflow();
continue;
}
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
}