#![deny(missing_docs)]
use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Type {
Int,
Float,
Bool,
String,
Array,
Object,
}
#[derive(Debug, Clone)]
pub struct Field {
pub name: &'static str,
pub ty: Type,
pub default: Option<Value>,
}
pub fn coerce(mut v: Value, schema: &[Field]) -> Value {
v = unwrap_single_wrapper(v);
let mut out = serde_json::Map::new();
let src = v.as_object();
for f in schema {
let raw = src.and_then(|m| m.get(f.name));
let coerced = raw
.and_then(|x| coerce_value(x, f.ty))
.or_else(|| f.default.clone())
.unwrap_or(Value::Null);
out.insert(f.name.to_string(), coerced);
}
Value::Object(out)
}
fn unwrap_single_wrapper(v: Value) -> Value {
if let Value::Object(map) = &v {
if map.len() == 1 {
let only = map.values().next().unwrap();
if only.is_object() {
return only.clone();
}
}
}
v
}
fn coerce_value(v: &Value, ty: Type) -> Option<Value> {
match ty {
Type::Int => match v {
Value::Number(n) => n.as_i64().map(|i| json!(i)),
Value::String(s) => s.trim().parse::<i64>().ok().map(|i| json!(i)),
Value::Bool(b) => Some(json!(if *b { 1 } else { 0 })),
_ => None,
},
Type::Float => match v {
Value::Number(n) => n.as_f64().map(|f| json!(f)),
Value::String(s) => s.trim().parse::<f64>().ok().map(|f| json!(f)),
_ => None,
},
Type::Bool => match v {
Value::Bool(b) => Some(json!(*b)),
Value::String(s) => match s.trim().to_ascii_lowercase().as_str() {
"true" | "yes" | "y" | "1" | "on" => Some(json!(true)),
"false" | "no" | "n" | "0" | "off" => Some(json!(false)),
_ => None,
},
Value::Number(n) => n.as_i64().map(|i| json!(i != 0)),
_ => None,
},
Type::String => Some(match v {
Value::String(s) => json!(s),
_ => json!(v.to_string()),
}),
Type::Array => match v {
Value::Array(_) => Some(v.clone()),
_ => None,
},
Type::Object => match v {
Value::Object(_) => Some(v.clone()),
_ => None,
},
}
}