mod commands;
mod loader;
mod registry;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Args, Parser, Subcommand, ValueEnum};
use crate::registry::Registry;
#[derive(Debug, Parser)]
#[command(
name = "cadmpeg",
version,
about = "Inspect, decode, validate, compare, and convert CAD models",
after_help = "Exit codes: 0 success, 1 semantic failure, 2 operational error."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum Format {
#[value(alias = "json")]
Cadir,
Step,
F3d,
Sldprt,
}
impl Format {
fn from_extension(extension: &str) -> Option<Self> {
match extension.to_ascii_lowercase().as_str() {
"cadir" | "json" => Some(Self::Cadir),
"step" | "stp" => Some(Self::Step),
"f3d" => Some(Self::F3d),
"sldprt" => Some(Self::Sldprt),
_ => None,
}
}
fn is_geometry_export(self) -> bool {
matches!(self, Self::Step | Self::F3d | Self::Sldprt)
}
fn from_path(path: Option<&std::path::Path>) -> Option<Self> {
path.and_then(std::path::Path::extension)
.and_then(|extension| extension.to_str())
.and_then(Self::from_extension)
}
fn name(self) -> &'static str {
match self {
Self::Cadir => "cadir",
Self::Step => "step",
Self::F3d => "f3d",
Self::Sldprt => "sldprt",
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum InputFormat {
F3d,
Sldprt,
#[value(alias = "catia")]
Catpart,
Nx,
Creo,
Cadir,
}
#[derive(Debug, Clone, Copy)]
enum ForcedInput {
Codec(&'static str),
Cadir,
}
impl InputFormat {
fn resolution(self) -> ForcedInput {
match self {
Self::F3d => ForcedInput::Codec("f3d"),
Self::Sldprt => ForcedInput::Codec("sldprt"),
Self::Catpart => ForcedInput::Codec("catia"),
Self::Nx => ForcedInput::Codec("nx"),
Self::Creo => ForcedInput::Codec("creo"),
Self::Cadir => ForcedInput::Cadir,
}
}
}
#[derive(Debug, Clone, Args)]
struct InputArgs {
#[arg(long, value_enum)]
input_format: Option<InputFormat>,
}
impl InputArgs {
fn forced(&self) -> Option<ForcedInput> {
self.input_format.map(InputFormat::resolution)
}
}
#[derive(Debug, Clone, Args)]
struct DecodeArgs {
#[arg(long)]
container_only: bool,
}
impl DecodeArgs {
fn options(&self) -> cadmpeg_ir::DecodeOptions {
cadmpeg_ir::DecodeOptions {
container_only: self.container_only,
}
}
}
#[derive(Debug, Subcommand)]
enum Command {
Inspect {
input: PathBuf,
#[arg(long)]
json: bool,
#[command(flatten)]
input_args: InputArgs,
},
Decode {
input: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
Validate {
input: PathBuf,
#[arg(long)]
json: bool,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
Export {
input: PathBuf,
#[arg(short, long, value_enum)]
format: Option<Format>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_empty: bool,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
Diff {
a: PathBuf,
b: PathBuf,
#[arg(long)]
json: bool,
#[command(flatten)]
decode: DecodeArgs,
},
Convert {
input: PathBuf,
#[arg(short, long, value_enum)]
format: Option<Format>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
force: bool,
#[arg(long)]
report: Option<PathBuf>,
#[arg(long)]
allow_invalid: bool,
#[arg(long)]
allow_empty: bool,
#[command(flatten)]
input_args: InputArgs,
#[command(flatten)]
decode: DecodeArgs,
},
}
fn main() -> ExitCode {
let registry = Registry::with_builtins();
let result = match Cli::parse().command {
Command::Inspect {
input,
json,
input_args,
} => commands::inspect(®istry, &input, input_args.forced(), json)
.map(|()| ExitCode::SUCCESS),
Command::Decode {
input,
output,
force,
report,
input_args,
decode,
} => commands::decode(
®istry,
&input,
output.as_deref(),
force,
report.as_deref(),
input_args.forced(),
&decode,
)
.map(|()| ExitCode::SUCCESS),
Command::Validate {
input,
json,
input_args,
decode,
} => commands::validate_cmd(®istry, &input, input_args.forced(), &decode, json)
.map(|()| ExitCode::SUCCESS),
Command::Export {
input,
format,
output,
force,
report,
allow_empty,
input_args,
decode,
} => commands::export(
®istry,
&input,
format,
output.as_deref(),
commands::ExportSettings {
force,
report,
allow_empty,
forced_input: input_args.forced(),
},
&decode,
)
.map(|()| ExitCode::SUCCESS),
Command::Diff { a, b, json, decode } => commands::diff(®istry, &a, &b, &decode, json),
Command::Convert {
input,
format,
output,
force,
report,
allow_invalid,
allow_empty,
input_args,
decode,
} => commands::convert(
®istry,
&input,
format,
output.as_deref(),
&commands::ConvertSettings {
force,
report,
allow_invalid,
allow_empty,
forced_input: input_args.forced(),
},
&decode,
)
.map(|()| ExitCode::SUCCESS),
};
result.unwrap_or_else(|err| {
eprintln!("error: {err:#}");
if err.downcast_ref::<commands::SemanticFailure>().is_some() {
ExitCode::from(1)
} else {
ExitCode::from(2)
}
})
}