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_pkg = fixtures.iter().any(|f| f.mock_response.is_some());
156
157 let needs_os = fixtures.iter().any(|f| {
160 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
161 call_args.iter().any(|a| a.arg_type == "mock_url")
162 });
163
164 let needs_json = fixtures.iter().any(|f| {
167 let call = e2e_config.resolve_call(f.call.as_deref());
168 let call_args = &call.args;
169 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
171 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
172 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
173 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
174 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
175 })
176 };
177 let go_override = call.overrides.get("go");
179 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
180 e2e_config
181 .call
182 .overrides
183 .get("go")
184 .and_then(|o| o.options_type.as_deref())
185 });
186 let has_json_obj = call_args.iter().any(|a| {
187 if a.arg_type != "json_object" {
188 return false;
189 }
190 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
191 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
192 if v.is_array() {
193 return true;
194 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
196 });
197 has_handle || has_json_obj
198 });
199
200 let needs_base64 = fixtures.iter().any(|f| {
202 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
203 call_args.iter().any(|a| {
204 if a.arg_type != "bytes" {
205 return false;
206 }
207 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
208 matches!(f.input.get(field), Some(serde_json::Value::String(_)))
209 })
210 });
211
212 let needs_fmt = fixtures.iter().any(|f| {
214 f.visitor.as_ref().is_some_and(|v| {
215 v.callbacks.values().any(|action| {
216 if let CallbackAction::CustomTemplate { template } = action {
217 template.contains('{')
218 } else {
219 false
220 }
221 })
222 })
223 });
224
225 let needs_strings = fixtures.iter().any(|f| {
228 f.assertions.iter().any(|a| {
229 let type_needs_strings = if a.assertion_type == "equals" {
230 a.value.as_ref().is_some_and(|v| v.is_string())
232 } else {
233 matches!(
234 a.assertion_type.as_str(),
235 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
236 )
237 };
238 let field_valid = a
239 .field
240 .as_ref()
241 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
242 .unwrap_or(true);
243 type_needs_strings && field_valid
244 })
245 });
246
247 let needs_assert = fixtures.iter().any(|f| {
250 f.assertions.iter().any(|a| {
251 let field_valid = a
252 .field
253 .as_ref()
254 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
255 .unwrap_or(true);
256 let type_needs_assert = matches!(
257 a.assertion_type.as_str(),
258 "count_min"
259 | "count_max"
260 | "is_true"
261 | "is_false"
262 | "method_result"
263 | "min_length"
264 | "max_length"
265 | "matches_regex"
266 );
267 type_needs_assert && field_valid
268 })
269 });
270
271 let _ = writeln!(out, "// E2e tests for category: {category}");
272 let _ = writeln!(out, "package e2e_test");
273 let _ = writeln!(out);
274 let _ = writeln!(out, "import (");
275 if needs_base64 {
276 let _ = writeln!(out, "\t\"encoding/base64\"");
277 }
278 if needs_json {
279 let _ = writeln!(out, "\t\"encoding/json\"");
280 }
281 if needs_fmt {
282 let _ = writeln!(out, "\t\"fmt\"");
283 }
284 if needs_os {
285 let _ = writeln!(out, "\t\"os\"");
286 }
287 if needs_strings {
288 let _ = writeln!(out, "\t\"strings\"");
289 }
290 let _ = writeln!(out, "\t\"testing\"");
291 if needs_assert {
292 let _ = writeln!(out);
293 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
294 }
295 if needs_pkg {
296 let _ = writeln!(out);
297 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
298 }
299 let _ = writeln!(out, ")");
300 let _ = writeln!(out);
301
302 for fixture in fixtures.iter() {
304 if let Some(visitor_spec) = &fixture.visitor {
305 let struct_name = visitor_struct_name(&fixture.id);
306 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
307 let _ = writeln!(out);
308 }
309 }
310
311 for (i, fixture) in fixtures.iter().enumerate() {
312 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
313 if i + 1 < fixtures.len() {
314 let _ = writeln!(out);
315 }
316 }
317
318 while out.ends_with("\n\n") {
320 out.pop();
321 }
322 if !out.ends_with('\n') {
323 out.push('\n');
324 }
325 out
326}
327
328fn render_test_function(
329 out: &mut String,
330 fixture: &Fixture,
331 import_alias: &str,
332 field_resolver: &FieldResolver,
333 e2e_config: &crate::config::E2eConfig,
334) {
335 let fn_name = fixture.id.to_upper_camel_case();
336 let description = &fixture.description;
337
338 if fixture.mock_response.is_none() {
342 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
343 let _ = writeln!(out, "\t// {description}");
344 let _ = writeln!(
345 out,
346 "\tt.Skip(\"TODO: implement Go e2e tests via the spikard Go binding API\")"
347 );
348 let _ = writeln!(out, "}}");
349 return;
350 }
351
352 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
354 let lang = "go";
355 let overrides = call_config.overrides.get(lang);
356 let function_name = to_go_name(
357 overrides
358 .and_then(|o| o.function.as_ref())
359 .map(String::as_str)
360 .unwrap_or(&call_config.function),
361 );
362 let result_var = &call_config.result_var;
363 let args = &call_config.args;
364
365 let returns_result = overrides
368 .and_then(|o| o.returns_result)
369 .unwrap_or(call_config.returns_result);
370
371 let returns_void = call_config.returns_void;
374
375 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
378 call_config
379 .overrides
380 .get("rust")
381 .map(|o| o.result_is_simple)
382 .unwrap_or(false)
383 });
384
385 let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
388
389 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
391 e2e_config
392 .call
393 .overrides
394 .get("go")
395 .and_then(|o| o.options_type.as_deref())
396 });
397
398 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
399
400 let (mut setup_lines, args_str) =
401 build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
402
403 let mut visitor_arg = String::new();
405 if fixture.visitor.is_some() {
406 let struct_name = visitor_struct_name(&fixture.id);
407 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
408 visitor_arg = "visitor".to_string();
409 }
410
411 let final_args = if visitor_arg.is_empty() {
412 args_str
413 } else {
414 format!("{args_str}, {visitor_arg}")
415 };
416
417 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
418 let _ = writeln!(out, "\t// {description}");
419
420 for line in &setup_lines {
421 let _ = writeln!(out, "\t{line}");
422 }
423
424 if expects_error {
425 if returns_result && !returns_void {
426 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
427 } else {
428 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
429 }
430 let _ = writeln!(out, "\tif err == nil {{");
431 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
432 let _ = writeln!(out, "\t}}");
433 let _ = writeln!(out, "}}");
434 return;
435 }
436
437 let has_usable_assertion = fixture.assertions.iter().any(|a| {
441 if a.assertion_type == "not_error" || a.assertion_type == "error" {
442 return false;
443 }
444 if a.assertion_type == "method_result" {
446 return true;
447 }
448 match &a.field {
449 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
450 _ => true,
451 }
452 });
453
454 if !returns_result && result_is_simple {
460 let result_binding = if has_usable_assertion {
462 result_var.to_string()
463 } else {
464 "_".to_string()
465 };
466 let assign_op = if result_binding == "_" { "=" } else { ":=" };
468 let _ = writeln!(
469 out,
470 "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
471 );
472 if has_usable_assertion && result_binding != "_" {
473 let _ = writeln!(out, "\tif {result_var} == nil {{");
475 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
476 let _ = writeln!(out, "\t}}");
477 let _ = writeln!(out, "\tvalue := *{result_var}");
478 }
479 } else if !returns_result || returns_void {
480 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
483 let _ = writeln!(out, "\tif err != nil {{");
484 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
485 let _ = writeln!(out, "\t}}");
486 let _ = writeln!(out, "}}");
488 return;
489 } else {
490 let result_binding = if has_usable_assertion {
492 result_var.to_string()
493 } else {
494 "_".to_string()
495 };
496 let _ = writeln!(
497 out,
498 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
499 );
500 let _ = writeln!(out, "\tif err != nil {{");
501 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
502 let _ = writeln!(out, "\t}}");
503 if result_is_simple && has_usable_assertion && result_binding != "_" {
504 let _ = writeln!(out, "\tif {result_var} == nil {{");
506 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
507 let _ = writeln!(out, "\t}}");
508 let _ = writeln!(out, "\tvalue := *{result_var}");
509 }
510 }
511
512 let effective_result_var = if result_is_simple && has_usable_assertion {
514 "value".to_string()
515 } else {
516 result_var.to_string()
517 };
518
519 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
524 for assertion in &fixture.assertions {
525 if let Some(f) = &assertion.field {
526 if !f.is_empty() {
527 let resolved = field_resolver.resolve(f);
528 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
529 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
534 let is_array_field = field_resolver.is_array(resolved);
535 if !is_string_field || is_array_field {
536 continue;
539 }
540 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
541 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
542 if field_resolver.has_map_access(f) {
543 let _ = writeln!(out, "\t{local_var} := {field_expr}");
546 } else {
547 let _ = writeln!(out, "\tvar {local_var} string");
548 let _ = writeln!(out, "\tif {field_expr} != nil {{");
549 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
550 let _ = writeln!(out, "\t}}");
551 }
552 optional_locals.insert(f.clone(), local_var);
553 }
554 }
555 }
556 }
557
558 for assertion in &fixture.assertions {
560 if let Some(f) = &assertion.field {
561 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
562 let parts: Vec<&str> = f.split('.').collect();
565 let mut guard_expr: Option<String> = None;
566 for i in 1..parts.len() {
567 let prefix = parts[..i].join(".");
568 let resolved_prefix = field_resolver.resolve(&prefix);
569 if field_resolver.is_optional(resolved_prefix) {
570 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
571 guard_expr = Some(accessor);
572 break;
573 }
574 }
575 if let Some(guard) = guard_expr {
576 if field_resolver.is_valid_for_result(f) {
579 let _ = writeln!(out, "\tif {guard} != nil {{");
580 let mut nil_buf = String::new();
583 render_assertion(
584 &mut nil_buf,
585 assertion,
586 &effective_result_var,
587 import_alias,
588 field_resolver,
589 &optional_locals,
590 result_is_simple,
591 result_is_array,
592 );
593 for line in nil_buf.lines() {
594 let _ = writeln!(out, "\t{line}");
595 }
596 let _ = writeln!(out, "\t}}");
597 } else {
598 render_assertion(
599 out,
600 assertion,
601 &effective_result_var,
602 import_alias,
603 field_resolver,
604 &optional_locals,
605 result_is_simple,
606 result_is_array,
607 );
608 }
609 continue;
610 }
611 }
612 }
613 render_assertion(
614 out,
615 assertion,
616 &effective_result_var,
617 import_alias,
618 field_resolver,
619 &optional_locals,
620 result_is_simple,
621 result_is_array,
622 );
623 }
624
625 let _ = writeln!(out, "}}");
626}
627
628fn build_args_and_setup(
632 input: &serde_json::Value,
633 args: &[crate::config::ArgMapping],
634 import_alias: &str,
635 options_type: Option<&str>,
636 fixture_id: &str,
637) -> (Vec<String>, String) {
638 use heck::ToUpperCamelCase;
639
640 if args.is_empty() {
641 return (Vec::new(), String::new());
642 }
643
644 let mut setup_lines: Vec<String> = Vec::new();
645 let mut parts: Vec<String> = Vec::new();
646
647 for arg in args {
648 if arg.arg_type == "mock_url" {
649 setup_lines.push(format!(
650 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
651 arg.name,
652 ));
653 parts.push(arg.name.clone());
654 continue;
655 }
656
657 if arg.arg_type == "handle" {
658 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
660 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
661 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
662 if config_value.is_null()
663 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
664 {
665 setup_lines.push(format!(
666 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
667 name = arg.name,
668 ));
669 } else {
670 let json_str = serde_json::to_string(config_value).unwrap_or_default();
671 let go_literal = go_string_literal(&json_str);
672 let name = &arg.name;
673 setup_lines.push(format!(
674 "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}}"
675 ));
676 setup_lines.push(format!(
677 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
678 ));
679 }
680 parts.push(arg.name.clone());
681 continue;
682 }
683
684 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
685 let val = input.get(field);
686
687 if arg.arg_type == "bytes" {
690 let var_name = format!("{}Bytes", arg.name);
691 match val {
692 None | Some(serde_json::Value::Null) => {
693 if arg.optional {
694 parts.push("nil".to_string());
695 } else {
696 parts.push("[]byte{}".to_string());
697 }
698 }
699 Some(serde_json::Value::String(s)) => {
700 let go_b64 = go_string_literal(s);
701 setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
702 parts.push(var_name);
703 }
704 Some(other) => {
705 parts.push(format!("[]byte({})", json_to_go(other)));
706 }
707 }
708 continue;
709 }
710
711 match val {
712 None | Some(serde_json::Value::Null) if arg.optional => {
713 match arg.arg_type.as_str() {
715 "string" => {
716 parts.push("nil".to_string());
718 }
719 "json_object" => {
720 if let Some(opts_type) = options_type {
722 parts.push(format!("{import_alias}.{opts_type}{{}}"));
723 } else {
724 parts.push("nil".to_string());
725 }
726 }
727 _ => {
728 parts.push("nil".to_string());
729 }
730 }
731 }
732 None | Some(serde_json::Value::Null) => {
733 let default_val = match arg.arg_type.as_str() {
735 "string" => "\"\"".to_string(),
736 "int" | "integer" | "i64" => "0".to_string(),
737 "float" | "number" => "0.0".to_string(),
738 "bool" | "boolean" => "false".to_string(),
739 "json_object" => {
740 if let Some(opts_type) = options_type {
741 format!("{import_alias}.{opts_type}{{}}")
742 } else {
743 "nil".to_string()
744 }
745 }
746 _ => "nil".to_string(),
747 };
748 parts.push(default_val);
749 }
750 Some(v) => {
751 match arg.arg_type.as_str() {
752 "json_object" => {
753 let is_array = v.is_array();
756 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
757 if is_empty_obj {
758 if let Some(opts_type) = options_type {
759 parts.push(format!("{import_alias}.{opts_type}{{}}"));
760 } else {
761 parts.push("nil".to_string());
762 }
763 } else if is_array {
764 let json_str = serde_json::to_string(v).unwrap_or_default();
766 let go_literal = go_string_literal(&json_str);
767 let var_name = &arg.name;
768 setup_lines.push(format!(
769 "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}}"
770 ));
771 parts.push(var_name.to_string());
772 } else if let Some(opts_type) = options_type {
773 let json_str = serde_json::to_string(v).unwrap_or_default();
775 let go_literal = go_string_literal(&json_str);
776 let var_name = &arg.name;
777 setup_lines.push(format!(
778 "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}}"
779 ));
780 parts.push(var_name.to_string());
781 } else {
782 parts.push(json_to_go(v));
783 }
784 }
785 "string" if arg.optional => {
786 let var_name = format!("{}Val", arg.name);
788 let go_val = json_to_go(v);
789 setup_lines.push(format!("{var_name} := {go_val}"));
790 parts.push(format!("&{var_name}"));
791 }
792 _ => {
793 parts.push(json_to_go(v));
794 }
795 }
796 }
797 }
798 }
799
800 (setup_lines, parts.join(", "))
801}
802
803#[allow(clippy::too_many_arguments)]
804fn render_assertion(
805 out: &mut String,
806 assertion: &Assertion,
807 result_var: &str,
808 import_alias: &str,
809 field_resolver: &FieldResolver,
810 optional_locals: &std::collections::HashMap<String, String>,
811 result_is_simple: bool,
812 result_is_array: bool,
813) {
814 if !result_is_simple {
817 if let Some(f) = &assertion.field {
818 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
819 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
820 return;
821 }
822 }
823 }
824
825 let field_expr = if result_is_simple {
826 result_var.to_string()
828 } else {
829 match &assertion.field {
830 Some(f) if !f.is_empty() => {
831 if let Some(local_var) = optional_locals.get(f.as_str()) {
833 local_var.clone()
834 } else {
835 field_resolver.accessor(f, "go", result_var)
836 }
837 }
838 _ => result_var.to_string(),
839 }
840 };
841
842 let is_optional = assertion
846 .field
847 .as_ref()
848 .map(|f| {
849 let resolved = field_resolver.resolve(f);
850 let check_path = resolved
851 .strip_suffix(".length")
852 .or_else(|| resolved.strip_suffix(".count"))
853 .or_else(|| resolved.strip_suffix(".size"))
854 .unwrap_or(resolved);
855 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
856 })
857 .unwrap_or(false);
858
859 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
862 let inner = &field_expr[4..field_expr.len() - 1];
863 format!("len(*{inner})")
864 } else {
865 field_expr
866 };
867 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
869 Some(field_expr[5..field_expr.len() - 1].to_string())
870 } else {
871 None
872 };
873
874 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
877 format!("*{field_expr}")
878 } else {
879 field_expr.clone()
880 };
881
882 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
887 let array_expr = &field_expr[..idx];
888 Some(array_expr.to_string())
889 } else {
890 None
891 };
892
893 let mut assertion_buf = String::new();
896 let out_ref = &mut assertion_buf;
897
898 match assertion.assertion_type.as_str() {
899 "equals" => {
900 if let Some(expected) = &assertion.value {
901 let go_val = json_to_go(expected);
902 if expected.is_string() {
904 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
906 format!("strings.TrimSpace(*{field_expr})")
907 } else {
908 format!("strings.TrimSpace({field_expr})")
909 };
910 if is_optional && !field_expr.starts_with("len(") {
911 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
912 } else {
913 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
914 }
915 } else if is_optional && !field_expr.starts_with("len(") {
916 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
917 } else {
918 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
919 }
920 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
921 let _ = writeln!(out_ref, "\t}}");
922 }
923 }
924 "contains" => {
925 if let Some(expected) = &assertion.value {
926 let go_val = json_to_go(expected);
927 let resolved_field = assertion.field.as_deref().unwrap_or("");
933 let resolved_name = field_resolver.resolve(resolved_field);
934 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
935 let is_opt =
936 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
937 let field_for_contains = if is_opt && field_is_array {
938 format!("strings.Join(*{field_expr}, \" \")")
939 } else if is_opt {
940 format!("string(*{field_expr})")
941 } else if field_is_array {
942 format!("strings.Join({field_expr}, \" \")")
943 } else {
944 format!("string({field_expr})")
945 };
946 if is_opt {
947 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
948 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
949 let _ = writeln!(
950 out_ref,
951 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
952 );
953 let _ = writeln!(out_ref, "\t}}");
954 let _ = writeln!(out_ref, "\t}}");
955 } else {
956 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
957 let _ = writeln!(
958 out_ref,
959 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
960 );
961 let _ = writeln!(out_ref, "\t}}");
962 }
963 }
964 }
965 "contains_all" => {
966 if let Some(values) = &assertion.values {
967 let resolved_field = assertion.field.as_deref().unwrap_or("");
968 let resolved_name = field_resolver.resolve(resolved_field);
969 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
970 let is_opt =
971 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
972 for val in values {
973 let go_val = json_to_go(val);
974 let field_for_contains = if is_opt && field_is_array {
975 format!("strings.Join(*{field_expr}, \" \")")
976 } else if is_opt {
977 format!("string(*{field_expr})")
978 } else if field_is_array {
979 format!("strings.Join({field_expr}, \" \")")
980 } else {
981 format!("string({field_expr})")
982 };
983 if is_opt {
984 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
985 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
986 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
987 let _ = writeln!(out_ref, "\t}}");
988 let _ = writeln!(out_ref, "\t}}");
989 } else {
990 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
991 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
992 let _ = writeln!(out_ref, "\t}}");
993 }
994 }
995 }
996 }
997 "not_contains" => {
998 if let Some(expected) = &assertion.value {
999 let go_val = json_to_go(expected);
1000 let resolved_field = assertion.field.as_deref().unwrap_or("");
1001 let resolved_name = field_resolver.resolve(resolved_field);
1002 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1003 let is_opt =
1004 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1005 let field_for_contains = if is_opt && field_is_array {
1006 format!("strings.Join(*{field_expr}, \" \")")
1007 } else if is_opt {
1008 format!("string(*{field_expr})")
1009 } else if field_is_array {
1010 format!("strings.Join({field_expr}, \" \")")
1011 } else {
1012 format!("string({field_expr})")
1013 };
1014 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1015 let _ = writeln!(
1016 out_ref,
1017 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1018 );
1019 let _ = writeln!(out_ref, "\t}}");
1020 }
1021 }
1022 "not_empty" => {
1023 let field_is_array = {
1026 let rf = assertion.field.as_deref().unwrap_or("");
1027 let rn = field_resolver.resolve(rf);
1028 field_resolver.is_array(rn)
1029 };
1030 if is_optional && !field_is_array {
1031 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1033 } else if is_optional {
1034 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1035 } else {
1036 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1037 }
1038 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1039 let _ = writeln!(out_ref, "\t}}");
1040 }
1041 "is_empty" => {
1042 let field_is_array = {
1043 let rf = assertion.field.as_deref().unwrap_or("");
1044 let rn = field_resolver.resolve(rf);
1045 field_resolver.is_array(rn)
1046 };
1047 if is_optional && !field_is_array {
1048 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1050 } else if is_optional {
1051 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1052 } else {
1053 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1054 }
1055 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1056 let _ = writeln!(out_ref, "\t}}");
1057 }
1058 "contains_any" => {
1059 if let Some(values) = &assertion.values {
1060 let resolved_field = assertion.field.as_deref().unwrap_or("");
1061 let resolved_name = field_resolver.resolve(resolved_field);
1062 let field_is_array = field_resolver.is_array(resolved_name);
1063 let is_opt =
1064 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1065 let field_for_contains = if is_opt && field_is_array {
1066 format!("strings.Join(*{field_expr}, \" \")")
1067 } else if is_opt {
1068 format!("*{field_expr}")
1069 } else if field_is_array {
1070 format!("strings.Join({field_expr}, \" \")")
1071 } else {
1072 field_expr.clone()
1073 };
1074 let _ = writeln!(out_ref, "\t{{");
1075 let _ = writeln!(out_ref, "\t\tfound := false");
1076 for val in values {
1077 let go_val = json_to_go(val);
1078 let _ = writeln!(
1079 out_ref,
1080 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1081 );
1082 }
1083 let _ = writeln!(out_ref, "\t\tif !found {{");
1084 let _ = writeln!(
1085 out_ref,
1086 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1087 );
1088 let _ = writeln!(out_ref, "\t\t}}");
1089 let _ = writeln!(out_ref, "\t}}");
1090 }
1091 }
1092 "greater_than" => {
1093 if let Some(val) = &assertion.value {
1094 let go_val = json_to_go(val);
1095 if is_optional {
1099 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1100 if let Some(n) = val.as_u64() {
1101 let next = n + 1;
1102 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1103 } else {
1104 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1105 }
1106 let _ = writeln!(
1107 out_ref,
1108 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1109 );
1110 let _ = writeln!(out_ref, "\t\t}}");
1111 let _ = writeln!(out_ref, "\t}}");
1112 } else if let Some(n) = val.as_u64() {
1113 let next = n + 1;
1114 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1115 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1116 let _ = writeln!(out_ref, "\t}}");
1117 } else {
1118 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1119 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1120 let _ = writeln!(out_ref, "\t}}");
1121 }
1122 }
1123 }
1124 "less_than" => {
1125 if let Some(val) = &assertion.value {
1126 let go_val = json_to_go(val);
1127 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1128 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1129 let _ = writeln!(out_ref, "\t}}");
1130 }
1131 }
1132 "greater_than_or_equal" => {
1133 if let Some(val) = &assertion.value {
1134 let go_val = json_to_go(val);
1135 if let Some(ref guard) = nil_guard_expr {
1136 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1137 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1138 let _ = writeln!(
1139 out_ref,
1140 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1141 );
1142 let _ = writeln!(out_ref, "\t\t}}");
1143 let _ = writeln!(out_ref, "\t}}");
1144 } else if is_optional && !field_expr.starts_with("len(") {
1145 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1147 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1148 let _ = writeln!(
1149 out_ref,
1150 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1151 );
1152 let _ = writeln!(out_ref, "\t\t}}");
1153 let _ = writeln!(out_ref, "\t}}");
1154 } else {
1155 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1156 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1157 let _ = writeln!(out_ref, "\t}}");
1158 }
1159 }
1160 }
1161 "less_than_or_equal" => {
1162 if let Some(val) = &assertion.value {
1163 let go_val = json_to_go(val);
1164 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1165 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1166 let _ = writeln!(out_ref, "\t}}");
1167 }
1168 }
1169 "starts_with" => {
1170 if let Some(expected) = &assertion.value {
1171 let go_val = json_to_go(expected);
1172 let field_for_prefix = if is_optional
1173 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1174 {
1175 format!("string(*{field_expr})")
1176 } else {
1177 format!("string({field_expr})")
1178 };
1179 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1180 let _ = writeln!(
1181 out_ref,
1182 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1183 );
1184 let _ = writeln!(out_ref, "\t}}");
1185 }
1186 }
1187 "count_min" => {
1188 if let Some(val) = &assertion.value {
1189 if let Some(n) = val.as_u64() {
1190 if is_optional {
1191 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1192 let _ = writeln!(
1193 out_ref,
1194 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1195 );
1196 let _ = writeln!(out_ref, "\t}}");
1197 } else {
1198 let _ = writeln!(
1199 out_ref,
1200 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1201 );
1202 }
1203 }
1204 }
1205 }
1206 "count_equals" => {
1207 if let Some(val) = &assertion.value {
1208 if let Some(n) = val.as_u64() {
1209 if is_optional {
1210 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1211 let _ = writeln!(
1212 out_ref,
1213 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1214 );
1215 let _ = writeln!(out_ref, "\t}}");
1216 } else {
1217 let _ = writeln!(
1218 out_ref,
1219 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1220 );
1221 }
1222 }
1223 }
1224 }
1225 "is_true" => {
1226 if is_optional {
1227 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1228 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1229 let _ = writeln!(out_ref, "\t}}");
1230 } else {
1231 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1232 }
1233 }
1234 "is_false" => {
1235 if is_optional {
1236 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1237 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1238 let _ = writeln!(out_ref, "\t}}");
1239 } else {
1240 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1241 }
1242 }
1243 "method_result" => {
1244 if let Some(method_name) = &assertion.method {
1245 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1246 let check = assertion.check.as_deref().unwrap_or("is_true");
1247 let deref_expr = if info.is_pointer {
1250 format!("*{}", info.call_expr)
1251 } else {
1252 info.call_expr.clone()
1253 };
1254 match check {
1255 "equals" => {
1256 if let Some(val) = &assertion.value {
1257 if val.is_boolean() {
1258 if val.as_bool() == Some(true) {
1259 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1260 } else {
1261 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1262 }
1263 } else {
1264 let go_val = if let Some(cast) = info.value_cast {
1268 if val.is_number() {
1269 format!("{cast}({})", json_to_go(val))
1270 } else {
1271 json_to_go(val)
1272 }
1273 } else {
1274 json_to_go(val)
1275 };
1276 let _ = writeln!(
1277 out_ref,
1278 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1279 );
1280 }
1281 }
1282 }
1283 "is_true" => {
1284 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1285 }
1286 "is_false" => {
1287 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1288 }
1289 "greater_than_or_equal" => {
1290 if let Some(val) = &assertion.value {
1291 let n = val.as_u64().unwrap_or(0);
1292 let cast = info.value_cast.unwrap_or("uint");
1294 let _ = writeln!(
1295 out_ref,
1296 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1297 );
1298 }
1299 }
1300 "count_min" => {
1301 if let Some(val) = &assertion.value {
1302 let n = val.as_u64().unwrap_or(0);
1303 let _ = writeln!(
1304 out_ref,
1305 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1306 );
1307 }
1308 }
1309 "contains" => {
1310 if let Some(val) = &assertion.value {
1311 let go_val = json_to_go(val);
1312 let _ = writeln!(
1313 out_ref,
1314 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1315 );
1316 }
1317 }
1318 "is_error" => {
1319 let _ = writeln!(out_ref, "\t{{");
1320 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1321 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1322 let _ = writeln!(out_ref, "\t}}");
1323 }
1324 other_check => {
1325 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1326 }
1327 }
1328 } else {
1329 panic!("Go e2e generator: method_result assertion missing 'method' field");
1330 }
1331 }
1332 "min_length" => {
1333 if let Some(val) = &assertion.value {
1334 if let Some(n) = val.as_u64() {
1335 if is_optional {
1336 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1337 let _ = writeln!(
1338 out_ref,
1339 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1340 );
1341 let _ = writeln!(out_ref, "\t}}");
1342 } else {
1343 let _ = writeln!(
1344 out_ref,
1345 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1346 );
1347 }
1348 }
1349 }
1350 }
1351 "max_length" => {
1352 if let Some(val) = &assertion.value {
1353 if let Some(n) = val.as_u64() {
1354 if is_optional {
1355 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1356 let _ = writeln!(
1357 out_ref,
1358 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1359 );
1360 let _ = writeln!(out_ref, "\t}}");
1361 } else {
1362 let _ = writeln!(
1363 out_ref,
1364 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1365 );
1366 }
1367 }
1368 }
1369 }
1370 "ends_with" => {
1371 if let Some(expected) = &assertion.value {
1372 let go_val = json_to_go(expected);
1373 let field_for_suffix = if is_optional
1374 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1375 {
1376 format!("string(*{field_expr})")
1377 } else {
1378 format!("string({field_expr})")
1379 };
1380 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1381 let _ = writeln!(
1382 out_ref,
1383 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1384 );
1385 let _ = writeln!(out_ref, "\t}}");
1386 }
1387 }
1388 "matches_regex" => {
1389 if let Some(expected) = &assertion.value {
1390 let go_val = json_to_go(expected);
1391 let field_for_regex = if is_optional
1392 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1393 {
1394 format!("*{field_expr}")
1395 } else {
1396 field_expr.clone()
1397 };
1398 let _ = writeln!(
1399 out_ref,
1400 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1401 );
1402 }
1403 }
1404 "not_error" => {
1405 }
1407 "error" => {
1408 }
1410 other => {
1411 panic!("Go e2e generator: unsupported assertion type: {other}");
1412 }
1413 }
1414
1415 if let Some(ref arr) = array_guard {
1418 if !assertion_buf.is_empty() {
1419 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1420 for line in assertion_buf.lines() {
1422 let _ = writeln!(out, "\t{line}");
1423 }
1424 let _ = writeln!(out, "\t}}");
1425 }
1426 } else {
1427 out.push_str(&assertion_buf);
1428 }
1429}
1430
1431struct GoMethodCallInfo {
1433 call_expr: String,
1435 is_pointer: bool,
1437 value_cast: Option<&'static str>,
1440}
1441
1442fn build_go_method_call(
1457 result_var: &str,
1458 method_name: &str,
1459 args: Option<&serde_json::Value>,
1460 import_alias: &str,
1461) -> GoMethodCallInfo {
1462 match method_name {
1463 "root_node_type" => GoMethodCallInfo {
1464 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1465 is_pointer: false,
1466 value_cast: None,
1467 },
1468 "named_children_count" => GoMethodCallInfo {
1469 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1470 is_pointer: false,
1471 value_cast: Some("uint"),
1472 },
1473 "has_error_nodes" => GoMethodCallInfo {
1474 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1475 is_pointer: true,
1476 value_cast: None,
1477 },
1478 "error_count" | "tree_error_count" => GoMethodCallInfo {
1479 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1480 is_pointer: true,
1481 value_cast: Some("uint"),
1482 },
1483 "tree_to_sexp" => GoMethodCallInfo {
1484 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1485 is_pointer: true,
1486 value_cast: None,
1487 },
1488 "contains_node_type" => {
1489 let node_type = args
1490 .and_then(|a| a.get("node_type"))
1491 .and_then(|v| v.as_str())
1492 .unwrap_or("");
1493 GoMethodCallInfo {
1494 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1495 is_pointer: true,
1496 value_cast: None,
1497 }
1498 }
1499 "find_nodes_by_type" => {
1500 let node_type = args
1501 .and_then(|a| a.get("node_type"))
1502 .and_then(|v| v.as_str())
1503 .unwrap_or("");
1504 GoMethodCallInfo {
1505 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1506 is_pointer: true,
1507 value_cast: None,
1508 }
1509 }
1510 "run_query" => {
1511 let query_source = args
1512 .and_then(|a| a.get("query_source"))
1513 .and_then(|v| v.as_str())
1514 .unwrap_or("");
1515 let language = args
1516 .and_then(|a| a.get("language"))
1517 .and_then(|v| v.as_str())
1518 .unwrap_or("");
1519 let query_lit = go_string_literal(query_source);
1520 let lang_lit = go_string_literal(language);
1521 GoMethodCallInfo {
1523 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1524 is_pointer: false,
1525 value_cast: None,
1526 }
1527 }
1528 other => {
1529 let method_pascal = other.to_upper_camel_case();
1530 GoMethodCallInfo {
1531 call_expr: format!("{result_var}.{method_pascal}()"),
1532 is_pointer: false,
1533 value_cast: None,
1534 }
1535 }
1536 }
1537}
1538
1539fn json_to_go(value: &serde_json::Value) -> String {
1541 match value {
1542 serde_json::Value::String(s) => go_string_literal(s),
1543 serde_json::Value::Bool(b) => b.to_string(),
1544 serde_json::Value::Number(n) => n.to_string(),
1545 serde_json::Value::Null => "nil".to_string(),
1546 other => go_string_literal(&other.to_string()),
1548 }
1549}
1550
1551fn visitor_struct_name(fixture_id: &str) -> String {
1560 use heck::ToUpperCamelCase;
1561 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1563}
1564
1565fn emit_go_visitor_struct(
1567 out: &mut String,
1568 struct_name: &str,
1569 visitor_spec: &crate::fixture::VisitorSpec,
1570 import_alias: &str,
1571) {
1572 let _ = writeln!(out, "type {struct_name} struct{{}}");
1573 for (method_name, action) in &visitor_spec.callbacks {
1574 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1575 }
1576}
1577
1578fn emit_go_visitor_method(
1580 out: &mut String,
1581 struct_name: &str,
1582 method_name: &str,
1583 action: &CallbackAction,
1584 import_alias: &str,
1585) {
1586 let camel_method = method_to_camel(method_name);
1587 let params = match method_name {
1588 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1589 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1590 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1591 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1592 "visit_code_inline"
1593 | "visit_strong"
1594 | "visit_emphasis"
1595 | "visit_strikethrough"
1596 | "visit_underline"
1597 | "visit_subscript"
1598 | "visit_superscript"
1599 | "visit_mark"
1600 | "visit_button"
1601 | "visit_summary"
1602 | "visit_figcaption"
1603 | "visit_definition_term"
1604 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1605 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1606 "visit_list_item" => {
1607 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1608 }
1609 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1610 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1611 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1612 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1613 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1614 "visit_audio" | "visit_video" | "visit_iframe" => {
1615 format!("_ {import_alias}.NodeContext, src string")
1616 }
1617 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1618 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1619 format!("_ {import_alias}.NodeContext, output string")
1620 }
1621 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1622 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1623 _ => format!("_ {import_alias}.NodeContext"),
1624 };
1625
1626 let _ = writeln!(
1627 out,
1628 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1629 );
1630 match action {
1631 CallbackAction::Skip => {
1632 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1633 }
1634 CallbackAction::Continue => {
1635 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1636 }
1637 CallbackAction::PreserveHtml => {
1638 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1639 }
1640 CallbackAction::Custom { output } => {
1641 let escaped = go_string_literal(output);
1642 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1643 }
1644 CallbackAction::CustomTemplate { template } => {
1645 let (fmt_str, fmt_args) = template_to_sprintf(template);
1648 let escaped_fmt = go_string_literal(&fmt_str);
1649 if fmt_args.is_empty() {
1650 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1651 } else {
1652 let args_str = fmt_args.join(", ");
1653 let _ = writeln!(
1654 out,
1655 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1656 );
1657 }
1658 }
1659 }
1660 let _ = writeln!(out, "}}");
1661}
1662
1663fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1667 let mut fmt_str = String::new();
1668 let mut args: Vec<String> = Vec::new();
1669 let mut chars = template.chars().peekable();
1670 while let Some(c) = chars.next() {
1671 if c == '{' {
1672 let mut name = String::new();
1674 for inner in chars.by_ref() {
1675 if inner == '}' {
1676 break;
1677 }
1678 name.push(inner);
1679 }
1680 fmt_str.push_str("%s");
1681 args.push(name);
1682 } else {
1683 fmt_str.push(c);
1684 }
1685 }
1686 (fmt_str, args)
1687}
1688
1689fn method_to_camel(snake: &str) -> String {
1691 use heck::ToUpperCamelCase;
1692 snake.to_upper_camel_case()
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697 use super::*;
1698 use crate::config::{CallConfig, E2eConfig};
1699 use crate::field_access::FieldResolver;
1700 use crate::fixture::{Assertion, Fixture};
1701
1702 fn make_fixture(id: &str) -> Fixture {
1703 Fixture {
1704 id: id.to_string(),
1705 category: None,
1706 description: "test fixture".to_string(),
1707 tags: vec![],
1708 skip: None,
1709 call: None,
1710 input: serde_json::Value::Null,
1711 mock_response: None,
1712 source: String::new(),
1713 http: None,
1714 assertions: vec![Assertion {
1715 assertion_type: "not_error".to_string(),
1716 field: None,
1717 value: None,
1718 values: None,
1719 method: None,
1720 args: None,
1721 check: None,
1722 }],
1723 visitor: None,
1724 }
1725 }
1726
1727 #[test]
1731 fn test_go_method_name_uses_go_casing() {
1732 let e2e_config = E2eConfig {
1733 call: CallConfig {
1734 function: "clean_extracted_text".to_string(),
1735 module: "github.com/example/mylib".to_string(),
1736 result_var: "result".to_string(),
1737 r#async: false,
1738 path: None,
1739 method: None,
1740 args: vec![],
1741 overrides: std::collections::HashMap::new(),
1742 returns_result: true,
1743 returns_void: false,
1744 skip_languages: vec![],
1745 },
1746 ..E2eConfig::default()
1747 };
1748
1749 let fixture = make_fixture("basic_text");
1750 let resolver = FieldResolver::new(
1751 &std::collections::HashMap::new(),
1752 &std::collections::HashSet::new(),
1753 &std::collections::HashSet::new(),
1754 &std::collections::HashSet::new(),
1755 );
1756 let mut out = String::new();
1757 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
1758
1759 assert!(
1760 out.contains("kreuzberg.CleanExtractedText("),
1761 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
1762 );
1763 assert!(
1764 !out.contains("kreuzberg.clean_extracted_text("),
1765 "must not emit raw snake_case method name, got:\n{out}"
1766 );
1767 }
1768}