feat: implement CLI for XBRL parsing and validation

- Parse command with optional stats flag
- Validate command with SEC EDGAR profile support
- Benchmark command for performance testing
- Colored output for better UX
This commit is contained in:
Stefano Amorelli
2025-08-16 17:25:06 +03:00
parent fd5b3a968d
commit 46ecbd2635
20 changed files with 25911 additions and 0 deletions

159
src/main.rs Normal file
View File

@@ -0,0 +1,159 @@
//! crabrl CLI - High-performance XBRL parser and validator
use anyhow::{Context, Result};
use clap::{Parser as ClapParser, Subcommand};
use colored::*;
use std::path::PathBuf;
use std::time::Instant;
use crabrl::{Parser, Validator, ValidationConfig};
/// High-performance XBRL parser and validator
#[derive(ClapParser)]
#[command(name = "crabrl")]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Parse an XBRL file
Parse {
/// Input file
input: PathBuf,
/// Output as JSON
#[arg(short, long)]
json: bool,
/// Show statistics
#[arg(short, long)]
stats: bool,
},
/// Validate an XBRL file
Validate {
/// Input file
input: PathBuf,
/// Validation profile (generic, sec-edgar)
#[arg(short, long, default_value = "generic")]
profile: String,
/// Treat warnings as errors
#[arg(long)]
strict: bool,
},
/// Benchmark parsing performance
Bench {
/// Input file
input: PathBuf,
/// Number of iterations
#[arg(short, long, default_value = "100")]
iterations: usize,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Parse { input, json: _, stats } => {
let start = Instant::now();
let parser = Parser::new();
let doc = parser.parse_file(&input)
.with_context(|| format!("Failed to parse {}", input.display()))?;
let elapsed = start.elapsed();
println!("{} {}", "".green().bold(), input.display());
println!(" Facts: {}", doc.facts.len());
println!(" Contexts: {}", doc.contexts.len());
println!(" Units: {}", doc.units.len());
if stats {
println!(" Time: {:.2}ms", elapsed.as_secs_f64() * 1000.0);
println!(" Throughput: {:.0} facts/sec",
doc.facts.len() as f64 / elapsed.as_secs_f64());
}
}
Commands::Validate { input, profile, strict } => {
let parser = Parser::new();
let doc = parser.parse_file(&input)
.with_context(|| format!("Failed to parse {}", input.display()))?;
let config = match profile.as_str() {
"sec-edgar" => ValidationConfig::sec_edgar(),
_ => ValidationConfig::default(),
};
let validator = Validator::with_config(config);
let result = validator.validate(&doc)?;
if result.is_valid {
println!("{} {} - Document is valid", "".green().bold(), input.display());
} else {
println!("{} {} - Validation failed", "".red().bold(), input.display());
println!(" Errors: {}", result.errors.len());
println!(" Warnings: {}", result.warnings.len());
for error in result.errors.iter().take(5) {
println!(" {} {}", "ERROR:".red(), error);
}
if result.errors.len() > 5 {
println!(" ... and {} more errors", result.errors.len() - 5);
}
if strict && !result.warnings.is_empty() {
std::process::exit(1);
}
if !result.is_valid {
std::process::exit(1);
}
}
}
Commands::Bench { input, iterations } => {
let parser = Parser::new();
// Warmup
for _ in 0..3 {
let _ = parser.parse_file(&input)?;
}
let mut times = Vec::with_capacity(iterations);
let mut doc_facts = 0;
for _ in 0..iterations {
let start = Instant::now();
let doc = parser.parse_file(&input)?;
times.push(start.elapsed());
doc_facts = doc.facts.len();
}
times.sort();
let min = times[0];
let max = times[times.len() - 1];
let median = times[times.len() / 2];
let mean = times.iter().sum::<std::time::Duration>() / times.len() as u32;
println!("Benchmark Results for {}", input.display());
println!(" Iterations: {}", iterations);
println!(" Facts: {}", doc_facts);
println!(" Min: {:.3}ms", min.as_secs_f64() * 1000.0);
println!(" Median: {:.3}ms", median.as_secs_f64() * 1000.0);
println!(" Mean: {:.3}ms", mean.as_secs_f64() * 1000.0);
println!(" Max: {:.3}ms", max.as_secs_f64() * 1000.0);
println!(" Throughput: {:.0} facts/sec",
doc_facts as f64 / mean.as_secs_f64());
}
}
Ok(())
}