use std::collections::HashSet;
use anyhow::{Context, Result, anyhow, bail};
use schemars::schema::{
ArrayValidation, InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec,
};
use serde_json::{Map, Value};
use crate::schema::{loader::load_root_schema, resolver::SchemaResolver};
use super::types::{CompositeMode, ScalarKind, UiAst, UiNode, UiNodeKind, UiVariant};
pub fn build_ui_ast(raw: &Value) -> Result<UiAst> {
let root_schema = load_root_schema(raw)?;
let resolver = SchemaResolver::new(raw, &root_schema);
let root_object = resolver
.root_object()
.cloned()
.ok_or_else(|| anyhow!("root schema must be an object"))?;
if !is_object_schema(&root_object) {
bail!("root schema must describe an object");
}
let object = root_object
.object
.as_ref()
.context("root schema must define properties")?;
let required = required_set(object);
let mut roots = Vec::new();
for (name, schema) in &object.properties {
let resolved = resolver.resolve_schema(schema)?;
let pointer = append_pointer("", name);
let node = visit_schema(&resolver, &resolved, pointer, required.contains(name))?;
roots.push(node);
}
Ok(UiAst { roots })
}
fn visit_schema(
resolver: &SchemaResolver<'_>,
schema: &SchemaObject,
pointer: String,
required: bool,
) -> Result<UiNode> {
if let Some(subs) = schema.subschemas.as_ref()
&& let Some(all_of) = subs.all_of.as_ref()
&& !all_of.is_empty()
{
let merged = merge_all_of(resolver, all_of)?;
return visit_schema(resolver, &merged, pointer, required);
}
if let Some(subs) = schema.subschemas.as_ref() {
if let Some(one_of) = subs.one_of.as_ref() {
return build_composite_node(
resolver,
one_of,
CompositeMode::OneOf,
schema,
pointer,
required,
);
}
if let Some(any_of) = subs.any_of.as_ref() {
return build_composite_node(
resolver,
any_of,
CompositeMode::AnyOf,
schema,
pointer,
required,
);
}
}
if is_array_schema(schema) {
let array = schema
.array
.as_ref()
.context("array schema must define array metadata")?;
let items_schema = resolve_array_items(resolver, array)?;
let item_node =
if is_object_schema(&items_schema) && !has_composite_subschemas(&items_schema) {
build_composite_kind(
resolver,
&[Schema::Object(items_schema.clone())],
CompositeMode::OneOf,
)?
} else {
visit_kind(resolver, &items_schema)?
};
let default_value = schema_default(schema).or_else(|| Some(Value::Array(Vec::new())));
return Ok(UiNode {
pointer,
title: schema_title(schema),
description: schema_description(schema),
required,
default_value,
kind: UiNodeKind::Array {
item: Box::new(item_node),
min_items: array.min_items.map(|v| v as u64),
max_items: array.max_items.map(|v| v as u64),
},
});
}
if is_object_schema(schema) {
let obj = schema
.object
.as_ref()
.context("object schema missing properties")?;
let mut children = Vec::new();
let required_set = required_set(obj);
for (name, child_schema) in &obj.properties {
let resolved = resolver.resolve_schema(child_schema)?;
let child_ptr = append_pointer(&pointer, name);
let child = visit_schema(resolver, &resolved, child_ptr, required_set.contains(name))?;
children.push(child);
}
let default_value = schema_default(schema).or(Some(Value::Object(Map::new())));
return Ok(UiNode {
pointer,
title: schema_title(schema),
description: schema_description(schema),
required,
default_value,
kind: UiNodeKind::Object {
children,
required: required_set.into_iter().collect(),
},
});
}
let (scalar, enum_options, enum_values) = detect_scalar(schema)?;
let default_value =
schema_default(schema).or_else(|| infer_default_scalar(scalar, enum_values.as_ref()));
Ok(UiNode {
pointer,
title: schema_title(schema),
description: schema_description(schema),
required,
default_value,
kind: UiNodeKind::Field {
scalar,
enum_options,
enum_values,
},
})
}
fn build_composite_kind(
resolver: &SchemaResolver<'_>,
schemas: &[Schema],
mode: CompositeMode,
) -> Result<UiNodeKind> {
let variants = build_variants(resolver, schemas)?;
let allow_multiple = false;
Ok(UiNodeKind::Composite {
mode,
allow_multiple,
variants,
})
}
fn build_composite_node(
resolver: &SchemaResolver<'_>,
schemas: &[Schema],
mode: CompositeMode,
schema: &SchemaObject,
pointer: String,
required: bool,
) -> Result<UiNode> {
let kind = build_composite_kind(resolver, schemas, mode)?;
let default_value = if let UiNodeKind::Composite {
variants,
allow_multiple,
..
} = &kind
{
infer_default_for_composite(variants, *allow_multiple)
} else {
None
};
Ok(UiNode {
pointer,
title: schema_title(schema),
description: schema_description(schema),
required,
default_value,
kind,
})
}
fn visit_kind(resolver: &SchemaResolver<'_>, schema: &SchemaObject) -> Result<UiNodeKind> {
if let Some(subs) = schema.subschemas.as_ref()
&& let Some(all_of) = subs.all_of.as_ref()
&& !all_of.is_empty()
{
let merged = merge_all_of(resolver, all_of)?;
return visit_kind(resolver, &merged);
}
if let Some(subs) = schema.subschemas.as_ref() {
if let Some(one_of) = subs.one_of.as_ref() {
return build_composite_kind(resolver, one_of, CompositeMode::OneOf);
}
if let Some(any_of) = subs.any_of.as_ref() {
return build_composite_kind(resolver, any_of, CompositeMode::AnyOf);
}
}
if is_array_schema(schema) {
let array = schema
.array
.as_ref()
.context("array schema must define array metadata")?;
let items_schema = resolve_array_items(resolver, array)?;
if is_object_schema(&items_schema) && !has_composite_subschemas(&items_schema) {
let composite_kind = build_composite_kind(
resolver,
&[Schema::Object(items_schema.clone())],
CompositeMode::OneOf,
)?;
return Ok(UiNodeKind::Array {
item: Box::new(composite_kind),
min_items: array.min_items.map(|v| v as u64),
max_items: array.max_items.map(|v| v as u64),
});
}
let item_node = visit_kind(resolver, &items_schema)?;
return Ok(UiNodeKind::Array {
item: Box::new(item_node),
min_items: array.min_items.map(|v| v as u64),
max_items: array.max_items.map(|v| v as u64),
});
}
if is_object_schema(schema) {
let obj = schema
.object
.as_ref()
.context("object schema missing properties")?;
let required = required_set(obj);
let mut children = Vec::new();
for (name, schema) in &obj.properties {
let resolved = resolver.resolve_schema(schema)?;
let pointer = append_pointer("", name);
let node = visit_schema(resolver, &resolved, pointer, required.contains(name))?;
children.push(node);
}
return Ok(UiNodeKind::Object {
children,
required: required.into_iter().collect(),
});
}
let (scalar, enum_options, enum_values) = detect_scalar(schema)?;
Ok(UiNodeKind::Field {
scalar,
enum_options,
enum_values,
})
}
fn build_variants(resolver: &SchemaResolver<'_>, schemas: &[Schema]) -> Result<Vec<UiVariant>> {
let mut out = Vec::new();
for (index, variant) in schemas.iter().enumerate() {
let resolved = resolver.resolve_schema(variant)?;
let node = visit_kind(resolver, &resolved)?;
let mut schema_value = schema_to_value(&resolved)?;
if let Some(defs) = resolver.definitions_snapshot()
&& let Value::Object(ref mut map) = schema_value
{
map.entry("$defs".to_string()).or_insert(defs);
}
let title = resolved
.metadata
.as_ref()
.and_then(|m| m.title.clone())
.or_else(|| Some(default_variant_title(index, &resolved)));
let description = resolved
.metadata
.as_ref()
.and_then(|m| m.description.clone());
let is_object = is_object_schema(&resolved);
out.push(UiVariant {
id: format!("variant_{}", index),
title,
description,
is_object,
node,
schema: schema_value,
});
}
Ok(out)
}
fn schema_default(schema: &SchemaObject) -> Option<Value> {
schema.metadata.as_ref().and_then(|m| m.default.clone())
}
fn infer_default_scalar(scalar: ScalarKind, opts: Option<&Vec<Value>>) -> Option<Value> {
if let Some(options) = opts
&& let Some(first) = options.first()
{
return Some(first.clone());
}
let val = match scalar {
ScalarKind::String => Value::String(String::new()),
ScalarKind::Integer => Value::Number(0.into()),
ScalarKind::Number => Value::Number(0.into()),
ScalarKind::Boolean => Value::Bool(false),
};
Some(val)
}
fn infer_default_for_composite(variants: &[UiVariant], allow_multiple: bool) -> Option<Value> {
if allow_multiple {
return Some(Value::Array(Vec::new()));
}
variants.first().and_then(generate_variant_default)
}
fn generate_variant_default(variant: &UiVariant) -> Option<Value> {
if variant.is_object
&& let UiNodeKind::Object { children, required } = &variant.node
{
let mut obj = Map::new();
if let Value::Object(schema_obj) = &variant.schema
&& let Some(Value::Object(props)) = schema_obj.get("properties")
{
for (key, prop_schema) in props {
if let Value::Object(prop_obj) = prop_schema {
if let Some(const_val) = prop_obj.get("const") {
obj.insert(key.clone(), const_val.clone());
}
}
}
}
for child in children {
let field_name = child.pointer.split('/').next_back().unwrap_or("");
if !field_name.is_empty()
&& required.contains(&field_name.to_string())
&& !obj.contains_key(field_name)
{
if let Some(default) = &child.default_value {
obj.insert(field_name.to_string(), default.clone());
} else if let Some(default) = default_for_kind(&child.kind) {
obj.insert(field_name.to_string(), default);
}
}
}
return Some(Value::Object(obj));
}
if let UiNodeKind::Array { item, .. } = &variant.node
&& let Some(item_default) = default_for_kind(item)
{
return Some(Value::Array(vec![item_default]));
}
default_for_kind(&variant.node)
}
fn default_for_kind(kind: &UiNodeKind) -> Option<Value> {
match kind {
UiNodeKind::Field {
scalar,
enum_values,
..
} => infer_default_scalar(*scalar, enum_values.as_ref()),
UiNodeKind::Array { .. } => Some(Value::Array(Vec::new())),
UiNodeKind::Composite {
variants,
allow_multiple,
..
} => infer_default_for_composite(variants, *allow_multiple),
UiNodeKind::Object { .. } => Some(Value::Object(Map::new())),
}
}
type DetectedScalar = (ScalarKind, Option<Vec<String>>, Option<Vec<Value>>);
fn detect_scalar(schema: &SchemaObject) -> Result<DetectedScalar> {
if let Some(enum_values) = schema.enum_values.as_ref()
&& !enum_values.is_empty()
{
let labels = enum_values.iter().map(enum_label).collect::<Vec<_>>();
return Ok((
infer_enum_scalar(enum_values),
Some(labels),
Some(enum_values.clone()),
));
}
let instance = instance_type(schema);
let scalar = match instance {
Some(InstanceType::String) | None => ScalarKind::String,
Some(InstanceType::Integer) => ScalarKind::Integer,
Some(InstanceType::Number) => ScalarKind::Number,
Some(InstanceType::Boolean) => ScalarKind::Boolean,
Some(InstanceType::Null) => ScalarKind::String,
Some(InstanceType::Array | InstanceType::Object) => {
bail!("composite types should be handled earlier")
}
};
Ok((scalar, None, None))
}
fn enum_label(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
Value::Number(num) => num.to_string(),
Value::Bool(flag) => flag.to_string(),
Value::Array(items) => items.iter().map(enum_label).collect::<Vec<_>>().join(", "),
other => other.to_string(),
}
}
fn infer_enum_scalar(values: &[Value]) -> ScalarKind {
let mut inferred = None;
for value in values {
let current = match value {
Value::Number(num) if num.is_i64() || num.is_u64() => ScalarKind::Integer,
Value::Number(_) => ScalarKind::Number,
Value::Bool(_) => ScalarKind::Boolean,
Value::String(_) => ScalarKind::String,
_ => return ScalarKind::String,
};
match inferred {
Some(existing) if existing != current => return ScalarKind::String,
Some(_) => {}
None => inferred = Some(current),
}
}
inferred.unwrap_or(ScalarKind::String)
}
fn merge_all_of(resolver: &SchemaResolver<'_>, all_of: &[Schema]) -> Result<SchemaObject> {
if all_of.is_empty() {
bail!("allOf must contain at least one schema");
}
let mut acc = Value::Object(Map::new());
for schema in all_of {
let resolved = resolver.resolve_schema(schema)?;
let value = schema_to_value(&resolved)?;
acc = deep_merge(acc, value);
}
serde_json::from_value::<SchemaObject>(acc).context("failed to deserialize merged allOf schema")
}
fn resolve_array_items(
resolver: &SchemaResolver<'_>,
array: &ArrayValidation,
) -> Result<SchemaObject> {
let items = array
.items
.as_ref()
.context("array items must be present")?;
let first = match items {
SingleOrVec::Single(schema) => schema.as_ref(),
SingleOrVec::Vec(list) => list
.first()
.context("tuple arrays must have at least one item")?,
};
resolver.resolve_schema(first)
}
fn instance_type(schema: &SchemaObject) -> Option<InstanceType> {
schema.instance_type.as_ref().and_then(|inner| match inner {
SingleOrVec::Single(single) => Some(**single),
SingleOrVec::Vec(list) => list
.iter()
.cloned()
.find(|item| *item != InstanceType::Null),
})
}
fn is_object_schema(schema: &SchemaObject) -> bool {
match instance_type(schema) {
Some(InstanceType::Object) => true,
None => schema.object.is_some(),
_ => false,
}
}
fn has_composite_subschemas(schema: &SchemaObject) -> bool {
schema.subschemas.as_ref().is_some_and(|subs| {
subs.one_of.as_ref().is_some_and(|list| !list.is_empty())
|| subs.any_of.as_ref().is_some_and(|list| !list.is_empty())
})
}
fn is_array_schema(schema: &SchemaObject) -> bool {
match instance_type(schema) {
Some(InstanceType::Array) => true,
_ => schema.array.is_some(),
}
}
fn required_set(object: &ObjectValidation) -> HashSet<String> {
object.required.iter().cloned().collect()
}
fn schema_to_value(schema: &SchemaObject) -> Result<Value> {
serde_json::to_value(Schema::Object(schema.clone())).context("failed to serialize schema")
}
fn schema_title(schema: &SchemaObject) -> Option<String> {
schema.metadata.as_ref()?.title.clone()
}
fn schema_description(schema: &SchemaObject) -> Option<String> {
schema.metadata.as_ref()?.description.clone()
}
fn default_variant_title(index: usize, schema: &SchemaObject) -> String {
if let Some(reference) = schema.reference.as_ref()
&& let Some(name) = reference.split('/').next_back()
{
return humanize_identifier(name);
}
if let Some(obj) = schema.object.as_ref() {
if let Some(type_prop) = obj.properties.get("type")
&& let Some(const_val) = get_const_value(type_prop)
&& let Some(s) = const_val.as_str()
{
return s.to_string();
}
if let Some(kind_prop) = obj.properties.get("kind")
&& let Some(const_val) = get_const_value(kind_prop)
&& let Some(s) = const_val.as_str()
{
return humanize_identifier(s);
}
for key in ["id", "name", "key"] {
if obj.properties.contains_key(key) {
let base_type = instance_type(schema)
.map(|t| format!("{:?}", t).to_lowercase())
.unwrap_or_else(|| "variant".to_string());
return format!("{} with {}", base_type, key);
}
}
}
if let Some(array) = schema.array.as_ref()
&& let Some(items) = &array.items
{
match items {
SingleOrVec::Single(item_schema) => {
if let Schema::Object(item_obj) = item_schema.as_ref() {
if let Some(item_ref) = item_obj.reference.as_ref()
&& let Some(name) = item_ref.split('/').next_back()
{
return format!("{} array", humanize_identifier(name));
}
if let Some(item_instance) = instance_type(item_obj) {
let kind = match item_instance {
InstanceType::String => Some("string"),
InstanceType::Integer => Some("integer"),
InstanceType::Number => Some("number"),
InstanceType::Boolean => Some("boolean"),
_ => None,
};
if let Some(kind) = kind {
return format!("List<{}>", kind);
}
}
}
}
SingleOrVec::Vec(_) => {
return "Tuple array".to_string();
}
}
}
if let Some(instance) = instance_type(schema) {
return match instance {
InstanceType::String => "Text".to_string(),
InstanceType::Integer => "Integer".to_string(),
InstanceType::Number => "Number".to_string(),
InstanceType::Boolean => "Boolean".to_string(),
InstanceType::Array => "List".to_string(),
InstanceType::Object => "Object".to_string(),
InstanceType::Null => format!("Option {}", index + 1),
};
}
format!("Option {}", index + 1)
}
fn humanize_identifier(s: &str) -> String {
let mut result = String::new();
let mut prev_upper = false;
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 && !prev_upper {
result.push(' ');
}
result.push(ch);
prev_upper = true;
} else {
if i == 0 {
result.push(ch.to_ascii_uppercase());
} else {
result.push(ch);
}
prev_upper = false;
}
}
result
}
fn get_const_value(schema: &Schema) -> Option<&Value> {
if let Schema::Object(obj) = schema {
if let Some(const_val) = obj.const_value.as_ref() {
return Some(const_val);
}
if let Some(const_val) = obj.extensions.get("const") {
return Some(const_val);
}
}
None
}
fn deep_merge(base: Value, addition: Value) -> Value {
match (base, addition) {
(Value::Object(mut a), Value::Object(b)) => {
for (key, value) in b {
let merged = if let Some(existing) = a.remove(&key) {
deep_merge(existing, value)
} else {
value
};
a.insert(key, merged);
}
Value::Object(a)
}
(Value::Array(mut a), Value::Array(mut b)) => {
a.append(&mut b);
dedup_array(&mut a);
Value::Array(a)
}
(_, new_value) => new_value,
}
}
fn dedup_array(values: &mut Vec<Value>) {
let mut index = 0;
while index < values.len() {
let is_duplicate = values[..index]
.iter()
.any(|existing| existing == &values[index]);
if is_duplicate {
values.remove(index);
} else {
index += 1;
}
}
}
fn append_pointer(base: &str, segment: &str) -> String {
let encoded = segment.replace('~', "~0").replace('/', "~1");
if base.is_empty() || base == "/" {
format!("/{}", encoded)
} else if base.ends_with('/') {
format!("{base}{encoded}")
} else {
format!("{base}/{encoded}")
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use schemars::schema::SubschemaValidation;
#[test]
fn variant_title_uses_kind_const() {
let mut kind_schema = SchemaObject::default();
kind_schema.const_value = Some(Value::String("simple".to_string()));
let mut obj = ObjectValidation::default();
obj.properties
.insert("kind".to_string(), Schema::Object(kind_schema));
let mut schema = SchemaObject::default();
schema.object = Some(Box::new(obj));
let title = default_variant_title(0, &schema);
assert_eq!(title, "Simple");
}
#[test]
fn variant_title_for_scalar_array_is_list_of_type() {
let mut item_schema = SchemaObject::default();
item_schema.instance_type = Some(SingleOrVec::Single(Box::new(InstanceType::String)));
let mut array = ArrayValidation::default();
array.items = Some(SingleOrVec::Single(Box::new(Schema::Object(item_schema))));
let mut schema = SchemaObject::default();
schema.array = Some(Box::new(array));
let title = default_variant_title(0, &schema);
assert_eq!(title, "List<string>");
}
#[test]
fn has_composite_subschemas_detects_one_of_and_any_of() {
let mut with_one_of = SchemaObject::default();
let mut subs_one = SubschemaValidation::default();
subs_one.one_of = Some(vec![Schema::Bool(true)]);
with_one_of.subschemas = Some(Box::new(subs_one));
assert!(has_composite_subschemas(&with_one_of));
let mut with_any_of = SchemaObject::default();
let mut subs_any = SubschemaValidation::default();
subs_any.any_of = Some(vec![Schema::Bool(true)]);
with_any_of.subschemas = Some(Box::new(subs_any));
assert!(has_composite_subschemas(&with_any_of));
let plain = SchemaObject::default();
assert!(!has_composite_subschemas(&plain));
}
}