cfs-synapse 0.2.3

NASA cFS-friendly IDL and code generator for C headers and Rust repr(C) bindings
Documentation
use std::{path::PathBuf, process};

use clap::{Args as ClapArgs, CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind};

#[derive(Parser)]
#[command(
    name = "synapse",
    about = "NASA cFS message definition compiler — generates C headers and Rust bindings from .syn files"
)]
struct Args {
    #[command(subcommand)]
    command: Option<Command>,

    #[command(flatten)]
    generate: GenerateArgs,
}

#[derive(Subcommand)]
enum Command {
    /// Validate a .syn file and its imports without writing generated output.
    Check(CheckArgs),
    /// Generate static HTML documentation from .syn files.
    Doc(DocArgs),
    /// Generate C headers or Rust bindings.
    Generate(GenerateArgs),
    /// Emit a machine-readable packet registry from .syn files.
    Registry(RegistryArgs),
}

#[derive(ClapArgs)]
struct CheckArgs {
    /// Input .syn files. Multiple roots are checked together for mission-wide packet ID conflicts.
    #[arg(required = true)]
    files: Vec<PathBuf>,
}

#[derive(ClapArgs)]
struct DocArgs {
    /// Write documentation to this directory instead of stdout.
    #[arg(long, short = 'o')]
    out_dir: Option<PathBuf>,

    /// Input .syn files. Multiple roots are documented together.
    #[arg(required = true)]
    files: Vec<PathBuf>,
}

#[derive(ClapArgs)]
struct RegistryArgs {
    /// Registry output format.
    #[arg(long, value_enum, default_value_t = CliRegistryFormat::Json)]
    format: CliRegistryFormat,

    /// Write registry output to this file instead of stdout.
    #[arg(long, short = 'o')]
    output: Option<PathBuf>,

    /// Input .syn files. Multiple roots are exported together.
    #[arg(required = true)]
    files: Vec<PathBuf>,
}

#[derive(ClapArgs)]
struct GenerateArgs {
    /// Target language
    #[arg(long, value_enum)]
    lang: Option<CliLang>,

    /// Write output to this directory instead of stdout.
    /// By default, this writes the root file plus all transitive imports.
    #[arg(long, short = 'o')]
    out_dir: Option<PathBuf>,

    /// With --out-dir, generate only the requested root file.
    #[arg(long)]
    single_file: bool,

    /// Input .syn file
    file: Option<PathBuf>,
}

#[derive(Clone, ValueEnum)]
enum CliLang {
    /// NASA cFS C header (.h)
    C,
    /// Rust #[repr(C)] bindings (.rs)
    Rust,
}

#[derive(Clone, ValueEnum)]
enum CliRegistryFormat {
    /// JSON packet registry.
    Json,
    /// CSV packet registry.
    Csv,
}

impl From<CliLang> for cfs_synapse::Lang {
    fn from(value: CliLang) -> Self {
        match value {
            CliLang::C => cfs_synapse::Lang::C,
            CliLang::Rust => cfs_synapse::Lang::Rust,
        }
    }
}

impl From<CliRegistryFormat> for cfs_synapse::RegistryFormat {
    fn from(value: CliRegistryFormat) -> Self {
        match value {
            CliRegistryFormat::Json => cfs_synapse::RegistryFormat::Json,
            CliRegistryFormat::Csv => cfs_synapse::RegistryFormat::Csv,
        }
    }
}

pub(crate) fn run() {
    let args = Args::parse();
    match args.command {
        Some(Command::Check(check)) => check_path(check),
        Some(Command::Doc(doc)) => doc_path(doc),
        Some(Command::Generate(generate)) => generate_path(generate),
        Some(Command::Registry(registry)) => registry_path(registry),
        None => generate_path(args.generate),
    }
}

fn check_path(args: CheckArgs) {
    cfs_synapse::check_paths(&args.files).unwrap_or_else(|e| {
        eprintln!("Error checking inputs:\n{e}");
        process::exit(1);
    });
    for file in args.files {
        eprintln!("checked {}", file.display());
    }
}

fn doc_path(args: DocArgs) {
    match args.out_dir {
        None => {
            let output = cfs_synapse::generate_docs(&args.files).unwrap_or_else(|e| {
                eprintln!("Error documenting inputs:\n{e}");
                process::exit(1);
            });
            print!("{output}");
        }
        Some(dir) => {
            let out_path = cfs_synapse::write_docs(&args.files, &dir).unwrap_or_else(|e| {
                eprintln!("Error documenting inputs:\n{e}");
                process::exit(1);
            });
            eprintln!("wrote {}", out_path.display());
        }
    }
}

fn registry_path(args: RegistryArgs) {
    let format = cfs_synapse::RegistryFormat::from(args.format);
    match args.output {
        None => {
            let output = cfs_synapse::generate_registry(&args.files, format).unwrap_or_else(|e| {
                eprintln!("Error exporting registry:\n{e}");
                process::exit(1);
            });
            print!("{output}");
        }
        Some(path) => {
            let out_path =
                cfs_synapse::write_registry(&args.files, &path, format).unwrap_or_else(|e| {
                    eprintln!("Error exporting registry:\n{e}");
                    process::exit(1);
                });
            eprintln!("wrote {}", out_path.display());
        }
    }
}

fn generate_path(args: GenerateArgs) {
    let file = args.file.unwrap_or_else(|| {
        Args::command()
            .error(
                ErrorKind::MissingRequiredArgument,
                "missing required input .syn file",
            )
            .exit()
    });
    let lang = args.lang.map(cfs_synapse::Lang::from).unwrap_or_else(|| {
        Args::command()
            .error(
                ErrorKind::MissingRequiredArgument,
                "missing required `--lang <c|rust>`",
            )
            .exit()
    });

    match args.out_dir {
        None if args.single_file => {
            eprintln!(
                "Error generating {}: --single-file only applies with --out-dir",
                file.display()
            );
            process::exit(1);
        }
        None => {
            let output = cfs_synapse::generate_path(&file, lang).unwrap_or_else(|e| {
                eprintln!("Error generating {}:\n{e}", file.display());
                process::exit(1);
            });

            print!("{output}");
        }
        Some(dir) if args.single_file => {
            let out_path = cfs_synapse::generate_file(&file, &dir, lang).unwrap_or_else(|e| {
                eprintln!("Error generating {}: {e}", file.display());
                process::exit(1);
            });
            eprintln!("wrote {}", out_path.display());
        }
        Some(dir) => {
            let out_paths = cfs_synapse::generate_files(&file, &dir, lang).unwrap_or_else(|e| {
                eprintln!("Error generating {}: {e}", file.display());
                process::exit(1);
            });
            for out_path in out_paths {
                eprintln!("wrote {}", out_path.display());
            }
        }
    }
}