geno 0.4.0

A cross-language schema compiler that generates type definitions and serialization code from a simple, declarative schema language.
Documentation
use anyhow::Context;
use geno::{ast, case};
use std::fmt::Write as _;
use std::io::{self, Read};

fn main() {
    if let Err(err) = run() {
        eprintln!("error: {err:#}");
        std::process::exit(1);
    }

    std::process::exit(0);
}

fn run() -> anyhow::Result<()> {
    let stdin = io::stdin();
    let mut handle = stdin.lock();
    let mut buffer = Vec::new();

    // Read all bytes from stdin into the buffer
    handle
        .read_to_end(&mut buffer)
        .context("Unable to read AST from stdin")?;

    let schema: ast::Schema =
        rmp_serde::from_slice(&buffer).context("Unable to deserialize AST from stdin")?;

    let output = generate(&schema);
    print!("{}", output);

    Ok(())
}

fn generate(schema: &ast::Schema) -> String {
    let mut out = String::new();

    writeln!(out, "#![allow(unused_imports)]").unwrap();
    writeln!(out).unwrap();
    writeln!(out, "use serde::{{Deserialize, Serialize}};").unwrap();
    writeln!(out, "use std::collections::HashMap;").unwrap();

    let decls = schema.flatten_decls();

    for decl in decls {
        writeln!(out).unwrap();
        match decl {
            ast::Declaration::Enum {
                ident,
                base_type,
                variants,
            } => generate_enum(&mut out, ident, base_type, variants),
            ast::Declaration::Struct { ident, fields } => generate_struct(&mut out, ident, fields),
        }
    }

    out
}

fn generate_enum(
    out: &mut String,
    ident: &ast::Ident,
    base_type: &ast::IntegerType,
    variants: &[(ast::Ident, ast::IntegerValue)],
) {
    let rust_name = case::to_pascal(ident.as_str());

    writeln!(
        out,
        "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]"
    )
    .unwrap();
    writeln!(out, "#[repr({})]", integer_type_str(base_type)).unwrap();
    writeln!(out, "pub enum {rust_name} {{").unwrap();

    let mut first = true;

    for (variant_ident, value) in variants {
        let rust_variant = case::to_pascal(variant_ident.as_str());

        if first {
            writeln!(out, "    #[default]").unwrap();
            first = false;
        }
        if rust_variant != *variant_ident.as_str() {
            writeln!(
                out,
                "    #[serde(rename = \"{0}\")]",
                variant_ident.as_str()
            )
            .unwrap();
        }
        writeln!(out, "    {rust_variant} = {},", integer_value_str(value)).unwrap()
    }

    writeln!(out, "}}").unwrap();
}

fn generate_struct(out: &mut String, ident: &ast::Ident, fields: &[(ast::Ident, ast::FieldType)]) {
    let rust_name = case::to_pascal(ident.as_str());

    writeln!(
        out,
        "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
    )
    .unwrap();
    writeln!(out, "pub struct {rust_name} {{").unwrap();

    for (field_ident, field_type) in fields {
        let rust_field = case::to_snake(field_ident.as_str());
        if rust_field != *field_ident.as_str() {
            writeln!(out, "    #[serde(rename = \"{0}\")]", field_ident.as_str()).unwrap();
        }
        writeln!(out, "    pub {rust_field}: {},", field_type_str(field_type)).unwrap();
    }

    writeln!(out, "}}").unwrap();
}

fn field_type_str(ft: &ast::FieldType) -> String {
    match ft {
        ast::FieldType::Builtin(bt, nullable) => {
            let base = builtin_type_str(bt);
            if *nullable {
                format!("Option<{base}>")
            } else {
                base
            }
        }
        ast::FieldType::UserDefined(ident, nullable) => {
            let rust_name = case::to_pascal(ident.as_str());

            if *nullable {
                format!("Option<{rust_name}>")
            } else {
                rust_name
            }
        }
        ast::FieldType::Array(inner, length, nullable) => {
            let inner_str = field_type_str(inner);
            let base = match length {
                Some(len) => format!("[{inner_str}; {len}]"),
                None => format!("Vec<{inner_str}>"),
            };
            if *nullable {
                format!("Option<{base}>")
            } else {
                base
            }
        }
        ast::FieldType::Map(key_type, value_type, nullable) => {
            let key_str = builtin_type_str(key_type);
            let value_str = field_type_str(value_type);
            let base = format!("HashMap<{key_str}, {value_str}>");
            if *nullable {
                format!("Option<{base}>")
            } else {
                base
            }
        }
    }
}

fn builtin_type_str(bt: &ast::BuiltinType) -> String {
    match bt {
        ast::BuiltinType::Integer(it) => integer_type_str(it).to_string(),
        ast::BuiltinType::Float(ft) => match ft {
            ast::FloatType::F32 => "f32".to_string(),
            ast::FloatType::F64 => "f64".to_string(),
        },
        ast::BuiltinType::String => "String".to_string(),
        ast::BuiltinType::Bool => "bool".to_string(),
    }
}

fn integer_type_str(t: &ast::IntegerType) -> &'static str {
    match t {
        ast::IntegerType::I8 => "i8",
        ast::IntegerType::I16 => "i16",
        ast::IntegerType::I32 => "i32",
        ast::IntegerType::I64 => "i64",
        ast::IntegerType::U8 => "u8",
        ast::IntegerType::U16 => "u16",
        ast::IntegerType::U32 => "u32",
        ast::IntegerType::U64 => "u64",
    }
}

fn integer_value_str(v: &ast::IntegerValue) -> String {
    match v {
        ast::IntegerValue::I8(n) => n.to_string(),
        ast::IntegerValue::I16(n) => n.to_string(),
        ast::IntegerValue::I32(n) => n.to_string(),
        ast::IntegerValue::I64(n) => n.to_string(),
        ast::IntegerValue::U8(n) => n.to_string(),
        ast::IntegerValue::U16(n) => n.to_string(),
        ast::IntegerValue::U32(n) => n.to_string(),
        ast::IntegerValue::U64(n) => n.to_string(),
    }
}