use crate::cst::document::Document;
use crate::error::{Error, Result};
use crate::value::{Number, Value};
pub fn coerce_to_schema(doc: &mut Document, schema: &Value) -> Result<usize> {
use jsonschema::JsonType;
use jsonschema::error::{TypeKind, ValidationErrorKind};
let schema_json = crate::schema_validate::value_to_json(schema)
.map_err(|e| Error::Custom(format!("cst::coerce_to_schema: schema -> JSON: {e}")))?;
let validator = jsonschema::validator_for(&schema_json).map_err(|e| {
Error::Custom(format!(
"cst::coerce_to_schema: schema is not a valid JSON Schema: {e}"
))
})?;
let mut applied: usize = 0;
let max_iterations = 1024;
for _ in 0..max_iterations {
let value: Value = crate::from_str(&doc.to_string())?;
let instance_json = crate::schema_validate::value_to_json(&value)
.map_err(|e| Error::Parse(format!("cst::coerce_to_schema: value -> JSON: {e}")))?;
let mut applied_this_pass = false;
let mut targets: Vec<(String, JsonType)> = Vec::new();
for err in validator.iter_errors(&instance_json) {
if let ValidationErrorKind::Type {
kind: TypeKind::Single(target),
} = &err.kind
{
targets.push((err.instance_path.to_string(), *target));
}
}
for (json_pointer, target) in targets {
let cst_path = match value_path_from_pointer(&value, &json_pointer) {
Some(p) => p,
None => continue, };
let current = match doc.entry(&cst_path).get() {
Some(s) => s.to_owned(),
None => continue,
};
let logical = strip_quotes(¤t);
let coerced = match coerce_logical(&logical, target) {
Some(v) => v,
None => continue,
};
doc.entry(&cst_path).set(&coerced)?;
applied += 1;
applied_this_pass = true;
}
if !applied_this_pass {
break;
}
}
Ok(applied)
}
fn value_path_from_pointer(root: &Value, pointer: &str) -> Option<String> {
let segments = parse_json_pointer(pointer);
if segments.is_empty() {
return None;
}
let mut cursor = root;
let mut out = String::new();
for (i, seg) in segments.iter().enumerate() {
match cursor {
Value::Mapping(m) => {
if i > 0 {
out.push('.');
}
out.push_str(seg);
cursor = m.get(seg.as_str())?;
}
Value::Sequence(s) => {
let idx: usize = seg.parse().ok()?;
use core::fmt::Write;
let _ = write!(out, "[{idx}]");
cursor = s.get(idx)?;
}
_ => return None,
}
}
Some(out)
}
fn parse_json_pointer(s: &str) -> Vec<String> {
if s.is_empty() || s == "/" {
return Vec::new();
}
s.trim_start_matches('/')
.split('/')
.map(|seg| seg.replace("~1", "/").replace("~0", "~"))
.collect()
}
fn strip_quotes(s: &str) -> String {
let bytes = s.as_bytes();
if bytes.len() >= 2 {
let first = bytes[0];
let last = bytes[bytes.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return s[1..s.len() - 1].to_owned();
}
}
s.to_owned()
}
fn coerce_logical(logical: &str, target: jsonschema::JsonType) -> Option<String> {
use jsonschema::JsonType;
let _ = Number::Integer(0); match target {
JsonType::Integer => {
let n: i64 = logical.parse().ok()?;
Some(n.to_string())
}
JsonType::Number => {
let f: f64 = logical.parse().ok()?;
crate::ser::to_string(&Value::Number(Number::Float(f)))
.ok()
.map(|s| s.trim().to_owned())
}
JsonType::Boolean => match logical {
"true" => Some("true".to_owned()),
"false" => Some("false".to_owned()),
_ => None,
},
_ => None,
}
}