use clap::Args;
use rossi::{
NamedComponent, NamedProject, to_multi_project_zip, write_multi_project_directory,
write_project_directory, write_project_zip_file,
};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use super::eventb_io::{self, CmdResult, InputFamily};
#[derive(Args)]
pub struct ExportArgs {
#[arg(required = true, value_name = "INPUT")]
inputs: Vec<PathBuf>,
#[arg(short, long, required = true, value_name = "OUTPUT")]
output: PathBuf,
#[arg(short, long)]
verbose: bool,
}
pub fn run(cli: ExportArgs) -> ExitCode {
match run_inner(&cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("rossi export: {e}");
ExitCode::from(1)
}
}
}
fn run_inner(cli: &ExportArgs) -> CmdResult<()> {
if eventb_io::stdin_is_sole_input(&cli.inputs)? {
let source = eventb_io::read_stdin_to_string()?;
let components = eventb_io::parse_text_components("<stdin>", &source)?;
return write_flat_project(cli, &components);
}
for input in &cli.inputs {
eventb_io::ensure_input(input, InputFamily::Text)?;
}
if let [only] = cli.inputs.as_slice()
&& only.is_dir()
&& let Some(projects) = discover_text_projects(only, cli.verbose)?
{
return write_multi_projects(cli, &projects);
}
let eventb_files = eventb_io::collect_eventb_files(&cli.inputs)?;
if eventb_files.is_empty() {
return Err("No .eventb or .txt files found in inputs".into());
}
let components = parse_eventb_files(&eventb_files, cli.verbose)?;
write_flat_project(cli, &components)
}
fn discover_text_projects(dir: &Path, verbose: bool) -> CmdResult<Option<Vec<NamedProject>>> {
let mut subdirs = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let file_type = entry.file_type()?;
if file_type.is_file() {
if entry
.path()
.extension()
.and_then(|e| e.to_str())
.is_some_and(eventb_io::is_eventb_ext)
{
return Ok(None);
}
} else if file_type.is_dir() {
subdirs.push(entry.path());
}
}
subdirs.sort();
let mut projects = Vec::new();
for subdir in subdirs {
let files = eventb_io::collect_eventb_files(std::slice::from_ref(&subdir))?;
if files.is_empty() {
continue;
}
let name = subdir
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| format!("invalid project directory name: {}", subdir.display()))?
.to_string();
let components = parse_eventb_files(&files, verbose)?;
projects.push(NamedProject { name, components });
}
Ok((!projects.is_empty()).then_some(projects))
}
fn parse_eventb_files(files: &[PathBuf], verbose: bool) -> CmdResult<Vec<NamedComponent>> {
let mut components = Vec::new();
for path in files {
if verbose {
eprintln!("Parsing: {}", path.display());
}
let source = fs::read_to_string(path)?;
components.extend(eventb_io::parse_text_components(
&path.display().to_string(),
&source,
)?);
}
Ok(components)
}
fn write_flat_project(cli: &ExportArgs, components: &[NamedComponent]) -> CmdResult<()> {
let project_name = project_name_from_output(&cli.output);
if is_zip_output(&cli.output) {
eventb_io::ensure_parent_dir(&cli.output)?;
write_project_zip_file(&cli.output, components, project_name)?;
} else {
write_project_directory(&cli.output, components, project_name)?;
}
if cli.verbose {
eprintln!(
"Wrote {} component(s) to {}",
components.len(),
cli.output.display()
);
}
Ok(())
}
fn write_multi_projects(cli: &ExportArgs, projects: &[NamedProject]) -> CmdResult<()> {
if is_zip_output(&cli.output) {
eventb_io::ensure_parent_dir(&cli.output)?;
let bytes = to_multi_project_zip(projects)?;
fs::write(&cli.output, bytes)?;
} else {
write_multi_project_directory(&cli.output, projects)?;
}
if cli.verbose {
let total: usize = projects.iter().map(|p| p.components.len()).sum();
eprintln!(
"Wrote {} component(s) across {} project(s) to {}",
total,
projects.len(),
cli.output.display()
);
}
Ok(())
}
fn is_zip_output(output: &Path) -> bool {
output
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(eventb_io::is_zip_ext)
}
fn project_name_from_output(output: &Path) -> &str {
output
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or_default()
}