use std::fmt::Write as _;
use wesley_core::{OperationType, SchemaOperation, WesleyIR};
use wesley_emit_codec::{plan, CodecDef, CodecOp, FieldPlan, ScalarKind};
use crate::{
operation_scope_name, rust_field_name, rust_type_name, rust_variant_name, to_snake_case,
};
pub const DEFAULT_CODEC_IMPORT: &str = "crate::codec";
pub const LE_BINARY_GENERATOR_NAME: &str = "wesley-emit-rust:le-binary";
#[must_use]
pub fn emit_le_binary_rust(
ir: &WesleyIR,
operations: &[SchemaOperation],
codec_import_path: &str,
) -> String {
let mut out = String::new();
let _ = writeln!(out, "// @generated by Wesley. Do not edit.");
let _ = writeln!(
out,
"use {codec_import_path}::{{CodecError, Reader, Writer}};"
);
emit_runtime_port_contract(&mut out);
for def in plan(ir, operations) {
render_def(&mut out, &def);
}
out
}
fn emit_runtime_port_contract(out: &mut String) {
let _ = write!(
out,
"\n\
/// Runtime port contract expected by the generated LE-binary codecs.\n\
pub mod codec_port {{\n\
\x20 /// Error type constructible from a diagnostic message.\n\
\x20 pub trait CodecError {{\n\
\x20 /// Build a codec error from a human-readable message.\n\
\x20 fn new(message: String) -> Self;\n\
\x20 }}\n\
\n\
\x20 /// Writer primitives required by generated encoders.\n\
\x20 pub trait Writer {{\n\
\x20 /// Create an empty byte writer.\n\
\x20 fn new() -> Self\n\
\x20 where\n\
\x20 Self: Sized;\n\
\x20 /// Write an unsigned 32-bit integer in little-endian order.\n\
\x20 fn write_u32_le(&mut self, value: u32);\n\
\x20 /// Write a signed 32-bit integer in little-endian order.\n\
\x20 fn write_i32_le(&mut self, value: i32);\n\
\x20 /// Write a canonical 32-bit float in little-endian order.\n\
\x20 fn write_f32_le(&mut self, value: f32);\n\
\x20 /// Write a boolean tag byte.\n\
\x20 fn write_bool(&mut self, value: bool);\n\
\x20 /// Write a length-prefixed UTF-8 string.\n\
\x20 fn write_string(&mut self, value: &str);\n\
\x20 /// Write a nullable value with a presence tag.\n\
\x20 fn write_option<T, F>(&mut self, value: &Option<T>, write: F)\n\
\x20 where\n\
\x20 F: FnOnce(&mut Self, &T);\n\
\x20 /// Write a length-prefixed list.\n\
\x20 fn write_list<T, F>(&mut self, value: &[T], write: F)\n\
\x20 where\n\
\x20 F: FnMut(&mut Self, &T);\n\
\x20 /// Finish the writer and return its bytes.\n\
\x20 fn finish(self) -> Vec<u8>;\n\
\x20 }}\n\
\n\
\x20 /// Reader primitives required by generated decoders.\n\
\x20 pub trait Reader<'a> {{\n\
\x20 /// Create a reader over encoded bytes.\n\
\x20 fn new(bytes: &'a [u8]) -> Self\n\
\x20 where\n\
\x20 Self: Sized;\n\
\x20 /// Read an unsigned 32-bit little-endian integer.\n\
\x20 fn read_u32_le(&mut self) -> Result<u32, super::CodecError>;\n\
\x20 /// Read a signed 32-bit little-endian integer.\n\
\x20 fn read_i32_le(&mut self) -> Result<i32, super::CodecError>;\n\
\x20 /// Read a canonical 32-bit little-endian float.\n\
\x20 fn read_f32_le(&mut self) -> Result<f32, super::CodecError>;\n\
\x20 /// Read a boolean tag byte.\n\
\x20 fn read_bool(&mut self) -> Result<bool, super::CodecError>;\n\
\x20 /// Read a length-prefixed UTF-8 string.\n\
\x20 fn read_string(&mut self) -> Result<String, super::CodecError>;\n\
\x20 /// Read a nullable value with a presence tag.\n\
\x20 fn read_option<T, F>(&mut self, read: F) -> Result<Option<T>, super::CodecError>\n\
\x20 where\n\
\x20 F: FnOnce(&mut Self) -> Result<T, super::CodecError>;\n\
\x20 /// Read a length-prefixed list.\n\
\x20 fn read_list<T, F>(&mut self, read: F) -> Result<Vec<T>, super::CodecError>\n\
\x20 where\n\
\x20 F: FnMut(&mut Self) -> Result<T, super::CodecError>;\n\
\x20 /// Number of unread bytes remaining.\n\
\x20 fn remaining(&self) -> usize;\n\
\x20 }}\n\
}}\n"
);
}
fn render_def(out: &mut String, def: &CodecDef) {
match def {
CodecDef::Enum { name, variants } => {
render_enum(out, &rust_type_name(name), &to_snake_case(name), variants);
}
CodecDef::Struct { name, fields, .. } => {
render_struct(out, &rust_type_name(name), &to_snake_case(name), fields);
}
CodecDef::Operation {
operation_type,
field_name,
fields,
} => {
let ty = request_type_name(*operation_type, field_name);
let snake = to_snake_case(&ty);
render_struct(out, &ty, &snake, fields);
}
}
}
fn request_type_name(operation_type: OperationType, field_name: &str) -> String {
rust_type_name(&format!(
"{}{}Request",
operation_scope_name(operation_type),
rust_type_name(field_name)
))
}
fn emit_wrappers(out: &mut String, ty: &str, snake: &str) {
let _ = write!(
out,
"\n\
/// Encode a `{ty}` into LE-binary bytes.\n\
pub fn encode_{snake}(value: &{ty}) -> Vec<u8> {{\n\
\x20 let mut writer = Writer::new();\n\
\x20 enc_{snake}(&mut writer, value);\n\
\x20 writer.finish()\n\
}}\n\
\n\
/// Decode a `{ty}` from LE-binary bytes, rejecting trailing input.\n\
pub fn decode_{snake}(bytes: &[u8]) -> Result<{ty}, CodecError> {{\n\
\x20 let mut reader = Reader::new(bytes);\n\
\x20 let value = dec_{snake}(&mut reader)?;\n\
\x20 if reader.remaining() > 0 {{\n\
\x20 return Err(CodecError::new(\"trailing bytes after decode\".to_string()));\n\
\x20 }}\n\
\x20 Ok(value)\n\
}}\n"
);
}
fn render_enum(out: &mut String, ty: &str, snake: &str, variants: &[String]) {
emit_wrappers(out, ty, snake);
let _ = write!(
out,
"\nfn enc_{snake}(writer: &mut Writer, value: &{ty}) {{\n"
);
let _ = writeln!(out, " match value {{");
for (index, value) in variants.iter().enumerate() {
let variant = rust_variant_name(value);
let _ = writeln!(
out,
" {ty}::{variant} => writer.write_u32_le({index}),"
);
}
let _ = writeln!(out, " }}");
let _ = writeln!(out, "}}");
let _ = write!(
out,
"\nfn dec_{snake}(reader: &mut Reader) -> Result<{ty}, CodecError> {{\n"
);
let _ = writeln!(out, " let discriminant = reader.read_u32_le()?;");
let _ = writeln!(out, " match discriminant {{");
for (index, value) in variants.iter().enumerate() {
let variant = rust_variant_name(value);
let _ = writeln!(out, " {index} => Ok({ty}::{variant}),");
}
let _ = writeln!(
out,
" other => Err(CodecError::new(format!(\"invalid {ty} discriminant: {{other}}\"))),"
);
let _ = writeln!(out, " }}");
let _ = writeln!(out, "}}");
}
fn render_struct(out: &mut String, ty: &str, snake: &str, fields: &[FieldPlan]) {
emit_wrappers(out, ty, snake);
let _ = write!(
out,
"\nfn enc_{snake}(writer: &mut Writer, value: &{ty}) {{\n"
);
if fields.is_empty() {
let _ = writeln!(out, " let _ = (writer, value);");
}
for field in fields {
let access = format!("&value.{}", rust_field_name(&field.name));
let _ = writeln!(out, " {};", encode_op(&access, &field.op));
}
let _ = writeln!(out, "}}");
let _ = write!(
out,
"\nfn dec_{snake}(reader: &mut Reader) -> Result<{ty}, CodecError> {{\n"
);
if fields.is_empty() {
let _ = writeln!(out, " let _ = reader;");
let _ = writeln!(out, " Ok({ty} {{}})");
} else {
let _ = writeln!(out, " Ok({ty} {{");
for field in fields {
let name = rust_field_name(&field.name);
let _ = writeln!(out, " {name}: {}?,", decode_op(&field.op));
}
let _ = writeln!(out, " }})");
}
let _ = writeln!(out, "}}");
}
fn encode_op(expr: &str, op: &CodecOp) -> String {
match op {
CodecOp::Option(inner) => format!(
"writer.write_option({expr}, |writer, x| {})",
encode_op("x", inner)
),
CodecOp::List(inner) => format!(
"writer.write_list({expr}, |writer, x| {})",
encode_op("x", inner)
),
CodecOp::Scalar(ScalarKind::Bool) => format!("writer.write_bool({})", deref_copy(expr)),
CodecOp::Scalar(ScalarKind::Int) => format!("writer.write_i32_le({})", deref_copy(expr)),
CodecOp::Scalar(ScalarKind::Float) => format!("writer.write_f32_le({})", deref_copy(expr)),
CodecOp::Scalar(ScalarKind::String) => format!("writer.write_string({expr})"),
CodecOp::Named(name) => format!("enc_{}(writer, {expr})", to_snake_case(name)),
}
}
fn decode_op(op: &CodecOp) -> String {
match op {
CodecOp::Option(inner) => format!("reader.read_option(|reader| {})", decode_op(inner)),
CodecOp::List(inner) => format!("reader.read_list(|reader| {})", decode_op(inner)),
CodecOp::Scalar(ScalarKind::Bool) => "reader.read_bool()".to_string(),
CodecOp::Scalar(ScalarKind::Int) => "reader.read_i32_le()".to_string(),
CodecOp::Scalar(ScalarKind::Float) => "reader.read_f32_le()".to_string(),
CodecOp::Scalar(ScalarKind::String) => "reader.read_string()".to_string(),
CodecOp::Named(name) => format!("dec_{}(reader)", to_snake_case(name)),
}
}
fn deref_copy(expr: &str) -> String {
expr.strip_prefix('&')
.map_or_else(|| format!("*{expr}"), str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use wesley_core::{Field, TypeDefinition, TypeKind, TypeListWrapper, TypeReference};
fn scalar(base: &str, nullable: bool) -> TypeReference {
TypeReference {
base: base.to_string(),
nullable,
is_list: false,
list_item_nullable: None,
list_wrappers: Vec::new(),
leaf_nullable: None,
}
}
fn list_of(base: &str) -> TypeReference {
TypeReference {
base: base.to_string(),
nullable: false,
is_list: true,
list_item_nullable: Some(false),
list_wrappers: vec![TypeListWrapper { nullable: false }],
leaf_nullable: Some(false),
}
}
fn field(name: &str, ty: TypeReference) -> Field {
Field {
name: name.to_string(),
description: None,
r#type: ty,
arguments: Vec::new(),
default_value: None,
directives: Default::default(),
}
}
fn type_def(
name: &str,
kind: TypeKind,
fields: Vec<Field>,
enum_values: Vec<String>,
) -> TypeDefinition {
TypeDefinition {
name: name.to_string(),
kind,
description: None,
directives: Default::default(),
implements: Vec::new(),
fields,
enum_values,
union_members: Vec::new(),
}
}
fn emit(types: Vec<TypeDefinition>) -> String {
let ir = WesleyIR {
version: "1.0.0".to_string(),
metadata: None,
types,
};
emit_le_binary_rust(&ir, &[], DEFAULT_CODEC_IMPORT)
}
#[test]
fn emits_enum_codec_with_ordinal_discriminants() {
let color = type_def(
"Color",
TypeKind::Enum,
Vec::new(),
vec!["RED".into(), "GREEN".into(), "BLUE".into()],
);
let rust = emit(vec![color]);
assert!(rust.contains("use crate::codec::{CodecError, Reader, Writer};"));
assert!(rust.contains("pub mod codec_port {"));
assert!(rust.contains("pub trait Writer {"));
assert!(rust.contains("pub trait Reader<'a> {"));
assert!(rust.contains("fn remaining(&self) -> usize;"));
assert!(rust.contains("pub fn encode_color(value: &Color) -> Vec<u8> {"));
assert!(rust.contains("pub fn decode_color(bytes: &[u8]) -> Result<Color, CodecError> {"));
assert!(
rust.contains("Color::Red => writer.write_u32_le(0),"),
"{rust}"
);
assert!(
rust.contains("Color::Blue => writer.write_u32_le(2),"),
"{rust}"
);
assert!(rust.contains("let discriminant = reader.read_u32_le()?;"));
assert!(rust.contains("0 => Ok(Color::Red),"), "{rust}");
assert!(
rust.contains(
"other => Err(CodecError::new(format!(\"invalid Color discriminant: {other}\"))),"
),
"{rust}"
);
}
#[test]
fn emits_object_codec_with_required_nullable_and_list_fields() {
let widget = type_def(
"Widget",
TypeKind::Object,
vec![
field("label", scalar("String", false)),
field("count", scalar("Int", false)),
field("color", scalar("Color", true)),
field("tags", list_of("String")),
],
Vec::new(),
);
let rust = emit(vec![widget]);
assert!(
rust.contains("writer.write_string(&value.label);"),
"{rust}"
);
assert!(rust.contains("writer.write_i32_le(value.count);"), "{rust}");
assert!(
rust.contains("writer.write_option(&value.color, |writer, x| enc_color(writer, x));"),
"{rust}"
);
assert!(
rust.contains("writer.write_list(&value.tags, |writer, x| writer.write_string(x));"),
"{rust}"
);
assert!(rust.contains("label: reader.read_string()?,"), "{rust}");
assert!(rust.contains("count: reader.read_i32_le()?,"), "{rust}");
assert!(
rust.contains("color: reader.read_option(|reader| dec_color(reader))?,"),
"{rust}"
);
assert!(
rust.contains("tags: reader.read_list(|reader| reader.read_string())?,"),
"{rust}"
);
}
#[test]
fn decode_wrappers_reject_trailing_bytes() {
let color = type_def(
"Color",
TypeKind::Enum,
Vec::new(),
vec!["RED".into(), "GREEN".into()],
);
let rust = emit(vec![color]);
assert!(
rust.contains("let value = dec_color(&mut reader)?;"),
"{rust}"
);
assert!(rust.contains("if reader.remaining() > 0 {"), "{rust}");
assert!(
rust.contains(
"return Err(CodecError::new(\"trailing bytes after decode\".to_string()));"
),
"{rust}"
);
}
#[test]
fn runtime_port_contract_ties_reader_errors_to_imported_codec_error() {
let color = type_def(
"Color",
TypeKind::Enum,
Vec::new(),
vec!["RED".into(), "GREEN".into()],
);
let rust = emit(vec![color]);
assert!(
!rust.contains("type Error;"),
"reader port contract must not allow an arbitrary error type:\n{rust}"
);
assert!(
rust.contains("fn read_u32_le(&mut self) -> Result<u32, super::CodecError>;"),
"{rust}"
);
assert!(
rust.contains("F: FnOnce(&mut Self) -> Result<T, super::CodecError>;"),
"{rust}"
);
assert!(
rust.contains("F: FnMut(&mut Self) -> Result<T, super::CodecError>;"),
"{rust}"
);
}
#[test]
fn emits_le_binary_rust_from_golden_fixture() {
use wesley_core::{list_schema_operations_sdl, lower_schema_sdl};
let sdl = include_str!("../../../test/fixtures/typescript-emitter/le-binary-codec.graphql");
let expected =
include_str!("../../../test/fixtures/rust-emitter/le-binary-codec.generated.rs");
let ir = lower_schema_sdl(sdl).expect("golden schema lowers");
let ops = list_schema_operations_sdl(sdl).expect("golden operations enumerable");
assert_eq!(
emit_le_binary_rust(&ir, &ops, DEFAULT_CODEC_IMPORT),
expected
);
}
#[test]
fn skips_root_object_type_but_emits_its_operation_vars() {
let mutation = type_def("Mutation", TypeKind::Object, Vec::new(), Vec::new());
let ir = WesleyIR {
version: "1.0.0".to_string(),
metadata: None,
types: vec![mutation],
};
let operations = [make_operation("Mutation")];
let rust = emit_le_binary_rust(&ir, &operations, DEFAULT_CODEC_IMPORT);
assert!(!rust.contains("fn enc_mutation("), "{rust}");
assert!(
rust.contains("pub fn encode_mutation_noop_request("),
"{rust}"
);
}
fn make_operation(root: &str) -> SchemaOperation {
SchemaOperation {
operation_type: OperationType::Mutation,
root_type_name: root.to_string(),
field_name: "noop".to_string(),
arguments: Vec::new(),
result_type: scalar("Boolean", false),
directives: Default::default(),
}
}
}