Skip to main content

wesley_emit_rust/
le_binary.rs

1//! LE-binary codec emitter for Wesley L1 IR (Rust).
2//!
3//! This is the Rust backend of the shared codec plan
4//! ([`wesley_emit_codec::plan`]): it renders each [`CodecDef`] into `encode_*` /
5//! `decode_*` functions over a consumer-provided runtime (`Writer`, `Reader`,
6//! `CodecError`). The structural decisions (nullable → option, list →
7//! length-prefixed, scalar → primitive, named type → sub-codec) live in the
8//! plan, so this backend only spells them in Rust; the TypeScript backend spells
9//! the same plan, and the two cannot drift on the wire.
10//!
11//! The emitted code **references** the Wesley-generated Rust types (it does not
12//! redefine them), so place it in a module where those types — and the runtime
13//! `Writer` / `Reader` / `CodecError` — are in scope. Each `decode_*` rejects
14//! trailing input via `Reader::remaining()`, so a decoder cannot silently accept
15//! a padded or corrupted envelope.
16
17use std::fmt::Write as _;
18
19use wesley_core::{OperationType, SchemaOperation, WesleyIR};
20use wesley_emit_codec::{plan, CodecDef, CodecOp, FieldPlan, ScalarKind};
21
22use crate::{
23    operation_scope_name, rust_field_name, rust_type_name, rust_variant_name, to_snake_case,
24};
25
26/// The default Rust module path the emitted codec imports its runtime from.
27pub const DEFAULT_CODEC_IMPORT: &str = "crate::codec";
28
29/// Stable generator identifier recorded in native emit metadata for this codec.
30pub const LE_BINARY_GENERATOR_NAME: &str = "wesley-emit-rust:le-binary";
31
32/// Emit a Rust LE-binary codec for the data types in `ir`.
33///
34/// Enums, input objects, and output objects each get an `encode_*` / `decode_*`
35/// pair; operation root types (Query / Mutation / Subscription) are skipped as
36/// data types but their variables get a codec. The codec imports `Writer`,
37/// `Reader`, and `CodecError` from `codec_import_path`.
38#[must_use]
39pub fn emit_le_binary_rust(
40    ir: &WesleyIR,
41    operations: &[SchemaOperation],
42    codec_import_path: &str,
43) -> String {
44    let mut out = String::new();
45    let _ = writeln!(out, "// @generated by Wesley. Do not edit.");
46    let _ = writeln!(
47        out,
48        "use {codec_import_path}::{{CodecError, Reader, Writer}};"
49    );
50    emit_runtime_port_contract(&mut out);
51
52    for def in plan(ir, operations) {
53        render_def(&mut out, &def);
54    }
55
56    out
57}
58
59fn emit_runtime_port_contract(out: &mut String) {
60    let _ = write!(
61        out,
62        "\n\
63         /// Runtime port contract expected by the generated LE-binary codecs.\n\
64         pub mod codec_port {{\n\
65         \x20   /// Error type constructible from a diagnostic message.\n\
66         \x20   pub trait CodecError {{\n\
67         \x20       /// Build a codec error from a human-readable message.\n\
68         \x20       fn new(message: String) -> Self;\n\
69         \x20   }}\n\
70         \n\
71         \x20   /// Writer primitives required by generated encoders.\n\
72         \x20   pub trait Writer {{\n\
73         \x20       /// Create an empty byte writer.\n\
74         \x20       fn new() -> Self\n\
75         \x20       where\n\
76         \x20           Self: Sized;\n\
77         \x20       /// Write an unsigned 32-bit integer in little-endian order.\n\
78         \x20       fn write_u32_le(&mut self, value: u32);\n\
79         \x20       /// Write a signed 32-bit integer in little-endian order.\n\
80         \x20       fn write_i32_le(&mut self, value: i32);\n\
81         \x20       /// Write a canonical 32-bit float in little-endian order.\n\
82         \x20       fn write_f32_le(&mut self, value: f32);\n\
83         \x20       /// Write a boolean tag byte.\n\
84         \x20       fn write_bool(&mut self, value: bool);\n\
85         \x20       /// Write a length-prefixed UTF-8 string.\n\
86         \x20       fn write_string(&mut self, value: &str);\n\
87         \x20       /// Write a nullable value with a presence tag.\n\
88         \x20       fn write_option<T, F>(&mut self, value: &Option<T>, write: F)\n\
89         \x20       where\n\
90         \x20           F: FnOnce(&mut Self, &T);\n\
91         \x20       /// Write a length-prefixed list.\n\
92         \x20       fn write_list<T, F>(&mut self, value: &[T], write: F)\n\
93         \x20       where\n\
94         \x20           F: FnMut(&mut Self, &T);\n\
95         \x20       /// Finish the writer and return its bytes.\n\
96         \x20       fn finish(self) -> Vec<u8>;\n\
97         \x20   }}\n\
98         \n\
99         \x20   /// Reader primitives required by generated decoders.\n\
100         \x20   pub trait Reader<'a> {{\n\
101         \x20       /// Create a reader over encoded bytes.\n\
102         \x20       fn new(bytes: &'a [u8]) -> Self\n\
103         \x20       where\n\
104         \x20           Self: Sized;\n\
105         \x20       /// Read an unsigned 32-bit little-endian integer.\n\
106         \x20       fn read_u32_le(&mut self) -> Result<u32, super::CodecError>;\n\
107         \x20       /// Read a signed 32-bit little-endian integer.\n\
108         \x20       fn read_i32_le(&mut self) -> Result<i32, super::CodecError>;\n\
109         \x20       /// Read a canonical 32-bit little-endian float.\n\
110         \x20       fn read_f32_le(&mut self) -> Result<f32, super::CodecError>;\n\
111         \x20       /// Read a boolean tag byte.\n\
112         \x20       fn read_bool(&mut self) -> Result<bool, super::CodecError>;\n\
113         \x20       /// Read a length-prefixed UTF-8 string.\n\
114         \x20       fn read_string(&mut self) -> Result<String, super::CodecError>;\n\
115         \x20       /// Read a nullable value with a presence tag.\n\
116         \x20       fn read_option<T, F>(&mut self, read: F) -> Result<Option<T>, super::CodecError>\n\
117         \x20       where\n\
118         \x20           F: FnOnce(&mut Self) -> Result<T, super::CodecError>;\n\
119         \x20       /// Read a length-prefixed list.\n\
120         \x20       fn read_list<T, F>(&mut self, read: F) -> Result<Vec<T>, super::CodecError>\n\
121         \x20       where\n\
122         \x20           F: FnMut(&mut Self) -> Result<T, super::CodecError>;\n\
123         \x20       /// Number of unread bytes remaining.\n\
124         \x20       fn remaining(&self) -> usize;\n\
125         \x20   }}\n\
126         }}\n"
127    );
128}
129
130/// Render one codec definition into the output buffer.
131fn render_def(out: &mut String, def: &CodecDef) {
132    match def {
133        CodecDef::Enum { name, variants } => {
134            render_enum(out, &rust_type_name(name), &to_snake_case(name), variants);
135        }
136        CodecDef::Struct { name, fields, .. } => {
137            render_struct(out, &rust_type_name(name), &to_snake_case(name), fields);
138        }
139        CodecDef::Operation {
140            operation_type,
141            field_name,
142            fields,
143        } => {
144            let ty = request_type_name(*operation_type, field_name);
145            let snake = to_snake_case(&ty);
146            render_struct(out, &ty, &snake, fields);
147        }
148    }
149}
150
151/// The Rust name of an operation's variables struct (`<Operation><Field>Request`).
152fn request_type_name(operation_type: OperationType, field_name: &str) -> String {
153    rust_type_name(&format!(
154        "{}{}Request",
155        operation_scope_name(operation_type),
156        rust_type_name(field_name)
157    ))
158}
159
160/// Emit the public `encode_*` / `decode_*` wrappers shared by every type.
161fn emit_wrappers(out: &mut String, ty: &str, snake: &str) {
162    let _ = write!(
163        out,
164        "\n\
165         /// Encode a `{ty}` into LE-binary bytes.\n\
166         pub fn encode_{snake}(value: &{ty}) -> Vec<u8> {{\n\
167         \x20   let mut writer = Writer::new();\n\
168         \x20   enc_{snake}(&mut writer, value);\n\
169         \x20   writer.finish()\n\
170         }}\n\
171         \n\
172         /// Decode a `{ty}` from LE-binary bytes, rejecting trailing input.\n\
173         pub fn decode_{snake}(bytes: &[u8]) -> Result<{ty}, CodecError> {{\n\
174         \x20   let mut reader = Reader::new(bytes);\n\
175         \x20   let value = dec_{snake}(&mut reader)?;\n\
176         \x20   if reader.remaining() > 0 {{\n\
177         \x20       return Err(CodecError::new(\"trailing bytes after decode\".to_string()));\n\
178         \x20   }}\n\
179         \x20   Ok(value)\n\
180         }}\n"
181    );
182}
183
184fn render_enum(out: &mut String, ty: &str, snake: &str, variants: &[String]) {
185    emit_wrappers(out, ty, snake);
186
187    let _ = write!(
188        out,
189        "\nfn enc_{snake}(writer: &mut Writer, value: &{ty}) {{\n"
190    );
191    let _ = writeln!(out, "    match value {{");
192    for (index, value) in variants.iter().enumerate() {
193        let variant = rust_variant_name(value);
194        let _ = writeln!(
195            out,
196            "        {ty}::{variant} => writer.write_u32_le({index}),"
197        );
198    }
199    let _ = writeln!(out, "    }}");
200    let _ = writeln!(out, "}}");
201
202    let _ = write!(
203        out,
204        "\nfn dec_{snake}(reader: &mut Reader) -> Result<{ty}, CodecError> {{\n"
205    );
206    let _ = writeln!(out, "    let discriminant = reader.read_u32_le()?;");
207    let _ = writeln!(out, "    match discriminant {{");
208    for (index, value) in variants.iter().enumerate() {
209        let variant = rust_variant_name(value);
210        let _ = writeln!(out, "        {index} => Ok({ty}::{variant}),");
211    }
212    let _ = writeln!(
213        out,
214        "        other => Err(CodecError::new(format!(\"invalid {ty} discriminant: {{other}}\"))),"
215    );
216    let _ = writeln!(out, "    }}");
217    let _ = writeln!(out, "}}");
218}
219
220fn render_struct(out: &mut String, ty: &str, snake: &str, fields: &[FieldPlan]) {
221    emit_wrappers(out, ty, snake);
222
223    let _ = write!(
224        out,
225        "\nfn enc_{snake}(writer: &mut Writer, value: &{ty}) {{\n"
226    );
227    if fields.is_empty() {
228        let _ = writeln!(out, "    let _ = (writer, value);");
229    }
230    for field in fields {
231        let access = format!("&value.{}", rust_field_name(&field.name));
232        let _ = writeln!(out, "    {};", encode_op(&access, &field.op));
233    }
234    let _ = writeln!(out, "}}");
235
236    let _ = write!(
237        out,
238        "\nfn dec_{snake}(reader: &mut Reader) -> Result<{ty}, CodecError> {{\n"
239    );
240    if fields.is_empty() {
241        let _ = writeln!(out, "    let _ = reader;");
242        let _ = writeln!(out, "    Ok({ty} {{}})");
243    } else {
244        let _ = writeln!(out, "    Ok({ty} {{");
245        for field in fields {
246            let name = rust_field_name(&field.name);
247            let _ = writeln!(out, "        {name}: {}?,", decode_op(&field.op));
248        }
249        let _ = writeln!(out, "    }})");
250    }
251    let _ = writeln!(out, "}}");
252}
253
254/// Build the encode expression for a value reachable as the `&T` expression
255/// `expr`.
256fn encode_op(expr: &str, op: &CodecOp) -> String {
257    match op {
258        CodecOp::Option(inner) => format!(
259            "writer.write_option({expr}, |writer, x| {})",
260            encode_op("x", inner)
261        ),
262        CodecOp::List(inner) => format!(
263            "writer.write_list({expr}, |writer, x| {})",
264            encode_op("x", inner)
265        ),
266        CodecOp::Scalar(ScalarKind::Bool) => format!("writer.write_bool({})", deref_copy(expr)),
267        CodecOp::Scalar(ScalarKind::Int) => format!("writer.write_i32_le({})", deref_copy(expr)),
268        CodecOp::Scalar(ScalarKind::Float) => format!("writer.write_f32_le({})", deref_copy(expr)),
269        CodecOp::Scalar(ScalarKind::String) => format!("writer.write_string({expr})"),
270        CodecOp::Named(name) => format!("enc_{}(writer, {expr})", to_snake_case(name)),
271    }
272}
273
274/// Build the decode expression (of type `Result<_, CodecError>`) for `op`.
275fn decode_op(op: &CodecOp) -> String {
276    match op {
277        CodecOp::Option(inner) => format!("reader.read_option(|reader| {})", decode_op(inner)),
278        CodecOp::List(inner) => format!("reader.read_list(|reader| {})", decode_op(inner)),
279        CodecOp::Scalar(ScalarKind::Bool) => "reader.read_bool()".to_string(),
280        CodecOp::Scalar(ScalarKind::Int) => "reader.read_i32_le()".to_string(),
281        CodecOp::Scalar(ScalarKind::Float) => "reader.read_f32_le()".to_string(),
282        CodecOp::Scalar(ScalarKind::String) => "reader.read_string()".to_string(),
283        CodecOp::Named(name) => format!("dec_{}(reader)", to_snake_case(name)),
284    }
285}
286
287/// Turn a `&T` expression into an owned `T` expression for a `Copy` scalar: drop
288/// a leading borrow (`&value.x` -> `value.x`) or dereference (`x` -> `*x`).
289fn deref_copy(expr: &str) -> String {
290    expr.strip_prefix('&')
291        .map_or_else(|| format!("*{expr}"), str::to_string)
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use wesley_core::{Field, TypeDefinition, TypeKind, TypeListWrapper, TypeReference};
298
299    fn scalar(base: &str, nullable: bool) -> TypeReference {
300        TypeReference {
301            base: base.to_string(),
302            nullable,
303            is_list: false,
304            list_item_nullable: None,
305            list_wrappers: Vec::new(),
306            leaf_nullable: None,
307        }
308    }
309
310    /// A non-null list of non-null `base` (`[base!]!`).
311    fn list_of(base: &str) -> TypeReference {
312        TypeReference {
313            base: base.to_string(),
314            nullable: false,
315            is_list: true,
316            list_item_nullable: Some(false),
317            list_wrappers: vec![TypeListWrapper { nullable: false }],
318            leaf_nullable: Some(false),
319        }
320    }
321
322    fn field(name: &str, ty: TypeReference) -> Field {
323        Field {
324            name: name.to_string(),
325            description: None,
326            r#type: ty,
327            arguments: Vec::new(),
328            default_value: None,
329            directives: Default::default(),
330        }
331    }
332
333    fn type_def(
334        name: &str,
335        kind: TypeKind,
336        fields: Vec<Field>,
337        enum_values: Vec<String>,
338    ) -> TypeDefinition {
339        TypeDefinition {
340            name: name.to_string(),
341            kind,
342            description: None,
343            directives: Default::default(),
344            implements: Vec::new(),
345            fields,
346            enum_values,
347            union_members: Vec::new(),
348        }
349    }
350
351    fn emit(types: Vec<TypeDefinition>) -> String {
352        let ir = WesleyIR {
353            version: "1.0.0".to_string(),
354            metadata: None,
355            types,
356        };
357        emit_le_binary_rust(&ir, &[], DEFAULT_CODEC_IMPORT)
358    }
359
360    #[test]
361    fn emits_enum_codec_with_ordinal_discriminants() {
362        let color = type_def(
363            "Color",
364            TypeKind::Enum,
365            Vec::new(),
366            vec!["RED".into(), "GREEN".into(), "BLUE".into()],
367        );
368        let rust = emit(vec![color]);
369
370        assert!(rust.contains("use crate::codec::{CodecError, Reader, Writer};"));
371        assert!(rust.contains("pub mod codec_port {"));
372        assert!(rust.contains("pub trait Writer {"));
373        assert!(rust.contains("pub trait Reader<'a> {"));
374        assert!(rust.contains("fn remaining(&self) -> usize;"));
375        assert!(rust.contains("pub fn encode_color(value: &Color) -> Vec<u8> {"));
376        assert!(rust.contains("pub fn decode_color(bytes: &[u8]) -> Result<Color, CodecError> {"));
377        assert!(
378            rust.contains("Color::Red => writer.write_u32_le(0),"),
379            "{rust}"
380        );
381        assert!(
382            rust.contains("Color::Blue => writer.write_u32_le(2),"),
383            "{rust}"
384        );
385        assert!(rust.contains("let discriminant = reader.read_u32_le()?;"));
386        assert!(rust.contains("0 => Ok(Color::Red),"), "{rust}");
387        assert!(
388            rust.contains(
389                "other => Err(CodecError::new(format!(\"invalid Color discriminant: {other}\"))),"
390            ),
391            "{rust}"
392        );
393    }
394
395    #[test]
396    fn emits_object_codec_with_required_nullable_and_list_fields() {
397        let widget = type_def(
398            "Widget",
399            TypeKind::Object,
400            vec![
401                field("label", scalar("String", false)),
402                field("count", scalar("Int", false)),
403                field("color", scalar("Color", true)),
404                field("tags", list_of("String")),
405            ],
406            Vec::new(),
407        );
408        let rust = emit(vec![widget]);
409
410        // required String -> direct write_string with a borrow
411        assert!(
412            rust.contains("writer.write_string(&value.label);"),
413            "{rust}"
414        );
415        // required Int (Copy) -> no stray `*&`
416        assert!(rust.contains("writer.write_i32_le(value.count);"), "{rust}");
417        // nullable enum -> write_option wrapping the enum encoder
418        assert!(
419            rust.contains("writer.write_option(&value.color, |writer, x| enc_color(writer, x));"),
420            "{rust}"
421        );
422        // required list of String! -> write_list, element borrowed
423        assert!(
424            rust.contains("writer.write_list(&value.tags, |writer, x| writer.write_string(x));"),
425            "{rust}"
426        );
427        // decode builds the struct with `?` per field
428        assert!(rust.contains("label: reader.read_string()?,"), "{rust}");
429        assert!(rust.contains("count: reader.read_i32_le()?,"), "{rust}");
430        assert!(
431            rust.contains("color: reader.read_option(|reader| dec_color(reader))?,"),
432            "{rust}"
433        );
434        assert!(
435            rust.contains("tags: reader.read_list(|reader| reader.read_string())?,"),
436            "{rust}"
437        );
438    }
439
440    #[test]
441    fn decode_wrappers_reject_trailing_bytes() {
442        let color = type_def(
443            "Color",
444            TypeKind::Enum,
445            Vec::new(),
446            vec!["RED".into(), "GREEN".into()],
447        );
448        let rust = emit(vec![color]);
449        assert!(
450            rust.contains("let value = dec_color(&mut reader)?;"),
451            "{rust}"
452        );
453        assert!(rust.contains("if reader.remaining() > 0 {"), "{rust}");
454        assert!(
455            rust.contains(
456                "return Err(CodecError::new(\"trailing bytes after decode\".to_string()));"
457            ),
458            "{rust}"
459        );
460    }
461
462    #[test]
463    fn runtime_port_contract_ties_reader_errors_to_imported_codec_error() {
464        let color = type_def(
465            "Color",
466            TypeKind::Enum,
467            Vec::new(),
468            vec!["RED".into(), "GREEN".into()],
469        );
470        let rust = emit(vec![color]);
471
472        assert!(
473            !rust.contains("type Error;"),
474            "reader port contract must not allow an arbitrary error type:\n{rust}"
475        );
476        assert!(
477            rust.contains("fn read_u32_le(&mut self) -> Result<u32, super::CodecError>;"),
478            "{rust}"
479        );
480        assert!(
481            rust.contains("F: FnOnce(&mut Self) -> Result<T, super::CodecError>;"),
482            "{rust}"
483        );
484        assert!(
485            rust.contains("F: FnMut(&mut Self) -> Result<T, super::CodecError>;"),
486            "{rust}"
487        );
488    }
489
490    #[test]
491    fn emits_le_binary_rust_from_golden_fixture() {
492        use wesley_core::{list_schema_operations_sdl, lower_schema_sdl};
493        let sdl = include_str!("../../../test/fixtures/typescript-emitter/le-binary-codec.graphql");
494        let expected =
495            include_str!("../../../test/fixtures/rust-emitter/le-binary-codec.generated.rs");
496        let ir = lower_schema_sdl(sdl).expect("golden schema lowers");
497        let ops = list_schema_operations_sdl(sdl).expect("golden operations enumerable");
498
499        assert_eq!(
500            emit_le_binary_rust(&ir, &ops, DEFAULT_CODEC_IMPORT),
501            expected
502        );
503    }
504
505    #[test]
506    fn skips_root_object_type_but_emits_its_operation_vars() {
507        let mutation = type_def("Mutation", TypeKind::Object, Vec::new(), Vec::new());
508        let ir = WesleyIR {
509            version: "1.0.0".to_string(),
510            metadata: None,
511            types: vec![mutation],
512        };
513        let operations = [make_operation("Mutation")];
514        let rust = emit_le_binary_rust(&ir, &operations, DEFAULT_CODEC_IMPORT);
515        // The Mutation *root object type* gets no data codec...
516        assert!(!rust.contains("fn enc_mutation("), "{rust}");
517        // ...but its operation's variables (the generated Request struct) do.
518        assert!(
519            rust.contains("pub fn encode_mutation_noop_request("),
520            "{rust}"
521        );
522    }
523
524    /// A minimal `SchemaOperation` whose root type is `root`.
525    fn make_operation(root: &str) -> SchemaOperation {
526        SchemaOperation {
527            operation_type: OperationType::Mutation,
528            root_type_name: root.to_string(),
529            field_name: "noop".to_string(),
530            arguments: Vec::new(),
531            result_type: scalar("Boolean", false),
532            directives: Default::default(),
533        }
534    }
535}