1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::AlefConfig;
10use alef_core::hash::{self, CommentStyle};
11use anyhow::Result;
12use heck::ToUpperCamelCase;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16use super::E2eCodegen;
17
18pub struct GoCodegen;
20
21impl E2eCodegen for GoCodegen {
22 fn generate(
23 &self,
24 groups: &[FixtureGroup],
25 e2e_config: &E2eConfig,
26 alef_config: &AlefConfig,
27 ) -> Result<Vec<GeneratedFile>> {
28 let lang = self.language_name();
29 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
30
31 let mut files = Vec::new();
32
33 let call = &e2e_config.call;
35 let overrides = call.overrides.get(lang);
36 let module_path = overrides
37 .and_then(|o| o.module.as_ref())
38 .cloned()
39 .unwrap_or_else(|| call.module.clone());
40 let import_alias = overrides
41 .and_then(|o| o.alias.as_ref())
42 .cloned()
43 .unwrap_or_else(|| "pkg".to_string());
44
45 let go_pkg = e2e_config.resolve_package("go");
47 let go_module_path = go_pkg
48 .as_ref()
49 .and_then(|p| p.module.as_ref())
50 .cloned()
51 .unwrap_or_else(|| module_path.clone());
52 let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
53 let go_version = go_pkg
54 .as_ref()
55 .and_then(|p| p.version.as_ref())
56 .cloned()
57 .unwrap_or_else(|| {
58 alef_config
59 .resolved_version()
60 .map(|v| format!("v{v}"))
61 .unwrap_or_else(|| "v0.0.0".to_string())
62 });
63 let field_resolver = FieldResolver::new(
64 &e2e_config.fields,
65 &e2e_config.fields_optional,
66 &e2e_config.result_fields,
67 &e2e_config.fields_array,
68 );
69
70 let effective_replace = match e2e_config.dep_mode {
73 crate::config::DependencyMode::Registry => None,
74 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
75 };
76 files.push(GeneratedFile {
77 path: output_base.join("go.mod"),
78 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
79 generated_header: false,
80 });
81
82 for group in groups {
84 let active: Vec<&Fixture> = group
85 .fixtures
86 .iter()
87 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
88 .collect();
89
90 if active.is_empty() {
91 continue;
92 }
93
94 let filename = format!("{}_test.go", sanitize_filename(&group.category));
95 let content = render_test_file(
96 &group.category,
97 &active,
98 &module_path,
99 &import_alias,
100 &field_resolver,
101 e2e_config,
102 );
103 files.push(GeneratedFile {
104 path: output_base.join(filename),
105 content,
106 generated_header: true,
107 });
108 }
109
110 Ok(files)
111 }
112
113 fn language_name(&self) -> &'static str {
114 "go"
115 }
116}
117
118fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
119 let mut out = String::new();
120 let _ = writeln!(out, "module e2e_go");
121 let _ = writeln!(out);
122 let _ = writeln!(out, "go 1.26");
123 let _ = writeln!(out);
124 let _ = writeln!(out, "require (");
125 let _ = writeln!(out, "\t{go_module_path} {version}");
126 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
127 let _ = writeln!(out, ")");
128
129 if let Some(path) = replace_path {
130 let _ = writeln!(out);
131 let _ = writeln!(out, "replace {go_module_path} => {path}");
132 }
133
134 out
135}
136
137fn render_test_file(
138 category: &str,
139 fixtures: &[&Fixture],
140 go_module_path: &str,
141 import_alias: &str,
142 field_resolver: &FieldResolver,
143 e2e_config: &crate::config::E2eConfig,
144) -> String {
145 let mut out = String::new();
146
147 out.push_str(&hash::header(CommentStyle::DoubleSlash));
149 let _ = writeln!(out);
150
151 let needs_os = fixtures.iter().any(|f| {
154 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
155 call_args.iter().any(|a| a.arg_type == "mock_url")
156 });
157
158 let needs_json = fixtures.iter().any(|f| {
161 let call = e2e_config.resolve_call(f.call.as_deref());
162 let call_args = &call.args;
163 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
165 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
166 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
167 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
168 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
169 })
170 };
171 let go_override = call.overrides.get("go");
173 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
174 e2e_config
175 .call
176 .overrides
177 .get("go")
178 .and_then(|o| o.options_type.as_deref())
179 });
180 let has_json_obj = call_args.iter().any(|a| {
181 if a.arg_type != "json_object" {
182 return false;
183 }
184 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
185 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
186 if v.is_array() {
187 return true;
188 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
190 });
191 has_handle || has_json_obj
192 });
193
194 let needs_base64 = fixtures.iter().any(|f| {
196 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
197 call_args.iter().any(|a| {
198 if a.arg_type != "bytes" {
199 return false;
200 }
201 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
202 matches!(f.input.get(field), Some(serde_json::Value::String(_)))
203 })
204 });
205
206 let needs_fmt = fixtures.iter().any(|f| {
208 f.visitor.as_ref().is_some_and(|v| {
209 v.callbacks.values().any(|action| {
210 if let CallbackAction::CustomTemplate { template } = action {
211 template.contains('{')
212 } else {
213 false
214 }
215 })
216 })
217 });
218
219 let needs_strings = fixtures.iter().any(|f| {
222 f.assertions.iter().any(|a| {
223 let type_needs_strings = if a.assertion_type == "equals" {
224 a.value.as_ref().is_some_and(|v| v.is_string())
226 } else {
227 matches!(
228 a.assertion_type.as_str(),
229 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
230 )
231 };
232 let field_valid = a
233 .field
234 .as_ref()
235 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
236 .unwrap_or(true);
237 type_needs_strings && field_valid
238 })
239 });
240
241 let needs_assert = fixtures.iter().any(|f| {
244 f.assertions.iter().any(|a| {
245 let field_valid = a
246 .field
247 .as_ref()
248 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
249 .unwrap_or(true);
250 let type_needs_assert = matches!(
251 a.assertion_type.as_str(),
252 "count_min"
253 | "count_max"
254 | "is_true"
255 | "is_false"
256 | "method_result"
257 | "min_length"
258 | "max_length"
259 | "matches_regex"
260 );
261 type_needs_assert && field_valid
262 })
263 });
264
265 let _ = writeln!(out, "// E2e tests for category: {category}");
266 let _ = writeln!(out, "package e2e_test");
267 let _ = writeln!(out);
268 let _ = writeln!(out, "import (");
269 if needs_base64 {
270 let _ = writeln!(out, "\t\"encoding/base64\"");
271 }
272 if needs_json {
273 let _ = writeln!(out, "\t\"encoding/json\"");
274 }
275 if needs_fmt {
276 let _ = writeln!(out, "\t\"fmt\"");
277 }
278 if needs_os {
279 let _ = writeln!(out, "\t\"os\"");
280 }
281 if needs_strings {
282 let _ = writeln!(out, "\t\"strings\"");
283 }
284 let _ = writeln!(out, "\t\"testing\"");
285 if needs_assert {
286 let _ = writeln!(out);
287 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
288 }
289 let _ = writeln!(out);
290 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
291 let _ = writeln!(out, ")");
292 let _ = writeln!(out);
293
294 for fixture in fixtures.iter() {
296 if let Some(visitor_spec) = &fixture.visitor {
297 let struct_name = visitor_struct_name(&fixture.id);
298 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
299 let _ = writeln!(out);
300 }
301 }
302
303 for (i, fixture) in fixtures.iter().enumerate() {
304 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
305 if i + 1 < fixtures.len() {
306 let _ = writeln!(out);
307 }
308 }
309
310 while out.ends_with("\n\n") {
312 out.pop();
313 }
314 if !out.ends_with('\n') {
315 out.push('\n');
316 }
317 out
318}
319
320fn render_test_function(
321 out: &mut String,
322 fixture: &Fixture,
323 import_alias: &str,
324 field_resolver: &FieldResolver,
325 e2e_config: &crate::config::E2eConfig,
326) {
327 let fn_name = fixture.id.to_upper_camel_case();
328 let description = &fixture.description;
329
330 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
332 let lang = "go";
333 let overrides = call_config.overrides.get(lang);
334 let function_name = to_go_name(
335 overrides
336 .and_then(|o| o.function.as_ref())
337 .map(String::as_str)
338 .unwrap_or(&call_config.function),
339 );
340 let result_var = &call_config.result_var;
341 let args = &call_config.args;
342
343 let returns_result = overrides
346 .and_then(|o| o.returns_result)
347 .unwrap_or(call_config.returns_result);
348
349 let returns_void = call_config.returns_void;
352
353 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
356 call_config
357 .overrides
358 .get("rust")
359 .map(|o| o.result_is_simple)
360 .unwrap_or(false)
361 });
362
363 let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
366
367 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
369 e2e_config
370 .call
371 .overrides
372 .get("go")
373 .and_then(|o| o.options_type.as_deref())
374 });
375
376 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
377
378 let (mut setup_lines, args_str) =
379 build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
380
381 let mut visitor_arg = String::new();
383 if fixture.visitor.is_some() {
384 let struct_name = visitor_struct_name(&fixture.id);
385 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
386 visitor_arg = "visitor".to_string();
387 }
388
389 let final_args = if visitor_arg.is_empty() {
390 args_str
391 } else {
392 format!("{args_str}, {visitor_arg}")
393 };
394
395 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
396 let _ = writeln!(out, "\t// {description}");
397
398 for line in &setup_lines {
399 let _ = writeln!(out, "\t{line}");
400 }
401
402 if expects_error {
403 if returns_result && !returns_void {
404 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
405 } else {
406 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
407 }
408 let _ = writeln!(out, "\tif err == nil {{");
409 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
410 let _ = writeln!(out, "\t}}");
411 let _ = writeln!(out, "}}");
412 return;
413 }
414
415 let has_usable_assertion = fixture.assertions.iter().any(|a| {
419 if a.assertion_type == "not_error" || a.assertion_type == "error" {
420 return false;
421 }
422 if a.assertion_type == "method_result" {
424 return true;
425 }
426 match &a.field {
427 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
428 _ => true,
429 }
430 });
431
432 if !returns_result && result_is_simple {
438 let result_binding = if has_usable_assertion {
440 result_var.to_string()
441 } else {
442 "_".to_string()
443 };
444 let assign_op = if result_binding == "_" { "=" } else { ":=" };
446 let _ = writeln!(
447 out,
448 "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
449 );
450 if has_usable_assertion && result_binding != "_" {
451 let _ = writeln!(out, "\tif {result_var} == nil {{");
453 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
454 let _ = writeln!(out, "\t}}");
455 let _ = writeln!(out, "\tvalue := *{result_var}");
456 }
457 } else if !returns_result || returns_void {
458 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
461 let _ = writeln!(out, "\tif err != nil {{");
462 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
463 let _ = writeln!(out, "\t}}");
464 let _ = writeln!(out, "}}");
466 return;
467 } else {
468 let result_binding = if has_usable_assertion {
470 result_var.to_string()
471 } else {
472 "_".to_string()
473 };
474 let _ = writeln!(
475 out,
476 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
477 );
478 let _ = writeln!(out, "\tif err != nil {{");
479 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
480 let _ = writeln!(out, "\t}}");
481 if result_is_simple && has_usable_assertion && result_binding != "_" {
482 let _ = writeln!(out, "\tif {result_var} == nil {{");
484 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
485 let _ = writeln!(out, "\t}}");
486 let _ = writeln!(out, "\tvalue := *{result_var}");
487 }
488 }
489
490 let effective_result_var = if result_is_simple && has_usable_assertion {
492 "value".to_string()
493 } else {
494 result_var.to_string()
495 };
496
497 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
502 for assertion in &fixture.assertions {
503 if let Some(f) = &assertion.field {
504 if !f.is_empty() {
505 let resolved = field_resolver.resolve(f);
506 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
507 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
512 let is_array_field = field_resolver.is_array(resolved);
513 if !is_string_field || is_array_field {
514 continue;
517 }
518 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
519 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
520 if field_resolver.has_map_access(f) {
521 let _ = writeln!(out, "\t{local_var} := {field_expr}");
524 } else {
525 let _ = writeln!(out, "\tvar {local_var} string");
526 let _ = writeln!(out, "\tif {field_expr} != nil {{");
527 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
528 let _ = writeln!(out, "\t}}");
529 }
530 optional_locals.insert(f.clone(), local_var);
531 }
532 }
533 }
534 }
535
536 for assertion in &fixture.assertions {
538 if let Some(f) = &assertion.field {
539 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
540 let parts: Vec<&str> = f.split('.').collect();
543 let mut guard_expr: Option<String> = None;
544 for i in 1..parts.len() {
545 let prefix = parts[..i].join(".");
546 let resolved_prefix = field_resolver.resolve(&prefix);
547 if field_resolver.is_optional(resolved_prefix) {
548 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
549 guard_expr = Some(accessor);
550 break;
551 }
552 }
553 if let Some(guard) = guard_expr {
554 if field_resolver.is_valid_for_result(f) {
557 let _ = writeln!(out, "\tif {guard} != nil {{");
558 let mut nil_buf = String::new();
561 render_assertion(
562 &mut nil_buf,
563 assertion,
564 &effective_result_var,
565 import_alias,
566 field_resolver,
567 &optional_locals,
568 result_is_simple,
569 result_is_array,
570 );
571 for line in nil_buf.lines() {
572 let _ = writeln!(out, "\t{line}");
573 }
574 let _ = writeln!(out, "\t}}");
575 } else {
576 render_assertion(
577 out,
578 assertion,
579 &effective_result_var,
580 import_alias,
581 field_resolver,
582 &optional_locals,
583 result_is_simple,
584 result_is_array,
585 );
586 }
587 continue;
588 }
589 }
590 }
591 render_assertion(
592 out,
593 assertion,
594 &effective_result_var,
595 import_alias,
596 field_resolver,
597 &optional_locals,
598 result_is_simple,
599 result_is_array,
600 );
601 }
602
603 let _ = writeln!(out, "}}");
604}
605
606fn build_args_and_setup(
610 input: &serde_json::Value,
611 args: &[crate::config::ArgMapping],
612 import_alias: &str,
613 options_type: Option<&str>,
614 fixture_id: &str,
615) -> (Vec<String>, String) {
616 use heck::ToUpperCamelCase;
617
618 if args.is_empty() {
619 return (Vec::new(), String::new());
620 }
621
622 let mut setup_lines: Vec<String> = Vec::new();
623 let mut parts: Vec<String> = Vec::new();
624
625 for arg in args {
626 if arg.arg_type == "mock_url" {
627 setup_lines.push(format!(
628 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
629 arg.name,
630 ));
631 parts.push(arg.name.clone());
632 continue;
633 }
634
635 if arg.arg_type == "handle" {
636 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
638 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
639 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
640 if config_value.is_null()
641 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
642 {
643 setup_lines.push(format!(
644 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
645 name = arg.name,
646 ));
647 } else {
648 let json_str = serde_json::to_string(config_value).unwrap_or_default();
649 let go_literal = go_string_literal(&json_str);
650 let name = &arg.name;
651 setup_lines.push(format!(
652 "var {name}Config {import_alias}.CrawlConfig\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
653 ));
654 setup_lines.push(format!(
655 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
656 ));
657 }
658 parts.push(arg.name.clone());
659 continue;
660 }
661
662 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
663 let val = input.get(field);
664
665 if arg.arg_type == "bytes" {
668 let var_name = format!("{}Bytes", arg.name);
669 match val {
670 None | Some(serde_json::Value::Null) => {
671 if arg.optional {
672 parts.push("nil".to_string());
673 } else {
674 parts.push("[]byte{}".to_string());
675 }
676 }
677 Some(serde_json::Value::String(s)) => {
678 let go_b64 = go_string_literal(s);
679 setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
680 parts.push(var_name);
681 }
682 Some(other) => {
683 parts.push(format!("[]byte({})", json_to_go(other)));
684 }
685 }
686 continue;
687 }
688
689 match val {
690 None | Some(serde_json::Value::Null) if arg.optional => {
691 match arg.arg_type.as_str() {
693 "string" => {
694 parts.push("nil".to_string());
696 }
697 "json_object" => {
698 if let Some(opts_type) = options_type {
700 parts.push(format!("{import_alias}.{opts_type}{{}}"));
701 } else {
702 parts.push("nil".to_string());
703 }
704 }
705 _ => {
706 parts.push("nil".to_string());
707 }
708 }
709 }
710 None | Some(serde_json::Value::Null) => {
711 let default_val = match arg.arg_type.as_str() {
713 "string" => "\"\"".to_string(),
714 "int" | "integer" | "i64" => "0".to_string(),
715 "float" | "number" => "0.0".to_string(),
716 "bool" | "boolean" => "false".to_string(),
717 "json_object" => {
718 if let Some(opts_type) = options_type {
719 format!("{import_alias}.{opts_type}{{}}")
720 } else {
721 "nil".to_string()
722 }
723 }
724 _ => "nil".to_string(),
725 };
726 parts.push(default_val);
727 }
728 Some(v) => {
729 match arg.arg_type.as_str() {
730 "json_object" => {
731 let is_array = v.is_array();
734 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
735 if is_empty_obj {
736 if let Some(opts_type) = options_type {
737 parts.push(format!("{import_alias}.{opts_type}{{}}"));
738 } else {
739 parts.push("nil".to_string());
740 }
741 } else if is_array {
742 let json_str = serde_json::to_string(v).unwrap_or_default();
744 let go_literal = go_string_literal(&json_str);
745 let var_name = &arg.name;
746 setup_lines.push(format!(
747 "var {var_name} []string\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
748 ));
749 parts.push(var_name.to_string());
750 } else if let Some(opts_type) = options_type {
751 let json_str = serde_json::to_string(v).unwrap_or_default();
753 let go_literal = go_string_literal(&json_str);
754 let var_name = &arg.name;
755 setup_lines.push(format!(
756 "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
757 ));
758 parts.push(var_name.to_string());
759 } else {
760 parts.push(json_to_go(v));
761 }
762 }
763 "string" if arg.optional => {
764 let var_name = format!("{}Val", arg.name);
766 let go_val = json_to_go(v);
767 setup_lines.push(format!("{var_name} := {go_val}"));
768 parts.push(format!("&{var_name}"));
769 }
770 _ => {
771 parts.push(json_to_go(v));
772 }
773 }
774 }
775 }
776 }
777
778 (setup_lines, parts.join(", "))
779}
780
781#[allow(clippy::too_many_arguments)]
782fn render_assertion(
783 out: &mut String,
784 assertion: &Assertion,
785 result_var: &str,
786 import_alias: &str,
787 field_resolver: &FieldResolver,
788 optional_locals: &std::collections::HashMap<String, String>,
789 result_is_simple: bool,
790 result_is_array: bool,
791) {
792 if !result_is_simple {
795 if let Some(f) = &assertion.field {
796 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
797 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
798 return;
799 }
800 }
801 }
802
803 let field_expr = if result_is_simple {
804 result_var.to_string()
806 } else {
807 match &assertion.field {
808 Some(f) if !f.is_empty() => {
809 if let Some(local_var) = optional_locals.get(f.as_str()) {
811 local_var.clone()
812 } else {
813 field_resolver.accessor(f, "go", result_var)
814 }
815 }
816 _ => result_var.to_string(),
817 }
818 };
819
820 let is_optional = assertion
824 .field
825 .as_ref()
826 .map(|f| {
827 let resolved = field_resolver.resolve(f);
828 let check_path = resolved
829 .strip_suffix(".length")
830 .or_else(|| resolved.strip_suffix(".count"))
831 .or_else(|| resolved.strip_suffix(".size"))
832 .unwrap_or(resolved);
833 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
834 })
835 .unwrap_or(false);
836
837 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
840 let inner = &field_expr[4..field_expr.len() - 1];
841 format!("len(*{inner})")
842 } else {
843 field_expr
844 };
845 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
847 Some(field_expr[5..field_expr.len() - 1].to_string())
848 } else {
849 None
850 };
851
852 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
855 format!("*{field_expr}")
856 } else {
857 field_expr.clone()
858 };
859
860 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
865 let array_expr = &field_expr[..idx];
866 Some(array_expr.to_string())
867 } else {
868 None
869 };
870
871 let mut assertion_buf = String::new();
874 let out_ref = &mut assertion_buf;
875
876 match assertion.assertion_type.as_str() {
877 "equals" => {
878 if let Some(expected) = &assertion.value {
879 let go_val = json_to_go(expected);
880 if expected.is_string() {
882 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
884 format!("strings.TrimSpace(*{field_expr})")
885 } else {
886 format!("strings.TrimSpace({field_expr})")
887 };
888 if is_optional && !field_expr.starts_with("len(") {
889 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
890 } else {
891 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
892 }
893 } else if is_optional && !field_expr.starts_with("len(") {
894 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
895 } else {
896 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
897 }
898 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
899 let _ = writeln!(out_ref, "\t}}");
900 }
901 }
902 "contains" => {
903 if let Some(expected) = &assertion.value {
904 let go_val = json_to_go(expected);
905 let resolved_field = assertion.field.as_deref().unwrap_or("");
911 let resolved_name = field_resolver.resolve(resolved_field);
912 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
913 let is_opt =
914 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
915 let field_for_contains = if is_opt && field_is_array {
916 format!("strings.Join(*{field_expr}, \" \")")
917 } else if is_opt {
918 format!("string(*{field_expr})")
919 } else if field_is_array {
920 format!("strings.Join({field_expr}, \" \")")
921 } else {
922 format!("string({field_expr})")
923 };
924 if is_opt {
925 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
926 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
927 let _ = writeln!(
928 out_ref,
929 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
930 );
931 let _ = writeln!(out_ref, "\t}}");
932 let _ = writeln!(out_ref, "\t}}");
933 } else {
934 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
935 let _ = writeln!(
936 out_ref,
937 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
938 );
939 let _ = writeln!(out_ref, "\t}}");
940 }
941 }
942 }
943 "contains_all" => {
944 if let Some(values) = &assertion.values {
945 let resolved_field = assertion.field.as_deref().unwrap_or("");
946 let resolved_name = field_resolver.resolve(resolved_field);
947 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
948 let is_opt =
949 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
950 for val in values {
951 let go_val = json_to_go(val);
952 let field_for_contains = if is_opt && field_is_array {
953 format!("strings.Join(*{field_expr}, \" \")")
954 } else if is_opt {
955 format!("string(*{field_expr})")
956 } else if field_is_array {
957 format!("strings.Join({field_expr}, \" \")")
958 } else {
959 format!("string({field_expr})")
960 };
961 if is_opt {
962 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
963 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
964 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
965 let _ = writeln!(out_ref, "\t}}");
966 let _ = writeln!(out_ref, "\t}}");
967 } else {
968 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
969 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
970 let _ = writeln!(out_ref, "\t}}");
971 }
972 }
973 }
974 }
975 "not_contains" => {
976 if let Some(expected) = &assertion.value {
977 let go_val = json_to_go(expected);
978 let resolved_field = assertion.field.as_deref().unwrap_or("");
979 let resolved_name = field_resolver.resolve(resolved_field);
980 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
981 let is_opt =
982 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
983 let field_for_contains = if is_opt && field_is_array {
984 format!("strings.Join(*{field_expr}, \" \")")
985 } else if is_opt {
986 format!("string(*{field_expr})")
987 } else if field_is_array {
988 format!("strings.Join({field_expr}, \" \")")
989 } else {
990 format!("string({field_expr})")
991 };
992 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
993 let _ = writeln!(
994 out_ref,
995 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
996 );
997 let _ = writeln!(out_ref, "\t}}");
998 }
999 }
1000 "not_empty" => {
1001 let field_is_array = {
1004 let rf = assertion.field.as_deref().unwrap_or("");
1005 let rn = field_resolver.resolve(rf);
1006 field_resolver.is_array(rn)
1007 };
1008 if is_optional && !field_is_array {
1009 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1011 } else if is_optional {
1012 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1013 } else {
1014 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1015 }
1016 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1017 let _ = writeln!(out_ref, "\t}}");
1018 }
1019 "is_empty" => {
1020 let field_is_array = {
1021 let rf = assertion.field.as_deref().unwrap_or("");
1022 let rn = field_resolver.resolve(rf);
1023 field_resolver.is_array(rn)
1024 };
1025 if is_optional && !field_is_array {
1026 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1028 } else if is_optional {
1029 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1030 } else {
1031 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1032 }
1033 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1034 let _ = writeln!(out_ref, "\t}}");
1035 }
1036 "contains_any" => {
1037 if let Some(values) = &assertion.values {
1038 let resolved_field = assertion.field.as_deref().unwrap_or("");
1039 let resolved_name = field_resolver.resolve(resolved_field);
1040 let field_is_array = field_resolver.is_array(resolved_name);
1041 let is_opt =
1042 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1043 let field_for_contains = if is_opt && field_is_array {
1044 format!("strings.Join(*{field_expr}, \" \")")
1045 } else if is_opt {
1046 format!("*{field_expr}")
1047 } else if field_is_array {
1048 format!("strings.Join({field_expr}, \" \")")
1049 } else {
1050 field_expr.clone()
1051 };
1052 let _ = writeln!(out_ref, "\t{{");
1053 let _ = writeln!(out_ref, "\t\tfound := false");
1054 for val in values {
1055 let go_val = json_to_go(val);
1056 let _ = writeln!(
1057 out_ref,
1058 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1059 );
1060 }
1061 let _ = writeln!(out_ref, "\t\tif !found {{");
1062 let _ = writeln!(
1063 out_ref,
1064 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1065 );
1066 let _ = writeln!(out_ref, "\t\t}}");
1067 let _ = writeln!(out_ref, "\t}}");
1068 }
1069 }
1070 "greater_than" => {
1071 if let Some(val) = &assertion.value {
1072 let go_val = json_to_go(val);
1073 if is_optional {
1077 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1078 if let Some(n) = val.as_u64() {
1079 let next = n + 1;
1080 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1081 } else {
1082 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1083 }
1084 let _ = writeln!(
1085 out_ref,
1086 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1087 );
1088 let _ = writeln!(out_ref, "\t\t}}");
1089 let _ = writeln!(out_ref, "\t}}");
1090 } else if let Some(n) = val.as_u64() {
1091 let next = n + 1;
1092 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1093 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1094 let _ = writeln!(out_ref, "\t}}");
1095 } else {
1096 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1097 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1098 let _ = writeln!(out_ref, "\t}}");
1099 }
1100 }
1101 }
1102 "less_than" => {
1103 if let Some(val) = &assertion.value {
1104 let go_val = json_to_go(val);
1105 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1106 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1107 let _ = writeln!(out_ref, "\t}}");
1108 }
1109 }
1110 "greater_than_or_equal" => {
1111 if let Some(val) = &assertion.value {
1112 let go_val = json_to_go(val);
1113 if let Some(ref guard) = nil_guard_expr {
1114 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1115 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1116 let _ = writeln!(
1117 out_ref,
1118 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1119 );
1120 let _ = writeln!(out_ref, "\t\t}}");
1121 let _ = writeln!(out_ref, "\t}}");
1122 } else if is_optional && !field_expr.starts_with("len(") {
1123 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1125 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1126 let _ = writeln!(
1127 out_ref,
1128 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1129 );
1130 let _ = writeln!(out_ref, "\t\t}}");
1131 let _ = writeln!(out_ref, "\t}}");
1132 } else {
1133 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1134 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1135 let _ = writeln!(out_ref, "\t}}");
1136 }
1137 }
1138 }
1139 "less_than_or_equal" => {
1140 if let Some(val) = &assertion.value {
1141 let go_val = json_to_go(val);
1142 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1143 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1144 let _ = writeln!(out_ref, "\t}}");
1145 }
1146 }
1147 "starts_with" => {
1148 if let Some(expected) = &assertion.value {
1149 let go_val = json_to_go(expected);
1150 let field_for_prefix = if is_optional
1151 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1152 {
1153 format!("string(*{field_expr})")
1154 } else {
1155 format!("string({field_expr})")
1156 };
1157 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1158 let _ = writeln!(
1159 out_ref,
1160 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1161 );
1162 let _ = writeln!(out_ref, "\t}}");
1163 }
1164 }
1165 "count_min" => {
1166 if let Some(val) = &assertion.value {
1167 if let Some(n) = val.as_u64() {
1168 if is_optional {
1169 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1170 let _ = writeln!(
1171 out_ref,
1172 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1173 );
1174 let _ = writeln!(out_ref, "\t}}");
1175 } else {
1176 let _ = writeln!(
1177 out_ref,
1178 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1179 );
1180 }
1181 }
1182 }
1183 }
1184 "count_equals" => {
1185 if let Some(val) = &assertion.value {
1186 if let Some(n) = val.as_u64() {
1187 if is_optional {
1188 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1189 let _ = writeln!(
1190 out_ref,
1191 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1192 );
1193 let _ = writeln!(out_ref, "\t}}");
1194 } else {
1195 let _ = writeln!(
1196 out_ref,
1197 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1198 );
1199 }
1200 }
1201 }
1202 }
1203 "is_true" => {
1204 if is_optional {
1205 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1206 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1207 let _ = writeln!(out_ref, "\t}}");
1208 } else {
1209 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1210 }
1211 }
1212 "is_false" => {
1213 if is_optional {
1214 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1215 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1216 let _ = writeln!(out_ref, "\t}}");
1217 } else {
1218 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1219 }
1220 }
1221 "method_result" => {
1222 if let Some(method_name) = &assertion.method {
1223 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1224 let check = assertion.check.as_deref().unwrap_or("is_true");
1225 let deref_expr = if info.is_pointer {
1228 format!("*{}", info.call_expr)
1229 } else {
1230 info.call_expr.clone()
1231 };
1232 match check {
1233 "equals" => {
1234 if let Some(val) = &assertion.value {
1235 if val.is_boolean() {
1236 if val.as_bool() == Some(true) {
1237 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1238 } else {
1239 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1240 }
1241 } else {
1242 let go_val = if let Some(cast) = info.value_cast {
1246 if val.is_number() {
1247 format!("{cast}({})", json_to_go(val))
1248 } else {
1249 json_to_go(val)
1250 }
1251 } else {
1252 json_to_go(val)
1253 };
1254 let _ = writeln!(
1255 out_ref,
1256 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1257 );
1258 }
1259 }
1260 }
1261 "is_true" => {
1262 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1263 }
1264 "is_false" => {
1265 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1266 }
1267 "greater_than_or_equal" => {
1268 if let Some(val) = &assertion.value {
1269 let n = val.as_u64().unwrap_or(0);
1270 let cast = info.value_cast.unwrap_or("uint");
1272 let _ = writeln!(
1273 out_ref,
1274 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1275 );
1276 }
1277 }
1278 "count_min" => {
1279 if let Some(val) = &assertion.value {
1280 let n = val.as_u64().unwrap_or(0);
1281 let _ = writeln!(
1282 out_ref,
1283 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1284 );
1285 }
1286 }
1287 "contains" => {
1288 if let Some(val) = &assertion.value {
1289 let go_val = json_to_go(val);
1290 let _ = writeln!(
1291 out_ref,
1292 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1293 );
1294 }
1295 }
1296 "is_error" => {
1297 let _ = writeln!(out_ref, "\t{{");
1298 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1299 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1300 let _ = writeln!(out_ref, "\t}}");
1301 }
1302 other_check => {
1303 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1304 }
1305 }
1306 } else {
1307 panic!("Go e2e generator: method_result assertion missing 'method' field");
1308 }
1309 }
1310 "min_length" => {
1311 if let Some(val) = &assertion.value {
1312 if let Some(n) = val.as_u64() {
1313 if is_optional {
1314 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1315 let _ = writeln!(
1316 out_ref,
1317 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1318 );
1319 let _ = writeln!(out_ref, "\t}}");
1320 } else {
1321 let _ = writeln!(
1322 out_ref,
1323 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1324 );
1325 }
1326 }
1327 }
1328 }
1329 "max_length" => {
1330 if let Some(val) = &assertion.value {
1331 if let Some(n) = val.as_u64() {
1332 if is_optional {
1333 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1334 let _ = writeln!(
1335 out_ref,
1336 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1337 );
1338 let _ = writeln!(out_ref, "\t}}");
1339 } else {
1340 let _ = writeln!(
1341 out_ref,
1342 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1343 );
1344 }
1345 }
1346 }
1347 }
1348 "ends_with" => {
1349 if let Some(expected) = &assertion.value {
1350 let go_val = json_to_go(expected);
1351 let field_for_suffix = if is_optional
1352 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1353 {
1354 format!("string(*{field_expr})")
1355 } else {
1356 format!("string({field_expr})")
1357 };
1358 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1359 let _ = writeln!(
1360 out_ref,
1361 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1362 );
1363 let _ = writeln!(out_ref, "\t}}");
1364 }
1365 }
1366 "matches_regex" => {
1367 if let Some(expected) = &assertion.value {
1368 let go_val = json_to_go(expected);
1369 let field_for_regex = if is_optional
1370 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1371 {
1372 format!("*{field_expr}")
1373 } else {
1374 field_expr.clone()
1375 };
1376 let _ = writeln!(
1377 out_ref,
1378 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1379 );
1380 }
1381 }
1382 "not_error" => {
1383 }
1385 "error" => {
1386 }
1388 other => {
1389 panic!("Go e2e generator: unsupported assertion type: {other}");
1390 }
1391 }
1392
1393 if let Some(ref arr) = array_guard {
1396 if !assertion_buf.is_empty() {
1397 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1398 for line in assertion_buf.lines() {
1400 let _ = writeln!(out, "\t{line}");
1401 }
1402 let _ = writeln!(out, "\t}}");
1403 }
1404 } else {
1405 out.push_str(&assertion_buf);
1406 }
1407}
1408
1409struct GoMethodCallInfo {
1411 call_expr: String,
1413 is_pointer: bool,
1415 value_cast: Option<&'static str>,
1418}
1419
1420fn build_go_method_call(
1435 result_var: &str,
1436 method_name: &str,
1437 args: Option<&serde_json::Value>,
1438 import_alias: &str,
1439) -> GoMethodCallInfo {
1440 match method_name {
1441 "root_node_type" => GoMethodCallInfo {
1442 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1443 is_pointer: false,
1444 value_cast: None,
1445 },
1446 "named_children_count" => GoMethodCallInfo {
1447 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1448 is_pointer: false,
1449 value_cast: Some("uint"),
1450 },
1451 "has_error_nodes" => GoMethodCallInfo {
1452 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1453 is_pointer: true,
1454 value_cast: None,
1455 },
1456 "error_count" | "tree_error_count" => GoMethodCallInfo {
1457 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1458 is_pointer: true,
1459 value_cast: Some("uint"),
1460 },
1461 "tree_to_sexp" => GoMethodCallInfo {
1462 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1463 is_pointer: true,
1464 value_cast: None,
1465 },
1466 "contains_node_type" => {
1467 let node_type = args
1468 .and_then(|a| a.get("node_type"))
1469 .and_then(|v| v.as_str())
1470 .unwrap_or("");
1471 GoMethodCallInfo {
1472 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1473 is_pointer: true,
1474 value_cast: None,
1475 }
1476 }
1477 "find_nodes_by_type" => {
1478 let node_type = args
1479 .and_then(|a| a.get("node_type"))
1480 .and_then(|v| v.as_str())
1481 .unwrap_or("");
1482 GoMethodCallInfo {
1483 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1484 is_pointer: true,
1485 value_cast: None,
1486 }
1487 }
1488 "run_query" => {
1489 let query_source = args
1490 .and_then(|a| a.get("query_source"))
1491 .and_then(|v| v.as_str())
1492 .unwrap_or("");
1493 let language = args
1494 .and_then(|a| a.get("language"))
1495 .and_then(|v| v.as_str())
1496 .unwrap_or("");
1497 let query_lit = go_string_literal(query_source);
1498 let lang_lit = go_string_literal(language);
1499 GoMethodCallInfo {
1501 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1502 is_pointer: false,
1503 value_cast: None,
1504 }
1505 }
1506 other => {
1507 let method_pascal = other.to_upper_camel_case();
1508 GoMethodCallInfo {
1509 call_expr: format!("{result_var}.{method_pascal}()"),
1510 is_pointer: false,
1511 value_cast: None,
1512 }
1513 }
1514 }
1515}
1516
1517fn json_to_go(value: &serde_json::Value) -> String {
1519 match value {
1520 serde_json::Value::String(s) => go_string_literal(s),
1521 serde_json::Value::Bool(b) => b.to_string(),
1522 serde_json::Value::Number(n) => n.to_string(),
1523 serde_json::Value::Null => "nil".to_string(),
1524 other => go_string_literal(&other.to_string()),
1526 }
1527}
1528
1529fn visitor_struct_name(fixture_id: &str) -> String {
1538 use heck::ToUpperCamelCase;
1539 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1541}
1542
1543fn emit_go_visitor_struct(
1545 out: &mut String,
1546 struct_name: &str,
1547 visitor_spec: &crate::fixture::VisitorSpec,
1548 import_alias: &str,
1549) {
1550 let _ = writeln!(out, "type {struct_name} struct{{}}");
1551 for (method_name, action) in &visitor_spec.callbacks {
1552 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1553 }
1554}
1555
1556fn emit_go_visitor_method(
1558 out: &mut String,
1559 struct_name: &str,
1560 method_name: &str,
1561 action: &CallbackAction,
1562 import_alias: &str,
1563) {
1564 let camel_method = method_to_camel(method_name);
1565 let params = match method_name {
1566 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1567 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1568 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1569 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1570 "visit_code_inline"
1571 | "visit_strong"
1572 | "visit_emphasis"
1573 | "visit_strikethrough"
1574 | "visit_underline"
1575 | "visit_subscript"
1576 | "visit_superscript"
1577 | "visit_mark"
1578 | "visit_button"
1579 | "visit_summary"
1580 | "visit_figcaption"
1581 | "visit_definition_term"
1582 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1583 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1584 "visit_list_item" => {
1585 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1586 }
1587 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1588 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1589 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1590 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1591 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1592 "visit_audio" | "visit_video" | "visit_iframe" => {
1593 format!("_ {import_alias}.NodeContext, src string")
1594 }
1595 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1596 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1597 format!("_ {import_alias}.NodeContext, output string")
1598 }
1599 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1600 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1601 _ => format!("_ {import_alias}.NodeContext"),
1602 };
1603
1604 let _ = writeln!(
1605 out,
1606 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1607 );
1608 match action {
1609 CallbackAction::Skip => {
1610 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1611 }
1612 CallbackAction::Continue => {
1613 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1614 }
1615 CallbackAction::PreserveHtml => {
1616 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1617 }
1618 CallbackAction::Custom { output } => {
1619 let escaped = go_string_literal(output);
1620 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1621 }
1622 CallbackAction::CustomTemplate { template } => {
1623 let (fmt_str, fmt_args) = template_to_sprintf(template);
1626 let escaped_fmt = go_string_literal(&fmt_str);
1627 if fmt_args.is_empty() {
1628 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1629 } else {
1630 let args_str = fmt_args.join(", ");
1631 let _ = writeln!(
1632 out,
1633 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1634 );
1635 }
1636 }
1637 }
1638 let _ = writeln!(out, "}}");
1639}
1640
1641fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1645 let mut fmt_str = String::new();
1646 let mut args: Vec<String> = Vec::new();
1647 let mut chars = template.chars().peekable();
1648 while let Some(c) = chars.next() {
1649 if c == '{' {
1650 let mut name = String::new();
1652 for inner in chars.by_ref() {
1653 if inner == '}' {
1654 break;
1655 }
1656 name.push(inner);
1657 }
1658 fmt_str.push_str("%s");
1659 args.push(name);
1660 } else {
1661 fmt_str.push(c);
1662 }
1663 }
1664 (fmt_str, args)
1665}
1666
1667fn method_to_camel(snake: &str) -> String {
1669 use heck::ToUpperCamelCase;
1670 snake.to_upper_camel_case()
1671}
1672
1673#[cfg(test)]
1674mod tests {
1675 use super::*;
1676 use crate::config::{CallConfig, E2eConfig};
1677 use crate::field_access::FieldResolver;
1678 use crate::fixture::{Assertion, Fixture};
1679
1680 fn make_fixture(id: &str) -> Fixture {
1681 Fixture {
1682 id: id.to_string(),
1683 category: None,
1684 description: "test fixture".to_string(),
1685 tags: vec![],
1686 skip: None,
1687 call: None,
1688 input: serde_json::Value::Null,
1689 mock_response: None,
1690 source: String::new(),
1691 http: None,
1692 assertions: vec![Assertion {
1693 assertion_type: "not_error".to_string(),
1694 field: None,
1695 value: None,
1696 values: None,
1697 method: None,
1698 args: None,
1699 check: None,
1700 }],
1701 visitor: None,
1702 }
1703 }
1704
1705 #[test]
1709 fn test_go_method_name_uses_go_casing() {
1710 let e2e_config = E2eConfig {
1711 call: CallConfig {
1712 function: "clean_extracted_text".to_string(),
1713 module: "github.com/example/mylib".to_string(),
1714 result_var: "result".to_string(),
1715 r#async: false,
1716 path: None,
1717 method: None,
1718 args: vec![],
1719 overrides: std::collections::HashMap::new(),
1720 returns_result: true,
1721 returns_void: false,
1722 skip_languages: vec![],
1723 },
1724 ..E2eConfig::default()
1725 };
1726
1727 let fixture = make_fixture("basic_text");
1728 let resolver = FieldResolver::new(
1729 &std::collections::HashMap::new(),
1730 &std::collections::HashSet::new(),
1731 &std::collections::HashSet::new(),
1732 &std::collections::HashSet::new(),
1733 );
1734 let mut out = String::new();
1735 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
1736
1737 assert!(
1738 out.contains("kreuzberg.CleanExtractedText("),
1739 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
1740 );
1741 assert!(
1742 !out.contains("kreuzberg.clean_extracted_text("),
1743 "must not emit raw snake_case method name, got:\n{out}"
1744 );
1745 }
1746}