use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use colored::Colorize;
use std::fs;
use std::path::PathBuf;
use splicer::types::ContractResult;
use splicer::{compose, splice, ComponentInput, ComposeRequest, SpliceRequest};
const DEFAULT_PKG: &str = "example:composition";
const DEFAULT_OUTPUT_WAC: &str = "output.wac";
const DEFAULT_SPLITS_DIR: &str = "./splits";
#[derive(Parser, Debug)]
#[command(name = "splicer")]
#[command(
version,
about = "Plan and generate WebAssembly component compositions."
)]
struct Args {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Splice {
#[arg(value_name = "SPLICE_CFG")]
splice_cfg_file: PathBuf,
#[arg(value_name = "COMP_WASM")]
comp_wasm: PathBuf,
#[arg(short, long)]
output_wac: Option<PathBuf>,
#[arg(short, long)]
dir_splits: Option<String>,
#[arg(long, default_value = DEFAULT_PKG)]
package: String,
#[arg(long, default_value_t = false)]
skip_type_check: bool,
},
Compose {
#[arg(value_name = "COMP_WASM", num_args = 2..)]
wasms: Vec<String>,
#[arg(short, long)]
output_wac: Option<PathBuf>,
#[arg(long, default_value = DEFAULT_PKG)]
package: String,
},
}
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("off")),
)
.with_writer(std::io::stderr)
.init();
match Args::parse().command {
Command::Splice {
splice_cfg_file,
comp_wasm,
output_wac,
dir_splits,
package,
skip_type_check,
} => {
let rules_yaml = fs::read_to_string(&splice_cfg_file)
.with_context(|| format!("Failed to read: {}", splice_cfg_file.display()))?;
let splits_dir =
PathBuf::from(dir_splits.unwrap_or_else(|| DEFAULT_SPLITS_DIR.to_string()));
let out = splice(SpliceRequest {
composition_wasm: comp_wasm,
rules_yaml,
package_name: package,
splits_dir,
skip_type_check,
})?;
print_diagnostics(&out.diagnostics, skip_type_check);
write_and_announce(&out.wac, output_wac, |path| out.wac_compose_cmd(path))
}
Command::Compose {
wasms,
output_wac,
package,
} => {
let components: Vec<ComponentInput> = wasms
.iter()
.map(|entry| {
if let Some((alias, rest)) = entry.split_once('=') {
ComponentInput {
alias: Some(alias.to_string()),
path: PathBuf::from(rest),
}
} else {
ComponentInput {
alias: None,
path: PathBuf::from(entry),
}
}
})
.collect();
let out = compose(ComposeRequest {
components,
package_name: package,
})?;
print_diagnostics(&out.diagnostics, false);
write_and_announce(&out.wac, output_wac, |path| out.wac_compose_cmd(path))
}
}
}
fn write_and_announce(
wac: &str,
output_wac: Option<PathBuf>,
format_cmd: impl FnOnce(&str) -> String,
) -> Result<()> {
let output_path = output_wac.unwrap_or_else(|| PathBuf::from(DEFAULT_OUTPUT_WAC));
fs::write(&output_path, wac)
.with_context(|| format!("Failed to write output: {}", output_path.display()))?;
eprintln!("Generated `wac` written to: {}\n", output_path.display());
let wac_path_str = output_path.to_str().ok_or_else(|| {
anyhow::anyhow!(
"output WAC path contains non-UTF-8 bytes: {}",
output_path.display()
)
})?;
println!("{}", format_cmd(wac_path_str));
Ok(())
}
fn print_diagnostics(diagnostics: &[ContractResult], skip_type_check: bool) {
for diag in diagnostics {
match diag {
ContractResult::Ok => {}
ContractResult::Tier1Compatible(_) => unreachable!(
"Tier1Compatible should not surface in the diagnostics list returned by splicer::splice"
),
ContractResult::Warn(msg) => {
eprintln!("{}: {}", "WARN".yellow().bold(), msg.yellow())
}
ContractResult::Error(msg) => {
let _ = skip_type_check;
eprintln!(
"{}: type check skipped — {}",
"WARN".yellow().bold(),
msg.yellow()
);
}
}
}
}