1use 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
26pub const DEFAULT_CODEC_IMPORT: &str = "crate::codec";
28
29pub const LE_BINARY_GENERATOR_NAME: &str = "wesley-emit-rust:le-binary";
31
32#[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
130fn 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
151fn 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
160fn 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
254fn 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
274fn 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
287fn 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 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 assert!(
412 rust.contains("writer.write_string(&value.label);"),
413 "{rust}"
414 );
415 assert!(rust.contains("writer.write_i32_le(value.count);"), "{rust}");
417 assert!(
419 rust.contains("writer.write_option(&value.color, |writer, x| enc_color(writer, x));"),
420 "{rust}"
421 );
422 assert!(
424 rust.contains("writer.write_list(&value.tags, |writer, x| writer.write_string(x));"),
425 "{rust}"
426 );
427 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 assert!(!rust.contains("fn enc_mutation("), "{rust}");
517 assert!(
519 rust.contains("pub fn encode_mutation_noop_request("),
520 "{rust}"
521 );
522 }
523
524 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}