mod build;
mod config;
mod emit;
mod exclude;
mod load;
mod validate;
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
#[derive(Parser)]
#[command(
name = "rspyts",
version,
about = "Generate Python, TypeScript, and JSON Schema surfaces from a bridged Rust crate."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Generate {
#[arg(long, default_value = "rspyts.toml")]
config: PathBuf,
#[arg(long)]
release: bool,
},
Check {
#[arg(long, default_value = "rspyts.toml")]
config: PathBuf,
#[arg(long)]
release: bool,
},
Init {
#[arg(long, default_value = ".")]
dir: PathBuf,
},
}
struct Failure {
code: u8,
error: anyhow::Error,
}
impl Failure {
fn config(error: anyhow::Error) -> Self {
Self { code: 2, error }
}
fn build(error: anyhow::Error) -> Self {
Self { code: 3, error }
}
fn drift() -> Self {
Self {
code: 1,
error: anyhow::anyhow!("generated code is out of date; run `rspyts generate`"),
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
let result = match cli.command {
Command::Generate { config, release } => run_pipeline(&config, release, Mode::Generate),
Command::Check { config, release } => run_pipeline(&config, release, Mode::Check),
Command::Init { dir } => run_init(&dir),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Err(failure) => {
eprintln!("error: {:#}", failure.error);
ExitCode::from(failure.code)
}
}
}
enum Mode {
Generate,
Check,
}
fn run_pipeline(config_path: &Path, release: bool, mode: Mode) -> Result<(), Failure> {
let cfg = config::load(config_path).map_err(Failure::config)?;
if cfg.python.is_none() && cfg.typescript.is_none() && cfg.schema.is_none() {
return Err(Failure::config(anyhow::anyhow!(
"`{}` enables no outputs — add a [python], [typescript], or [schema] section",
config_path.display()
)));
}
let meta = build::crate_meta(&cfg.crate_dir).map_err(Failure::config)?;
eprintln!(
"building `{}` v{} ({})",
meta.name,
meta.version,
if release { "release" } else { "debug" }
);
let artifact =
build::build_cdylib(&cfg.crate_dir, &meta.name, release).map_err(Failure::build)?;
let loaded = load::load_manifest(&artifact).map_err(Failure::build)?;
validate::validate(&loaded.manifest).map_err(Failure::build)?;
eprintln!(
"loaded manifest: {} type(s), {} constant(s), {} function(s), {} class(es)",
loaded.manifest.types.len(),
loaded.manifest.constants.len(),
loaded.manifest.functions.len(),
loaded.manifest.classes.len()
);
let hash = emit::util::manifest_hash_hex(&loaded.json);
let plan = emit::plan(&cfg, &loaded.manifest, &hash).map_err(Failure::config)?;
match mode {
Mode::Generate => emit::write(&plan).map_err(Failure::build),
Mode::Check => match emit::check(&plan).map_err(Failure::build)? {
true => Err(Failure::drift()),
false => {
eprintln!("generated code is up to date");
Ok(())
}
},
}
}
fn run_init(dir: &Path) -> Result<(), Failure> {
let path = config::init(dir).map_err(Failure::config)?;
println!("wrote {}", path.display());
Ok(())
}