use std::io::{self, Write};
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use crate::compatibility::{analyze as analyze_compatibility, analyze_evolution, ComparisonScope};
use crate::diagnostics::{inspect_contract, DiagnosticReport};
use crate::lineage::analyze_with_options;
use crate::model::TransformationContract;
use crate::parser::parse_file;
#[derive(Debug, Parser)]
#[command(
name = "dtcs",
version,
about = "Validate and analyze DTCS transformation contracts"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Validate {
path: PathBuf,
#[arg(long)]
json: bool,
},
Inspect {
path: PathBuf,
#[arg(long)]
json: bool,
},
Diagnostics {
path: PathBuf,
#[arg(long)]
json: bool,
},
Compat {
source: PathBuf,
target: PathBuf,
#[arg(long, value_delimiter = ',')]
scope: Vec<String>,
#[arg(long)]
json: bool,
},
Evolve {
older: PathBuf,
newer: PathBuf,
#[arg(long)]
json: bool,
},
Lineage {
path: PathBuf,
#[arg(long)]
impact: Option<String>,
#[arg(long)]
dependency: Option<String>,
#[arg(long)]
json: bool,
},
Version {
#[arg(long)]
json: bool,
},
}
pub fn run(cli: Cli) -> miette::Result<i32> {
match cli.command {
Command::Validate { path, json } => {
let result = parse_file(&path)?;
let report = result.validate();
render_report(&report, json, ReportMode::Validate)
.map_err(|e| miette::miette!("{e}"))?;
Ok(if report.is_valid() { 0 } else { 1 })
}
Command::Inspect { path, json } => {
let contract = load_valid_contract(&path)?;
if json {
let summary = InspectSummary::from_contract(&contract);
println!(
"{}",
serde_json::to_string_pretty(&summary).map_err(|e| miette::miette!("{e}"))?
);
} else {
print!("{}", inspect_contract(&contract));
}
Ok(0)
}
Command::Diagnostics { path, json } => {
let result = parse_file(&path)?;
let report = result.validate();
render_report(&report, json, ReportMode::Diagnostics)
.map_err(|e| miette::miette!("{e}"))?;
Ok(if report.is_valid() { 0 } else { 1 })
}
Command::Compat {
source,
target,
scope,
json,
} => {
let source_contract = load_valid_contract(&source)?;
let target_contract = load_valid_contract(&target)?;
let scope = match ComparisonScope::from_tokens(&scope) {
Ok(scope) => scope,
Err(invalid) => {
eprintln!("invalid scope token(s): {}", invalid.join(", "));
return Ok(2);
}
};
let report = analyze_compatibility(&source_contract, &target_contract, scope);
if json {
println!(
"{}",
serde_json::to_string_pretty(&report).map_err(|e| miette::miette!("{e}"))?
);
} else {
println!("compatibility: {:?}", report.level);
for aspect in &report.aspects {
println!(" {}: {}", aspect.aspect, aspect.message);
}
for diagnostic in &report.diagnostics {
println!(
"[{:?}] {} - {}",
diagnostic.severity, diagnostic.id, diagnostic.message
);
}
}
Ok(if report.is_compatible() { 0 } else { 1 })
}
Command::Evolve { older, newer, json } => {
let older_contract = load_valid_contract(&older)?;
let newer_contract = load_valid_contract(&newer)?;
let report = analyze_evolution(&older_contract, &newer_contract);
if json {
println!(
"{}",
serde_json::to_string_pretty(&report).map_err(|e| miette::miette!("{e}"))?
);
} else {
println!(
"evolution: {:?} (same identity: {})",
report.compatibility, report.same_identity
);
for change in &report.changes {
println!(" [{:?}] {}", change.category, change.message);
}
for hint in &report.migration_hints {
println!(" hint: {hint}");
}
}
Ok(
if report.same_identity
&& report.compatibility != crate::CompatibilityLevel::Incompatible
{
0
} else {
1
},
)
}
Command::Lineage {
path,
impact,
dependency,
json,
} => {
let contract = load_valid_contract(&path)?;
let report = analyze_with_options(&contract, impact.as_deref(), dependency.as_deref());
if json {
println!(
"{}",
serde_json::to_string_pretty(&report).map_err(|e| miette::miette!("{e}"))?
);
} else {
for edge in &report.graph {
println!("{} <- {:?}", edge.output, edge.inputs);
}
if let Some(impact) = &report.impact {
println!("impact {} -> {:?}", impact.input, impact.outputs);
}
if let Some(dep) = &report.dependency {
println!("dependency {} <- {:?}", dep.output, dep.inputs);
}
}
Ok(0)
}
Command::Version { json } => {
if json {
println!(
"{}",
serde_json::json!({
"crateVersion": env!("CARGO_PKG_VERSION"),
"specVersion": crate::SPEC_VERSION,
})
);
} else {
println!("dtcs {}", env!("CARGO_PKG_VERSION"));
println!("spec {}", crate::SPEC_VERSION);
}
Ok(0)
}
}
}
fn load_valid_contract(path: &PathBuf) -> miette::Result<TransformationContract> {
let result = parse_file(path)?;
if !result.report.is_valid() {
return Err(miette::miette!("parse failed for {}", path.display()));
}
result
.contract
.ok_or_else(|| miette::miette!("no contract in {}", path.display()))
.and_then(|contract| {
let report = crate::validate(&contract);
if !report.is_valid() {
return Err(miette::miette!("validation failed for {}", path.display()));
}
Ok(contract)
})
}
#[derive(Debug)]
enum ReportMode {
Validate,
Diagnostics,
}
#[derive(Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct InspectSummary {
id: String,
name: String,
version: String,
dtcs_version: String,
inputs: usize,
outputs: usize,
semantic_actions: usize,
rules: usize,
expressions: usize,
functions: usize,
}
impl InspectSummary {
fn from_contract(contract: &crate::TransformationContract) -> Self {
Self {
id: contract.id.clone(),
name: contract.name.clone(),
version: contract.version.clone(),
dtcs_version: contract.dtcs_version.clone(),
inputs: contract.inputs.len(),
outputs: contract.outputs.len(),
semantic_actions: contract.semantic_actions.len(),
rules: contract.rules.len(),
expressions: contract.expressions.len(),
functions: contract.functions.len(),
}
}
}
fn render_report(report: &DiagnosticReport, json: bool, mode: ReportMode) -> std::io::Result<()> {
let mut stdout = io::stdout().lock();
if json {
let payload = match mode {
ReportMode::Validate => serde_json::json!({
"valid": report.is_valid(),
"diagnostics": report.diagnostics,
}),
ReportMode::Diagnostics => serde_json::json!({
"diagnostics": report.diagnostics,
}),
};
writeln!(
stdout,
"{}",
serde_json::to_string_pretty(&payload)
.map_err(|e| std::io::Error::other(e.to_string()))?
)?;
return Ok(());
}
if report.diagnostics.is_empty() {
match mode {
ReportMode::Validate => writeln!(stdout, "valid")?,
ReportMode::Diagnostics => writeln!(stdout, "no diagnostics")?,
}
return Ok(());
}
for diagnostic in &report.diagnostics {
writeln!(
stdout,
"[{}] {} ({}) - {}",
format!("{:?}", diagnostic.severity).to_lowercase(),
diagnostic.id,
format!("{:?}", diagnostic.category).to_lowercase(),
diagnostic.message,
)?;
if let Some(object_ref) = &diagnostic.object_ref {
writeln!(stdout, " at: {object_ref}")?;
}
if let Some(remediation) = &diagnostic.remediation {
writeln!(stdout, " hint: {remediation}")?;
}
}
if matches!(mode, ReportMode::Validate) && report.is_valid() {
writeln!(stdout, "valid")?;
}
Ok(())
}