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;
use geno::{ast, case};
use std::fmt::Write as _;
use std::io::{self, Read};

static NO_CODE_GEN: &str = "noCodeGen";

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,
        "// Add #![allow(unused_imports)] to root project if needed"
    )
    .unwrap();
    writeln!(out).unwrap();
    writeln!(out, "use serde::{{Deserialize, Serialize}};").unwrap();
    writeln!(out, "use serde_repr::{{Deserialize_repr, Serialize_repr}};").unwrap();
    writeln!(out, "use std::collections::HashMap;").unwrap();

    generate_elements(&mut out, &schema.elements);

    out
}

fn generate_elements(out: &mut String, elements: &Vec<ast::Element>) {
    for element in elements {
        match element {
            ast::Element::Enum {
                attributes: _,
                ident,
                base_type,
                variants,
            } => {
                writeln!(out).unwrap();
                generate_enum(out, ident, base_type, variants);
            }
            ast::Element::Struct {
                attributes: _,
                ident,
                fields,
            } => {
                writeln!(out).unwrap();
                generate_struct(out, ident, fields)
            }
            ast::Element::Include { attributes, schema } => {
                if attributes
                    .iter()
                    .find(|attr| attr.0.name == NO_CODE_GEN)
                    .is_some()
                {
                    continue;
                }

                generate_elements(out, &schema.elements);
            }
        }
    }
}

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

    writeln!(
        out,
        "#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize_repr, Deserialize_repr, 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::Attributes, ast::Ident, ast::NullableFieldType)],
) {
    let rust_name = case::to_pascal(ident.as_str());

    writeln!(
        out,
        "#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]"
    )
    .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(field_type: &ast::NullableFieldType) -> String {
    match field_type {
        ast::NullableFieldType {
            field_type: ast::FieldType::Builtin(bt),
            nullable,
        } => {
            let base = builtin_type_str(bt);
            if *nullable {
                format!("Option<{base}>")
            } else {
                base
            }
        }
        ast::NullableFieldType {
            field_type: ast::FieldType::UserDefined(ident),
            nullable,
        } => {
            let rust_name = case::to_pascal(ident.as_str());

            if *nullable {
                format!("Option<{rust_name}>")
            } else {
                rust_name
            }
        }
        ast::NullableFieldType {
            field_type: ast::FieldType::Array(inner, length),
            nullable,
        } => {
            let inner_str = field_type_str(inner);
            let base = match length {
                Some(len) => format!("[{inner_str}; {0}]", integer_value_str(len)),
                None => format!("Vec<{inner_str}>"),
            };
            if *nullable {
                format!("Option<{base}>")
            } else {
                base
            }
        }
        ast::NullableFieldType {
            field_type: 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(),
    }
}