use clap::{Parser, Subcommand};
use packr::{
codegen, parse_pact_dir_with_registry, parse_pact_file, PactInterface, Type, TypeDef,
TypeRegistry,
};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "pact")]
#[command(about = "Tools for working with Pact interface definitions", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Check {
file: PathBuf,
},
CheckDir {
dir: PathBuf,
},
Codegen {
path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Check { file } => check_command(&file),
Commands::CheckDir { dir } => check_dir_command(&dir),
Commands::Codegen { path, output } => codegen_command(&path, output.as_deref()),
}
}
fn check_command(file: &PathBuf) -> anyhow::Result<()> {
let interface = parse_pact_file(file).map_err(|e| anyhow::anyhow!("{}", e))?;
print_interface_summary(&interface, 0);
println!("\n✓ {} parsed successfully", file.display());
Ok(())
}
fn check_dir_command(dir: &PathBuf) -> anyhow::Result<()> {
let (root, registry) =
parse_pact_dir_with_registry(dir).map_err(|e| anyhow::anyhow!("{}", e))?;
println!("Parsed {} interfaces:", root.children.len());
for child in &root.children {
print_interface_summary(child, 1);
}
println!("\nValidating cross-file references...");
let mut errors = Vec::new();
for child in &root.children {
validate_uses(child, ®istry, &mut errors);
}
if errors.is_empty() {
println!("✓ All references valid");
} else {
println!("\nErrors:");
for error in &errors {
println!(" ✗ {}", error);
}
return Err(anyhow::anyhow!("{} validation error(s)", errors.len()));
}
Ok(())
}
fn validate_uses(interface: &PactInterface, registry: &TypeRegistry, errors: &mut Vec<String>) {
for use_decl in &interface.uses {
match registry.resolve_use(use_decl) {
Ok(types) => {
if use_decl.items.is_empty() {
println!(
" ✓ {}: use {} (all {} types)",
interface.name,
use_decl.interface,
types.len()
);
} else {
println!(
" ✓ {}: use {}.{{{}}}",
interface.name,
use_decl.interface,
use_decl.items.join(", ")
);
}
}
Err(e) => {
errors.push(format!("{}: {}", interface.name, e));
}
}
}
for child in &interface.children {
validate_uses(child, registry, errors);
}
}
fn codegen_command(path: &PathBuf, output: Option<&std::path::Path>) -> anyhow::Result<()> {
let interface = if path.is_dir() {
let (root, _registry) =
parse_pact_dir_with_registry(path).map_err(|e| anyhow::anyhow!("{}", e))?;
root
} else {
parse_pact_file(path).map_err(|e| anyhow::anyhow!("{}", e))?
};
let code = codegen::generate_rust(&interface);
match output {
Some(out_path) => {
std::fs::write(out_path, &code)?;
println!("Generated code written to {}", out_path.display());
}
None => {
println!("{}", code);
}
}
Ok(())
}
fn print_interface_summary(interface: &PactInterface, indent: usize) {
let prefix = " ".repeat(indent);
println!("{}interface {} {{", prefix, interface.name);
for meta in &interface.metadata {
println!("{} @{}: {:?}", prefix, meta.name, meta.value);
}
for use_decl in &interface.uses {
if use_decl.items.is_empty() {
println!("{} use {}", prefix, use_decl.interface);
} else {
println!(
"{} use {}.{{{}}}",
prefix,
use_decl.interface,
use_decl.items.join(", ")
);
}
}
for tp in &interface.type_params {
if let Some(ref constraint) = tp.constraint {
println!("{} type {}: {}", prefix, tp.name, constraint);
} else {
println!("{} type {}", prefix, tp.name);
}
}
for typedef in &interface.types {
println!("{} {}", prefix, format_typedef(typedef));
}
if !interface.exports.is_empty() {
println!("{} exports: {} item(s)", prefix, interface.exports.len());
}
if !interface.imports.is_empty() {
println!("{} imports: {} item(s)", prefix, interface.imports.len());
}
for child in &interface.children {
print_interface_summary(child, indent + 1);
}
println!("{}}}", prefix);
}
fn format_typedef(typedef: &TypeDef) -> String {
match typedef {
TypeDef::Alias { name, ty } => format!("type {} = {}", name, format_type(ty)),
TypeDef::Record { name, fields } => {
format!("record {} {{ {} fields }}", name, fields.len())
}
TypeDef::Variant { name, cases } => {
format!("variant {} {{ {} cases }}", name, cases.len())
}
TypeDef::Enum { name, cases } => {
format!("enum {} {{ {} cases }}", name, cases.len())
}
TypeDef::Flags { name, flags } => {
format!("flags {} {{ {} flags }}", name, flags.len())
}
}
}
fn format_type(ty: &Type) -> String {
match ty {
Type::Unit => "unit".to_string(),
Type::Bool => "bool".to_string(),
Type::U8 => "u8".to_string(),
Type::U16 => "u16".to_string(),
Type::U32 => "u32".to_string(),
Type::U64 => "u64".to_string(),
Type::S8 => "s8".to_string(),
Type::S16 => "s16".to_string(),
Type::S32 => "s32".to_string(),
Type::S64 => "s64".to_string(),
Type::F32 => "f32".to_string(),
Type::F64 => "f64".to_string(),
Type::Char => "char".to_string(),
Type::String => "string".to_string(),
Type::List(inner) => format!("list<{}>", format_type(inner)),
Type::Option(inner) => format!("option<{}>", format_type(inner)),
Type::Result { ok, err } => format!("result<{}, {}>", format_type(ok), format_type(err)),
Type::Tuple(types) => {
format!(
"tuple<{}>",
types.iter().map(format_type).collect::<Vec<_>>().join(", ")
)
}
Type::Ref(path) => path.to_string(),
Type::Value => "value".to_string(),
}
}