geno 0.7.0

A cross-language schema compiler that generates type definitions and serialization code from a simple, declarative schema language.
Documentation
use anyhow::{Context, bail};
use duct::cmd;
use geno::StandardFileResolver;
use std::{
    cell::RefCell,
    fs::{self, File},
    io::{Write, stdout},
    path::PathBuf,
    process::exit,
    rc::Rc,
};

#[derive(clap::Parser)]
#[command(
    name = "geno",
    version,
    about = "Geno Structure Compiler",
    long_about = "Geno is a compiler for generating source code for different languages from from generic structure definitions.  It works by compiling a .geno file into an intermediate AST representation, then generating source code from that AST. It uses a pipeline model to generate code.  Generators are simply executables with the name `geno-` followed by the format name (e.g. `geno-dart-json`). You can pass parameters to the generator by adding a `--` separator after the last `geno` argument and then the generator's parameters."
)]
struct Cli {
    /// Input .geno file
    #[arg(value_name = "INPUT_FILE")]
    input_path: PathBuf,

    /// Output file path for the generated source code, or STDOUT if not provided
    #[arg(value_name = "OUTPUT_FILE", short = 'o', long)]
    output_path: Option<PathBuf>,

    /// Intermediate AST file path for debugging. Program will write the AST
    /// to this file in MessagePack format then exit.
    #[arg(value_name = "AST_FILE", short = 't', long)]
    ast_path: Option<PathBuf>,

    /// Output source code format (e.g. -f dart-json or -f rust-rmp)
    #[arg(value_name = "FORMAT", short = 'f', long)]
    format: Option<String>,

    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    extra_args: Vec<String>,
}

fn main() {
    match run() {
        Ok(code) => exit(code),
        Err(err) => {
            eprintln!("error: {}", err);
            let mut source = err.source();
            while let Some(e) = source {
                eprintln!("caused by: {}", e);
                source = e.source();
            }
            exit(1);
        }
    }
}

fn run() -> anyhow::Result<i32> {
    use clap::Parser;

    let cli = match Cli::try_parse() {
        Ok(cli) => cli,
        Err(err) => {
            // This prints the error message from clap
            eprintln!("{}", err);
            return Ok(0);
        }
    };

    // Parse the input string into an AST
    let ast = geno::Parser::new(Rc::new(RefCell::new(StandardFileResolver::new())))
        .parse(&cli.input_path)?;

    // Validate the AST
    ast.validate()?;

    // If the user specified an AST output path, write the AST to that file and exit
    if let Some(ast_path) = cli.ast_path {
        let mut file = File::create(&ast_path).context(format!(
            "Could not create AST file '{}'",
            ast_path.to_string_lossy()
        ))?;

        rmp_serde::encode::write(&mut file, &ast)
            .context("Failed to serialize AST to MessagePack")?;
        return Ok(0);
    }

    let format = match cli.format {
        Some(s) => s,
        None => bail!("No output format specified"),
    };

    let extra_args: Vec<&str> = cli.extra_args.iter().map(|s| s.as_str()).collect();
    let cmd_expr = if std::env::var("GENO_DEBUG").is_ok() {
        cmd(
            "cargo",
            itertools::concat(vec![
                vec!["run", "--bin", &format!("geno-{}", format), "--"],
                extra_args,
            ]),
        )
    } else {
        cmd(format!("geno-{}", format), extra_args)
    };
    let ast_bytes = rmp_serde::to_vec(&ast).context("Failed to serialize AST to MessagePack")?;
    let output = cmd_expr
        .stdin_bytes(ast_bytes)
        .stdout_capture()
        .read()
        .with_context(|| format!("Failed to run AST formatter '{:?}'", cmd_expr))?;

    match cli.output_path {
        Some(path) => {
            fs::write(path, output)?;
        }
        None => {
            stdout().write(output.as_bytes())?;
        }
    };

    Ok(0)
}