use clap::{Parser, ValueEnum};
use rvt::RevitFile;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(
name = "rvt-schema",
version,
about = "Dump the embedded serialization schema from a Revit file",
after_help = "Examples:\n \
rvt-schema family.rfa --top 20\n \
rvt-schema family.rfa --grep Wall --with-fields\n \
rvt-schema family.rfa --json > schema.json"
)]
struct Cli {
file: PathBuf,
#[arg(short = 'f', long = "format", default_value = "text", value_enum)]
format: Format,
#[arg(long, conflicts_with = "format")]
json: bool,
#[arg(long = "grep")]
grep: Option<String>,
#[arg(long = "with-fields")]
with_fields: bool,
#[arg(long = "top")]
top: Option<usize>,
#[arg(long = "diagnostics")]
diagnostics: bool,
}
#[derive(ValueEnum, Clone, Debug)]
enum Format {
Text,
Json,
}
fn main() -> ExitCode {
rvt::cli::exit_quietly_on_broken_pipe();
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
ExitCode::from(1)
}
}
}
fn run() -> anyhow::Result<()> {
let mut cli = Cli::parse();
if cli.json {
cli.format = Format::Json;
}
let mut rf = RevitFile::open(&cli.file)?;
let schema = rf.schema()?;
if cli.diagnostics {
let diagnostics = schema.diagnostics();
match cli.format {
Format::Json => {
println!("{}", serde_json::to_string_pretty(&diagnostics)?);
}
Format::Text => {
println!("Schema diagnostics");
println!(" classes: {}", diagnostics.class_count);
println!(" parsed fields: {}", diagnostics.parsed_field_count);
println!(
" declared fields: {}",
diagnostics.declared_field_count_sum
);
println!(
" field-count mismatches: {}",
diagnostics.field_count_mismatches
);
println!(" tagged classes: {}", diagnostics.tagged_class_count);
println!(
" parent-only classes: {}",
diagnostics.parent_only_class_count
);
println!(" ancestor tags: {}", diagnostics.ancestor_tag_count);
println!(" skipped records: {}", diagnostics.skipped_records);
println!(" C++ type signatures: {}", diagnostics.cpp_type_count);
}
}
return Ok(());
}
let mut classes: Vec<_> = schema.classes.iter().collect();
if let Some(pat) = &cli.grep {
classes.retain(|c| c.name.contains(pat));
}
if cli.with_fields {
classes.retain(|c| !c.fields.is_empty());
}
if let Some(n) = cli.top {
classes.sort_by_key(|c| std::cmp::Reverse(c.fields.len()));
classes.truncate(n);
}
match cli.format {
Format::Json => {
let filtered = rvt::formats::SchemaTable {
classes: classes.iter().map(|c| (*c).clone()).collect(),
cpp_types: schema.cpp_types.clone(),
skipped_records: schema.skipped_records,
scanned_bytes: schema.scanned_bytes,
total_bytes: schema.total_bytes,
scan_truncated: schema.scan_truncated,
};
println!("{}", serde_json::to_string_pretty(&filtered)?);
}
Format::Text => {
println!("Schema · {} classes", schema.classes.len());
let total_fields: usize = schema.classes.iter().map(|c| c.fields.len()).sum();
println!(" {} fields total", total_fields);
println!(
" {} unique C++ type signatures",
schema.cpp_types.len()
);
if schema.skipped_records > 0 {
println!(
" ({} records skipped during parse)",
schema.skipped_records
);
}
if schema.scan_truncated {
println!(
" scan truncated: {} of {} decompressed bytes scanned (cap {})",
schema.scanned_bytes,
schema.total_bytes,
rvt::formats::SCHEMA_SCAN_LIMIT
);
}
println!();
if cli.top.is_some() {
println!("Top {} classes by field count:", classes.len());
} else if let Some(p) = &cli.grep {
println!("Classes matching /{p}/:");
} else {
println!("(showing first 40 classes; use --grep or --top to filter)");
classes.truncate(40);
}
for c in &classes {
println!(
"\n {} [offset 0x{:x}, {} fields]",
c.name,
c.offset,
c.fields.len()
);
for f in c.fields.iter().take(8) {
match &f.cpp_type {
Some(t) => println!(" . {} : {}", f.name, t),
None => println!(" . {}", f.name),
}
}
if c.fields.len() > 8 {
println!(" . ... ({} more)", c.fields.len() - 8);
}
}
if !schema.cpp_types.is_empty() {
println!("\nC++ type signatures (top 20):");
for t in schema.cpp_types.iter().take(20) {
println!(" {t}");
}
}
}
}
Ok(())
}