use anyhow::Context;
use geno::{ast, case};
use std::collections::HashSet;
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();
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();
let enum_names: HashSet<&str> = schema
.elements
.iter()
.filter_map(|d| match d {
ast::Element::Enum { ident, .. } => Some(ident.as_str()),
_ => None,
})
.collect();
writeln!(out, "import 'dart:typed_data';").unwrap();
writeln!(out).unwrap();
writeln!(out, "import 'package:messagepack/messagepack.dart';").unwrap();
let elements = schema.flatten_elements();
for element in elements {
writeln!(out).unwrap();
match element {
ast::Element::Enum {
attributes: _,
ident,
base_type,
variants,
} => generate_enum(&mut out, ident, base_type, variants),
ast::Element::Struct {
attributes: _,
ident,
fields,
} => generate_struct(&mut out, ident, fields, &enum_names),
ast::Element::Include { .. } => {}
}
}
out
}
fn generate_enum(
out: &mut String,
ident: &ast::Ident,
_base_type: &ast::IntegerType,
variants: &[(ast::Attributes, ast::Ident, ast::IntegerValue)],
) {
let dart_name = case::to_pascal(ident.as_str());
writeln!(out, "enum {dart_name} {{").unwrap();
for (i, (_, variant_name, value)) in variants.iter().enumerate() {
let dart_variant = case::to_camel(variant_name.as_str());
let trailing = if i < variants.len() - 1 { "," } else { ";" };
let actual_value = integer_value_str(value);
writeln!(out, " {dart_variant}({actual_value}){trailing}").unwrap();
}
writeln!(out).unwrap();
writeln!(out, " final int value;").unwrap();
writeln!(out, " const {dart_name}(this.value);").unwrap();
writeln!(out).unwrap();
writeln!(out, " Uint8List toBytes() {{").unwrap();
writeln!(out, " final p = Packer();").unwrap();
writeln!(out, " _pack(p);").unwrap();
writeln!(out, " return p.takeBytes();").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name} fromBytes(Uint8List bytes) {{").unwrap();
writeln!(out, " return _unpack(Unpacker(bytes));").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " void _pack(Packer p) {{").unwrap();
writeln!(out, " p.packInt(value);").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name} _unpack(Unpacker u) {{").unwrap();
writeln!(
out,
" return values.firstWhere((e) => e.value == u.unpackInt()!);"
)
.unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name}? _unpackNullable(Unpacker u) {{").unwrap();
writeln!(out, " final v = u.unpackInt();").unwrap();
writeln!(
out,
" return v == null ? null : values.firstWhere((e) => e.value == v);"
)
.unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
}
fn generate_struct(
out: &mut String,
ident: &ast::Ident,
fields: &[(ast::Attributes, ast::Ident, ast::NullableFieldType)],
enum_names: &HashSet<&str>,
) {
let dart_name = case::to_pascal(ident.as_str());
writeln!(out, "class {dart_name} {{").unwrap();
for (_, field_ident, field_type) in fields {
let dart_field = case::to_camel(field_ident.as_str());
writeln!(out, " final {} {dart_field};", field_type_str(field_type)).unwrap();
}
writeln!(out).unwrap();
writeln!(out, " {dart_name}({{").unwrap();
for (_, field_ident, field_type) in fields {
let dart_field = case::to_camel(field_ident.as_str());
if field_type.nullable {
writeln!(out, " this.{dart_field},").unwrap();
} else {
writeln!(out, " required this.{dart_field},").unwrap();
}
}
writeln!(out, " }});").unwrap();
writeln!(out).unwrap();
writeln!(out, " Uint8List toBytes() {{").unwrap();
writeln!(out, " final p = Packer();").unwrap();
writeln!(out, " _pack(p);").unwrap();
writeln!(out, " return p.takeBytes();").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name} fromBytes(Uint8List bytes) {{").unwrap();
writeln!(out, " return _unpack(Unpacker(bytes));").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " void _pack(Packer p) {{").unwrap();
for (_, field_ident, field_type) in fields {
let dart_field = case::to_camel(field_ident.as_str());
generate_pack_field(out, &dart_field, field_type, " ", enum_names, 0);
}
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name} _unpack(Unpacker u) {{").unwrap();
for (_, field_ident, field_type) in fields {
let dart_field = case::to_camel(field_ident.as_str());
let expr = generate_unpack_expr(field_type, enum_names);
writeln!(out, " final {dart_field} = {expr};").unwrap();
}
writeln!(out, " return {dart_name}(").unwrap();
for (_, field_ident, _) in fields {
let dart_field = case::to_camel(field_ident.as_str());
writeln!(out, " {dart_field}: {dart_field},").unwrap();
}
writeln!(out, " );").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out).unwrap();
writeln!(out, " static {dart_name}? _unpackNullable(Unpacker u) {{").unwrap();
writeln!(out, " if (u.unpackBool() == null) return null;").unwrap();
writeln!(out, " return _unpack(u);").unwrap();
writeln!(out, " }}").unwrap();
writeln!(out, "}}").unwrap();
}
fn generate_pack_field(
out: &mut String,
expr: &str,
field_type: &ast::NullableFieldType,
indent: &str,
enum_names: &HashSet<&str>,
depth: usize,
) {
match field_type {
ast::NullableFieldType {
field_type: ast::FieldType::Builtin(bt),
nullable,
} => {
let method = builtin_pack_method(bt);
if *nullable {
writeln!(out, "{indent}if ({expr} != null) {{").unwrap();
writeln!(out, "{indent} p.{method}({expr}!);").unwrap();
writeln!(out, "{indent}}} else {{").unwrap();
writeln!(out, "{indent} p.packNull();").unwrap();
writeln!(out, "{indent}}}").unwrap();
} else {
writeln!(out, "{indent}p.{method}({expr});").unwrap();
}
}
ast::NullableFieldType {
field_type: ast::FieldType::UserDefined(ident),
nullable,
} => {
let is_enum = enum_names.contains(ident.as_str());
if *nullable {
writeln!(out, "{indent}if ({expr} != null) {{").unwrap();
if !is_enum {
writeln!(out, "{indent} p.packBool(true);").unwrap();
}
writeln!(out, "{indent} {expr}!._pack(p);").unwrap();
writeln!(out, "{indent}}} else {{").unwrap();
writeln!(out, "{indent} p.packNull();").unwrap();
writeln!(out, "{indent}}}").unwrap();
} else {
writeln!(out, "{indent}{expr}._pack(p);").unwrap();
}
}
ast::NullableFieldType {
field_type: ast::FieldType::Array(inner, _),
nullable,
} => {
let var = format!("e{depth}");
let src = if *nullable {
format!("{expr}!")
} else {
expr.to_string()
};
if *nullable {
writeln!(out, "{indent}if ({expr} != null) {{").unwrap();
writeln!(out, "{indent} p.packBool(true);").unwrap();
writeln!(out, "{indent} p.packListLength({src}.length);").unwrap();
writeln!(out, "{indent} for (final {var} in {src}) {{").unwrap();
generate_pack_field(
out,
&var,
inner,
&format!("{indent} "),
enum_names,
depth + 1,
);
writeln!(out, "{indent} }}").unwrap();
writeln!(out, "{indent}}} else {{").unwrap();
writeln!(out, "{indent} p.packNull();").unwrap();
writeln!(out, "{indent}}}").unwrap();
} else {
writeln!(out, "{indent}p.packListLength({expr}.length);").unwrap();
writeln!(out, "{indent}for (final {var} in {expr}) {{").unwrap();
generate_pack_field(
out,
&var,
inner,
&format!("{indent} "),
enum_names,
depth + 1,
);
writeln!(out, "{indent}}}").unwrap();
}
}
ast::NullableFieldType {
field_type: ast::FieldType::Map(key_type, value_type),
nullable,
} => {
let var = format!("e{depth}");
let key_method = builtin_pack_method(key_type);
let src = if *nullable {
format!("{expr}!")
} else {
expr.to_string()
};
if *nullable {
writeln!(out, "{indent}if ({expr} != null) {{").unwrap();
writeln!(out, "{indent} p.packBool(true);").unwrap();
writeln!(out, "{indent} p.packMapLength({src}.length);").unwrap();
writeln!(out, "{indent} for (final {var} in {src}.entries) {{").unwrap();
writeln!(out, "{indent} p.{key_method}({var}.key);").unwrap();
generate_pack_field(
out,
&format!("{var}.value"),
value_type,
&format!("{indent} "),
enum_names,
depth + 1,
);
writeln!(out, "{indent} }}").unwrap();
writeln!(out, "{indent}}} else {{").unwrap();
writeln!(out, "{indent} p.packNull();").unwrap();
writeln!(out, "{indent}}}").unwrap();
} else {
writeln!(out, "{indent}p.packMapLength({expr}.length);").unwrap();
writeln!(out, "{indent}for (final {var} in {expr}.entries) {{").unwrap();
writeln!(out, "{indent} p.{key_method}({var}.key);").unwrap();
generate_pack_field(
out,
&format!("{var}.value"),
value_type,
&format!("{indent} "),
enum_names,
depth + 1,
);
writeln!(out, "{indent}}}").unwrap();
}
}
}
}
fn generate_unpack_expr(field_type: &ast::NullableFieldType, enum_names: &HashSet<&str>) -> String {
match field_type {
ast::NullableFieldType {
field_type: ast::FieldType::Builtin(bt),
nullable,
} => {
let method = builtin_unpack_method(bt);
if *nullable {
format!("u.{method}()")
} else {
format!("u.{method}()!")
}
}
ast::NullableFieldType {
field_type: ast::FieldType::UserDefined(ident),
nullable,
} => {
let dart_name = case::to_pascal(ident.as_str());
if *nullable {
format!("{dart_name}._unpackNullable(u)")
} else {
format!("{dart_name}._unpack(u)")
}
}
ast::NullableFieldType {
field_type: ast::FieldType::Array(inner, _),
nullable,
} => {
let inner_expr = generate_unpack_expr(inner, enum_names);
let base = format!("List.generate(u.unpackListLength(), (_) => {inner_expr})");
if *nullable {
format!("u.unpackBool() == null ? null : {base}")
} else {
base
}
}
ast::NullableFieldType {
field_type: ast::FieldType::Map(key_type, value_type),
nullable,
} => {
let key_method = builtin_unpack_method(key_type);
let value_expr = generate_unpack_expr(value_type, enum_names);
let base = format!(
"Map.fromEntries(List.generate(u.unpackMapLength(), (_) => MapEntry(u.{key_method}()!, {value_expr})))"
);
if *nullable {
format!("u.unpackBool() == null ? null : {base}")
} else {
base
}
}
}
}
fn field_type_str(ft: &ast::NullableFieldType) -> String {
match ft {
ast::NullableFieldType {
field_type: ast::FieldType::Builtin(bt),
nullable,
} => {
let base = builtin_type_str(bt);
if *nullable { format!("{base}?") } else { base }
}
ast::NullableFieldType {
field_type: ast::FieldType::UserDefined(ident),
nullable,
} => {
let dart_name = case::to_pascal(ident.as_str());
if *nullable {
format!("{dart_name}?")
} else {
dart_name
}
}
ast::NullableFieldType {
field_type: ast::FieldType::Array(inner, _length),
nullable,
} => {
let inner_str = field_type_str(inner);
let base = format!("List<{inner_str}>");
if *nullable { format!("{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!("Map<{key_str}, {value_str}>");
if *nullable { format!("{base}?") } else { base }
}
}
}
fn builtin_type_str(bt: &ast::BuiltinType) -> String {
match bt {
ast::BuiltinType::Integer(_) => "int".to_string(),
ast::BuiltinType::Float(_) => "double".to_string(),
ast::BuiltinType::String => "String".to_string(),
ast::BuiltinType::Bool => "bool".to_string(),
}
}
fn builtin_pack_method(bt: &ast::BuiltinType) -> &'static str {
match bt {
ast::BuiltinType::Integer(_) => "packInt",
ast::BuiltinType::Float(_) => "packDouble",
ast::BuiltinType::String => "packString",
ast::BuiltinType::Bool => "packBool",
}
}
fn builtin_unpack_method(bt: &ast::BuiltinType) -> &'static str {
match bt {
ast::BuiltinType::Integer(_) => "unpackInt",
ast::BuiltinType::Float(_) => "unpackDouble",
ast::BuiltinType::String => "unpackString",
ast::BuiltinType::Bool => "unpackBool",
}
}
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(),
}
}