use anyhow::{bail, Context, Result};
use clap::Args;
use serde_json::{Map, Value};
use super::item::ArenaTarget;
use super::{cell, print_json};
#[derive(Debug, Args)]
pub struct SchemaArgs {
pub arena: Option<String>,
#[arg(long)]
pub json: bool,
}
const SHAPE: &str = "the generated schema does not have the expected shape";
pub fn run(args: &SchemaArgs) -> Result<()> {
let root =
serde_json::to_value(cadmpeg_ir::cadir_json_schema()).context("serializing the schema")?;
let defs = root
.get("$defs")
.and_then(Value::as_object)
.context(SHAPE)?;
let model = root
.get("properties")
.and_then(|p| p.get("model"))
.map(|node| resolve(defs, node))
.and_then(Value::as_object)
.context(SHAPE)?;
match args.arena.as_deref() {
None => listing(model, defs, args.json),
Some("sidecar") => sidecar(args.json),
Some(spec) if spec.contains('/') || spec.to_ascii_lowercase().ends_with(".json") => {
bail!(
"`query schema` takes an arena name, not a file: the schema is a \
property of this binary's IR types, not of a document. Use \
`cadmpeg query schema model.<arena>`, and `cadmpeg query counts \
{spec}` for the arenas that document actually has"
)
}
Some(spec) => {
let target = ArenaTarget::parse(spec)?;
match &target {
ArenaTarget::Native { codec, arena } => bail!(
"native arena records are codec-owned; the generated schema \
does not describe them. Fetch one record and read it: \
`cadmpeg query item FILE native.{codec}.{arena}`"
),
ArenaTarget::Model { arena } => arena_table(arena, model, defs, args.json),
}
}
}
}
fn listing(model: &Map<String, Value>, defs: &Map<String, Value>, json: bool) -> Result<()> {
let properties = model
.get("properties")
.and_then(Value::as_object)
.context(SHAPE)?;
let required = required_set(model.get("required"));
if json {
let mut payload = Map::new();
for (arena, node) in properties {
payload.insert(
format!("model.{arena}"),
Value::String(element_label(node, defs)),
);
}
print_json("schema", &Value::Object(payload));
return Ok(());
}
println!("arena\telement\trequired");
for (arena, node) in properties {
let flag = if required.contains(arena.as_str()) {
"yes"
} else {
"no"
};
println!("model.{arena}\t{}\t{flag}", element_label(node, defs));
}
eprintln!(
"note: native arenas are codec-owned and per-document — list them with \
`cadmpeg query counts FILE`; the decode sidecar shape is \
`cadmpeg query schema sidecar`"
);
Ok(())
}
fn arena_table(
arena: &str,
model: &Map<String, Value>,
defs: &Map<String, Value>,
json: bool,
) -> Result<()> {
let properties = model
.get("properties")
.and_then(Value::as_object)
.context(SHAPE)?;
let Some(node) = properties.get(arena) else {
let known: Vec<&str> = properties.keys().map(String::as_str).collect();
bail!(
"unknown model arena {arena:?}; the IR defines: {}; a document's \
actual arenas come from `cadmpeg query counts FILE`",
known.join(", ")
);
};
let items = node.get("items").context(SHAPE)?;
let element = resolve(defs, items);
if json {
let payload = serde_json::json!({
"arena": format!("model.{arena}"),
"element": ref_name(items).unwrap_or("(inline)"),
"schema": element,
"defs": defs_closure(element, defs),
});
print_json("schema", &payload);
return Ok(());
}
print_type_table(element, defs);
Ok(())
}
fn sidecar(json: bool) -> Result<()> {
let root = serde_json::to_value(cadmpeg_ir::decode_sidecar_json_schema())
.context("serializing the sidecar schema")?;
let empty = Map::new();
let defs = root
.get("$defs")
.and_then(Value::as_object)
.unwrap_or(&empty);
if json {
let payload = serde_json::json!({
"element": "DecodeSidecar",
"schema": root,
});
print_json("schema", &payload);
return Ok(());
}
print_type_table(&root, defs);
Ok(())
}
fn print_type_table(element: &Value, defs: &Map<String, Value>) {
let Some(properties) = element.get("properties").and_then(Value::as_object) else {
println!("type\t{}", cell(&type_label(element, defs)));
return;
};
let required = required_set(element.get("required"));
println!("field\ttype\trequired\tdescription");
for (field, node) in properties {
let flag = if required.contains(field.as_str()) {
"yes"
} else {
"no"
};
let description = node
.get("description")
.and_then(Value::as_str)
.unwrap_or_default();
println!(
"{field}\t{}\t{flag}\t{}",
cell(&type_label(node, defs)),
cell(description)
);
}
}
fn required_set(required: Option<&Value>) -> std::collections::BTreeSet<&str> {
required
.and_then(Value::as_array)
.map(|list| list.iter().filter_map(Value::as_str).collect())
.unwrap_or_default()
}
fn resolve<'a>(defs: &'a Map<String, Value>, node: &'a Value) -> &'a Value {
match ref_name(node) {
Some(name) => defs.get(name).unwrap_or(node),
None => node,
}
}
fn ref_name(node: &Value) -> Option<&str> {
node.get("$ref")?.as_str()?.strip_prefix("#/$defs/")
}
fn element_label(node: &Value, defs: &Map<String, Value>) -> String {
match node.get("items") {
Some(items) => type_label(items, defs),
None => type_label(node, defs),
}
}
fn type_label(node: &Value, defs: &Map<String, Value>) -> String {
if let Some(name) = ref_name(node) {
let suffix = defs
.get(name)
.and_then(|def| enum_suffix(def, defs))
.unwrap_or_default();
return format!("{name}{suffix}");
}
if let Some(any_of) = node.get("anyOf").and_then(Value::as_array) {
let labels: Vec<String> = any_of
.iter()
.filter(|branch| branch.get("type").and_then(Value::as_str) != Some("null"))
.map(|branch| type_label(branch, defs))
.collect();
let nullable = any_of.len() != labels.len();
let joined = labels.join(" | ");
return if nullable {
format!("{joined}?")
} else {
joined
};
}
if node.get("oneOf").is_some() {
let suffix = enum_suffix(node, defs).unwrap_or_default();
return format!("(union){suffix}");
}
if let Some(values) = node.get("enum").and_then(Value::as_array) {
let names: Vec<&str> = values.iter().filter_map(Value::as_str).collect();
return format!("enum({})", names.join(", "));
}
match node.get("type").and_then(Value::as_str) {
Some("array") => {
let items = node.get("items").map_or("any".to_owned(), |items| {
ref_name(items).map_or_else(|| type_label(items, defs), str::to_owned)
});
format!("array<{items}>")
}
Some(name) => name.to_owned(),
None => match node.get("type").and_then(Value::as_array) {
Some(list) => {
let names: Vec<&str> = list
.iter()
.filter_map(Value::as_str)
.filter(|name| *name != "null")
.collect();
let nullable = list.len() != names.len();
format!("{}{}", names.join(" | "), if nullable { "?" } else { "" })
}
None => "any".to_owned(),
},
}
}
fn enum_suffix(def: &Value, _defs: &Map<String, Value>) -> Option<String> {
if let Some(values) = def.get("enum").and_then(Value::as_array) {
let names: Vec<&str> = values.iter().filter_map(Value::as_str).collect();
return Some(format!(" (enum: {})", names.join(", ")));
}
let variants = def.get("oneOf").and_then(Value::as_array)?;
if variants.is_empty() {
return None;
}
if let Some((tag, values)) = common_tag(variants) {
return Some(format!(
" (tagged by {tag}, {} variants: {})",
values.len(),
values.join(", ")
));
}
let shapes: Vec<String> = variants
.iter()
.map(|variant| {
variant.get("type").and_then(Value::as_str).map_or_else(
|| ref_name(variant).map_or_else(|| "object".to_owned(), str::to_owned),
str::to_owned,
)
})
.collect();
Some(format!(
" (untagged, {} shapes: {})",
shapes.len(),
shapes.join(" | ")
))
}
fn common_tag(variants: &[Value]) -> Option<(String, Vec<String>)> {
let first = variants
.first()?
.get("properties")
.and_then(Value::as_object)?;
'candidate: for key in first.keys() {
let mut values = Vec::with_capacity(variants.len());
for variant in variants {
let Some(constant) = variant
.get("properties")
.and_then(|p| p.get(key))
.and_then(|node| node.get("const"))
.and_then(Value::as_str)
else {
continue 'candidate;
};
values.push(constant.to_owned());
}
return Some((key.clone(), values));
}
None
}
fn defs_closure(element: &Value, defs: &Map<String, Value>) -> Value {
let mut wanted = std::collections::BTreeSet::new();
let mut queue = vec![element];
while let Some(node) = queue.pop() {
match node {
Value::Object(map) => {
if let Some(name) = ref_name(node) {
if wanted.insert(name.to_owned()) {
if let Some(def) = defs.get(name) {
queue.push(def);
}
}
}
queue.extend(map.values());
}
Value::Array(items) => queue.extend(items.iter()),
_ => {}
}
}
let mut closure = Map::new();
for name in wanted {
if let Some(def) = defs.get(&name) {
closure.insert(name, def.clone());
}
}
Value::Object(closure)
}