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();
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(),
}
}