use serde_json::Value;
use crate::error::{ForgeError, ForgeResult};
use super::types::{FieldSchema, SchemaType, StructSchema, TopLevel};
use crate::codegen::util::sanitize_ident;
use crate::rename::RenameRule;
pub fn infer_top_level(root: &Value) -> ForgeResult<TopLevel> {
match root {
Value::Object(map) => {
if map.is_empty() {
return Err(ForgeError::call_site(
"JSON object is empty; cannot infer schema",
));
}
let first_val = map.values().next().unwrap();
let all_objects = map.values().all(|v| v.is_object());
if all_objects && map.len() > 1 {
let schema = merge_object_schemas(map.values())?;
Ok(TopLevel::Map { entry: schema })
} else if first_val.is_object() {
let obj = first_val.as_object().unwrap();
Ok(TopLevel::Struct(infer_struct_schema(obj)?))
} else {
Err(ForgeError::call_site(
"top-level JSON object values must all be JSON objects to infer a struct schema",
))
}
},
Value::Array(arr) => {
if arr.is_empty() {
return Err(ForgeError::call_site(
"JSON array is empty; cannot infer schema",
));
}
let mut ty = infer_value(&arr[0])?;
for val in arr.iter().skip(1) {
ty = merge_types(ty, infer_value(val)?);
}
Ok(TopLevel::Array { entry: ty })
},
_ => Err(ForgeError::call_site(
"top-level JSON must be an object or array",
)),
}
}
pub fn infer_value(value: &Value) -> ForgeResult<SchemaType> {
match value {
Value::Null => Ok(SchemaType::Optional(Box::new(SchemaType::Str))),
Value::Bool(_) => Ok(SchemaType::Bool),
Value::Number(n) => {
if n.is_f64() && !n.is_i64() && !n.is_u64() {
Ok(SchemaType::Float)
} else {
Ok(SchemaType::Integer)
}
},
Value::String(_) => Ok(SchemaType::Str),
Value::Array(arr) => {
if arr.is_empty() {
return Ok(SchemaType::Array(Box::new(SchemaType::Str)));
}
let mut inner = infer_value(&arr[0])?;
for v in arr.iter().skip(1) {
inner = merge_types(inner, infer_value(v)?);
}
Ok(SchemaType::Array(Box::new(inner)))
},
Value::Object(obj) => Ok(SchemaType::Struct(infer_struct_schema(obj)?)),
}
}
fn infer_struct_schema(obj: &serde_json::Map<String, Value>) -> ForgeResult<StructSchema> {
let mut fields = Vec::with_capacity(obj.len());
for (key, val) in obj {
let ty = infer_value(val)?;
fields.push(FieldSchema {
rust_name: sanitize_ident(key),
json_key: key.clone(),
ty,
});
}
Ok(StructSchema { fields })
}
fn merge_object_schemas<'a>(values: impl Iterator<Item = &'a Value>) -> ForgeResult<StructSchema> {
let mut merged: Option<StructSchema> = None;
for val in values {
let obj = val
.as_object()
.ok_or_else(|| ForgeError::call_site("expected a JSON object in map values"))?;
let schema = infer_struct_schema(obj)?;
merged = Some(match merged.take() {
None => schema,
Some(existing) => reconcile_structs(existing, schema),
});
}
merged.ok_or_else(|| ForgeError::call_site("no values to merge"))
}
fn reconcile_structs(mut base: StructSchema, other: StructSchema) -> StructSchema {
let other_keys: std::collections::HashSet<String> =
other.fields.iter().map(|f| f.json_key.clone()).collect();
for other_field in other.fields {
if let Some(base_field) = base
.fields
.iter_mut()
.find(|f| f.json_key == other_field.json_key)
{
base_field.ty = merge_types(base_field.ty.clone(), other_field.ty);
} else {
let ty = make_optional(other_field.ty);
base.fields.push(FieldSchema {
json_key: other_field.json_key,
rust_name: other_field.rust_name,
ty,
});
}
}
for base_field in &mut base.fields {
if !other_keys.contains(&base_field.json_key) {
base_field.ty = make_optional(base_field.ty.clone());
}
}
base
}
pub fn merge_types(a: SchemaType, b: SchemaType) -> SchemaType {
if a == b {
return a;
}
match (a, b) {
(SchemaType::Optional(inner), other) | (other, SchemaType::Optional(inner)) => {
let merged = merge_types(*inner, other);
SchemaType::Optional(Box::new(merged))
},
(SchemaType::Integer, SchemaType::Float) | (SchemaType::Float, SchemaType::Integer) => {
SchemaType::Float
},
(SchemaType::Array(a), SchemaType::Array(b)) => {
SchemaType::Array(Box::new(merge_types(*a, *b)))
},
(SchemaType::Struct(a), SchemaType::Struct(b)) => {
SchemaType::Struct(reconcile_structs(a, b))
},
_ => SchemaType::Str,
}
}
pub fn apply_rename(top: TopLevel, rule: RenameRule) -> TopLevel {
match top {
TopLevel::Map { entry } => TopLevel::Map {
entry: rename_struct(entry, rule),
},
TopLevel::Array { entry } => TopLevel::Array {
entry: rename_type(entry, rule),
},
TopLevel::Struct(s) => TopLevel::Struct(rename_struct(s, rule)),
}
}
fn rename_struct(
mut schema: super::types::StructSchema,
rule: RenameRule,
) -> super::types::StructSchema {
for field in &mut schema.fields {
let raw = rule.json_key_to_rust_name(&field.json_key);
field.rust_name = sanitize_ident(&raw);
field.ty = rename_type(std::mem::replace(&mut field.ty, SchemaType::Str), rule);
}
schema
}
fn rename_type(ty: SchemaType, rule: RenameRule) -> SchemaType {
match ty {
SchemaType::Struct(s) => SchemaType::Struct(rename_struct(s, rule)),
SchemaType::Array(inner) => SchemaType::Array(Box::new(rename_type(*inner, rule))),
SchemaType::Optional(inner) => SchemaType::Optional(Box::new(rename_type(*inner, rule))),
other => other,
}
}
fn make_optional(ty: SchemaType) -> SchemaType {
match ty {
SchemaType::Optional(_) => ty,
other => SchemaType::Optional(Box::new(other)),
}
}