use std::sync::{Arc, Mutex};
use indexmap::IndexMap;
use jsonschema::JSONSchema;
use mumu::parser::interpreter::{DynamicFn, DynamicFnInfo, Interpreter};
use mumu::parser::types::{FunctionValue, Value};
use serde_json::Value as JsonVal;
pub fn register_json_schema(interp: &mut Interpreter) {
let f: DynamicFn = Arc::new(Mutex::new(json_schema_bridge));
let info = DynamicFnInfo::new(f, false);
interp.register_dynamic_function_ex("json:schema", info);
interp.set_variable(
"json:schema",
Value::Function(Box::new(FunctionValue::Named("json:schema".to_string()))),
);
}
fn json_schema_bridge(_intp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
match args.len() {
0 | 1 => Ok(build_partial(None, args.get(0).cloned())),
2 => {
let sch = &args[0];
let data = &args[1];
match (is_placeholder(sch), is_placeholder(data)) {
(false, false) => validate_pair(sch.clone(), data.clone()),
_ => Ok(build_partial(Some(sch.clone()), Some(data.clone()))),
}
}
n => Err(format!("json:schema ⇒ expected 0-2 arguments, got {n}")),
}
}
#[derive(Clone)]
struct PartialState {
schema: Option<Value>,
data: Option<Value>,
}
fn build_partial(schema: Option<Value>, data: Option<Value>) -> Value {
use FunctionValue::RustClosure;
let shared = Arc::new(Mutex::new(PartialState { schema, data }));
let closure: DynamicFn = Arc::new(Mutex::new(
move |_intp: &mut Interpreter, new_args: Vec<Value>| -> Result<Value, String> {
let mut st = shared
.lock()
.map_err(|_| "json:schema partial lock error".to_string())?;
for v in new_args {
if st.schema.is_none() && !is_placeholder(&v) {
st.schema = Some(v);
continue;
}
if st.data.is_none() && !is_placeholder(&v) {
st.data = Some(v);
continue;
}
return Err("json:schema partial ⇒ too many arguments".to_string());
}
match (&st.schema, &st.data) {
(Some(s), Some(d)) if !is_placeholder(s) && !is_placeholder(d) => {
validate_pair(s.clone(), d.clone())
}
_ => Ok(build_partial(st.schema.clone(), st.data.clone())),
}
},
));
Value::Function(Box::new(RustClosure(
"json:schema-partial".to_string(),
closure,
0,
)))
}
fn validate_pair(schema_val: Value, data_val: Value) -> Result<Value, String> {
let mut errors: Vec<String> = Vec::new();
let schema_json = match to_json(schema_val) {
Ok(j) => Some(j),
Err(e) => {
errors.push(format!("schema error: {e}"));
None
}
};
let data_json = match to_json(data_val) {
Ok(j) => Some(j),
Err(e) => {
errors.push(format!("data error: {e}"));
None
}
};
if errors.is_empty() {
if let Some(schema_doc) = schema_json {
match JSONSchema::compile(&schema_doc) {
Ok(compiled) => {
if let Some(instance_doc) = data_json {
if let Err(iter) = compiled.validate(&instance_doc) {
errors.extend(iter.map(|e| e.to_string()));
}
}
}
Err(e) => {
errors.push(format!("schema compile error: {e}"));
}
}
}
}
let mut map = IndexMap::new();
let ok_flag = errors.is_empty();
map.insert("ok".to_string(), Value::Bool(ok_flag));
map.insert("errors".to_string(), Value::StrArray(errors));
Ok(Value::KeyedArray(map))
}
fn to_json(v: Value) -> Result<JsonVal, String> {
match v {
Value::SingleString(s) => serde_json::from_str(&s)
.map_err(|e| e.to_string()),
Value::StrArray(sa) if sa.len() == 1 => serde_json::from_str(&sa[0])
.map_err(|e| e.to_string()),
Value::KeyedArray(_)
| Value::IntArray(_)
| Value::FloatArray(_)
| Value::BoolArray(_)
| Value::Int2DArray(_)
| Value::Float2DArray(_) => Ok(crate::mumu_value_to_json(&v)),
other => Err(format!("unsupported value {other:?}")),
}
}
fn is_placeholder(v: &Value) -> bool {
match v {
Value::Placeholder => true,
Value::SingleString(s) if s == "_" => true,
Value::StrArray(sa) if sa.len() == 1 && sa[0] == "_" => true,
_ => false,
}
}