1#![deny(warnings)]
2#![deny(missing_docs)]
3
4use std::collections::BTreeSet;
7use std::fmt::Write;
8use wesley_core::{
9 Field, OperationArgument, OperationType, SchemaOperation, TypeDefinition, TypeKind,
10 TypeReference, WesleyIR,
11};
12
13pub fn emit_rust(ir: &WesleyIR) -> String {
15 let file = RustFile::from_ir(ir);
16
17 print_file(&file)
18}
19
20pub fn emit_rust_with_operations(ir: &WesleyIR, operations: &[SchemaOperation]) -> String {
22 let file = RustFile::from_ir_and_operations(ir, operations);
23
24 print_file(&file)
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28struct RustFile {
29 items: Vec<RustItem>,
30}
31
32impl RustFile {
33 fn from_ir(ir: &WesleyIR) -> Self {
34 Self::from_ir_and_operations(ir, &[])
35 }
36
37 fn from_ir_and_operations(ir: &WesleyIR, operations: &[SchemaOperation]) -> Self {
38 let mut items = Vec::new();
39 let root_type_names = operations
40 .iter()
41 .map(|operation| operation.root_type_name.as_str())
42 .collect::<BTreeSet<_>>();
43
44 for type_def in &ir.types {
45 if root_type_names.contains(type_def.name.as_str()) {
46 continue;
47 }
48
49 match type_def.kind {
50 TypeKind::Scalar if is_builtin_scalar(&type_def.name) => {}
51 TypeKind::Scalar => items.push(RustItem::TypeAlias(RustTypeAlias {
52 name: rust_type_name(&type_def.name),
53 target: RustType::String,
54 })),
55 TypeKind::Object | TypeKind::Interface | TypeKind::InputObject => {
56 items.push(RustItem::Struct(struct_from_type(type_def)));
57 }
58 TypeKind::Enum => items.push(RustItem::Enum(enum_from_type(type_def))),
59 TypeKind::Union => items.push(RustItem::Enum(union_enum_from_type(type_def))),
60 }
61 }
62
63 items.extend(
64 operations
65 .iter()
66 .map(|operation| RustItem::Operation(operation_binding_from_schema(operation))),
67 );
68
69 Self { items }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74enum RustItem {
75 TypeAlias(RustTypeAlias),
76 Struct(RustStruct),
77 Enum(RustEnum),
78 Operation(RustOperationBinding),
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82struct RustTypeAlias {
83 name: String,
84 target: RustType,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
88struct RustStruct {
89 name: String,
90 fields: Vec<RustField>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94struct RustField {
95 source_name: String,
96 rust_name: String,
97 ty: RustType,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101struct RustEnum {
102 name: String,
103 derive_eq: bool,
104 variants: Vec<RustVariant>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108struct RustOperationBinding {
109 operation_type: &'static str,
110 field_name: String,
111 request: RustStruct,
112 response_alias: RustTypeAlias,
113 directives_json: String,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117struct RustVariant {
118 source_name: String,
119 rust_name: String,
120 payload: Option<RustType>,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124enum RustType {
125 String,
126 I32,
127 F64,
128 Bool,
129 Named(String),
130 Vec(Box<RustType>),
131 Option(Box<RustType>),
132}
133
134fn struct_from_type(type_def: &TypeDefinition) -> RustStruct {
135 RustStruct {
136 name: rust_type_name(&type_def.name),
137 fields: type_def.fields.iter().map(field_from_ir).collect(),
138 }
139}
140
141fn field_from_ir(field: &Field) -> RustField {
142 RustField {
143 source_name: field.name.clone(),
144 rust_name: rust_field_name(&field.name),
145 ty: rust_type_from_reference(&field.r#type),
146 }
147}
148
149fn enum_from_type(type_def: &TypeDefinition) -> RustEnum {
150 RustEnum {
151 name: rust_type_name(&type_def.name),
152 derive_eq: true,
153 variants: type_def
154 .enum_values
155 .iter()
156 .map(|value| RustVariant {
157 source_name: value.clone(),
158 rust_name: rust_variant_name(value),
159 payload: None,
160 })
161 .collect(),
162 }
163}
164
165fn union_enum_from_type(type_def: &TypeDefinition) -> RustEnum {
166 RustEnum {
167 name: rust_type_name(&type_def.name),
168 derive_eq: false,
169 variants: type_def
170 .union_members
171 .iter()
172 .map(|member| RustVariant {
173 source_name: member.clone(),
174 rust_name: rust_variant_name(member),
175 payload: Some(RustType::Named(rust_type_name(member))),
176 })
177 .collect(),
178 }
179}
180
181fn operation_binding_from_schema(operation: &SchemaOperation) -> RustOperationBinding {
182 let request_type_name = operation_type_name(operation, "Request");
183 let response_type_name = operation_type_name(operation, "Response");
184
185 RustOperationBinding {
186 operation_type: operation_type_literal(operation.operation_type),
187 field_name: operation.field_name.clone(),
188 request: RustStruct {
189 name: request_type_name,
190 fields: operation
191 .arguments
192 .iter()
193 .map(field_from_operation_argument)
194 .collect(),
195 },
196 response_alias: RustTypeAlias {
197 name: response_type_name,
198 target: rust_type_from_reference(&operation.result_type),
199 },
200 directives_json: serde_json::to_string(&operation.directives)
201 .expect("schema operation directives should serialize"),
202 }
203}
204
205fn field_from_operation_argument(argument: &OperationArgument) -> RustField {
206 let mut ty = rust_type_from_reference(&argument.r#type);
207 if argument.default_value.is_some() && !matches!(ty, RustType::Option(_)) {
208 ty = RustType::Option(Box::new(ty));
209 }
210
211 RustField {
212 source_name: argument.name.clone(),
213 rust_name: rust_field_name(&argument.name),
214 ty,
215 }
216}
217
218fn rust_type_from_reference(type_ref: &TypeReference) -> RustType {
219 let base = match type_ref.base.as_str() {
220 "ID" | "String" => RustType::String,
221 "Int" => RustType::I32,
222 "Float" => RustType::F64,
223 "Boolean" => RustType::Bool,
224 name => RustType::Named(rust_type_name(name)),
225 };
226
227 if !type_ref.list_wrappers.is_empty() {
228 let mut ty = if type_ref.leaf_nullable.unwrap_or(true) {
229 RustType::Option(Box::new(base))
230 } else {
231 base
232 };
233
234 for wrapper in type_ref.list_wrappers.iter().rev() {
235 ty = RustType::Vec(Box::new(ty));
236 if wrapper.nullable {
237 ty = RustType::Option(Box::new(ty));
238 }
239 }
240
241 return ty;
242 }
243
244 let mut ty = if type_ref.is_list {
245 let item = match type_ref.list_item_nullable {
246 Some(true) | None => RustType::Option(Box::new(base)),
247 Some(false) => base,
248 };
249 RustType::Vec(Box::new(item))
250 } else {
251 base
252 };
253
254 if type_ref.nullable {
255 ty = RustType::Option(Box::new(ty));
256 }
257
258 ty
259}
260
261fn print_file(file: &RustFile) -> String {
262 let mut out = String::from("// @generated by Wesley. Do not edit.\n");
263
264 for item in &file.items {
265 out.push('\n');
266 print_item(&mut out, item);
267 }
268
269 out
270}
271
272fn print_item(out: &mut String, item: &RustItem) {
273 match item {
274 RustItem::TypeAlias(alias) => print_type_alias(out, alias),
275 RustItem::Struct(struct_item) => print_struct(out, struct_item),
276 RustItem::Enum(enum_item) => print_enum(out, enum_item),
277 RustItem::Operation(operation) => print_operation_binding(out, operation),
278 }
279}
280
281fn print_type_alias(out: &mut String, alias: &RustTypeAlias) {
282 write!(out, "pub type {} = ", alias.name).expect("writing to string should not fail");
283 print_type(out, &alias.target);
284 out.push_str(";\n");
285}
286
287fn print_struct(out: &mut String, struct_item: &RustStruct) {
288 out.push_str("#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n");
289 writeln!(out, "pub struct {} {{", struct_item.name).expect("writing to string should not fail");
290
291 for field in &struct_item.fields {
292 if field.source_name != field.rust_name.trim_start_matches("r#") {
293 writeln!(
294 out,
295 " #[serde(rename = \"{}\")]",
296 escape_attribute(&field.source_name)
297 )
298 .expect("writing to string should not fail");
299 }
300 write!(out, " pub {}: ", field.rust_name).expect("writing to string should not fail");
301 print_type(out, &field.ty);
302 out.push_str(",\n");
303 }
304
305 out.push_str("}\n");
306}
307
308fn print_enum(out: &mut String, enum_item: &RustEnum) {
309 if enum_item.derive_eq {
310 out.push_str(
311 "#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
312 );
313 } else {
314 out.push_str("#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\n");
315 }
316 writeln!(out, "pub enum {} {{", enum_item.name).expect("writing to string should not fail");
317
318 for variant in &enum_item.variants {
319 writeln!(
320 out,
321 " #[serde(rename = \"{}\")]",
322 escape_attribute(&variant.source_name)
323 )
324 .expect("writing to string should not fail");
325 write!(out, " {}", variant.rust_name).expect("writing to string should not fail");
326 if let Some(payload) = &variant.payload {
327 out.push('(');
328 print_type(out, payload);
329 out.push(')');
330 }
331 out.push_str(",\n");
332 }
333
334 out.push_str("}\n");
335}
336
337fn print_operation_binding(out: &mut String, operation: &RustOperationBinding) {
338 print_struct(out, &operation.request);
339 out.push('\n');
340 print_type_alias(out, &operation.response_alias);
341 out.push('\n');
342 writeln!(out, "impl {} {{", operation.request.name).expect("writing to string should not fail");
343 write!(out, " pub const OPERATION_TYPE: &'static str = ")
344 .expect("writing to string should not fail");
345 print_rust_string_literal(out, operation.operation_type);
346 out.push_str(";\n");
347 write!(out, " pub const FIELD_NAME: &'static str = ")
348 .expect("writing to string should not fail");
349 print_rust_string_literal(out, &operation.field_name);
350 out.push_str(";\n");
351 write!(out, " pub const DIRECTIVES_JSON: &'static str = ")
352 .expect("writing to string should not fail");
353 print_rust_string_literal(out, &operation.directives_json);
354 out.push_str(";\n");
355 out.push_str("}\n");
356}
357
358fn print_type(out: &mut String, ty: &RustType) {
359 match ty {
360 RustType::String => out.push_str("String"),
361 RustType::I32 => out.push_str("i32"),
362 RustType::F64 => out.push_str("f64"),
363 RustType::Bool => out.push_str("bool"),
364 RustType::Named(name) => out.push_str(name),
365 RustType::Vec(item) => {
366 out.push_str("Vec<");
367 print_type(out, item);
368 out.push('>');
369 }
370 RustType::Option(item) => {
371 out.push_str("Option<");
372 print_type(out, item);
373 out.push('>');
374 }
375 }
376}
377
378fn operation_type_literal(operation_type: OperationType) -> &'static str {
379 match operation_type {
380 OperationType::Query => "QUERY",
381 OperationType::Mutation => "MUTATION",
382 OperationType::Subscription => "SUBSCRIPTION",
383 }
384}
385
386fn is_builtin_scalar(name: &str) -> bool {
387 matches!(name, "ID" | "String" | "Int" | "Float" | "Boolean")
388}
389
390fn rust_type_name(name: &str) -> String {
391 let mut candidate = sanitize_pascal_identifier(name);
392 if candidate.is_empty() {
393 candidate.push_str("GeneratedType");
394 }
395 if candidate
396 .chars()
397 .next()
398 .is_some_and(|ch| ch.is_ascii_digit())
399 {
400 candidate.insert(0, '_');
401 }
402 if is_reserved_word(&candidate) {
403 candidate.push_str("Type");
404 }
405
406 candidate
407}
408
409fn operation_type_name(operation: &SchemaOperation, suffix: &str) -> String {
410 rust_type_name(&format!(
411 "{}{}{suffix}",
412 operation_scope_name(operation.operation_type),
413 rust_type_name(&operation.field_name)
414 ))
415}
416
417fn operation_scope_name(operation_type: OperationType) -> &'static str {
418 match operation_type {
419 OperationType::Query => "Query",
420 OperationType::Mutation => "Mutation",
421 OperationType::Subscription => "Subscription",
422 }
423}
424
425fn rust_variant_name(name: &str) -> String {
426 let mut candidate = if name.contains('_') || name.chars().all(|ch| !ch.is_ascii_lowercase()) {
427 to_pascal_case(name)
428 } else {
429 sanitize_pascal_identifier(name)
430 };
431 if candidate.is_empty() {
432 candidate.push_str("GeneratedVariant");
433 }
434 if candidate
435 .chars()
436 .next()
437 .is_some_and(|ch| ch.is_ascii_digit())
438 {
439 candidate.insert(0, '_');
440 }
441 if is_reserved_word(&candidate) {
442 candidate.push_str("Variant");
443 }
444
445 candidate
446}
447
448fn rust_field_name(name: &str) -> String {
449 let mut candidate = to_snake_case(name);
450 if candidate.is_empty() {
451 candidate.push_str("generated_field");
452 }
453 if candidate
454 .chars()
455 .next()
456 .is_some_and(|ch| ch.is_ascii_digit())
457 {
458 candidate.insert(0, '_');
459 }
460 if is_reserved_word(&candidate) {
461 candidate.insert_str(0, "r#");
462 }
463
464 candidate
465}
466
467fn to_pascal_case(name: &str) -> String {
468 let mut out = String::new();
469 let mut uppercase_next = true;
470
471 for ch in name.chars() {
472 if ch.is_ascii_alphanumeric() {
473 if uppercase_next {
474 out.push(ch.to_ascii_uppercase());
475 uppercase_next = false;
476 } else {
477 out.push(ch.to_ascii_lowercase());
478 }
479 } else {
480 uppercase_next = true;
481 }
482 }
483
484 out
485}
486
487fn sanitize_pascal_identifier(name: &str) -> String {
488 let mut out = String::new();
489
490 for ch in name.chars() {
491 if ch.is_ascii_alphanumeric() || ch == '_' {
492 out.push(ch);
493 } else if !out.ends_with('_') {
494 out.push('_');
495 }
496 }
497
498 let mut chars = out.chars();
499 let Some(first) = chars.next() else {
500 return String::new();
501 };
502
503 if first.is_ascii_alphabetic() || first == '_' {
504 let mut sanitized = String::new();
505 sanitized.push(first.to_ascii_uppercase());
506 sanitized.extend(chars);
507 sanitized.trim_matches('_').to_string()
508 } else {
509 format!("_{}", out.trim_matches('_'))
510 }
511}
512
513fn to_snake_case(name: &str) -> String {
514 let mut out = String::new();
515
516 for (index, ch) in name.chars().enumerate() {
517 if ch.is_ascii_uppercase() {
518 if index > 0 && !out.ends_with('_') {
519 out.push('_');
520 }
521 out.push(ch.to_ascii_lowercase());
522 } else if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
523 out.push(ch);
524 } else if !out.ends_with('_') {
525 out.push('_');
526 }
527 }
528
529 out.trim_matches('_').to_string()
530}
531
532fn is_reserved_word(name: &str) -> bool {
533 matches!(
534 name,
535 "Self"
536 | "abstract"
537 | "as"
538 | "async"
539 | "await"
540 | "become"
541 | "box"
542 | "break"
543 | "const"
544 | "continue"
545 | "crate"
546 | "do"
547 | "dyn"
548 | "else"
549 | "enum"
550 | "extern"
551 | "false"
552 | "final"
553 | "fn"
554 | "for"
555 | "if"
556 | "impl"
557 | "in"
558 | "let"
559 | "loop"
560 | "macro"
561 | "match"
562 | "mod"
563 | "move"
564 | "mut"
565 | "override"
566 | "priv"
567 | "pub"
568 | "ref"
569 | "return"
570 | "self"
571 | "static"
572 | "struct"
573 | "super"
574 | "trait"
575 | "true"
576 | "try"
577 | "type"
578 | "typeof"
579 | "union"
580 | "unsafe"
581 | "unsized"
582 | "use"
583 | "virtual"
584 | "where"
585 | "while"
586 | "yield"
587 )
588}
589
590fn escape_attribute(value: &str) -> String {
591 value.replace('\\', "\\\\").replace('"', "\\\"")
592}
593
594fn print_rust_string_literal(out: &mut String, value: &str) {
595 out.push('"');
596 for ch in value.chars() {
597 match ch {
598 '\\' => out.push_str("\\\\"),
599 '"' => out.push_str("\\\""),
600 '\n' => out.push_str("\\n"),
601 '\r' => out.push_str("\\r"),
602 '\t' => out.push_str("\\t"),
603 _ => out.push(ch),
604 }
605 }
606 out.push('"');
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612 use pretty_assertions::assert_eq;
613 use wesley_core::{list_schema_operations_sdl, lower_schema_sdl};
614
615 #[test]
616 fn emits_rust_models_from_l1_ir() {
617 let ir = lower_schema_sdl(
618 r#"
619 scalar DateTime
620
621 enum Role {
622 ADMIN
623 READ_ONLY
624 }
625
626 type User {
627 id: ID!
628 displayName: String
629 createdAt: DateTime!
630 tags: [String]
631 }
632
633 input UserFilter {
634 role: Role
635 limit: Int!
636 }
637 "#,
638 )
639 .expect("schema should lower");
640
641 let actual = emit_rust(&ir);
642
643 syn::parse_file(&actual).expect("generated Rust should parse");
644 assert_eq!(
645 actual,
646 r#"// @generated by Wesley. Do not edit.
647
648pub type DateTime = String;
649
650#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
651pub enum Role {
652 #[serde(rename = "ADMIN")]
653 Admin,
654 #[serde(rename = "READ_ONLY")]
655 ReadOnly,
656}
657
658#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
659pub struct User {
660 pub id: String,
661 #[serde(rename = "displayName")]
662 pub display_name: Option<String>,
663 #[serde(rename = "createdAt")]
664 pub created_at: DateTime,
665 pub tags: Option<Vec<Option<String>>>,
666}
667
668#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
669pub struct UserFilter {
670 pub role: Option<Role>,
671 pub limit: i32,
672}
673"#
674 );
675 }
676
677 #[test]
678 fn emits_nested_graphql_lists_as_nested_rust_vectors() {
679 let ir = lower_schema_sdl(
680 r#"
681 type Matrix {
682 values: [[Int!]!]!
683 maybeValues: [[String]]
684 }
685 "#,
686 )
687 .expect("schema should lower");
688
689 let actual = emit_rust(&ir);
690
691 syn::parse_file(&actual).expect("generated Rust should parse");
692 assert!(actual.contains("pub values: Vec<Vec<i32>>,"));
693 assert!(actual.contains(
694 "#[serde(rename = \"maybeValues\")]\n pub maybe_values: Option<Vec<Option<Vec<Option<String>>>>>,"
695 ));
696 }
697
698 #[test]
699 fn emits_jedit_shaped_hot_text_fixture() {
700 let ir = lower_schema_sdl(include_str!(
701 "../../../test/fixtures/consumer-models/jedit-hot-text-core.graphql"
702 ))
703 .expect("jedit-shaped fixture should lower");
704
705 let actual = emit_rust(&ir);
706
707 syn::parse_file(&actual).expect("generated Rust should parse");
708 assert!(actual.contains("pub struct BufferWorldline {"));
709 assert!(actual.contains("pub enum AnchorKind {"));
710 assert!(actual.contains("pub checkpoints: Vec<Checkpoint>,"));
711 assert!(actual.contains("pub created_at_tick_id: Option<String>,"));
712 assert!(actual.contains("pub create_initial_checkpoint: Option<bool>,"));
713 }
714
715 #[test]
716 fn emits_jedit_operation_bindings() {
717 let sdl =
718 include_str!("../../../test/fixtures/consumer-models/jedit-hot-text-runtime.graphql");
719 let ir = lower_schema_sdl(sdl).expect("jedit runtime fixture should lower");
720 let operations =
721 list_schema_operations_sdl(sdl).expect("jedit runtime operations should resolve");
722
723 let actual = emit_rust_with_operations(&ir, &operations);
724
725 syn::parse_file(&actual).expect("generated Rust should parse");
726 assert!(!actual.contains("pub struct Mutation {"));
727 assert!(actual.contains("pub struct MutationCreateBufferWorldlineRequest {"));
728 assert!(actual.contains("pub input: CreateBufferWorldlineInput,"));
729 assert!(actual.contains(
730 "pub type MutationCreateBufferWorldlineResponse = CreateBufferWorldlineResult;"
731 ));
732 assert!(actual.contains("pub const OPERATION_TYPE: &'static str = \"MUTATION\";"));
733 assert!(actual.contains("pub const FIELD_NAME: &'static str = \"createBufferWorldline\";"));
734 assert!(actual.contains("pub const DIRECTIVES_JSON: &'static str = "));
735 assert!(actual.contains("\\\"wes_footprint\\\""));
736 }
737
738 #[test]
739 fn emits_stack_witness_0001_fixture_operation_bindings() {
740 let sdl = include_str!(
741 "../../../test/fixtures/consumer-models/stack-witness-0001-file-history.graphql"
742 );
743 let ir = lower_schema_sdl(sdl).expect("stack witness fixture should lower");
744 let operations =
745 list_schema_operations_sdl(sdl).expect("stack witness operations should resolve");
746
747 let actual = emit_rust_with_operations(&ir, &operations);
748
749 syn::parse_file(&actual).expect("generated Rust should parse");
750 assert!(actual.contains("pub struct MutationCreateBufferRequest {"));
751 assert!(actual.contains("pub input: CreateBufferInput,"));
752 assert!(actual.contains("pub type MutationCreateBufferResponse = MutationReceipt;"));
753 assert!(actual.contains("pub struct MutationReplaceRangeRequest {"));
754 assert!(actual.contains("pub type MutationReplaceRangeResponse = MutationReceipt;"));
755 assert!(actual.contains("pub struct QueryTextWindowRequest {"));
756 assert!(actual.contains("pub type QueryTextWindowResponse = TextWindowReading;"));
757 assert!(actual.contains("pub data_base64: String,"));
758
759 let create_buffer = rust_operation_impl_block(&actual, "MutationCreateBufferRequest");
760 assert!(create_buffer.contains("pub const FIELD_NAME: &'static str = \"createBuffer\";"));
761 assert!(create_buffer.contains("\\\"artifactId\\\":\\\"fixture-file-history-v0\\\""));
762 assert!(create_buffer.contains("\\\"opId\\\":\\\"0x53570001\\\""));
763 assert!(create_buffer.contains("\\\"helperKind\\\":\\\"EINT\\\""));
764 assert!(create_buffer.contains("\\\"targetCodec\\\":\\\"wesley-binary/v0\\\""));
765 assert!(create_buffer.contains(
766 "\\\"fixtureVarsBytes\\\":\\\"stack-witness-0001/createBuffer;name=demo.txt;artifact=fixture-file-history-v0\\\""
767 ));
768
769 let replace_range = rust_operation_impl_block(&actual, "MutationReplaceRangeRequest");
770 assert!(replace_range.contains("pub const FIELD_NAME: &'static str = \"replaceRange\";"));
771 assert!(replace_range.contains("\\\"artifactId\\\":\\\"fixture-file-history-v0\\\""));
772 assert!(replace_range.contains("\\\"opId\\\":\\\"0x53570002\\\""));
773 assert!(replace_range.contains("\\\"helperKind\\\":\\\"EINT\\\""));
774 assert!(replace_range.contains("\\\"targetCodec\\\":\\\"wesley-binary/v0\\\""));
775 assert!(replace_range.contains(
776 "\\\"fixtureVarsBytes\\\":\\\"stack-witness-0001/replaceRange;bufferId=demo.txt;basis=B0;coord=utf8-bytes;start=0;end=0;text=hello;artifact=fixture-file-history-v0\\\""
777 ));
778
779 let text_window = rust_operation_impl_block(&actual, "QueryTextWindowRequest");
780 assert!(text_window.contains("pub const FIELD_NAME: &'static str = \"textWindow\";"));
781 assert!(text_window.contains("\\\"artifactId\\\":\\\"fixture-file-history-v0\\\""));
782 assert!(text_window.contains("\\\"opId\\\":\\\"0x53571001\\\""));
783 assert!(text_window.contains("\\\"helperKind\\\":\\\"QueryView\\\""));
784 assert!(text_window.contains("\\\"targetCodec\\\":\\\"wesley-binary/v0\\\""));
785 assert!(text_window.contains(
786 "\\\"fixtureVarsBytes\\\":\\\"stack-witness-0001/textWindow;bufferId=demo.txt;basis=B1;coord=utf8-bytes;start=0;length=5;artifact=fixture-file-history-v0\\\""
787 ));
788 assert!(text_window.contains("\\\"payloadCodec\\\":\\\"QueryBytes\\\""));
789 assert!(text_window.contains("\\\"envelope\\\":\\\"ReadingEnvelope\\\""));
790 }
791
792 #[test]
793 fn operation_bindings_include_operation_scope_in_type_names() {
794 let sdl = r#"
795 type Query {
796 status: Status!
797 }
798
799 type Mutation {
800 status(input: StatusInput!): Status!
801 }
802
803 type Status {
804 ok: Boolean!
805 }
806
807 input StatusInput {
808 reason: String
809 }
810 "#;
811 let ir = lower_schema_sdl(sdl).expect("schema should lower");
812 let operations = list_schema_operations_sdl(sdl).expect("operations should resolve");
813
814 let actual = emit_rust_with_operations(&ir, &operations);
815
816 syn::parse_file(&actual).expect("generated Rust should parse");
817 assert!(actual.contains("pub struct QueryStatusRequest {"));
818 assert!(actual.contains("pub type QueryStatusResponse = Status;"));
819 assert!(actual.contains("pub struct MutationStatusRequest {"));
820 assert!(actual.contains("pub type MutationStatusResponse = Status;"));
821 }
822
823 #[test]
824 fn sanitizes_reserved_field_names() {
825 assert_eq!(rust_field_name("type"), "r#type");
826 assert_eq!(rust_field_name("displayName"), "display_name");
827 }
828
829 #[test]
830 fn union_payload_enums_do_not_derive_eq() {
831 let ir = lower_schema_sdl(
832 r#"
833 type User {
834 id: ID!
835 }
836
837 union SearchResult = User
838 "#,
839 )
840 .expect("schema should lower");
841
842 let actual = emit_rust(&ir);
843
844 syn::parse_file(&actual).expect("generated Rust should parse");
845 assert!(actual.contains(
846 "#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]\npub enum SearchResult"
847 ));
848 }
849
850 fn rust_operation_impl_block<'a>(actual: &'a str, request_name: &str) -> &'a str {
851 let marker = format!("impl {request_name} {{");
852 let start = actual
853 .find(&marker)
854 .expect("operation impl block should exist");
855 let tail = &actual[start..];
856 let end = tail
857 .find("\n}\n")
858 .expect("operation impl block should close")
859 + "\n}\n".len();
860 &tail[..end]
861 }
862}