use clap::Parser;
use nxs::convert::{
self, CommonOpts, ConflictPolicy, ImportArgs, ImportFormat, VerifyPolicy, XmlAttrsMode,
};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "nxs-import", about = "Import JSON/CSV/XML into .nxb")]
struct Cli {
#[arg(long, value_name = "json|csv|xml")]
from: String,
#[arg(long, value_name = "FILE")]
schema: Option<PathBuf>,
#[arg(
long,
value_name = "error|coerce-string|first-wins",
default_value = "error"
)]
on_conflict: String,
#[arg(long, value_name = "JSONPATH")]
root: Option<String>,
#[arg(long, value_name = "CHAR")]
csv_delimiter: Option<char>,
#[arg(long)]
csv_no_header: bool,
#[arg(long, value_name = "NAME")]
xml_record_tag: Option<String>,
#[arg(long, value_name = "as-fields|prefix", default_value = "as-fields")]
xml_attrs: String,
#[arg(long, value_name = "N", default_value = "4096")]
buffer_records: usize,
#[arg(long, value_name = "N", default_value = "64")]
max_depth: usize,
#[arg(long, value_name = "N", default_value = "64")]
xml_max_depth: usize,
#[arg(long)]
tail_index_spill: bool,
#[arg(long, value_name = "auto|force|off", default_value = "auto")]
verify: String,
input: String,
output: Option<String>,
}
fn parse_import_format(s: &str) -> Result<ImportFormat, String> {
match s {
"json" => Ok(ImportFormat::Json),
"csv" => Ok(ImportFormat::Csv),
"xml" => Ok(ImportFormat::Xml),
other => Err(format!(
"unknown format '{other}'; expected json, csv, or xml"
)),
}
}
fn parse_conflict(s: &str) -> Result<ConflictPolicy, String> {
match s {
"error" => Ok(ConflictPolicy::Error),
"coerce-string" => Ok(ConflictPolicy::CoerceString),
"first-wins" => Ok(ConflictPolicy::FirstWins),
other => Err(format!(
"unknown --on-conflict '{other}'; expected error, coerce-string, or first-wins"
)),
}
}
fn parse_verify(s: &str) -> Result<VerifyPolicy, String> {
match s {
"auto" => Ok(VerifyPolicy::Auto),
"force" => Ok(VerifyPolicy::Force),
"off" => Ok(VerifyPolicy::Off),
other => Err(format!(
"unknown --verify '{other}'; expected auto, force, or off"
)),
}
}
fn parse_xml_attrs(s: &str) -> Result<XmlAttrsMode, String> {
match s {
"as-fields" => Ok(XmlAttrsMode::AsFields),
"prefix" => Ok(XmlAttrsMode::Prefix),
other => Err(format!(
"unknown --xml-attrs '{other}'; expected as-fields or prefix"
)),
}
}
fn derive_output_path(input: &str, explicit: Option<&str>) -> Option<PathBuf> {
if let Some(out) = explicit {
if out == "-" {
return None; }
return Some(PathBuf::from(out));
}
if input == "-" {
return None; }
let p = std::path::Path::new(input);
let stem = p
.file_name()
.and_then(|n| std::path::Path::new(n).file_stem())?;
Some(PathBuf::from(stem).with_extension("nxb"))
}
fn main() {
let cli = Cli::parse();
let from = parse_import_format(&cli.from).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(2);
});
let conflict = parse_conflict(&cli.on_conflict).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(2);
});
let verify = parse_verify(&cli.verify).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(2);
});
let xml_attrs = parse_xml_attrs(&cli.xml_attrs).unwrap_or_else(|e| {
eprintln!("error: {e}");
std::process::exit(2);
});
let input_path = if cli.input == "-" {
None
} else {
Some(PathBuf::from(&cli.input))
};
let output_path = derive_output_path(&cli.input, cli.output.as_deref());
let args = ImportArgs {
common: CommonOpts {
input_path,
output_path,
},
from,
schema_hint: cli.schema,
conflict,
root: cli.root,
csv_delimiter: cli.csv_delimiter,
csv_no_header: cli.csv_no_header,
xml_record_tag: cli.xml_record_tag,
xml_attrs,
buffer_records: cli.buffer_records,
max_depth: cli.max_depth,
xml_max_depth: cli.xml_max_depth,
tail_index_spill: cli.tail_index_spill,
verify,
};
match convert::run_import(&args) {
Ok(report) => {
let out = args
.common
.output_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<stdout>".into());
eprintln!(
"imported {} records → {} ({} B)",
report.records_written, out, report.output_bytes
);
}
Err(e) => {
eprintln!("error: {e}");
std::process::exit(convert::exit_code_for(&e));
}
}
}