use super::traits::{BatchOperation, StreamingBatchOperation};
use crate::error::CliError;
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ValidationOperation {
pub strict: bool,
}
impl BatchOperation for ValidationOperation {
type Output = ValidationStats;
fn process_file(&self, path: &Path) -> Result<Self::Output, CliError> {
use hedl_core::{parse_with_limits, Item, Node, ParseOptions, ReferenceMode};
let content = std::fs::read_to_string(path).map_err(|e| CliError::io_error(path, e))?;
let options = ParseOptions {
reference_mode: if self.strict {
ReferenceMode::Strict
} else {
ReferenceMode::Lenient
},
..ParseOptions::default()
};
let doc = parse_with_limits(content.as_bytes(), options)
.map_err(|e| CliError::parse(e.to_string()))?;
let mut stats = ValidationStats::new();
stats.version = format!("{}.{}", doc.version.0, doc.version.1);
fn count_node(node: &Node, stats: &mut ValidationStats) {
stats.node_count += 1;
stats.field_count += node.fields.len();
let full_id = format!("{}:{}", node.type_name, node.id);
stats.seen_ids.insert(full_id);
if let Some(ref children) = node.children {
for child_nodes in children.values() {
for child in child_nodes {
count_node(child, stats);
}
}
}
}
fn traverse_item(item: &Item, stats: &mut ValidationStats) {
match item {
Item::List(list) => {
stats.list_count += 1;
for node in &list.rows {
count_node(node, stats);
}
}
Item::Object(obj) => {
for child_item in obj.values() {
traverse_item(child_item, stats);
}
}
Item::Scalar(_) => {
}
}
}
for item in doc.root.values() {
traverse_item(item, &mut stats);
}
Ok(stats)
}
fn name(&self) -> &'static str {
"validate"
}
}
#[derive(Debug, Clone)]
pub struct FormatOperation {
pub check: bool,
pub ditto: bool,
pub with_counts: bool,
}
impl BatchOperation for FormatOperation {
type Output = String;
fn process_file(&self, path: &Path) -> Result<Self::Output, CliError> {
use hedl_c14n::{canonicalize_with_config, CanonicalConfig};
use hedl_core::parse;
let content = std::fs::read_to_string(path).map_err(|e| CliError::io_error(path, e))?;
let mut doc = parse(content.as_bytes()).map_err(|e| CliError::parse(e.to_string()))?;
if self.with_counts {
add_count_hints(&mut doc);
}
let config = CanonicalConfig::new().with_ditto(self.ditto);
let canonical = canonicalize_with_config(&doc, &config)
.map_err(|e| CliError::canonicalization(e.to_string()))?;
if self.check && canonical != content {
return Err(CliError::NotCanonical);
}
Ok(canonical)
}
fn name(&self) -> &str {
if self.check {
"format-check"
} else {
"format"
}
}
}
#[derive(Debug, Clone)]
pub struct LintOperation {
pub warn_error: bool,
}
impl BatchOperation for LintOperation {
type Output = Vec<String>;
fn process_file(&self, path: &Path) -> Result<Self::Output, CliError> {
use hedl_core::parse;
use hedl_lint::lint;
let content = std::fs::read_to_string(path).map_err(|e| CliError::io_error(path, e))?;
let doc = parse(content.as_bytes()).map_err(|e| CliError::parse(e.to_string()))?;
let diagnostics = lint(&doc);
if self.warn_error && !diagnostics.is_empty() {
return Err(CliError::LintErrors);
}
Ok(diagnostics
.iter()
.map(std::string::ToString::to_string)
.collect())
}
fn name(&self) -> &'static str {
"lint"
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationStats {
pub version: String,
pub list_count: usize,
pub node_count: usize,
pub field_count: usize,
pub seen_ids: HashSet<String>,
}
impl ValidationStats {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct StreamingValidationOperation {
pub strict: bool,
}
impl StreamingBatchOperation for StreamingValidationOperation {
type Output = ValidationStats;
fn process_file_streaming(&self, path: &Path) -> Result<Self::Output, CliError> {
use hedl_stream::{NodeEvent, StreamError, StreamingParser};
use std::fs::File;
use std::io::BufReader;
let file = File::open(path).map_err(|e| CliError::io_error(path, e))?;
let reader = BufReader::with_capacity(8192, file);
let parser = StreamingParser::new(reader)
.map_err(|e: StreamError| CliError::parse(e.to_string()))?;
let mut stats = ValidationStats::new();
let mut _current_type = String::new();
for event in parser {
let event = event.map_err(|e: StreamError| CliError::parse(e.to_string()))?;
match event {
NodeEvent::Header(info) => {
let version_str = format!("{}.{}", info.version.0, info.version.1);
if version_str.is_empty() {
return Err(CliError::parse("Missing VERSION".to_string()));
}
stats.version = version_str;
}
NodeEvent::ListStart { type_name, .. } => {
stats.list_count += 1;
_current_type = type_name;
}
NodeEvent::Node(node) => {
stats.node_count += 1;
stats.field_count += node.fields.len();
let full_id = format!("{}:{}", node.type_name, node.id);
if self.strict {
stats.seen_ids.insert(full_id);
} else {
stats.seen_ids.insert(full_id);
}
}
NodeEvent::ListEnd { .. } => {
}
NodeEvent::Scalar { .. } => {
}
NodeEvent::ObjectStart { .. } => {
}
NodeEvent::ObjectEnd { .. } => {
}
NodeEvent::EndOfDocument => {
break;
}
}
}
Ok(stats)
}
fn name(&self) -> &'static str {
"validate-streaming"
}
fn supports_streaming(&self) -> bool {
true
}
}
fn add_count_hints(doc: &mut hedl_core::Document) {
for item in doc.root.values_mut() {
add_count_hints_to_item(item);
}
}
fn add_count_hints_to_item(item: &mut hedl_core::Item) {
use hedl_core::Item;
match item {
Item::List(list) => {
list.count_hint = Some(list.rows.len());
for node in &mut list.rows {
add_child_count_to_node(node);
}
}
Item::Object(map) => {
for nested_item in map.values_mut() {
add_count_hints_to_item(nested_item);
}
}
Item::Scalar(_) => {
}
}
}
fn add_child_count_to_node(node: &mut hedl_core::Node) {
let total_children: usize = node
.children()
.map_or(0, |c| c.values().map(std::vec::Vec::len).sum());
if total_children > 0 {
node.child_count = total_children.min(u16::MAX as usize) as u16;
if let Some(children) = node.children_mut() {
for child_list in children.values_mut() {
for child_node in child_list {
add_child_count_to_node(child_node);
}
}
}
}
}