use crate::error::{CliError, Result};
use crate::output;
use aprender::format::{apr_export, ExportFormat, ExportOptions, ExportReport, QuantizationType};
use humansize::{format_size, BINARY};
use std::path::Path;
fn resolve_export_options(
format: &str,
output: &Path,
quantize: Option<&str>,
) -> Result<(ExportFormat, Option<QuantizationType>)> {
let effective_format = if format == "safetensors" {
let ext = output.extension().and_then(|e| e.to_str()).unwrap_or("");
match ext {
"gguf" => "gguf",
"mlx" => "mlx",
"onnx" => "onnx",
_ => format,
}
} else {
format
};
let export_format: ExportFormat = effective_format.parse().map_err(|_| {
CliError::ValidationFailed(format!(
"Unknown export format: {effective_format}. Use: safetensors, gguf, mlx, onnx, openvino, coreml"
))
})?;
if !export_format.is_supported() {
return Err(CliError::ValidationFailed(format!(
"Export format '{}' is not yet implemented. Supported: safetensors, gguf, mlx",
export_format.display_name()
)));
}
let quant_type = parse_quantization(quantize)?;
Ok((export_format, quant_type))
}
fn execute_and_display(
file: &Path,
output: &Path,
export_format: ExportFormat,
quant_type: Option<QuantizationType>,
json_output: bool,
) -> Result<()> {
let options = ExportOptions {
format: export_format,
quantize: quant_type,
..Default::default()
};
match apr_export(file, output, options) {
Ok(report) => {
if json_output {
display_report_json(&report);
} else {
display_report(&report);
}
Ok(())
}
Err(e) => {
if !json_output {
println!();
println!(" {}", output::badge_fail("Export failed"));
}
Err(CliError::ValidationFailed(e.to_string()))
}
}
}
#[allow(clippy::disallowed_methods)]
fn run_plan_batch(
file: &Path,
file_size: u64,
batch_formats: &str,
quantize: Option<&str>,
output: Option<&Path>,
json_output: bool,
) -> Result<()> {
let formats: Vec<ExportFormat> = batch_formats
.split(',')
.map(|s| {
s.trim()
.parse::<ExportFormat>()
.map_err(|_| CliError::ValidationFailed(format!("Unknown format in batch: {s}")))
})
.collect::<Result<Vec<_>>>()?;
for f in &formats {
if !f.is_supported() {
return Err(CliError::ValidationFailed(format!(
"Format '{}' in batch is not yet supported",
f.display_name()
)));
}
}
let quant_type = parse_quantization(quantize)?;
if json_output {
let json = serde_json::json!({
"plan": true,
"status": "valid",
"input": file.display().to_string(),
"input_size": file_size,
"batch": true,
"formats": formats.iter().map(|f| f.display_name()).collect::<Vec<_>>(),
"quantization": quant_type.as_ref().map(|q| format!("{q:?}")),
"output_dir": output.map(|p| p.display().to_string()),
});
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
} else {
output::header("APR Export — Plan (Batch)");
let pairs: Vec<(&str, String)> = vec![
(
"Input",
format!(
"{} ({})",
file.display(),
humansize::format_size(file_size, BINARY)
),
),
(
"Formats",
formats
.iter()
.map(ExportFormat::display_name)
.collect::<Vec<_>>()
.join(", "),
),
(
"Output dir",
output.map_or("exports/".to_string(), |p| p.display().to_string()),
),
];
println!("{}", output::kv_table(&pairs));
println!();
println!(" {}", output::badge_pass("Plan valid — ready to export"));
}
Ok(())
}
#[allow(clippy::too_many_arguments, clippy::disallowed_methods)]
fn run_plan(
file: &Path,
format: &str,
output: Option<&Path>,
quantize: Option<&str>,
batch: Option<&str>,
json_output: bool,
) -> Result<()> {
if !file.exists() {
return Err(CliError::FileNotFound(file.to_path_buf()));
}
let file_size = std::fs::metadata(file).map(|m| m.len()).unwrap_or(0);
if let Some(batch_formats) = batch {
return run_plan_batch(
file,
file_size,
batch_formats,
quantize,
output,
json_output,
);
}
let effective_output = output.unwrap_or(Path::new("model.safetensors"));
let (export_format, quant_type) = resolve_export_options(format, effective_output, quantize)?;
if json_output {
let json = serde_json::json!({
"plan": true,
"status": "valid",
"input": file.display().to_string(),
"input_size": file_size,
"output": effective_output.display().to_string(),
"format": export_format.display_name(),
"quantization": quant_type.as_ref().map(|q| format!("{q:?}")),
});
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
} else {
output::header("APR Export — Plan");
let mut pairs: Vec<(&str, String)> = vec![
(
"Input",
format!(
"{} ({})",
file.display(),
humansize::format_size(file_size, BINARY)
),
),
("Output", effective_output.display().to_string()),
("Format", export_format.display_name().to_string()),
];
if let Some(ref q) = quant_type {
pairs.push(("Quantization", format!("{q:?}")));
}
println!("{}", output::kv_table(&pairs));
println!();
println!(" {}", output::badge_pass("Plan valid — ready to export"));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::disallowed_methods)]
pub(crate) fn run(
file: Option<&Path>,
format: &str,
output: Option<&Path>,
quantize: Option<&str>,
list_formats: bool,
batch: Option<&str>,
json_output: bool,
plan: bool,
) -> Result<()> {
contract_pre_format_conversion_roundtrip!();
contract_pre_atomic_write_safety!();
contract_pre_export_roundtrip!();
contract_pre_export_fidelity!();
if list_formats {
return run_list_formats(json_output);
}
let file = file.ok_or_else(|| {
CliError::ValidationFailed("Model file path required. Usage: apr export <FILE>".to_string())
})?;
if plan {
return run_plan(file, format, output, quantize, batch, json_output);
}
if !file.exists() {
return Err(CliError::FileNotFound(file.to_path_buf()));
}
if let Some(batch_formats) = batch {
return run_batch(file, batch_formats, output, quantize, json_output);
}
let output = output.ok_or_else(|| {
CliError::ValidationFailed("Output path required. Use -o <path>".to_string())
})?;
let (export_format, quant_type) = resolve_export_options(format, output, quantize)?;
let pipe_to_stdout = crate::pipe::is_stdout(&output.to_string_lossy());
if pipe_to_stdout {
return run_export_to_stdout(file, export_format, quant_type);
}
if !json_output {
output::header("APR Export");
let mut pairs = vec![
("Input", file.display().to_string()),
("Output", output.display().to_string()),
("Format", export_format.display_name().to_string()),
];
if let Some(ref q) = quant_type {
pairs.push(("Quantization", format!("{q:?}")));
}
println!("{}", output::kv_table(&pairs));
println!();
output::pipeline_stage("Exporting", output::StageStatus::Running);
}
let result = execute_and_display(file, output, export_format, quant_type, json_output);
contract_post_format_conversion_roundtrip!(&());
contract_post_atomic_write_safety!(&());
contract_post_export_roundtrip!(&());
contract_post_export_fidelity!(&());
result
}
fn run_export_to_stdout(
file: &Path,
export_format: ExportFormat,
quant_type: Option<QuantizationType>,
) -> Result<()> {
let tmp_dir = std::env::temp_dir();
let tmp_path = tmp_dir.join(format!("apr-export-{}.bin", std::process::id()));
let options = ExportOptions {
format: export_format,
quantize: quant_type,
..Default::default()
};
let result = apr_export(file, &tmp_path, options);
match result {
Ok(_) => {
let data = std::fs::read(&tmp_path).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
CliError::ValidationFailed(format!("Failed to read exported file: {e}"))
})?;
let _ = std::fs::remove_file(&tmp_path);
crate::pipe::write_stdout(&data)
}
Err(e) => {
let _ = std::fs::remove_file(&tmp_path);
Err(CliError::ValidationFailed(e.to_string()))
}
}
}
fn parse_quantization(quantize: Option<&str>) -> Result<Option<QuantizationType>> {
match quantize {
Some("int8") => Ok(Some(QuantizationType::Int8)),
Some("int4") => Ok(Some(QuantizationType::Int4)),
Some("fp16") => Ok(Some(QuantizationType::Fp16)),
Some("q4k") => Ok(Some(QuantizationType::Q4K)),
Some(other) => Err(CliError::ValidationFailed(format!(
"Unknown quantization: {other}. Supported: int8, int4, fp16, q4k"
))),
None => Ok(None),
}
}
#[allow(clippy::disallowed_methods)]
fn run_list_formats(json_output: bool) -> Result<()> {
if json_output {
let formats: Vec<serde_json::Value> = ExportFormat::all()
.iter()
.map(|f| {
serde_json::json!({
"name": f.display_name(),
"extension": f.extension(),
"supported": f.is_supported(),
"parse_aliases": format_aliases(*f),
})
})
.collect();
let json = serde_json::json!({ "formats": formats });
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
} else {
output::header("APR Export — Supported Formats");
println!();
for f in ExportFormat::all() {
let status = if f.is_supported() {
output::badge_pass("supported")
} else {
output::badge_info("planned")
};
println!(
" {:<14} .{:<14} {}",
f.display_name(),
f.extension(),
status
);
}
println!();
println!(
" {} Use --format <name> to select format.",
output::badge_info("INFO")
);
}
Ok(())
}
fn format_aliases(f: ExportFormat) -> Vec<String> {
match f {
ExportFormat::SafeTensors => vec!["safetensors".into(), "st".into()],
ExportFormat::Gguf => vec!["gguf".into()],
ExportFormat::Mlx => vec!["mlx".into()],
ExportFormat::Onnx => vec!["onnx".into()],
ExportFormat::OpenVino => vec!["openvino".into(), "ov".into()],
ExportFormat::CoreMl => vec!["coreml".into(), "mlpackage".into()],
ExportFormat::TorchScript => vec!["torchscript".into(), "pt".into(), "torch".into()],
}
}
#[allow(clippy::disallowed_methods)]
fn print_batch_summary(
file: &Path,
results: &[(&str, String, ExportReport)],
total_formats: usize,
json_output: bool,
) {
if json_output {
let json_results: Vec<serde_json::Value> = results
.iter()
.map(|(name, path, report)| {
serde_json::json!({
"format": name,
"output": path,
"original_size": report.original_size,
"exported_size": report.exported_size,
"tensor_count": report.tensor_count,
})
})
.collect();
let json = serde_json::json!({
"batch": true,
"input": file.display().to_string(),
"results": json_results,
});
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
} else {
println!();
println!(
" {} Batch export complete: {}/{} formats",
output::badge_pass("PASS"),
results.len(),
total_formats
);
}
}
fn export_single_format(
fmt: ExportFormat,
file: &Path,
out_dir: &Path,
quant_type: Option<QuantizationType>,
json_output: bool,
) -> Option<(&'static str, String, ExportReport)> {
let ext = fmt.extension();
let out_path = if fmt == ExportFormat::Mlx {
out_dir.join(format!("model-{ext}"))
} else {
out_dir.join(format!("model.{ext}"))
};
if let Some(parent) = out_path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("Cannot create output directory '{}': {e}", parent.display());
return None;
}
}
let options = ExportOptions {
format: fmt,
quantize: quant_type,
..Default::default()
};
if !json_output {
output::pipeline_stage(
&format!("Exporting to {}", fmt.display_name()),
output::StageStatus::Running,
);
}
match apr_export(file, &out_path, options) {
Ok(report) => {
if !json_output {
println!(
" {} → {} ({})",
fmt.display_name(),
out_path.display(),
format_size(report.exported_size, BINARY)
);
}
Some((fmt.display_name(), out_path.display().to_string(), report))
}
Err(e) => {
if !json_output {
println!(
" {} {} — {}",
output::badge_fail("FAIL"),
fmt.display_name(),
e
);
}
None
}
}
}
#[allow(clippy::disallowed_methods)]
fn run_batch(
file: &Path,
batch_formats: &str,
output_dir: Option<&Path>,
quantize: Option<&str>,
json_output: bool,
) -> Result<()> {
let out_dir = output_dir.unwrap_or(Path::new("exports"));
let formats: Vec<ExportFormat> = batch_formats
.split(',')
.map(|s| {
s.trim()
.parse::<ExportFormat>()
.map_err(|_| CliError::ValidationFailed(format!("Unknown format in batch: {s}")))
})
.collect::<Result<Vec<_>>>()?;
for f in &formats {
if !f.is_supported() {
return Err(CliError::ValidationFailed(format!(
"Format '{}' in batch is not yet supported",
f.display_name()
)));
}
}
let quant_type = parse_quantization(quantize)?;
if !json_output {
output::header("APR Export — Batch");
output::kv("Input", file.display().to_string());
output::kv("Output dir", out_dir.display().to_string());
output::kv(
"Formats",
formats
.iter()
.map(ExportFormat::display_name)
.collect::<Vec<_>>()
.join(", "),
);
println!();
}
let results: Vec<_> = formats
.iter()
.filter_map(|&fmt| export_single_format(fmt, file, out_dir, quant_type, json_output))
.collect();
print_batch_summary(file, &results, formats.len(), json_output);
Ok(())
}
fn display_report(report: &ExportReport) {
println!();
output::subheader("Export Report");
let mut pairs: Vec<(&str, String)> = vec![
("Original size", format_size(report.original_size, BINARY)),
("Exported size", format_size(report.exported_size, BINARY)),
("Tensors", output::count_fmt(report.tensor_count)),
("Format", report.format.display_name().to_string()),
];
if let Some(ref quant) = report.quantization {
pairs.push(("Quantization", format!("{quant:?}")));
}
println!("{}", output::kv_table(&pairs));
println!();
println!(" {}", output::badge_pass("Export successful"));
}
#[allow(clippy::disallowed_methods)]
fn display_report_json(report: &ExportReport) {
let json = serde_json::json!({
"status": "success",
"original_size": report.original_size,
"exported_size": report.exported_size,
"tensor_count": report.tensor_count,
"format": report.format.display_name(),
"quantization": report.quantization.as_ref().map(|q| format!("{q:?}")),
});
println!(
"{}",
serde_json::to_string_pretty(&json).unwrap_or_default()
);
}
include!("export_file.rs");