mirror of
https://github.com/stefanoamorelli/crabrl.git
synced 2026-04-18 07:10:42 +00:00
style: apply rustfmt to entire codebase
- Fix all formatting issues for CI compliance - Consistent code style across all files - Proper struct/enum formatting - Fixed import ordering
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
use crabrl::Parser;
|
use crabrl::Parser;
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
fn parse_small_file(c: &mut Criterion) {
|
fn parse_small_file(c: &mut Criterion) {
|
||||||
let parser = Parser::new();
|
let parser = Parser::new();
|
||||||
@@ -21,4 +21,3 @@ fn parse_medium_file(c: &mut Criterion) {
|
|||||||
|
|
||||||
criterion_group!(benches, parse_small_file, parse_medium_file);
|
criterion_group!(benches, parse_small_file, parse_medium_file);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
|||||||
@@ -34,4 +34,3 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ fn main() {
|
|||||||
Ok(doc) => {
|
Ok(doc) => {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let ms = elapsed.as_secs_f64() * 1000.0;
|
let ms = elapsed.as_secs_f64() * 1000.0;
|
||||||
println!("crabrl found: {} facts, {} contexts, {} units (in {:.3}ms)",
|
println!(
|
||||||
doc.facts.len(),
|
"crabrl found: {} facts, {} contexts, {} units (in {:.3}ms)",
|
||||||
doc.contexts.len(),
|
doc.facts.len(),
|
||||||
doc.units.len(),
|
doc.contexts.len(),
|
||||||
ms);
|
doc.units.len(),
|
||||||
|
ms
|
||||||
|
);
|
||||||
|
|
||||||
// Additional stats
|
// Additional stats
|
||||||
println!("Facts: {}", doc.facts.len());
|
println!("Facts: {}", doc.facts.len());
|
||||||
println!("Contexts: {}", doc.contexts.len());
|
println!("Contexts: {}", doc.contexts.len());
|
||||||
@@ -37,5 +39,3 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub mod validator;
|
|||||||
pub use simple_parser::Parser;
|
pub use simple_parser::Parser;
|
||||||
|
|
||||||
// Re-export main types
|
// Re-export main types
|
||||||
pub use model::{Document, Fact, Context, Unit};
|
pub use model::{Context, Document, Fact, Unit};
|
||||||
|
|
||||||
// Create validator wrapper for the CLI
|
// Create validator wrapper for the CLI
|
||||||
pub struct Validator {
|
pub struct Validator {
|
||||||
@@ -47,13 +47,13 @@ impl Validator {
|
|||||||
|
|
||||||
pub fn validate(&self, doc: &Document) -> Result<ValidationResult> {
|
pub fn validate(&self, doc: &Document) -> Result<ValidationResult> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
// Clone doc for validation (validator mutates it)
|
// Clone doc for validation (validator mutates it)
|
||||||
let mut doc_copy = doc.clone();
|
let mut doc_copy = doc.clone();
|
||||||
|
|
||||||
// Run validation
|
// Run validation
|
||||||
let is_valid = self.inner.validate(&mut doc_copy).is_ok();
|
let is_valid = self.inner.validate(&mut doc_copy).is_ok();
|
||||||
|
|
||||||
Ok(ValidationResult {
|
Ok(ValidationResult {
|
||||||
is_valid,
|
is_valid,
|
||||||
errors: if is_valid {
|
errors: if is_valid {
|
||||||
|
|||||||
94
src/main.rs
94
src/main.rs
@@ -6,7 +6,7 @@ use colored::*;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crabrl::{Parser, Validator, ValidationConfig};
|
use crabrl::{Parser, ValidationConfig, Validator};
|
||||||
|
|
||||||
/// High-performance XBRL parser and validator
|
/// High-performance XBRL parser and validator
|
||||||
#[derive(ClapParser)]
|
#[derive(ClapParser)]
|
||||||
@@ -23,35 +23,35 @@ enum Commands {
|
|||||||
Parse {
|
Parse {
|
||||||
/// Input file
|
/// Input file
|
||||||
input: PathBuf,
|
input: PathBuf,
|
||||||
|
|
||||||
/// Output as JSON
|
/// Output as JSON
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
||||||
/// Show statistics
|
/// Show statistics
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
stats: bool,
|
stats: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Validate an XBRL file
|
/// Validate an XBRL file
|
||||||
Validate {
|
Validate {
|
||||||
/// Input file
|
/// Input file
|
||||||
input: PathBuf,
|
input: PathBuf,
|
||||||
|
|
||||||
/// Validation profile (generic, sec-edgar)
|
/// Validation profile (generic, sec-edgar)
|
||||||
#[arg(short, long, default_value = "generic")]
|
#[arg(short, long, default_value = "generic")]
|
||||||
profile: String,
|
profile: String,
|
||||||
|
|
||||||
/// Treat warnings as errors
|
/// Treat warnings as errors
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
strict: bool,
|
strict: bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Benchmark parsing performance
|
/// Benchmark parsing performance
|
||||||
Bench {
|
Bench {
|
||||||
/// Input file
|
/// Input file
|
||||||
input: PathBuf,
|
input: PathBuf,
|
||||||
|
|
||||||
/// Number of iterations
|
/// Number of iterations
|
||||||
#[arg(short, long, default_value = "100")]
|
#[arg(short, long, default_value = "100")]
|
||||||
iterations: usize,
|
iterations: usize,
|
||||||
@@ -60,89 +60,109 @@ enum Commands {
|
|||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Parse { input, json: _, stats } => {
|
Commands::Parse {
|
||||||
|
input,
|
||||||
|
json: _,
|
||||||
|
stats,
|
||||||
|
} => {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let parser = Parser::new();
|
let parser = Parser::new();
|
||||||
let doc = parser.parse_file(&input)
|
let doc = parser
|
||||||
|
.parse_file(&input)
|
||||||
.with_context(|| format!("Failed to parse {}", input.display()))?;
|
.with_context(|| format!("Failed to parse {}", input.display()))?;
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
|
|
||||||
println!("{} {}", "✓".green().bold(), input.display());
|
println!("{} {}", "✓".green().bold(), input.display());
|
||||||
println!(" Facts: {}", doc.facts.len());
|
println!(" Facts: {}", doc.facts.len());
|
||||||
println!(" Contexts: {}", doc.contexts.len());
|
println!(" Contexts: {}", doc.contexts.len());
|
||||||
println!(" Units: {}", doc.units.len());
|
println!(" Units: {}", doc.units.len());
|
||||||
|
|
||||||
if stats {
|
if stats {
|
||||||
println!(" Time: {:.2}ms", elapsed.as_secs_f64() * 1000.0);
|
println!(" Time: {:.2}ms", elapsed.as_secs_f64() * 1000.0);
|
||||||
println!(" Throughput: {:.0} facts/sec",
|
println!(
|
||||||
doc.facts.len() as f64 / elapsed.as_secs_f64());
|
" Throughput: {:.0} facts/sec",
|
||||||
|
doc.facts.len() as f64 / elapsed.as_secs_f64()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Validate { input, profile, strict } => {
|
Commands::Validate {
|
||||||
|
input,
|
||||||
|
profile,
|
||||||
|
strict,
|
||||||
|
} => {
|
||||||
let parser = Parser::new();
|
let parser = Parser::new();
|
||||||
let doc = parser.parse_file(&input)
|
let doc = parser
|
||||||
|
.parse_file(&input)
|
||||||
.with_context(|| format!("Failed to parse {}", input.display()))?;
|
.with_context(|| format!("Failed to parse {}", input.display()))?;
|
||||||
|
|
||||||
let config = match profile.as_str() {
|
let config = match profile.as_str() {
|
||||||
"sec-edgar" => ValidationConfig::sec_edgar(),
|
"sec-edgar" => ValidationConfig::sec_edgar(),
|
||||||
_ => ValidationConfig::default(),
|
_ => ValidationConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let validator = Validator::with_config(config);
|
let validator = Validator::with_config(config);
|
||||||
let result = validator.validate(&doc)?;
|
let result = validator.validate(&doc)?;
|
||||||
|
|
||||||
if result.is_valid {
|
if result.is_valid {
|
||||||
println!("{} {} - Document is valid", "✓".green().bold(), input.display());
|
println!(
|
||||||
|
"{} {} - Document is valid",
|
||||||
|
"✓".green().bold(),
|
||||||
|
input.display()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
println!("{} {} - Validation failed", "✗".red().bold(), input.display());
|
println!(
|
||||||
|
"{} {} - Validation failed",
|
||||||
|
"✗".red().bold(),
|
||||||
|
input.display()
|
||||||
|
);
|
||||||
println!(" Errors: {}", result.errors.len());
|
println!(" Errors: {}", result.errors.len());
|
||||||
println!(" Warnings: {}", result.warnings.len());
|
println!(" Warnings: {}", result.warnings.len());
|
||||||
|
|
||||||
for error in result.errors.iter().take(5) {
|
for error in result.errors.iter().take(5) {
|
||||||
println!(" {} {}", "ERROR:".red(), error);
|
println!(" {} {}", "ERROR:".red(), error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.errors.len() > 5 {
|
if result.errors.len() > 5 {
|
||||||
println!(" ... and {} more errors", result.errors.len() - 5);
|
println!(" ... and {} more errors", result.errors.len() - 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if strict && !result.warnings.is_empty() {
|
if strict && !result.warnings.is_empty() {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !result.is_valid {
|
if !result.is_valid {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Commands::Bench { input, iterations } => {
|
Commands::Bench { input, iterations } => {
|
||||||
let parser = Parser::new();
|
let parser = Parser::new();
|
||||||
|
|
||||||
// Warmup
|
// Warmup
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let _ = parser.parse_file(&input)?;
|
let _ = parser.parse_file(&input)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut times = Vec::with_capacity(iterations);
|
let mut times = Vec::with_capacity(iterations);
|
||||||
let mut doc_facts = 0;
|
let mut doc_facts = 0;
|
||||||
|
|
||||||
for _ in 0..iterations {
|
for _ in 0..iterations {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let doc = parser.parse_file(&input)?;
|
let doc = parser.parse_file(&input)?;
|
||||||
times.push(start.elapsed());
|
times.push(start.elapsed());
|
||||||
doc_facts = doc.facts.len();
|
doc_facts = doc.facts.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
times.sort();
|
times.sort();
|
||||||
let min = times[0];
|
let min = times[0];
|
||||||
let max = times[times.len() - 1];
|
let max = times[times.len() - 1];
|
||||||
let median = times[times.len() / 2];
|
let median = times[times.len() / 2];
|
||||||
let mean = times.iter().sum::<std::time::Duration>() / times.len() as u32;
|
let mean = times.iter().sum::<std::time::Duration>() / times.len() as u32;
|
||||||
|
|
||||||
println!("Benchmark Results for {}", input.display());
|
println!("Benchmark Results for {}", input.display());
|
||||||
println!(" Iterations: {}", iterations);
|
println!(" Iterations: {}", iterations);
|
||||||
println!(" Facts: {}", doc_facts);
|
println!(" Facts: {}", doc_facts);
|
||||||
@@ -150,10 +170,12 @@ fn main() -> Result<()> {
|
|||||||
println!(" Median: {:.3}ms", median.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!(" Mean: {:.3}ms", mean.as_secs_f64() * 1000.0);
|
||||||
println!(" Max: {:.3}ms", max.as_secs_f64() * 1000.0);
|
println!(" Max: {:.3}ms", max.as_secs_f64() * 1000.0);
|
||||||
println!(" Throughput: {:.0} facts/sec",
|
println!(
|
||||||
doc_facts as f64 / mean.as_secs_f64());
|
" Throughput: {:.0} facts/sec",
|
||||||
|
doc_facts as f64 / mean.as_secs_f64()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/model.rs
13
src/model.rs
@@ -5,7 +5,6 @@ use std::collections::HashMap;
|
|||||||
// Core XBRL Data Structures - Full Specification Support
|
// Core XBRL Data Structures - Full Specification Support
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
|
||||||
#[repr(C, align(64))]
|
#[repr(C, align(64))]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FactStorage {
|
pub struct FactStorage {
|
||||||
@@ -111,8 +110,13 @@ pub struct Scenario {
|
|||||||
// Period with forever support
|
// Period with forever support
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Period {
|
pub enum Period {
|
||||||
Instant { date: CompactString },
|
Instant {
|
||||||
Duration { start: CompactString, end: CompactString },
|
date: CompactString,
|
||||||
|
},
|
||||||
|
Duration {
|
||||||
|
start: CompactString,
|
||||||
|
end: CompactString,
|
||||||
|
},
|
||||||
Forever,
|
Forever,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +351,3 @@ impl Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,29 +15,28 @@ impl Parser {
|
|||||||
load_linkbases: false,
|
load_linkbases: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_file<P: AsRef<Path>>(&self, path: P) -> Result<Document> {
|
pub fn parse_file<P: AsRef<Path>>(&self, path: P) -> Result<Document> {
|
||||||
let content = std::fs::read(path)?;
|
let content = std::fs::read(path)?;
|
||||||
self.parse_bytes(&content)
|
self.parse_bytes(&content)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_bytes(&self, data: &[u8]) -> Result<Document> {
|
pub fn parse_bytes(&self, data: &[u8]) -> Result<Document> {
|
||||||
// Simple XML parsing - just count elements for now
|
// Simple XML parsing - just count elements for now
|
||||||
let text = String::from_utf8_lossy(data);
|
let text = String::from_utf8_lossy(data);
|
||||||
|
|
||||||
// Count facts (very simplified)
|
// Count facts (very simplified)
|
||||||
let fact_count = text.matches("<us-gaap:").count() +
|
let fact_count = text.matches("<us-gaap:").count()
|
||||||
text.matches("<dei:").count() +
|
+ text.matches("<dei:").count()
|
||||||
text.matches("<ifrs:").count();
|
+ text.matches("<ifrs:").count();
|
||||||
|
|
||||||
// Count contexts
|
// Count contexts
|
||||||
let context_count = text.matches("<context ").count() +
|
let context_count =
|
||||||
text.matches("<xbrli:context").count();
|
text.matches("<context ").count() + text.matches("<xbrli:context").count();
|
||||||
|
|
||||||
// Count units
|
// Count units
|
||||||
let unit_count = text.matches("<unit ").count() +
|
let unit_count = text.matches("<unit ").count() + text.matches("<xbrli:unit").count();
|
||||||
text.matches("<xbrli:unit").count();
|
|
||||||
|
|
||||||
// Create dummy document with approximate counts
|
// Create dummy document with approximate counts
|
||||||
let mut doc = Document {
|
let mut doc = Document {
|
||||||
facts: FactStorage {
|
facts: FactStorage {
|
||||||
@@ -65,7 +64,7 @@ impl Parser {
|
|||||||
dimensions: Vec::new(),
|
dimensions: Vec::new(),
|
||||||
concept_names: Vec::new(),
|
concept_names: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add dummy contexts
|
// Add dummy contexts
|
||||||
for i in 0..context_count {
|
for i in 0..context_count {
|
||||||
doc.contexts.push(Context {
|
doc.contexts.push(Context {
|
||||||
@@ -81,7 +80,7 @@ impl Parser {
|
|||||||
scenario: None,
|
scenario: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add dummy units
|
// Add dummy units
|
||||||
for i in 0..unit_count {
|
for i in 0..unit_count {
|
||||||
doc.units.push(Unit {
|
doc.units.push(Unit {
|
||||||
@@ -92,7 +91,7 @@ impl Parser {
|
|||||||
}]),
|
}]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(doc)
|
Ok(doc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
170
src/validator.rs
170
src/validator.rs
@@ -1,15 +1,33 @@
|
|||||||
// Comprehensive XBRL validation
|
// Comprehensive XBRL validation
|
||||||
use crate::{model::*, Result, Error};
|
use crate::{model::*, Error, Result};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
InvalidContextRef { fact_index: usize, context_id: u16 },
|
InvalidContextRef {
|
||||||
InvalidUnitRef { fact_index: usize, unit_id: u16 },
|
fact_index: usize,
|
||||||
CalculationInconsistency { concept: String, expected: f64, actual: f64 },
|
context_id: u16,
|
||||||
InvalidDataType { concept: String, expected_type: String, actual_value: String },
|
},
|
||||||
MissingRequiredElement { element: String },
|
InvalidUnitRef {
|
||||||
DuplicateId { id: String },
|
fact_index: usize,
|
||||||
|
unit_id: u16,
|
||||||
|
},
|
||||||
|
CalculationInconsistency {
|
||||||
|
concept: String,
|
||||||
|
expected: f64,
|
||||||
|
actual: f64,
|
||||||
|
},
|
||||||
|
InvalidDataType {
|
||||||
|
concept: String,
|
||||||
|
expected_type: String,
|
||||||
|
actual_value: String,
|
||||||
|
},
|
||||||
|
MissingRequiredElement {
|
||||||
|
element: String,
|
||||||
|
},
|
||||||
|
DuplicateId {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct XbrlValidator {
|
pub struct XbrlValidator {
|
||||||
@@ -49,7 +67,7 @@ impl XbrlValidator {
|
|||||||
|
|
||||||
pub fn validate(&self, doc: &mut Document) -> Result<()> {
|
pub fn validate(&self, doc: &mut Document) -> Result<()> {
|
||||||
let mut validation_errors = Vec::new();
|
let mut validation_errors = Vec::new();
|
||||||
|
|
||||||
// Context validation
|
// Context validation
|
||||||
if self.check_contexts {
|
if self.check_contexts {
|
||||||
validation_errors.extend(self.validate_contexts(doc));
|
validation_errors.extend(self.validate_contexts(doc));
|
||||||
@@ -82,7 +100,7 @@ impl XbrlValidator {
|
|||||||
fn validate_contexts(&self, doc: &Document) -> Vec<ValidationError> {
|
fn validate_contexts(&self, doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut context_ids = HashSet::new();
|
let mut context_ids = HashSet::new();
|
||||||
|
|
||||||
for ctx in &doc.contexts {
|
for ctx in &doc.contexts {
|
||||||
// Check for duplicate context IDs
|
// Check for duplicate context IDs
|
||||||
if !context_ids.insert(ctx.id.clone()) {
|
if !context_ids.insert(ctx.id.clone()) {
|
||||||
@@ -112,14 +130,14 @@ impl XbrlValidator {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_units(&self, doc: &Document) -> Vec<ValidationError> {
|
fn validate_units(&self, doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut unit_ids = HashSet::new();
|
let mut unit_ids = HashSet::new();
|
||||||
|
|
||||||
for unit in &doc.units {
|
for unit in &doc.units {
|
||||||
// Check for duplicate unit IDs
|
// Check for duplicate unit IDs
|
||||||
if !unit_ids.insert(unit.id.clone()) {
|
if !unit_ids.insert(unit.id.clone()) {
|
||||||
@@ -137,7 +155,10 @@ impl XbrlValidator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UnitType::Divide { numerator, denominator } => {
|
UnitType::Divide {
|
||||||
|
numerator,
|
||||||
|
denominator,
|
||||||
|
} => {
|
||||||
if numerator.is_empty() || denominator.is_empty() {
|
if numerator.is_empty() || denominator.is_empty() {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: format!("Numerator/denominator for unit {}", unit.id),
|
element: format!("Numerator/denominator for unit {}", unit.id),
|
||||||
@@ -153,13 +174,13 @@ impl XbrlValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_facts(&self, doc: &Document) -> Vec<ValidationError> {
|
fn validate_facts(&self, doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
// Validate fact references
|
// Validate fact references
|
||||||
for i in 0..doc.facts.len() {
|
for i in 0..doc.facts.len() {
|
||||||
if i < doc.facts.context_ids.len() {
|
if i < doc.facts.context_ids.len() {
|
||||||
@@ -171,7 +192,7 @@ impl XbrlValidator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if i < doc.facts.unit_ids.len() {
|
if i < doc.facts.unit_ids.len() {
|
||||||
let unit_id = doc.facts.unit_ids[i];
|
let unit_id = doc.facts.unit_ids[i];
|
||||||
if unit_id > 0 && unit_id as usize > doc.units.len() {
|
if unit_id > 0 && unit_id as usize > doc.units.len() {
|
||||||
@@ -182,14 +203,14 @@ impl XbrlValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_duplicate_facts(&self, doc: &Document) -> Vec<ValidationError> {
|
fn check_duplicate_facts(&self, doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let mut fact_keys = HashSet::new();
|
let mut fact_keys = HashSet::new();
|
||||||
|
|
||||||
for i in 0..doc.facts.len() {
|
for i in 0..doc.facts.len() {
|
||||||
if i < doc.facts.concept_ids.len() && i < doc.facts.context_ids.len() {
|
if i < doc.facts.concept_ids.len() && i < doc.facts.context_ids.len() {
|
||||||
let key = (doc.facts.concept_ids[i], doc.facts.context_ids[i]);
|
let key = (doc.facts.concept_ids[i], doc.facts.context_ids[i]);
|
||||||
@@ -200,7 +221,7 @@ impl XbrlValidator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,16 +248,16 @@ impl ValidationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_rule<F>(&mut self, rule: F)
|
pub fn add_rule<F>(&mut self, rule: F)
|
||||||
where
|
where
|
||||||
F: Fn(&Document) -> Vec<ValidationError> + 'static
|
F: Fn(&Document) -> Vec<ValidationError> + 'static,
|
||||||
{
|
{
|
||||||
self.custom_rules.push(Box::new(rule));
|
self.custom_rules.push(Box::new(rule));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self, doc: &Document) -> Vec<ValidationError> {
|
pub fn validate(&self, doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
// Apply profile-specific rules
|
// Apply profile-specific rules
|
||||||
match self.profile {
|
match self.profile {
|
||||||
ValidationProfile::SecEdgar => {
|
ValidationProfile::SecEdgar => {
|
||||||
@@ -247,12 +268,12 @@ impl ValidationContext {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply custom rules
|
// Apply custom rules
|
||||||
for rule in &self.custom_rules {
|
for rule in &self.custom_rules {
|
||||||
errors.extend(rule(doc));
|
errors.extend(rule(doc));
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,19 +281,21 @@ impl ValidationContext {
|
|||||||
// SEC EDGAR specific validation rules
|
// SEC EDGAR specific validation rules
|
||||||
pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
// Check for required DEI contexts
|
// Check for required DEI contexts
|
||||||
let mut has_current_period = false;
|
let mut has_current_period = false;
|
||||||
let mut has_entity_info = false;
|
let mut has_entity_info = false;
|
||||||
let mut has_dei_elements = false;
|
let mut has_dei_elements = false;
|
||||||
|
|
||||||
for ctx in &doc.contexts {
|
for ctx in &doc.contexts {
|
||||||
// Check for current period context
|
// Check for current period context
|
||||||
if ctx.id.contains("CurrentYear") || ctx.id.contains("CurrentPeriod") ||
|
if ctx.id.contains("CurrentYear")
|
||||||
ctx.id.contains("DocumentPeriodEndDate") {
|
|| ctx.id.contains("CurrentPeriod")
|
||||||
|
|| ctx.id.contains("DocumentPeriodEndDate")
|
||||||
|
{
|
||||||
has_current_period = true;
|
has_current_period = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate CIK format (10 digits)
|
// Validate CIK format (10 digits)
|
||||||
if ctx.entity.scheme.contains("sec.gov/CIK") {
|
if ctx.entity.scheme.contains("sec.gov/CIK") {
|
||||||
has_entity_info = true;
|
has_entity_info = true;
|
||||||
@@ -286,37 +309,39 @@ pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for DEI elements in facts
|
// Check for DEI elements in facts
|
||||||
for i in 0..doc.facts.concept_ids.len() {
|
for i in 0..doc.facts.concept_ids.len() {
|
||||||
if i < doc.concept_names.len() {
|
if i < doc.concept_names.len() {
|
||||||
let concept = &doc.concept_names[i];
|
let concept = &doc.concept_names[i];
|
||||||
if concept.contains("dei:") || concept.contains("DocumentType") ||
|
if concept.contains("dei:")
|
||||||
concept.contains("EntityRegistrantName") {
|
|| concept.contains("DocumentType")
|
||||||
|
|| concept.contains("EntityRegistrantName")
|
||||||
|
{
|
||||||
has_dei_elements = true;
|
has_dei_elements = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required elements validation
|
// Required elements validation
|
||||||
if !has_current_period {
|
if !has_current_period {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Current period context required for SEC filing".to_string(),
|
element: "Current period context required for SEC filing".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_entity_info {
|
if !has_entity_info {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Entity CIK information required for SEC filing".to_string(),
|
element: "Entity CIK information required for SEC filing".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_dei_elements {
|
if !has_dei_elements {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "DEI (Document and Entity Information) elements required".to_string(),
|
element: "DEI (Document and Entity Information) elements required".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate segment reporting if present
|
// Validate segment reporting if present
|
||||||
for ctx in &doc.contexts {
|
for ctx in &doc.contexts {
|
||||||
if let Some(segment) = &ctx.entity.segment {
|
if let Some(segment) = &ctx.entity.segment {
|
||||||
@@ -332,7 +357,7 @@ pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate calculation consistency for monetary items
|
// Validate calculation consistency for monetary items
|
||||||
let mut monetary_facts: Vec<(usize, f64)> = Vec::new();
|
let mut monetary_facts: Vec<(usize, f64)> = Vec::new();
|
||||||
for i in 0..doc.facts.len() {
|
for i in 0..doc.facts.len() {
|
||||||
@@ -352,7 +377,7 @@ pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic calculation validation - check for reasonable values
|
// Basic calculation validation - check for reasonable values
|
||||||
for (idx, value) in monetary_facts {
|
for (idx, value) in monetary_facts {
|
||||||
if value.is_nan() || value.is_infinite() {
|
if value.is_nan() || value.is_infinite() {
|
||||||
@@ -371,27 +396,29 @@ pub fn sec_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|
||||||
// IFRS specific validation rules
|
// IFRS specific validation rules
|
||||||
pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
// Check for IFRS-required contexts
|
// Check for IFRS-required contexts
|
||||||
let mut has_reporting_period = false;
|
let mut has_reporting_period = false;
|
||||||
let mut has_comparative_period = false;
|
let mut has_comparative_period = false;
|
||||||
let mut has_entity_info = false;
|
let mut has_entity_info = false;
|
||||||
|
|
||||||
for ctx in &doc.contexts {
|
for ctx in &doc.contexts {
|
||||||
// Check for reporting period
|
// Check for reporting period
|
||||||
match &ctx.period {
|
match &ctx.period {
|
||||||
Period::Duration { start, end: _ } => {
|
Period::Duration { start, end: _ } => {
|
||||||
has_reporting_period = true;
|
has_reporting_period = true;
|
||||||
// IFRS requires comparative information
|
// IFRS requires comparative information
|
||||||
if start.contains("PY") || ctx.id.contains("PriorYear") ||
|
if start.contains("PY")
|
||||||
ctx.id.contains("Comparative") {
|
|| ctx.id.contains("PriorYear")
|
||||||
|
|| ctx.id.contains("Comparative")
|
||||||
|
{
|
||||||
has_comparative_period = true;
|
has_comparative_period = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -402,32 +429,32 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate entity information
|
// Validate entity information
|
||||||
if !ctx.entity.identifier.is_empty() {
|
if !ctx.entity.identifier.is_empty() {
|
||||||
has_entity_info = true;
|
has_entity_info = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required contexts validation
|
// Required contexts validation
|
||||||
if !has_reporting_period {
|
if !has_reporting_period {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Reporting period required for IFRS filing".to_string(),
|
element: "Reporting period required for IFRS filing".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_comparative_period {
|
if !has_comparative_period {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Comparative period information required by IFRS".to_string(),
|
element: "Comparative period information required by IFRS".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_entity_info {
|
if !has_entity_info {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Entity identification required for IFRS filing".to_string(),
|
element: "Entity identification required for IFRS filing".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate dimensional structure
|
// Validate dimensional structure
|
||||||
let mut dimension_validations = Vec::new();
|
let mut dimension_validations = Vec::new();
|
||||||
for ctx in &doc.contexts {
|
for ctx in &doc.contexts {
|
||||||
@@ -436,7 +463,8 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
for member in &segment.explicit_members {
|
for member in &segment.explicit_members {
|
||||||
// IFRS dimensions should follow specific patterns
|
// IFRS dimensions should follow specific patterns
|
||||||
if !member.dimension.contains(":") {
|
if !member.dimension.contains(":") {
|
||||||
dimension_validations.push(format!("Invalid dimension format: {}", member.dimension));
|
dimension_validations
|
||||||
|
.push(format!("Invalid dimension format: {}", member.dimension));
|
||||||
}
|
}
|
||||||
if member.dimension.contains("ifrs") || member.dimension.contains("ifrs-full") {
|
if member.dimension.contains("ifrs") || member.dimension.contains("ifrs-full") {
|
||||||
// Valid IFRS dimension
|
// Valid IFRS dimension
|
||||||
@@ -449,7 +477,7 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check typed members for IFRS compliance
|
// Check typed members for IFRS compliance
|
||||||
for typed in &segment.typed_members {
|
for typed in &segment.typed_members {
|
||||||
if typed.dimension.contains("ifrs") && typed.value.is_empty() {
|
if typed.dimension.contains("ifrs") && typed.value.is_empty() {
|
||||||
@@ -461,7 +489,7 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check scenario dimensions (alternative to segment)
|
// Check scenario dimensions (alternative to segment)
|
||||||
if let Some(scenario) = &ctx.scenario {
|
if let Some(scenario) = &ctx.scenario {
|
||||||
for member in &scenario.explicit_members {
|
for member in &scenario.explicit_members {
|
||||||
@@ -475,61 +503,67 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for mandatory IFRS disclosures in facts
|
// Check for mandatory IFRS disclosures in facts
|
||||||
let mut has_financial_position = false;
|
let mut has_financial_position = false;
|
||||||
let mut has_comprehensive_income = false;
|
let mut has_comprehensive_income = false;
|
||||||
let mut has_cash_flows = false;
|
let mut has_cash_flows = false;
|
||||||
let mut has_changes_in_equity = false;
|
let mut has_changes_in_equity = false;
|
||||||
|
|
||||||
for i in 0..doc.concept_names.len() {
|
for i in 0..doc.concept_names.len() {
|
||||||
let concept = &doc.concept_names[i];
|
let concept = &doc.concept_names[i];
|
||||||
let lower = concept.to_lowercase();
|
let lower = concept.to_lowercase();
|
||||||
|
|
||||||
if lower.contains("financialposition") || lower.contains("balancesheet") ||
|
if lower.contains("financialposition")
|
||||||
lower.contains("assets") || lower.contains("liabilities") {
|
|| lower.contains("balancesheet")
|
||||||
|
|| lower.contains("assets")
|
||||||
|
|| lower.contains("liabilities")
|
||||||
|
{
|
||||||
has_financial_position = true;
|
has_financial_position = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if lower.contains("comprehensiveincome") || lower.contains("profitorloss") ||
|
if lower.contains("comprehensiveincome")
|
||||||
lower.contains("income") || lower.contains("revenue") {
|
|| lower.contains("profitorloss")
|
||||||
|
|| lower.contains("income")
|
||||||
|
|| lower.contains("revenue")
|
||||||
|
{
|
||||||
has_comprehensive_income = true;
|
has_comprehensive_income = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if lower.contains("cashflow") || lower.contains("cashflows") {
|
if lower.contains("cashflow") || lower.contains("cashflows") {
|
||||||
has_cash_flows = true;
|
has_cash_flows = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if lower.contains("changesinequity") || lower.contains("equity") {
|
if lower.contains("changesinequity") || lower.contains("equity") {
|
||||||
has_changes_in_equity = true;
|
has_changes_in_equity = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mandatory statements
|
// Validate mandatory statements
|
||||||
if !has_financial_position {
|
if !has_financial_position {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Statement of Financial Position required by IFRS".to_string(),
|
element: "Statement of Financial Position required by IFRS".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_comprehensive_income {
|
if !has_comprehensive_income {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Statement of Comprehensive Income required by IFRS".to_string(),
|
element: "Statement of Comprehensive Income required by IFRS".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_cash_flows {
|
if !has_cash_flows {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Statement of Cash Flows required by IFRS".to_string(),
|
element: "Statement of Cash Flows required by IFRS".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_changes_in_equity {
|
if !has_changes_in_equity {
|
||||||
errors.push(ValidationError::MissingRequiredElement {
|
errors.push(ValidationError::MissingRequiredElement {
|
||||||
element: "Statement of Changes in Equity required by IFRS".to_string(),
|
element: "Statement of Changes in Equity required by IFRS".to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate presentation linkbase relationships
|
// Validate presentation linkbase relationships
|
||||||
for link in &doc.presentation_links {
|
for link in &doc.presentation_links {
|
||||||
// Check order is valid (typically 1.0 to 999.0)
|
// Check order is valid (typically 1.0 to 999.0)
|
||||||
@@ -541,7 +575,7 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate calculation relationships
|
// Validate calculation relationships
|
||||||
for link in &doc.calculation_links {
|
for link in &doc.calculation_links {
|
||||||
// Check weight is reasonable (-1.0 or 1.0 typically)
|
// Check weight is reasonable (-1.0 or 1.0 typically)
|
||||||
@@ -556,6 +590,6 @@ pub fn ifrs_validation_rules(doc: &Document) -> Vec<ValidationError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errors
|
errors
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user