1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::Language;
10use alef_core::config::ResolvedCrateConfig;
11use alef_core::hash::{self, CommentStyle};
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::fmt::Write as FmtWrite;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use super::client;
19
20pub struct GoCodegen;
22
23impl E2eCodegen for GoCodegen {
24 fn generate(
25 &self,
26 groups: &[FixtureGroup],
27 e2e_config: &E2eConfig,
28 config: &ResolvedCrateConfig,
29 ) -> Result<Vec<GeneratedFile>> {
30 let lang = self.language_name();
31 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
32
33 let mut files = Vec::new();
34
35 let call = &e2e_config.call;
37 let overrides = call.overrides.get(lang);
38 let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
39 let module_path = overrides
40 .and_then(|o| o.module.as_ref())
41 .cloned()
42 .or_else(|| configured_go_module_path.clone())
43 .unwrap_or_else(|| call.module.clone());
44 let import_alias = overrides
45 .and_then(|o| o.alias.as_ref())
46 .cloned()
47 .unwrap_or_else(|| "pkg".to_string());
48
49 let go_pkg = e2e_config.resolve_package("go");
51 let go_module_path = go_pkg
52 .as_ref()
53 .and_then(|p| p.module.as_ref())
54 .cloned()
55 .or_else(|| configured_go_module_path.clone())
56 .unwrap_or_else(|| module_path.clone());
57 let replace_path = go_pkg
58 .as_ref()
59 .and_then(|p| p.path.as_ref())
60 .cloned()
61 .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
62 let go_version = go_pkg
63 .as_ref()
64 .and_then(|p| p.version.as_ref())
65 .cloned()
66 .unwrap_or_else(|| {
67 config
68 .resolved_version()
69 .map(|v| format!("v{v}"))
70 .unwrap_or_else(|| "v0.0.0".to_string())
71 });
72 let field_resolver = FieldResolver::new(
73 &e2e_config.fields,
74 &e2e_config.fields_optional,
75 &e2e_config.result_fields,
76 &e2e_config.fields_array,
77 &std::collections::HashSet::new(),
78 );
79
80 let effective_replace = match e2e_config.dep_mode {
83 crate::config::DependencyMode::Registry => None,
84 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
85 };
86 let effective_go_version = if effective_replace.is_some() {
92 fix_go_major_version(&go_module_path, &go_version)
93 } else {
94 go_version.clone()
95 };
96 files.push(GeneratedFile {
97 path: output_base.join("go.mod"),
98 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
99 generated_header: false,
100 });
101
102 let emits_executable_test =
104 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
105 let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
106 emits_executable_test(f)
107 && f.assertions.iter().any(|a| {
108 matches!(
109 a.assertion_type.as_str(),
110 "contains" | "contains_all" | "contains_any" | "not_contains"
111 ) && {
112 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
113 e2e_config.resolve_call(f.call.as_deref()).result_is_array
114 } else {
115 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
116 field_resolver.is_array(resolved_name)
117 }
118 }
119 })
120 });
121
122 if needs_json_stringify {
124 files.push(GeneratedFile {
125 path: output_base.join("helpers_test.go"),
126 content: render_helpers_test_go(),
127 generated_header: true,
128 });
129 }
130
131 let has_file_fixtures = groups
139 .iter()
140 .flat_map(|g| g.fixtures.iter())
141 .any(|f| f.http.is_none() && !f.needs_mock_server());
142
143 let needs_main_test = has_file_fixtures
144 || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
145 if f.needs_mock_server() {
146 return true;
147 }
148 let cc = e2e_config.resolve_call(f.call.as_deref());
149 let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
150 go_override.and_then(|o| o.client_factory.as_deref()).is_some()
151 });
152
153 if needs_main_test {
154 files.push(GeneratedFile {
155 path: output_base.join("main_test.go"),
156 content: render_main_test_go(),
157 generated_header: true,
158 });
159 }
160
161 for group in groups {
163 let active: Vec<&Fixture> = group
164 .fixtures
165 .iter()
166 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
167 .collect();
168
169 if active.is_empty() {
170 continue;
171 }
172
173 let filename = format!("{}_test.go", sanitize_filename(&group.category));
174 let content = render_test_file(
175 &group.category,
176 &active,
177 &module_path,
178 &import_alias,
179 &field_resolver,
180 e2e_config,
181 );
182 files.push(GeneratedFile {
183 path: output_base.join(filename),
184 content,
185 generated_header: true,
186 });
187 }
188
189 Ok(files)
190 }
191
192 fn language_name(&self) -> &'static str {
193 "go"
194 }
195}
196
197fn fix_go_major_version(module_path: &str, version: &str) -> String {
204 let major = module_path
206 .rsplit('/')
207 .next()
208 .and_then(|seg| seg.strip_prefix('v'))
209 .and_then(|n| n.parse::<u64>().ok())
210 .filter(|&n| n >= 2);
211
212 let Some(n) = major else {
213 return version.to_string();
214 };
215
216 let expected_prefix = format!("v{n}.");
218 if version.starts_with(&expected_prefix) {
219 return version.to_string();
220 }
221
222 format!("v{n}.0.0")
223}
224
225fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
226 let mut out = String::new();
227 let _ = writeln!(out, "module e2e_go");
228 let _ = writeln!(out);
229 let _ = writeln!(out, "go 1.26");
230 let _ = writeln!(out);
231 let _ = writeln!(out, "require (");
232 let _ = writeln!(out, "\t{go_module_path} {version}");
233 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
234 let _ = writeln!(out, ")");
235
236 if let Some(path) = replace_path {
237 let _ = writeln!(out);
238 let _ = writeln!(out, "replace {go_module_path} => {path}");
239 }
240
241 out
242}
243
244fn render_main_test_go() -> String {
250 let mut out = String::new();
252 let _ = writeln!(out, "package e2e_test");
253 let _ = writeln!(out);
254 let _ = writeln!(out, "import (");
255 let _ = writeln!(out, "\t\"bufio\"");
256 let _ = writeln!(out, "\t\"io\"");
257 let _ = writeln!(out, "\t\"os\"");
258 let _ = writeln!(out, "\t\"os/exec\"");
259 let _ = writeln!(out, "\t\"path/filepath\"");
260 let _ = writeln!(out, "\t\"runtime\"");
261 let _ = writeln!(out, "\t\"strings\"");
262 let _ = writeln!(out, "\t\"testing\"");
263 let _ = writeln!(out, ")");
264 let _ = writeln!(out);
265 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
266 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
267 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
268 let _ = writeln!(out);
269 let _ = writeln!(
270 out,
271 "\t// Change to the test_documents directory so that fixture file paths like"
272 );
273 let _ = writeln!(
274 out,
275 "\t// \"pdf/fake_memo.pdf\" resolve correctly when running go test from e2e/go/."
276 );
277 let _ = writeln!(
278 out,
279 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"test_documents\")"
280 );
281 let _ = writeln!(out, "\tif err := os.Chdir(testDocumentsDir); err != nil {{");
282 let _ = writeln!(out, "\t\tpanic(err)");
283 let _ = writeln!(out, "\t}}");
284 let _ = writeln!(out);
285 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
286 let _ = writeln!(
287 out,
288 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
289 );
290 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
291 let _ = writeln!(
292 out,
293 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
294 );
295 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
296 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
297 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
298 let _ = writeln!(out, "\t\tif err != nil {{");
299 let _ = writeln!(out, "\t\t\tpanic(err)");
300 let _ = writeln!(out, "\t\t}}");
301 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
302 let _ = writeln!(out, "\t\t\tpanic(err)");
303 let _ = writeln!(out, "\t\t}}");
304 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
305 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
306 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
307 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
308 let _ = writeln!(
309 out,
310 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
311 );
312 let _ = writeln!(out, "\t\t\t\tbreak");
313 let _ = writeln!(out, "\t\t\t}}");
314 let _ = writeln!(out, "\t\t}}");
315 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
316 let _ = writeln!(out, "\t\tcode := m.Run()");
317 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
318 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
319 let _ = writeln!(out, "\t\tos.Exit(code)");
320 let _ = writeln!(out, "\t}} else {{");
321 let _ = writeln!(out, "\t\tcode := m.Run()");
322 let _ = writeln!(out, "\t\tos.Exit(code)");
323 let _ = writeln!(out, "\t}}");
324 let _ = writeln!(out, "}}");
325 out
326}
327
328fn render_helpers_test_go() -> String {
331 let mut out = String::new();
332 let _ = writeln!(out, "package e2e_test");
333 let _ = writeln!(out);
334 let _ = writeln!(out, "import \"encoding/json\"");
335 let _ = writeln!(out);
336 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
337 let _ = writeln!(
338 out,
339 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
340 );
341 let _ = writeln!(out, "func jsonString(value any) string {{");
342 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
343 let _ = writeln!(out, "\tif err != nil {{");
344 let _ = writeln!(out, "\t\treturn \"\"");
345 let _ = writeln!(out, "\t}}");
346 let _ = writeln!(out, "\treturn string(encoded)");
347 let _ = writeln!(out, "}}");
348 out
349}
350
351fn render_test_file(
352 category: &str,
353 fixtures: &[&Fixture],
354 go_module_path: &str,
355 import_alias: &str,
356 field_resolver: &FieldResolver,
357 e2e_config: &crate::config::E2eConfig,
358) -> String {
359 let mut out = String::new();
360 let emits_executable_test =
361 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
362
363 out.push_str(&hash::header(CommentStyle::DoubleSlash));
365 let _ = writeln!(out);
366
367 let needs_pkg = fixtures
376 .iter()
377 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
378
379 let needs_os = fixtures.iter().any(|f| {
382 if f.is_http_test() {
383 return true;
384 }
385 if !emits_executable_test(f) {
386 return false;
387 }
388 let call_config = e2e_config.resolve_call(f.call.as_deref());
389 let go_override = call_config
390 .overrides
391 .get("go")
392 .or_else(|| e2e_config.call.overrides.get("go"));
393 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
394 return true;
395 }
396 let call_args = &call_config.args;
397 if call_args.iter().any(|a| a.arg_type == "mock_url") {
400 return true;
401 }
402 call_args.iter().any(|a| {
403 if a.arg_type != "bytes" {
404 return false;
405 }
406 let mut current = &f.input;
409 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
410 for segment in path.split('.') {
411 match current.get(segment) {
412 Some(next) => current = next,
413 None => return false,
414 }
415 }
416 current.is_string()
417 })
418 });
419
420 let needs_filepath = false;
423
424 let _needs_json_stringify = fixtures.iter().any(|f| {
425 emits_executable_test(f)
426 && f.assertions.iter().any(|a| {
427 matches!(
428 a.assertion_type.as_str(),
429 "contains" | "contains_all" | "contains_any" | "not_contains"
430 ) && {
431 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
434 e2e_config.resolve_call(f.call.as_deref()).result_is_array
436 } else {
437 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
439 field_resolver.is_array(resolved_name)
440 }
441 }
442 })
443 });
444
445 let needs_json = fixtures.iter().any(|f| {
449 if let Some(http) = &f.http {
452 let body_needs_json = http
453 .expected_response
454 .body
455 .as_ref()
456 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
457 let partial_needs_json = http.expected_response.body_partial.is_some();
458 let ve_needs_json = http
459 .expected_response
460 .validation_errors
461 .as_ref()
462 .is_some_and(|v| !v.is_empty());
463 if body_needs_json || partial_needs_json || ve_needs_json {
464 return true;
465 }
466 }
467 if !emits_executable_test(f) {
468 return false;
469 }
470
471 let call = e2e_config.resolve_call(f.call.as_deref());
472 let call_args = &call.args;
473 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
475 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
476 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
477 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
478 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
479 })
480 };
481 let go_override = call.overrides.get("go");
483 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
484 e2e_config
485 .call
486 .overrides
487 .get("go")
488 .and_then(|o| o.options_type.as_deref())
489 });
490 let has_json_obj = call_args.iter().any(|a| {
491 if a.arg_type != "json_object" {
492 return false;
493 }
494 let v = if a.field == "input" {
495 &f.input
496 } else {
497 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
498 f.input.get(field).unwrap_or(&serde_json::Value::Null)
499 };
500 if v.is_array() {
501 return true;
502 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
504 });
505 has_handle || has_json_obj
506 });
507
508 let needs_base64 = false;
513
514 let needs_fmt = fixtures.iter().any(|f| {
519 f.visitor.as_ref().is_some_and(|v| {
520 v.callbacks.values().any(|action| {
521 if let CallbackAction::CustomTemplate { template } = action {
522 template.contains('{')
523 } else {
524 false
525 }
526 })
527 }) || (emits_executable_test(f)
528 && f.assertions.iter().any(|a| {
529 matches!(
530 a.assertion_type.as_str(),
531 "contains" | "contains_all" | "contains_any" | "not_contains"
532 ) && {
533 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
538 !e2e_config.resolve_call(f.call.as_deref()).result_is_array
540 } else {
541 let field = a.field.as_deref().unwrap_or("");
545 let resolved_name = field_resolver.resolve(field);
546 !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
547 }
548 }
549 }))
550 });
551
552 let needs_strings = fixtures.iter().any(|f| {
555 if !emits_executable_test(f) {
556 return false;
557 }
558 f.assertions.iter().any(|a| {
559 let type_needs_strings = if a.assertion_type == "equals" {
560 a.value.as_ref().is_some_and(|v| v.is_string())
562 } else {
563 matches!(
564 a.assertion_type.as_str(),
565 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
566 )
567 };
568 let field_valid = a
569 .field
570 .as_ref()
571 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
572 .unwrap_or(true);
573 type_needs_strings && field_valid
574 })
575 });
576
577 let needs_assert = fixtures.iter().any(|f| {
579 if !emits_executable_test(f) {
580 return false;
581 }
582 f.assertions.iter().any(|a| {
583 let field_valid = a
584 .field
585 .as_ref()
586 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
587 .unwrap_or(true);
588 let synthetic_field_needs_assert = match a.field.as_deref() {
589 Some("chunks_have_content" | "chunks_have_embeddings") => {
590 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
591 }
592 Some("embeddings") => {
593 matches!(
594 a.assertion_type.as_str(),
595 "count_equals" | "count_min" | "not_empty" | "is_empty"
596 )
597 }
598 _ => false,
599 };
600 let type_needs_assert = matches!(
601 a.assertion_type.as_str(),
602 "count_equals"
603 | "count_min"
604 | "count_max"
605 | "is_true"
606 | "is_false"
607 | "method_result"
608 | "min_length"
609 | "max_length"
610 | "matches_regex"
611 );
612 synthetic_field_needs_assert || type_needs_assert && field_valid
613 })
614 });
615
616 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
618 let needs_http = has_http_fixtures;
619 let needs_io = has_http_fixtures;
621
622 let needs_reflect = fixtures.iter().any(|f| {
625 if let Some(http) = &f.http {
626 let body_needs_reflect = http
627 .expected_response
628 .body
629 .as_ref()
630 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
631 let partial_needs_reflect = http.expected_response.body_partial.is_some();
632 body_needs_reflect || partial_needs_reflect
633 } else {
634 false
635 }
636 });
637
638 let _ = writeln!(out, "// E2e tests for category: {category}");
639 let _ = writeln!(out, "package e2e_test");
640 let _ = writeln!(out);
641 let _ = writeln!(out, "import (");
642 if needs_base64 {
643 let _ = writeln!(out, "\t\"encoding/base64\"");
644 }
645 if needs_json || needs_reflect {
646 let _ = writeln!(out, "\t\"encoding/json\"");
647 }
648 if needs_fmt {
649 let _ = writeln!(out, "\t\"fmt\"");
650 }
651 if needs_io {
652 let _ = writeln!(out, "\t\"io\"");
653 }
654 if needs_http {
655 let _ = writeln!(out, "\t\"net/http\"");
656 }
657 if needs_os {
658 let _ = writeln!(out, "\t\"os\"");
659 }
660 let _ = needs_filepath; if needs_reflect {
662 let _ = writeln!(out, "\t\"reflect\"");
663 }
664 if needs_strings {
665 let _ = writeln!(out, "\t\"strings\"");
666 }
667 let _ = writeln!(out, "\t\"testing\"");
668 if needs_assert {
669 let _ = writeln!(out);
670 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
671 }
672 if needs_pkg {
673 let _ = writeln!(out);
674 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
675 }
676 let _ = writeln!(out, ")");
677 let _ = writeln!(out);
678
679 for fixture in fixtures.iter() {
681 if let Some(visitor_spec) = &fixture.visitor {
682 let struct_name = visitor_struct_name(&fixture.id);
683 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
684 let _ = writeln!(out);
685 }
686 }
687
688 for (i, fixture) in fixtures.iter().enumerate() {
689 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
690 if i + 1 < fixtures.len() {
691 let _ = writeln!(out);
692 }
693 }
694
695 while out.ends_with("\n\n") {
697 out.pop();
698 }
699 if !out.ends_with('\n') {
700 out.push('\n');
701 }
702 out
703}
704
705fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
714 if fixture.is_http_test() {
716 return false;
717 }
718 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
719 if call_config.skip_languages.iter().any(|l| l == "go") {
722 return false;
723 }
724 let go_override = call_config
725 .overrides
726 .get("go")
727 .or_else(|| e2e_config.call.overrides.get("go"));
728 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
731 return true;
732 }
733 let fn_name = go_override
737 .and_then(|o| o.function.as_deref())
738 .filter(|s| !s.is_empty())
739 .unwrap_or(call_config.function.as_str());
740 !fn_name.is_empty()
741}
742
743fn render_test_function(
744 out: &mut String,
745 fixture: &Fixture,
746 import_alias: &str,
747 field_resolver: &FieldResolver,
748 e2e_config: &crate::config::E2eConfig,
749) {
750 let fn_name = fixture.id.to_upper_camel_case();
751 let description = &fixture.description;
752
753 if fixture.http.is_some() {
755 render_http_test_function(out, fixture);
756 return;
757 }
758
759 if !fixture_has_go_callable(fixture, e2e_config) {
764 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
765 let _ = writeln!(out, "\t// {description}");
766 let _ = writeln!(
767 out,
768 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
769 );
770 let _ = writeln!(out, "}}");
771 return;
772 }
773
774 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
776 let lang = "go";
777 let overrides = call_config.overrides.get(lang);
778
779 let base_function_name = overrides
783 .and_then(|o| o.function.as_deref())
784 .unwrap_or(&call_config.function);
785 let function_name = to_go_name(base_function_name);
786 let result_var = &call_config.result_var;
787 let args = &call_config.args;
788
789 let returns_result = overrides
792 .and_then(|o| o.returns_result)
793 .unwrap_or(call_config.returns_result);
794
795 let returns_void = call_config.returns_void;
798
799 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
802 if call_config.result_is_simple {
803 return true;
804 }
805 call_config
806 .overrides
807 .get("rust")
808 .map(|o| o.result_is_simple)
809 .unwrap_or(false)
810 });
811
812 let result_is_array = overrides
815 .map(|o| o.result_is_array)
816 .unwrap_or(call_config.result_is_array);
817
818 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
820 e2e_config
821 .call
822 .overrides
823 .get("go")
824 .and_then(|o| o.options_type.as_deref())
825 });
826
827 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
829 e2e_config
830 .call
831 .overrides
832 .get("go")
833 .map(|o| o.options_ptr)
834 .unwrap_or(false)
835 });
836
837 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
838
839 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
842 e2e_config
843 .call
844 .overrides
845 .get(lang)
846 .and_then(|o| o.client_factory.as_deref())
847 });
848
849 let (mut setup_lines, args_str) = build_args_and_setup(
850 &fixture.input,
851 args,
852 import_alias,
853 call_options_type,
854 &fixture.id,
855 call_options_ptr,
856 );
857
858 let mut visitor_opts_var: Option<String> = None;
861 if fixture.visitor.is_some() {
862 let struct_name = visitor_struct_name(&fixture.id);
863 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
864 let opts_type = call_options_type.unwrap_or("ConversionOptions");
866 let opts_var = "opts".to_string();
867 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
868 setup_lines.push("opts.Visitor = visitor".to_string());
869 visitor_opts_var = Some(opts_var);
870 }
871
872 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
873 let final_args = {
874 let mut parts: Vec<String> = Vec::new();
875 if !args_str.is_empty() {
876 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
878 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
879 } else {
880 args_str
881 };
882 parts.push(processed_args);
883 }
884 parts.extend(go_extra_args);
885 parts.join(", ")
886 };
887
888 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
889 let _ = writeln!(out, "\t// {description}");
890
891 for line in &setup_lines {
892 let _ = writeln!(out, "\t{line}");
893 }
894
895 let call_prefix = if let Some(factory) = client_factory {
899 let factory_name = to_go_name(factory);
900 let fixture_id = &fixture.id;
901 let _ = writeln!(
902 out,
903 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
904 );
905 let _ = writeln!(
906 out,
907 "\tclient, clientErr := {import_alias}.{factory_name}(\"test-key\", &mockURL, nil, nil, nil)"
908 );
909 let _ = writeln!(out, "\tif clientErr != nil {{");
910 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
911 let _ = writeln!(out, "\t}}");
912 "client".to_string()
913 } else {
914 import_alias.to_string()
915 };
916
917 let binding_returns_error_pre = args
922 .iter()
923 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
924 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
925
926 if expects_error {
927 if effective_returns_result_pre && !returns_void {
928 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
929 } else {
930 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
931 }
932 let _ = writeln!(out, "\tif err == nil {{");
933 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
934 let _ = writeln!(out, "\t}}");
935 let _ = writeln!(out, "}}");
936 return;
937 }
938
939 let has_usable_assertion = fixture.assertions.iter().any(|a| {
943 if a.assertion_type == "not_error" || a.assertion_type == "error" {
944 return false;
945 }
946 if a.assertion_type == "method_result" {
948 return true;
949 }
950 match &a.field {
951 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
952 _ => true,
953 }
954 });
955
956 let binding_returns_error = args
963 .iter()
964 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
965 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
967
968 if !effective_returns_result && result_is_simple {
974 let result_binding = if has_usable_assertion {
976 result_var.to_string()
977 } else {
978 "_".to_string()
979 };
980 let assign_op = if result_binding == "_" { "=" } else { ":=" };
982 let _ = writeln!(
983 out,
984 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
985 );
986 if has_usable_assertion && result_binding != "_" {
987 if result_is_array {
988 let _ = writeln!(out, "\tvalue := {result_var}");
990 } else {
991 let only_nil_assertions = fixture
994 .assertions
995 .iter()
996 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
997 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
998 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
999
1000 if !only_nil_assertions {
1001 let _ = writeln!(out, "\tif {result_var} == nil {{");
1003 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1004 let _ = writeln!(out, "\t}}");
1005 let _ = writeln!(out, "\tvalue := *{result_var}");
1006 }
1007 }
1008 }
1009 } else if !effective_returns_result || returns_void {
1010 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1013 let _ = writeln!(out, "\tif err != nil {{");
1014 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1015 let _ = writeln!(out, "\t}}");
1016 let _ = writeln!(out, "}}");
1018 return;
1019 } else {
1020 let result_binding = if has_usable_assertion {
1022 result_var.to_string()
1023 } else {
1024 "_".to_string()
1025 };
1026 let _ = writeln!(
1027 out,
1028 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1029 );
1030 let _ = writeln!(out, "\tif err != nil {{");
1031 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1032 let _ = writeln!(out, "\t}}");
1033 if result_is_simple && has_usable_assertion && result_binding != "_" {
1034 if result_is_array {
1035 let _ = writeln!(out, "\tvalue := {}", result_var);
1037 } else {
1038 let only_nil_assertions = fixture
1041 .assertions
1042 .iter()
1043 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1044 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1045 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1046
1047 if !only_nil_assertions {
1048 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1050 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1051 let _ = writeln!(out, "\t}}");
1052 let _ = writeln!(out, "\tvalue := *{}", result_var);
1053 }
1054 }
1055 }
1056 }
1057
1058 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1061 let only_nil_assertions = fixture
1062 .assertions
1063 .iter()
1064 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1065 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1066 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1067 !only_nil_assertions
1068 } else {
1069 result_is_simple && has_usable_assertion
1070 };
1071
1072 let effective_result_var = if has_deref_value {
1073 "value".to_string()
1074 } else {
1075 result_var.to_string()
1076 };
1077
1078 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1083 for assertion in &fixture.assertions {
1084 if let Some(f) = &assertion.field {
1085 if !f.is_empty() {
1086 let resolved = field_resolver.resolve(f);
1087 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1088 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1093 let is_array_field = field_resolver.is_array(resolved);
1094 if !is_string_field || is_array_field {
1095 continue;
1098 }
1099 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1100 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1101 if field_resolver.has_map_access(f) {
1102 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1105 } else {
1106 let _ = writeln!(out, "\tvar {local_var} string");
1107 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1108 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1112 let _ = writeln!(out, "\t}}");
1113 }
1114 optional_locals.insert(f.clone(), local_var);
1115 }
1116 }
1117 }
1118 }
1119
1120 for assertion in &fixture.assertions {
1122 if let Some(f) = &assertion.field {
1123 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1124 let parts: Vec<&str> = f.split('.').collect();
1127 let mut guard_expr: Option<String> = None;
1128 for i in 1..parts.len() {
1129 let prefix = parts[..i].join(".");
1130 let resolved_prefix = field_resolver.resolve(&prefix);
1131 if field_resolver.is_optional(resolved_prefix) {
1132 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
1133 guard_expr = Some(accessor);
1134 break;
1135 }
1136 }
1137 if let Some(guard) = guard_expr {
1138 if field_resolver.is_valid_for_result(f) {
1141 let _ = writeln!(out, "\tif {guard} != nil {{");
1142 let mut nil_buf = String::new();
1145 render_assertion(
1146 &mut nil_buf,
1147 assertion,
1148 &effective_result_var,
1149 import_alias,
1150 field_resolver,
1151 &optional_locals,
1152 result_is_simple,
1153 result_is_array,
1154 );
1155 for line in nil_buf.lines() {
1156 let _ = writeln!(out, "\t{line}");
1157 }
1158 let _ = writeln!(out, "\t}}");
1159 } else {
1160 render_assertion(
1161 out,
1162 assertion,
1163 &effective_result_var,
1164 import_alias,
1165 field_resolver,
1166 &optional_locals,
1167 result_is_simple,
1168 result_is_array,
1169 );
1170 }
1171 continue;
1172 }
1173 }
1174 }
1175 render_assertion(
1176 out,
1177 assertion,
1178 &effective_result_var,
1179 import_alias,
1180 field_resolver,
1181 &optional_locals,
1182 result_is_simple,
1183 result_is_array,
1184 );
1185 }
1186
1187 let _ = writeln!(out, "}}");
1188}
1189
1190fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1196 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1197}
1198
1199struct GoTestClientRenderer;
1211
1212impl client::TestClientRenderer for GoTestClientRenderer {
1213 fn language_name(&self) -> &'static str {
1214 "go"
1215 }
1216
1217 fn sanitize_test_name(&self, id: &str) -> String {
1221 id.to_upper_camel_case()
1222 }
1223
1224 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1227 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1228 let _ = writeln!(out, "\t// {description}");
1229 if let Some(reason) = skip_reason {
1230 let escaped = go_string_literal(reason);
1231 let _ = writeln!(out, "\tt.Skip({escaped})");
1232 }
1233 }
1234
1235 fn render_test_close(&self, out: &mut String) {
1236 let _ = writeln!(out, "}}");
1237 }
1238
1239 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1245 let method = ctx.method.to_uppercase();
1246 let path = ctx.path;
1247
1248 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1249 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1250 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1251 let _ = writeln!(out, "\t}}");
1252
1253 let body_expr = if let Some(body) = ctx.body {
1255 let json = serde_json::to_string(body).unwrap_or_default();
1256 let escaped = go_string_literal(&json);
1257 format!("strings.NewReader({})", escaped)
1258 } else {
1259 "strings.NewReader(\"\")".to_string()
1260 };
1261
1262 let _ = writeln!(out, "\tbody := {body_expr}");
1263 let _ = writeln!(
1264 out,
1265 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1266 );
1267 let _ = writeln!(out, "\tif err != nil {{");
1268 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1269 let _ = writeln!(out, "\t}}");
1270
1271 if ctx.body.is_some() {
1273 let content_type = ctx.content_type.unwrap_or("application/json");
1274 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1275 }
1276
1277 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1279 header_names.sort();
1280 for name in header_names {
1281 let value = &ctx.headers[name];
1282 let escaped_name = go_string_literal(name);
1283 let escaped_value = go_string_literal(value);
1284 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1285 }
1286
1287 if !ctx.cookies.is_empty() {
1289 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1290 cookie_names.sort();
1291 for name in cookie_names {
1292 let value = &ctx.cookies[name];
1293 let escaped_name = go_string_literal(name);
1294 let escaped_value = go_string_literal(value);
1295 let _ = writeln!(
1296 out,
1297 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1298 );
1299 }
1300 }
1301
1302 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1304 let _ = writeln!(
1305 out,
1306 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1307 );
1308 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1309 let _ = writeln!(out, "\t\t}},");
1310 let _ = writeln!(out, "\t}}");
1311 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1312 let _ = writeln!(out, "\tif err != nil {{");
1313 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1314 let _ = writeln!(out, "\t}}");
1315 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1316
1317 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1321 let _ = writeln!(out, "\tif err != nil {{");
1322 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1323 let _ = writeln!(out, "\t}}");
1324 let _ = writeln!(out, "\t_ = bodyBytes");
1325 }
1326
1327 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1328 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1329 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1330 let _ = writeln!(out, "\t}}");
1331 }
1332
1333 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1336 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1338 return;
1339 }
1340 if name.eq_ignore_ascii_case("connection") {
1342 return;
1343 }
1344 let escaped_name = go_string_literal(name);
1345 let escaped_value = go_string_literal(expected);
1346 let _ = writeln!(
1347 out,
1348 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1349 );
1350 let _ = writeln!(
1351 out,
1352 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1353 );
1354 let _ = writeln!(out, "\t}}");
1355 }
1356
1357 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1362 match expected {
1363 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1364 let json_str = serde_json::to_string(expected).unwrap_or_default();
1365 let escaped = go_string_literal(&json_str);
1366 let _ = writeln!(out, "\tvar got any");
1367 let _ = writeln!(out, "\tvar want any");
1368 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1369 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1370 let _ = writeln!(out, "\t}}");
1371 let _ = writeln!(
1372 out,
1373 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1374 );
1375 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1376 let _ = writeln!(out, "\t}}");
1377 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1378 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1379 let _ = writeln!(out, "\t}}");
1380 }
1381 serde_json::Value::String(s) => {
1382 let escaped = go_string_literal(s);
1383 let _ = writeln!(out, "\twant := {escaped}");
1384 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1385 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1386 let _ = writeln!(out, "\t}}");
1387 }
1388 other => {
1389 let escaped = go_string_literal(&other.to_string());
1390 let _ = writeln!(out, "\twant := {escaped}");
1391 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1392 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1393 let _ = writeln!(out, "\t}}");
1394 }
1395 }
1396 }
1397
1398 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1401 if let Some(obj) = expected.as_object() {
1402 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1403 let _ = writeln!(
1404 out,
1405 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1406 );
1407 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1408 let _ = writeln!(out, "\t}}");
1409 for (key, val) in obj {
1410 let escaped_key = go_string_literal(key);
1411 let json_val = serde_json::to_string(val).unwrap_or_default();
1412 let escaped_val = go_string_literal(&json_val);
1413 let _ = writeln!(out, "\t{{");
1414 let _ = writeln!(out, "\t\tvar _wantVal any");
1415 let _ = writeln!(
1416 out,
1417 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1418 );
1419 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1420 let _ = writeln!(out, "\t\t}}");
1421 let _ = writeln!(
1422 out,
1423 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1424 );
1425 let _ = writeln!(
1426 out,
1427 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1428 );
1429 let _ = writeln!(out, "\t\t}}");
1430 let _ = writeln!(out, "\t}}");
1431 }
1432 }
1433 }
1434
1435 fn render_assert_validation_errors(
1440 &self,
1441 out: &mut String,
1442 _response_var: &str,
1443 errors: &[ValidationErrorExpectation],
1444 ) {
1445 let _ = writeln!(out, "\tvar _veBody map[string]any");
1446 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1447 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1448 let _ = writeln!(out, "\t}}");
1449 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1450 for ve in errors {
1451 let escaped_msg = go_string_literal(&ve.msg);
1452 let _ = writeln!(out, "\t{{");
1453 let _ = writeln!(out, "\t\t_found := false");
1454 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1455 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1456 let _ = writeln!(
1457 out,
1458 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1459 );
1460 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1461 let _ = writeln!(out, "\t\t\t\t\tbreak");
1462 let _ = writeln!(out, "\t\t\t\t}}");
1463 let _ = writeln!(out, "\t\t\t}}");
1464 let _ = writeln!(out, "\t\t}}");
1465 let _ = writeln!(out, "\t\tif !_found {{");
1466 let _ = writeln!(
1467 out,
1468 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1469 );
1470 let _ = writeln!(out, "\t\t}}");
1471 let _ = writeln!(out, "\t}}");
1472 }
1473 }
1474}
1475
1476fn build_args_and_setup(
1484 input: &serde_json::Value,
1485 args: &[crate::config::ArgMapping],
1486 import_alias: &str,
1487 options_type: Option<&str>,
1488 fixture_id: &str,
1489 options_ptr: bool,
1490) -> (Vec<String>, String) {
1491 use heck::ToUpperCamelCase;
1492
1493 if args.is_empty() {
1494 return (Vec::new(), String::new());
1495 }
1496
1497 let mut setup_lines: Vec<String> = Vec::new();
1498 let mut parts: Vec<String> = Vec::new();
1499
1500 for arg in args {
1501 if arg.arg_type == "mock_url" {
1502 setup_lines.push(format!(
1503 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1504 arg.name,
1505 ));
1506 parts.push(arg.name.clone());
1507 continue;
1508 }
1509
1510 if arg.arg_type == "handle" {
1511 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1513 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1514 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1515 if config_value.is_null()
1516 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1517 {
1518 setup_lines.push(format!(
1519 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
1520 name = arg.name,
1521 ));
1522 } else {
1523 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1524 let go_literal = go_string_literal(&json_str);
1525 let name = &arg.name;
1526 setup_lines.push(format!(
1527 "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}}"
1528 ));
1529 setup_lines.push(format!(
1530 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
1531 ));
1532 }
1533 parts.push(arg.name.clone());
1534 continue;
1535 }
1536
1537 let val: Option<&serde_json::Value> = if arg.field == "input" {
1538 Some(input)
1539 } else {
1540 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1541 input.get(field)
1542 };
1543
1544 if arg.arg_type == "bytes" {
1551 let var_name = format!("{}Bytes", arg.name);
1552 match val {
1553 None | Some(serde_json::Value::Null) => {
1554 if arg.optional {
1555 parts.push("nil".to_string());
1556 } else {
1557 parts.push("[]byte{}".to_string());
1558 }
1559 }
1560 Some(serde_json::Value::String(s)) => {
1561 let go_path = go_string_literal(s);
1566 setup_lines.push(format!(
1567 "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
1568 ));
1569 parts.push(var_name);
1570 }
1571 Some(other) => {
1572 parts.push(format!("[]byte({})", json_to_go(other)));
1573 }
1574 }
1575 continue;
1576 }
1577
1578 match val {
1579 None | Some(serde_json::Value::Null) if arg.optional => {
1580 match arg.arg_type.as_str() {
1582 "string" => {
1583 parts.push("nil".to_string());
1585 }
1586 "json_object" => {
1587 if options_ptr {
1588 parts.push("nil".to_string());
1590 } else if let Some(opts_type) = options_type {
1591 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1593 } else {
1594 parts.push("nil".to_string());
1595 }
1596 }
1597 _ => {
1598 parts.push("nil".to_string());
1599 }
1600 }
1601 }
1602 None | Some(serde_json::Value::Null) => {
1603 let default_val = match arg.arg_type.as_str() {
1605 "string" => "\"\"".to_string(),
1606 "int" | "integer" | "i64" => "0".to_string(),
1607 "float" | "number" => "0.0".to_string(),
1608 "bool" | "boolean" => "false".to_string(),
1609 "json_object" => {
1610 if options_ptr {
1611 "nil".to_string()
1613 } else if let Some(opts_type) = options_type {
1614 format!("{import_alias}.{opts_type}{{}}")
1615 } else {
1616 "nil".to_string()
1617 }
1618 }
1619 _ => "nil".to_string(),
1620 };
1621 parts.push(default_val);
1622 }
1623 Some(v) => {
1624 match arg.arg_type.as_str() {
1625 "json_object" => {
1626 let is_array = v.is_array();
1629 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1630 if is_empty_obj {
1631 if options_ptr {
1632 parts.push("nil".to_string());
1634 } else if let Some(opts_type) = options_type {
1635 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1636 } else {
1637 parts.push("nil".to_string());
1638 }
1639 } else if is_array {
1640 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1645 if go_t.starts_with('[') {
1649 go_t.to_string()
1650 } else {
1651 let qualified = if go_t.contains('.') {
1653 go_t.to_string()
1654 } else {
1655 format!("{import_alias}.{go_t}")
1656 };
1657 format!("[]{qualified}")
1658 }
1659 } else {
1660 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1661 };
1662 let converted_v = convert_json_for_go(v.clone());
1664 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1665 let go_literal = go_string_literal(&json_str);
1666 let var_name = &arg.name;
1667 setup_lines.push(format!(
1668 "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1669 ));
1670 parts.push(var_name.to_string());
1671 } else if let Some(opts_type) = options_type {
1672 let remapped_v = if options_ptr {
1677 convert_json_for_go(v.clone())
1678 } else {
1679 v.clone()
1680 };
1681 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1682 let go_literal = go_string_literal(&json_str);
1683 let var_name = &arg.name;
1684 setup_lines.push(format!(
1685 "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}}"
1686 ));
1687 let arg_expr = if options_ptr {
1689 format!("&{var_name}")
1690 } else {
1691 var_name.to_string()
1692 };
1693 parts.push(arg_expr);
1694 } else {
1695 parts.push(json_to_go(v));
1696 }
1697 }
1698 "string" if arg.optional => {
1699 let var_name = format!("{}Val", arg.name);
1701 let go_val = json_to_go(v);
1702 setup_lines.push(format!("{var_name} := {go_val}"));
1703 parts.push(format!("&{var_name}"));
1704 }
1705 _ => {
1706 parts.push(json_to_go(v));
1707 }
1708 }
1709 }
1710 }
1711 }
1712
1713 (setup_lines, parts.join(", "))
1714}
1715
1716#[allow(clippy::too_many_arguments)]
1717fn render_assertion(
1718 out: &mut String,
1719 assertion: &Assertion,
1720 result_var: &str,
1721 import_alias: &str,
1722 field_resolver: &FieldResolver,
1723 optional_locals: &std::collections::HashMap<String, String>,
1724 result_is_simple: bool,
1725 result_is_array: bool,
1726) {
1727 if !result_is_simple {
1730 if let Some(f) = &assertion.field {
1731 let embed_deref = format!("(*{result_var})");
1734 match f.as_str() {
1735 "chunks_have_content" => {
1736 let pred = format!(
1737 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1738 );
1739 match assertion.assertion_type.as_str() {
1740 "is_true" => {
1741 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1742 }
1743 "is_false" => {
1744 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1745 }
1746 _ => {
1747 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1748 }
1749 }
1750 return;
1751 }
1752 "chunks_have_embeddings" => {
1753 let pred = format!(
1754 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Embedding == nil || len(*c.Embedding) == 0 {{ return false }} }}; return true }}()"
1755 );
1756 match assertion.assertion_type.as_str() {
1757 "is_true" => {
1758 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1759 }
1760 "is_false" => {
1761 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1762 }
1763 _ => {
1764 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1765 }
1766 }
1767 return;
1768 }
1769 "embeddings" => {
1770 match assertion.assertion_type.as_str() {
1771 "count_equals" => {
1772 if let Some(val) = &assertion.value {
1773 if let Some(n) = val.as_u64() {
1774 let _ = writeln!(
1775 out,
1776 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1777 );
1778 }
1779 }
1780 }
1781 "count_min" => {
1782 if let Some(val) = &assertion.value {
1783 if let Some(n) = val.as_u64() {
1784 let _ = writeln!(
1785 out,
1786 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1787 );
1788 }
1789 }
1790 }
1791 "not_empty" => {
1792 let _ = writeln!(
1793 out,
1794 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1795 );
1796 }
1797 "is_empty" => {
1798 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1799 }
1800 _ => {
1801 let _ = writeln!(
1802 out,
1803 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1804 );
1805 }
1806 }
1807 return;
1808 }
1809 "embedding_dimensions" => {
1810 let expr = format!(
1811 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1812 );
1813 match assertion.assertion_type.as_str() {
1814 "equals" => {
1815 if let Some(val) = &assertion.value {
1816 if let Some(n) = val.as_u64() {
1817 let _ = writeln!(
1818 out,
1819 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1820 );
1821 }
1822 }
1823 }
1824 "greater_than" => {
1825 if let Some(val) = &assertion.value {
1826 if let Some(n) = val.as_u64() {
1827 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1828 }
1829 }
1830 }
1831 _ => {
1832 let _ = writeln!(
1833 out,
1834 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1835 );
1836 }
1837 }
1838 return;
1839 }
1840 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1841 let pred = match f.as_str() {
1842 "embeddings_valid" => {
1843 format!(
1844 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1845 )
1846 }
1847 "embeddings_finite" => {
1848 format!(
1849 "func() bool {{ for _, e := range {embed_deref} {{ for _, v := range e {{ if v != v || v == float32(1.0/0.0) || v == float32(-1.0/0.0) {{ return false }} }} }}; return true }}()"
1850 )
1851 }
1852 "embeddings_non_zero" => {
1853 format!(
1854 "func() bool {{ for _, e := range {embed_deref} {{ hasNonZero := false; for _, v := range e {{ if v != 0 {{ hasNonZero = true; break }} }}; if !hasNonZero {{ return false }} }}; return true }}()"
1855 )
1856 }
1857 "embeddings_normalized" => {
1858 format!(
1859 "func() bool {{ for _, e := range {embed_deref} {{ var n float64; for _, v := range e {{ n += float64(v) * float64(v) }}; if n < 0.999 || n > 1.001 {{ return false }} }}; return true }}()"
1860 )
1861 }
1862 _ => unreachable!(),
1863 };
1864 match assertion.assertion_type.as_str() {
1865 "is_true" => {
1866 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1867 }
1868 "is_false" => {
1869 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1870 }
1871 _ => {
1872 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1873 }
1874 }
1875 return;
1876 }
1877 "keywords" | "keywords_count" => {
1880 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1881 return;
1882 }
1883 _ => {}
1884 }
1885 }
1886 }
1887
1888 if !result_is_simple {
1891 if let Some(f) = &assertion.field {
1892 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1893 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1894 return;
1895 }
1896 }
1897 }
1898
1899 let field_expr = if result_is_simple {
1900 result_var.to_string()
1902 } else {
1903 match &assertion.field {
1904 Some(f) if !f.is_empty() => {
1905 if let Some(local_var) = optional_locals.get(f.as_str()) {
1907 local_var.clone()
1908 } else {
1909 field_resolver.accessor(f, "go", result_var)
1910 }
1911 }
1912 _ => result_var.to_string(),
1913 }
1914 };
1915
1916 let is_optional = assertion
1920 .field
1921 .as_ref()
1922 .map(|f| {
1923 let resolved = field_resolver.resolve(f);
1924 let check_path = resolved
1925 .strip_suffix(".length")
1926 .or_else(|| resolved.strip_suffix(".count"))
1927 .or_else(|| resolved.strip_suffix(".size"))
1928 .unwrap_or(resolved);
1929 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1930 })
1931 .unwrap_or(false);
1932
1933 let field_is_array_for_len = assertion
1937 .field
1938 .as_ref()
1939 .map(|f| {
1940 let resolved = field_resolver.resolve(f);
1941 let check_path = resolved
1942 .strip_suffix(".length")
1943 .or_else(|| resolved.strip_suffix(".count"))
1944 .or_else(|| resolved.strip_suffix(".size"))
1945 .unwrap_or(resolved);
1946 field_resolver.is_array(check_path)
1947 })
1948 .unwrap_or(false);
1949 let field_expr =
1950 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
1951 let inner = &field_expr[4..field_expr.len() - 1];
1952 format!("len(*{inner})")
1953 } else {
1954 field_expr
1955 };
1956 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1958 Some(field_expr[5..field_expr.len() - 1].to_string())
1959 } else {
1960 None
1961 };
1962
1963 let field_is_slice = assertion
1967 .field
1968 .as_ref()
1969 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
1970 .unwrap_or(false);
1971 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
1972 format!("*{field_expr}")
1973 } else {
1974 field_expr.clone()
1975 };
1976
1977 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1982 let mut array_expr = field_expr[..idx].to_string();
1983 if let Some(stripped) = array_expr.strip_prefix("len(") {
1984 array_expr = stripped.to_string();
1985 }
1986 Some(array_expr)
1987 } else {
1988 None
1989 };
1990
1991 let mut assertion_buf = String::new();
1994 let out_ref = &mut assertion_buf;
1995
1996 match assertion.assertion_type.as_str() {
1997 "equals" => {
1998 if let Some(expected) = &assertion.value {
1999 let go_val = json_to_go(expected);
2000 if expected.is_string() {
2002 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2005 format!("strings.TrimSpace(string(*{field_expr}))")
2006 } else {
2007 format!("strings.TrimSpace(string({field_expr}))")
2008 };
2009 if is_optional && !field_expr.starts_with("len(") {
2010 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2011 } else {
2012 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2013 }
2014 } else if is_optional && !field_expr.starts_with("len(") {
2015 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2016 } else {
2017 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2018 }
2019 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2020 let _ = writeln!(out_ref, "\t}}");
2021 }
2022 }
2023 "contains" => {
2024 if let Some(expected) = &assertion.value {
2025 let go_val = json_to_go(expected);
2026 let resolved_field = assertion.field.as_deref().unwrap_or("");
2032 let resolved_name = field_resolver.resolve(resolved_field);
2033 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2034 let is_opt =
2035 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2036 let field_for_contains = if is_opt && field_is_array {
2037 format!("jsonString({field_expr})")
2039 } else if is_opt {
2040 format!("fmt.Sprint(*{field_expr})")
2041 } else if field_is_array {
2042 format!("jsonString({field_expr})")
2043 } else {
2044 format!("fmt.Sprint({field_expr})")
2045 };
2046 if is_opt {
2047 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2048 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2049 let _ = writeln!(
2050 out_ref,
2051 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2052 );
2053 let _ = writeln!(out_ref, "\t}}");
2054 let _ = writeln!(out_ref, "\t}}");
2055 } else {
2056 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2057 let _ = writeln!(
2058 out_ref,
2059 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2060 );
2061 let _ = writeln!(out_ref, "\t}}");
2062 }
2063 }
2064 }
2065 "contains_all" => {
2066 if let Some(values) = &assertion.values {
2067 let resolved_field = assertion.field.as_deref().unwrap_or("");
2068 let resolved_name = field_resolver.resolve(resolved_field);
2069 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2070 let is_opt =
2071 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2072 for val in values {
2073 let go_val = json_to_go(val);
2074 let field_for_contains = if is_opt && field_is_array {
2075 format!("jsonString({field_expr})")
2077 } else if is_opt {
2078 format!("fmt.Sprint(*{field_expr})")
2079 } else if field_is_array {
2080 format!("jsonString({field_expr})")
2081 } else {
2082 format!("fmt.Sprint({field_expr})")
2083 };
2084 if is_opt {
2085 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2086 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2087 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2088 let _ = writeln!(out_ref, "\t}}");
2089 let _ = writeln!(out_ref, "\t}}");
2090 } else {
2091 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2092 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2093 let _ = writeln!(out_ref, "\t}}");
2094 }
2095 }
2096 }
2097 }
2098 "not_contains" => {
2099 if let Some(expected) = &assertion.value {
2100 let go_val = json_to_go(expected);
2101 let resolved_field = assertion.field.as_deref().unwrap_or("");
2102 let resolved_name = field_resolver.resolve(resolved_field);
2103 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2104 let is_opt =
2105 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2106 let field_for_contains = if is_opt && field_is_array {
2107 format!("jsonString({field_expr})")
2109 } else if is_opt {
2110 format!("fmt.Sprint(*{field_expr})")
2111 } else if field_is_array {
2112 format!("jsonString({field_expr})")
2113 } else {
2114 format!("fmt.Sprint({field_expr})")
2115 };
2116 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2117 let _ = writeln!(
2118 out_ref,
2119 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2120 );
2121 let _ = writeln!(out_ref, "\t}}");
2122 }
2123 }
2124 "not_empty" => {
2125 let field_is_array = {
2128 let rf = assertion.field.as_deref().unwrap_or("");
2129 let rn = field_resolver.resolve(rf);
2130 field_resolver.is_array(rn)
2131 };
2132 if is_optional && !field_is_array {
2133 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2135 } else if is_optional && field_is_slice {
2136 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2138 } else if is_optional {
2139 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2141 } else if result_is_simple && result_is_array {
2142 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2144 } else {
2145 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2146 }
2147 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2148 let _ = writeln!(out_ref, "\t}}");
2149 }
2150 "is_empty" => {
2151 let field_is_array = {
2152 let rf = assertion.field.as_deref().unwrap_or("");
2153 let rn = field_resolver.resolve(rf);
2154 field_resolver.is_array(rn)
2155 };
2156 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2159 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2161 } else if is_optional && !field_is_array {
2162 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2164 } else if is_optional && field_is_slice {
2165 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2167 } else if is_optional {
2168 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2170 } else {
2171 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2172 }
2173 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2174 let _ = writeln!(out_ref, "\t}}");
2175 }
2176 "contains_any" => {
2177 if let Some(values) = &assertion.values {
2178 let resolved_field = assertion.field.as_deref().unwrap_or("");
2179 let resolved_name = field_resolver.resolve(resolved_field);
2180 let field_is_array = field_resolver.is_array(resolved_name);
2181 let is_opt =
2182 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2183 let field_for_contains = if is_opt && field_is_array {
2184 format!("jsonString({field_expr})")
2186 } else if is_opt {
2187 format!("fmt.Sprint(*{field_expr})")
2188 } else if field_is_array {
2189 format!("jsonString({field_expr})")
2190 } else {
2191 format!("fmt.Sprint({field_expr})")
2192 };
2193 let _ = writeln!(out_ref, "\t{{");
2194 let _ = writeln!(out_ref, "\t\tfound := false");
2195 for val in values {
2196 let go_val = json_to_go(val);
2197 let _ = writeln!(
2198 out_ref,
2199 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2200 );
2201 }
2202 let _ = writeln!(out_ref, "\t\tif !found {{");
2203 let _ = writeln!(
2204 out_ref,
2205 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2206 );
2207 let _ = writeln!(out_ref, "\t\t}}");
2208 let _ = writeln!(out_ref, "\t}}");
2209 }
2210 }
2211 "greater_than" => {
2212 if let Some(val) = &assertion.value {
2213 let go_val = json_to_go(val);
2214 if is_optional {
2218 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2219 if let Some(n) = val.as_u64() {
2220 let next = n + 1;
2221 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2222 } else {
2223 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2224 }
2225 let _ = writeln!(
2226 out_ref,
2227 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2228 );
2229 let _ = writeln!(out_ref, "\t\t}}");
2230 let _ = writeln!(out_ref, "\t}}");
2231 } else if let Some(n) = val.as_u64() {
2232 let next = n + 1;
2233 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2234 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2235 let _ = writeln!(out_ref, "\t}}");
2236 } else {
2237 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2238 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2239 let _ = writeln!(out_ref, "\t}}");
2240 }
2241 }
2242 }
2243 "less_than" => {
2244 if let Some(val) = &assertion.value {
2245 let go_val = json_to_go(val);
2246 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2247 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2248 let _ = writeln!(out_ref, "\t}}");
2249 }
2250 }
2251 "greater_than_or_equal" => {
2252 if let Some(val) = &assertion.value {
2253 let go_val = json_to_go(val);
2254 if let Some(ref guard) = nil_guard_expr {
2255 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2256 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2257 let _ = writeln!(
2258 out_ref,
2259 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2260 );
2261 let _ = writeln!(out_ref, "\t\t}}");
2262 let _ = writeln!(out_ref, "\t}}");
2263 } else if is_optional && !field_expr.starts_with("len(") {
2264 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2266 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2267 let _ = writeln!(
2268 out_ref,
2269 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2270 );
2271 let _ = writeln!(out_ref, "\t\t}}");
2272 let _ = writeln!(out_ref, "\t}}");
2273 } else {
2274 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2275 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2276 let _ = writeln!(out_ref, "\t}}");
2277 }
2278 }
2279 }
2280 "less_than_or_equal" => {
2281 if let Some(val) = &assertion.value {
2282 let go_val = json_to_go(val);
2283 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2284 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2285 let _ = writeln!(out_ref, "\t}}");
2286 }
2287 }
2288 "starts_with" => {
2289 if let Some(expected) = &assertion.value {
2290 let go_val = json_to_go(expected);
2291 let field_for_prefix = if is_optional
2292 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2293 {
2294 format!("string(*{field_expr})")
2295 } else {
2296 format!("string({field_expr})")
2297 };
2298 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2299 let _ = writeln!(
2300 out_ref,
2301 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2302 );
2303 let _ = writeln!(out_ref, "\t}}");
2304 }
2305 }
2306 "count_min" => {
2307 if let Some(val) = &assertion.value {
2308 if let Some(n) = val.as_u64() {
2309 if is_optional {
2310 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2311 let len_expr = if field_is_slice {
2313 format!("len({field_expr})")
2314 } else {
2315 format!("len(*{field_expr})")
2316 };
2317 let _ = writeln!(
2318 out_ref,
2319 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2320 );
2321 let _ = writeln!(out_ref, "\t}}");
2322 } else {
2323 let _ = writeln!(
2324 out_ref,
2325 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2326 );
2327 }
2328 }
2329 }
2330 }
2331 "count_equals" => {
2332 if let Some(val) = &assertion.value {
2333 if let Some(n) = val.as_u64() {
2334 if is_optional {
2335 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2336 let len_expr = if field_is_slice {
2338 format!("len({field_expr})")
2339 } else {
2340 format!("len(*{field_expr})")
2341 };
2342 let _ = writeln!(
2343 out_ref,
2344 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2345 );
2346 let _ = writeln!(out_ref, "\t}}");
2347 } else {
2348 let _ = writeln!(
2349 out_ref,
2350 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2351 );
2352 }
2353 }
2354 }
2355 }
2356 "is_true" => {
2357 if is_optional {
2358 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2359 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2360 let _ = writeln!(out_ref, "\t}}");
2361 } else {
2362 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2363 }
2364 }
2365 "is_false" => {
2366 if is_optional {
2367 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2368 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2369 let _ = writeln!(out_ref, "\t}}");
2370 } else {
2371 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2372 }
2373 }
2374 "method_result" => {
2375 if let Some(method_name) = &assertion.method {
2376 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2377 let check = assertion.check.as_deref().unwrap_or("is_true");
2378 let deref_expr = if info.is_pointer {
2381 format!("*{}", info.call_expr)
2382 } else {
2383 info.call_expr.clone()
2384 };
2385 match check {
2386 "equals" => {
2387 if let Some(val) = &assertion.value {
2388 if val.is_boolean() {
2389 if val.as_bool() == Some(true) {
2390 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2391 } else {
2392 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2393 }
2394 } else {
2395 let go_val = if let Some(cast) = info.value_cast {
2399 if val.is_number() {
2400 format!("{cast}({})", json_to_go(val))
2401 } else {
2402 json_to_go(val)
2403 }
2404 } else {
2405 json_to_go(val)
2406 };
2407 let _ = writeln!(
2408 out_ref,
2409 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2410 );
2411 }
2412 }
2413 }
2414 "is_true" => {
2415 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2416 }
2417 "is_false" => {
2418 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2419 }
2420 "greater_than_or_equal" => {
2421 if let Some(val) = &assertion.value {
2422 let n = val.as_u64().unwrap_or(0);
2423 let cast = info.value_cast.unwrap_or("uint");
2425 let _ = writeln!(
2426 out_ref,
2427 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2428 );
2429 }
2430 }
2431 "count_min" => {
2432 if let Some(val) = &assertion.value {
2433 let n = val.as_u64().unwrap_or(0);
2434 let _ = writeln!(
2435 out_ref,
2436 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2437 );
2438 }
2439 }
2440 "contains" => {
2441 if let Some(val) = &assertion.value {
2442 let go_val = json_to_go(val);
2443 let _ = writeln!(
2444 out_ref,
2445 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2446 );
2447 }
2448 }
2449 "is_error" => {
2450 let _ = writeln!(out_ref, "\t{{");
2451 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2452 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2453 let _ = writeln!(out_ref, "\t}}");
2454 }
2455 other_check => {
2456 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2457 }
2458 }
2459 } else {
2460 panic!("Go e2e generator: method_result assertion missing 'method' field");
2461 }
2462 }
2463 "min_length" => {
2464 if let Some(val) = &assertion.value {
2465 if let Some(n) = val.as_u64() {
2466 if is_optional {
2467 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2468 let _ = writeln!(
2469 out_ref,
2470 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2471 );
2472 let _ = writeln!(out_ref, "\t}}");
2473 } else if field_expr.starts_with("len(") {
2474 let _ = writeln!(
2475 out_ref,
2476 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2477 );
2478 } else {
2479 let _ = writeln!(
2480 out_ref,
2481 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2482 );
2483 }
2484 }
2485 }
2486 }
2487 "max_length" => {
2488 if let Some(val) = &assertion.value {
2489 if let Some(n) = val.as_u64() {
2490 if is_optional {
2491 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2492 let _ = writeln!(
2493 out_ref,
2494 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2495 );
2496 let _ = writeln!(out_ref, "\t}}");
2497 } else if field_expr.starts_with("len(") {
2498 let _ = writeln!(
2499 out_ref,
2500 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2501 );
2502 } else {
2503 let _ = writeln!(
2504 out_ref,
2505 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2506 );
2507 }
2508 }
2509 }
2510 }
2511 "ends_with" => {
2512 if let Some(expected) = &assertion.value {
2513 let go_val = json_to_go(expected);
2514 let field_for_suffix = if is_optional
2515 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2516 {
2517 format!("string(*{field_expr})")
2518 } else {
2519 format!("string({field_expr})")
2520 };
2521 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2522 let _ = writeln!(
2523 out_ref,
2524 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2525 );
2526 let _ = writeln!(out_ref, "\t}}");
2527 }
2528 }
2529 "matches_regex" => {
2530 if let Some(expected) = &assertion.value {
2531 let go_val = json_to_go(expected);
2532 let field_for_regex = if is_optional
2533 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2534 {
2535 format!("*{field_expr}")
2536 } else {
2537 field_expr.clone()
2538 };
2539 let _ = writeln!(
2540 out_ref,
2541 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2542 );
2543 }
2544 }
2545 "not_error" => {
2546 }
2548 "error" => {
2549 }
2551 other => {
2552 panic!("Go e2e generator: unsupported assertion type: {other}");
2553 }
2554 }
2555
2556 if let Some(ref arr) = array_guard {
2559 if !assertion_buf.is_empty() {
2560 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2561 for line in assertion_buf.lines() {
2563 let _ = writeln!(out, "\t{line}");
2564 }
2565 let _ = writeln!(out, "\t}}");
2566 }
2567 } else {
2568 out.push_str(&assertion_buf);
2569 }
2570}
2571
2572struct GoMethodCallInfo {
2574 call_expr: String,
2576 is_pointer: bool,
2578 value_cast: Option<&'static str>,
2581}
2582
2583fn build_go_method_call(
2598 result_var: &str,
2599 method_name: &str,
2600 args: Option<&serde_json::Value>,
2601 import_alias: &str,
2602) -> GoMethodCallInfo {
2603 match method_name {
2604 "root_node_type" => GoMethodCallInfo {
2605 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2606 is_pointer: false,
2607 value_cast: None,
2608 },
2609 "named_children_count" => GoMethodCallInfo {
2610 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2611 is_pointer: false,
2612 value_cast: Some("uint"),
2613 },
2614 "has_error_nodes" => GoMethodCallInfo {
2615 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2616 is_pointer: true,
2617 value_cast: None,
2618 },
2619 "error_count" | "tree_error_count" => GoMethodCallInfo {
2620 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2621 is_pointer: true,
2622 value_cast: Some("uint"),
2623 },
2624 "tree_to_sexp" => GoMethodCallInfo {
2625 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2626 is_pointer: true,
2627 value_cast: None,
2628 },
2629 "contains_node_type" => {
2630 let node_type = args
2631 .and_then(|a| a.get("node_type"))
2632 .and_then(|v| v.as_str())
2633 .unwrap_or("");
2634 GoMethodCallInfo {
2635 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2636 is_pointer: true,
2637 value_cast: None,
2638 }
2639 }
2640 "find_nodes_by_type" => {
2641 let node_type = args
2642 .and_then(|a| a.get("node_type"))
2643 .and_then(|v| v.as_str())
2644 .unwrap_or("");
2645 GoMethodCallInfo {
2646 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2647 is_pointer: true,
2648 value_cast: None,
2649 }
2650 }
2651 "run_query" => {
2652 let query_source = args
2653 .and_then(|a| a.get("query_source"))
2654 .and_then(|v| v.as_str())
2655 .unwrap_or("");
2656 let language = args
2657 .and_then(|a| a.get("language"))
2658 .and_then(|v| v.as_str())
2659 .unwrap_or("");
2660 let query_lit = go_string_literal(query_source);
2661 let lang_lit = go_string_literal(language);
2662 GoMethodCallInfo {
2664 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2665 is_pointer: false,
2666 value_cast: None,
2667 }
2668 }
2669 other => {
2670 let method_pascal = other.to_upper_camel_case();
2671 GoMethodCallInfo {
2672 call_expr: format!("{result_var}.{method_pascal}()"),
2673 is_pointer: false,
2674 value_cast: None,
2675 }
2676 }
2677 }
2678}
2679
2680fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2690 match value {
2691 serde_json::Value::Object(map) => {
2692 let new_map: serde_json::Map<String, serde_json::Value> = map
2693 .into_iter()
2694 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2695 .collect();
2696 serde_json::Value::Object(new_map)
2697 }
2698 serde_json::Value::Array(arr) => {
2699 if is_byte_array(&arr) {
2702 let bytes: Vec<u8> = arr
2703 .iter()
2704 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2705 .collect();
2706 let encoded = base64_encode(&bytes);
2708 serde_json::Value::String(encoded)
2709 } else {
2710 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2711 }
2712 }
2713 serde_json::Value::String(s) => {
2714 serde_json::Value::String(pascal_to_snake_case(&s))
2717 }
2718 other => other,
2719 }
2720}
2721
2722fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2724 if arr.is_empty() {
2725 return false;
2726 }
2727 arr.iter().all(|v| {
2728 if let serde_json::Value::Number(n) = v {
2729 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
2730 } else {
2731 false
2732 }
2733 })
2734}
2735
2736fn base64_encode(bytes: &[u8]) -> String {
2739 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2740 let mut result = String::new();
2741 let mut i = 0;
2742
2743 while i + 2 < bytes.len() {
2744 let b1 = bytes[i];
2745 let b2 = bytes[i + 1];
2746 let b3 = bytes[i + 2];
2747
2748 result.push(TABLE[(b1 >> 2) as usize] as char);
2749 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2750 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
2751 result.push(TABLE[(b3 & 0x3f) as usize] as char);
2752
2753 i += 3;
2754 }
2755
2756 if i < bytes.len() {
2758 let b1 = bytes[i];
2759 result.push(TABLE[(b1 >> 2) as usize] as char);
2760
2761 if i + 1 < bytes.len() {
2762 let b2 = bytes[i + 1];
2763 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2764 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
2765 result.push('=');
2766 } else {
2767 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
2768 result.push_str("==");
2769 }
2770 }
2771
2772 result
2773}
2774
2775fn camel_to_snake_case(s: &str) -> String {
2777 let mut result = String::new();
2778 let mut prev_upper = false;
2779 for (i, c) in s.char_indices() {
2780 if c.is_uppercase() {
2781 if i > 0 && !prev_upper {
2782 result.push('_');
2783 }
2784 result.push(c.to_lowercase().next().unwrap_or(c));
2785 prev_upper = true;
2786 } else {
2787 if prev_upper && i > 1 {
2788 }
2792 result.push(c);
2793 prev_upper = false;
2794 }
2795 }
2796 result
2797}
2798
2799fn pascal_to_snake_case(s: &str) -> String {
2804 let first_char = s.chars().next();
2806 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
2807 return s.to_string();
2808 }
2809 camel_to_snake_case(s)
2810}
2811
2812fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
2816 let elem = element_type.unwrap_or("String").trim();
2817 let go_elem = rust_type_to_go(elem, import_alias);
2818 format!("[]{go_elem}")
2819}
2820
2821fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
2824 let trimmed = rust.trim();
2825 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
2826 return format!("[]{}", rust_type_to_go(inner, import_alias));
2827 }
2828 match trimmed {
2829 "String" | "&str" | "str" => "string".to_string(),
2830 "bool" => "bool".to_string(),
2831 "f32" => "float32".to_string(),
2832 "f64" => "float64".to_string(),
2833 "i8" => "int8".to_string(),
2834 "i16" => "int16".to_string(),
2835 "i32" => "int32".to_string(),
2836 "i64" | "isize" => "int64".to_string(),
2837 "u8" => "uint8".to_string(),
2838 "u16" => "uint16".to_string(),
2839 "u32" => "uint32".to_string(),
2840 "u64" | "usize" => "uint64".to_string(),
2841 _ => format!("{import_alias}.{trimmed}"),
2842 }
2843}
2844
2845fn json_to_go(value: &serde_json::Value) -> String {
2846 match value {
2847 serde_json::Value::String(s) => go_string_literal(s),
2848 serde_json::Value::Bool(b) => b.to_string(),
2849 serde_json::Value::Number(n) => n.to_string(),
2850 serde_json::Value::Null => "nil".to_string(),
2851 other => go_string_literal(&other.to_string()),
2853 }
2854}
2855
2856fn visitor_struct_name(fixture_id: &str) -> String {
2865 use heck::ToUpperCamelCase;
2866 format!("testVisitor{}", fixture_id.to_upper_camel_case())
2868}
2869
2870fn emit_go_visitor_struct(
2875 out: &mut String,
2876 struct_name: &str,
2877 visitor_spec: &crate::fixture::VisitorSpec,
2878 import_alias: &str,
2879) {
2880 let _ = writeln!(out, "type {struct_name} struct{{");
2881 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
2882 let _ = writeln!(out, "}}");
2883 for (method_name, action) in &visitor_spec.callbacks {
2884 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
2885 }
2886}
2887
2888fn emit_go_visitor_method(
2890 out: &mut String,
2891 struct_name: &str,
2892 method_name: &str,
2893 action: &CallbackAction,
2894 import_alias: &str,
2895) {
2896 let camel_method = method_to_camel(method_name);
2897 let params = match method_name {
2900 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
2901 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
2902 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
2903 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
2904 "visit_code_inline"
2905 | "visit_strong"
2906 | "visit_emphasis"
2907 | "visit_strikethrough"
2908 | "visit_underline"
2909 | "visit_subscript"
2910 | "visit_superscript"
2911 | "visit_mark"
2912 | "visit_button"
2913 | "visit_summary"
2914 | "visit_figcaption"
2915 | "visit_definition_term"
2916 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
2917 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
2918 "visit_list_item" => {
2919 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
2920 }
2921 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
2922 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
2923 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
2924 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
2925 "visit_input" => {
2926 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
2927 }
2928 "visit_audio" | "visit_video" | "visit_iframe" => {
2929 format!("_ {import_alias}.NodeContext, src *string")
2930 }
2931 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
2932 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
2933 format!("_ {import_alias}.NodeContext, output string")
2934 }
2935 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
2936 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
2937 _ => format!("_ {import_alias}.NodeContext"),
2938 };
2939
2940 let _ = writeln!(
2941 out,
2942 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
2943 );
2944 match action {
2945 CallbackAction::Skip => {
2946 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
2947 }
2948 CallbackAction::Continue => {
2949 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
2950 }
2951 CallbackAction::PreserveHtml => {
2952 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
2953 }
2954 CallbackAction::Custom { output } => {
2955 let escaped = go_string_literal(output);
2956 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
2957 }
2958 CallbackAction::CustomTemplate { template } => {
2959 let ptr_params = go_visitor_ptr_params(method_name);
2966 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
2967 let escaped_fmt = go_string_literal(&fmt_str);
2968 if fmt_args.is_empty() {
2969 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
2970 } else {
2971 let args_str = fmt_args.join(", ");
2972 let _ = writeln!(
2973 out,
2974 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
2975 );
2976 }
2977 }
2978 }
2979 let _ = writeln!(out, "}}");
2980}
2981
2982fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
2985 match method_name {
2986 "visit_link" => ["title"].into(),
2987 "visit_image" => ["title"].into(),
2988 "visit_heading" => ["id"].into(),
2989 "visit_code_block" => ["lang"].into(),
2990 "visit_form" => ["action", "method"].into(),
2991 "visit_input" => ["name", "value"].into(),
2992 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
2993 _ => std::collections::HashSet::new(),
2994 }
2995}
2996
2997fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3009 let mut fmt_str = String::new();
3010 let mut args: Vec<String> = Vec::new();
3011 let mut chars = template.chars().peekable();
3012 while let Some(c) = chars.next() {
3013 if c == '{' {
3014 let mut name = String::new();
3016 for inner in chars.by_ref() {
3017 if inner == '}' {
3018 break;
3019 }
3020 name.push(inner);
3021 }
3022 fmt_str.push_str("%s");
3023 let go_name = go_param_name(&name);
3025 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3027 format!("*{go_name}")
3028 } else {
3029 go_name
3030 };
3031 args.push(arg_expr);
3032 } else {
3033 fmt_str.push(c);
3034 }
3035 }
3036 (fmt_str, args)
3037}
3038
3039fn method_to_camel(snake: &str) -> String {
3041 use heck::ToUpperCamelCase;
3042 snake.to_upper_camel_case()
3043}
3044
3045#[cfg(test)]
3046mod tests {
3047 use super::*;
3048 use crate::config::{CallConfig, E2eConfig};
3049 use crate::field_access::FieldResolver;
3050 use crate::fixture::{Assertion, Fixture};
3051
3052 fn make_fixture(id: &str) -> Fixture {
3053 Fixture {
3054 id: id.to_string(),
3055 category: None,
3056 description: "test fixture".to_string(),
3057 tags: vec![],
3058 skip: None,
3059 env: None,
3060 call: None,
3061 input: serde_json::Value::Null,
3062 mock_response: Some(crate::fixture::MockResponse {
3063 status: 200,
3064 body: Some(serde_json::Value::Null),
3065 stream_chunks: None,
3066 headers: std::collections::HashMap::new(),
3067 }),
3068 source: String::new(),
3069 http: None,
3070 assertions: vec![Assertion {
3071 assertion_type: "not_error".to_string(),
3072 field: None,
3073 value: None,
3074 values: None,
3075 method: None,
3076 args: None,
3077 check: None,
3078 }],
3079 visitor: None,
3080 }
3081 }
3082
3083 #[test]
3087 fn test_go_method_name_uses_go_casing() {
3088 let e2e_config = E2eConfig {
3089 call: CallConfig {
3090 function: "clean_extracted_text".to_string(),
3091 module: "github.com/example/mylib".to_string(),
3092 result_var: "result".to_string(),
3093 returns_result: true,
3094 ..CallConfig::default()
3095 },
3096 ..E2eConfig::default()
3097 };
3098
3099 let fixture = make_fixture("basic_text");
3100 let resolver = FieldResolver::new(
3101 &std::collections::HashMap::new(),
3102 &std::collections::HashSet::new(),
3103 &std::collections::HashSet::new(),
3104 &std::collections::HashSet::new(),
3105 &std::collections::HashSet::new(),
3106 );
3107 let mut out = String::new();
3108 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3109
3110 assert!(
3111 out.contains("kreuzberg.CleanExtractedText("),
3112 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3113 );
3114 assert!(
3115 !out.contains("kreuzberg.clean_extracted_text("),
3116 "must not emit raw snake_case method name, got:\n{out}"
3117 );
3118 }
3119}