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 _type_defs: &[alef_core::ir::TypeDef],
30 ) -> Result<Vec<GeneratedFile>> {
31 let lang = self.language_name();
32 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
33
34 let mut files = Vec::new();
35
36 let call = &e2e_config.call;
38 let overrides = call.overrides.get(lang);
39 let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
40 let module_path = overrides
41 .and_then(|o| o.module.as_ref())
42 .cloned()
43 .or_else(|| configured_go_module_path.clone())
44 .unwrap_or_else(|| call.module.clone());
45 let import_alias = overrides
46 .and_then(|o| o.alias.as_ref())
47 .cloned()
48 .unwrap_or_else(|| "pkg".to_string());
49
50 let go_pkg = e2e_config.resolve_package("go");
52 let go_module_path = go_pkg
53 .as_ref()
54 .and_then(|p| p.module.as_ref())
55 .cloned()
56 .or_else(|| configured_go_module_path.clone())
57 .unwrap_or_else(|| module_path.clone());
58 let replace_path = go_pkg
59 .as_ref()
60 .and_then(|p| p.path.as_ref())
61 .cloned()
62 .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
63 let go_version = go_pkg
64 .as_ref()
65 .and_then(|p| p.version.as_ref())
66 .cloned()
67 .unwrap_or_else(|| {
68 config
69 .resolved_version()
70 .map(|v| format!("v{v}"))
71 .unwrap_or_else(|| "v0.0.0".to_string())
72 });
73 let field_resolver = FieldResolver::new(
74 &e2e_config.fields,
75 &e2e_config.fields_optional,
76 &e2e_config.result_fields,
77 &e2e_config.fields_array,
78 &std::collections::HashSet::new(),
79 );
80
81 let effective_replace = match e2e_config.dep_mode {
84 crate::config::DependencyMode::Registry => None,
85 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
86 };
87 let effective_go_version = if effective_replace.is_some() {
93 fix_go_major_version(&go_module_path, &go_version)
94 } else {
95 go_version.clone()
96 };
97 files.push(GeneratedFile {
98 path: output_base.join("go.mod"),
99 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
100 generated_header: false,
101 });
102
103 let emits_executable_test =
105 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
106 let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
107 emits_executable_test(f)
108 && f.assertions.iter().any(|a| {
109 matches!(
110 a.assertion_type.as_str(),
111 "contains" | "contains_all" | "contains_any" | "not_contains"
112 ) && {
113 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
114 e2e_config
115 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
116 .result_is_array
117 } else {
118 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
119 field_resolver.is_array(resolved_name)
120 }
121 }
122 })
123 });
124
125 if needs_json_stringify {
127 files.push(GeneratedFile {
128 path: output_base.join("helpers_test.go"),
129 content: render_helpers_test_go(),
130 generated_header: true,
131 });
132 }
133
134 let has_file_fixtures = groups
142 .iter()
143 .flat_map(|g| g.fixtures.iter())
144 .any(|f| f.http.is_none() && !f.needs_mock_server());
145
146 let needs_main_test = has_file_fixtures
147 || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
148 if f.needs_mock_server() {
149 return true;
150 }
151 let cc = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
152 let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
153 go_override.and_then(|o| o.client_factory.as_deref()).is_some()
154 });
155
156 if needs_main_test {
157 files.push(GeneratedFile {
158 path: output_base.join("main_test.go"),
159 content: render_main_test_go(),
160 generated_header: true,
161 });
162 }
163
164 for group in groups {
166 let active: Vec<&Fixture> = group
167 .fixtures
168 .iter()
169 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
170 .collect();
171
172 if active.is_empty() {
173 continue;
174 }
175
176 let filename = format!("{}_test.go", sanitize_filename(&group.category));
177 let content = render_test_file(
178 &group.category,
179 &active,
180 &module_path,
181 &import_alias,
182 &field_resolver,
183 e2e_config,
184 );
185 files.push(GeneratedFile {
186 path: output_base.join(filename),
187 content,
188 generated_header: true,
189 });
190 }
191
192 Ok(files)
193 }
194
195 fn language_name(&self) -> &'static str {
196 "go"
197 }
198}
199
200fn fix_go_major_version(module_path: &str, version: &str) -> String {
207 let major = module_path
209 .rsplit('/')
210 .next()
211 .and_then(|seg| seg.strip_prefix('v'))
212 .and_then(|n| n.parse::<u64>().ok())
213 .filter(|&n| n >= 2);
214
215 let Some(n) = major else {
216 return version.to_string();
217 };
218
219 let expected_prefix = format!("v{n}.");
221 if version.starts_with(&expected_prefix) {
222 return version.to_string();
223 }
224
225 format!("v{n}.0.0")
226}
227
228fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
229 let mut out = String::new();
230 let _ = writeln!(out, "module e2e_go");
231 let _ = writeln!(out);
232 let _ = writeln!(out, "go 1.26");
233 let _ = writeln!(out);
234 let _ = writeln!(out, "require (");
235 let _ = writeln!(out, "\t{go_module_path} {version}");
236 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
237 let _ = writeln!(out, ")");
238
239 if let Some(path) = replace_path {
240 let _ = writeln!(out);
241 let _ = writeln!(out, "replace {go_module_path} => {path}");
242 }
243
244 out
245}
246
247fn render_main_test_go() -> String {
253 let mut out = String::new();
255 let _ = writeln!(out, "package e2e_test");
256 let _ = writeln!(out);
257 let _ = writeln!(out, "import (");
258 let _ = writeln!(out, "\t\"bufio\"");
259 let _ = writeln!(out, "\t\"io\"");
260 let _ = writeln!(out, "\t\"os\"");
261 let _ = writeln!(out, "\t\"os/exec\"");
262 let _ = writeln!(out, "\t\"path/filepath\"");
263 let _ = writeln!(out, "\t\"runtime\"");
264 let _ = writeln!(out, "\t\"strings\"");
265 let _ = writeln!(out, "\t\"testing\"");
266 let _ = writeln!(out, ")");
267 let _ = writeln!(out);
268 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
269 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
270 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
271 let _ = writeln!(out);
272 let _ = writeln!(
273 out,
274 "\t// Change to the test_documents directory so that fixture file paths like"
275 );
276 let _ = writeln!(
277 out,
278 "\t// \"pdf/fake_memo.pdf\" resolve correctly when running go test from e2e/go/."
279 );
280 let _ = writeln!(
281 out,
282 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"test_documents\")"
283 );
284 let _ = writeln!(out, "\tif err := os.Chdir(testDocumentsDir); err != nil {{");
285 let _ = writeln!(out, "\t\tpanic(err)");
286 let _ = writeln!(out, "\t}}");
287 let _ = writeln!(out);
288 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
289 let _ = writeln!(
290 out,
291 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
292 );
293 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
294 let _ = writeln!(
295 out,
296 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
297 );
298 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
299 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
300 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
301 let _ = writeln!(out, "\t\tif err != nil {{");
302 let _ = writeln!(out, "\t\t\tpanic(err)");
303 let _ = writeln!(out, "\t\t}}");
304 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
305 let _ = writeln!(
306 out,
307 "\t\t// server does not see EOF and exit immediately. The mock-server"
308 );
309 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
310 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
311 let _ = writeln!(out, "\t\tif err != nil {{");
312 let _ = writeln!(out, "\t\t\tpanic(err)");
313 let _ = writeln!(out, "\t\t}}");
314 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
315 let _ = writeln!(out, "\t\t\tpanic(err)");
316 let _ = writeln!(out, "\t\t}}");
317 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
318 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
319 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
320 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
321 let _ = writeln!(
322 out,
323 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
324 );
325 let _ = writeln!(out, "\t\t\t\tbreak");
326 let _ = writeln!(out, "\t\t\t}}");
327 let _ = writeln!(out, "\t\t}}");
328 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
329 let _ = writeln!(out, "\t\tcode := m.Run()");
330 let _ = writeln!(out, "\t\t_ = stdin.Close()");
331 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
332 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
333 let _ = writeln!(out, "\t\tos.Exit(code)");
334 let _ = writeln!(out, "\t}} else {{");
335 let _ = writeln!(out, "\t\tcode := m.Run()");
336 let _ = writeln!(out, "\t\tos.Exit(code)");
337 let _ = writeln!(out, "\t}}");
338 let _ = writeln!(out, "}}");
339 out
340}
341
342fn render_helpers_test_go() -> String {
345 let mut out = String::new();
346 let _ = writeln!(out, "package e2e_test");
347 let _ = writeln!(out);
348 let _ = writeln!(out, "import \"encoding/json\"");
349 let _ = writeln!(out);
350 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
351 let _ = writeln!(
352 out,
353 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
354 );
355 let _ = writeln!(out, "func jsonString(value any) string {{");
356 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
357 let _ = writeln!(out, "\tif err != nil {{");
358 let _ = writeln!(out, "\t\treturn \"\"");
359 let _ = writeln!(out, "\t}}");
360 let _ = writeln!(out, "\treturn string(encoded)");
361 let _ = writeln!(out, "}}");
362 out
363}
364
365fn render_test_file(
366 category: &str,
367 fixtures: &[&Fixture],
368 go_module_path: &str,
369 import_alias: &str,
370 field_resolver: &FieldResolver,
371 e2e_config: &crate::config::E2eConfig,
372) -> String {
373 let mut out = String::new();
374 let emits_executable_test =
375 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
376
377 out.push_str(&hash::header(CommentStyle::DoubleSlash));
379 let _ = writeln!(out);
380
381 let needs_pkg = fixtures
390 .iter()
391 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
392
393 let needs_os = fixtures.iter().any(|f| {
396 if f.is_http_test() {
397 return true;
398 }
399 if !emits_executable_test(f) {
400 return false;
401 }
402 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
403 let go_override = call_config
404 .overrides
405 .get("go")
406 .or_else(|| e2e_config.call.overrides.get("go"));
407 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
408 return true;
409 }
410 let call_args = &call_config.args;
411 if call_args.iter().any(|a| a.arg_type == "mock_url") {
414 return true;
415 }
416 call_args.iter().any(|a| {
417 if a.arg_type != "bytes" {
418 return false;
419 }
420 let mut current = &f.input;
423 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
424 for segment in path.split('.') {
425 match current.get(segment) {
426 Some(next) => current = next,
427 None => return false,
428 }
429 }
430 current.is_string()
431 })
432 });
433
434 let needs_filepath = false;
437
438 let _needs_json_stringify = fixtures.iter().any(|f| {
439 emits_executable_test(f)
440 && f.assertions.iter().any(|a| {
441 matches!(
442 a.assertion_type.as_str(),
443 "contains" | "contains_all" | "contains_any" | "not_contains"
444 ) && {
445 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
448 e2e_config
450 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
451 .result_is_array
452 } else {
453 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
455 field_resolver.is_array(resolved_name)
456 }
457 }
458 })
459 });
460
461 let needs_json = fixtures.iter().any(|f| {
465 if let Some(http) = &f.http {
468 let body_needs_json = http
469 .expected_response
470 .body
471 .as_ref()
472 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
473 let partial_needs_json = http.expected_response.body_partial.is_some();
474 let ve_needs_json = http
475 .expected_response
476 .validation_errors
477 .as_ref()
478 .is_some_and(|v| !v.is_empty());
479 if body_needs_json || partial_needs_json || ve_needs_json {
480 return true;
481 }
482 }
483 if !emits_executable_test(f) {
484 return false;
485 }
486
487 let call = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
488 let call_args = &call.args;
489 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
491 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
492 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
493 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
494 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
495 })
496 };
497 let go_override = call.overrides.get("go");
499 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
500 e2e_config
501 .call
502 .overrides
503 .get("go")
504 .and_then(|o| o.options_type.as_deref())
505 });
506 let has_json_obj = call_args.iter().any(|a| {
507 if a.arg_type != "json_object" {
508 return false;
509 }
510 let v = if a.field == "input" {
511 &f.input
512 } else {
513 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
514 f.input.get(field).unwrap_or(&serde_json::Value::Null)
515 };
516 if v.is_array() {
517 return true;
518 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
520 });
521 has_handle || has_json_obj
522 });
523
524 let needs_base64 = false;
529
530 let needs_fmt = fixtures.iter().any(|f| {
535 f.visitor.as_ref().is_some_and(|v| {
536 v.callbacks.values().any(|action| {
537 if let CallbackAction::CustomTemplate { template } = action {
538 template.contains('{')
539 } else {
540 false
541 }
542 })
543 }) || (emits_executable_test(f)
544 && f.assertions.iter().any(|a| {
545 matches!(
546 a.assertion_type.as_str(),
547 "contains" | "contains_all" | "contains_any" | "not_contains"
548 ) && {
549 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
554 !e2e_config
556 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
557 .result_is_array
558 } else {
559 let field = a.field.as_deref().unwrap_or("");
563 let resolved_name = field_resolver.resolve(field);
564 !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
565 }
566 }
567 }))
568 });
569
570 let needs_strings = fixtures.iter().any(|f| {
573 if !emits_executable_test(f) {
574 return false;
575 }
576 f.assertions.iter().any(|a| {
577 let type_needs_strings = if a.assertion_type == "equals" {
578 a.value.as_ref().is_some_and(|v| v.is_string())
580 } else {
581 matches!(
582 a.assertion_type.as_str(),
583 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
584 )
585 };
586 let field_valid = a
587 .field
588 .as_ref()
589 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
590 .unwrap_or(true);
591 type_needs_strings && field_valid
592 })
593 });
594
595 let needs_assert = fixtures.iter().any(|f| {
597 if !emits_executable_test(f) {
598 return false;
599 }
600 f.assertions.iter().any(|a| {
601 let field_valid = a
602 .field
603 .as_ref()
604 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
605 .unwrap_or(true);
606 let synthetic_field_needs_assert = match a.field.as_deref() {
607 Some("chunks_have_content" | "chunks_have_embeddings") => {
608 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
609 }
610 Some("embeddings") => {
611 matches!(
612 a.assertion_type.as_str(),
613 "count_equals" | "count_min" | "not_empty" | "is_empty"
614 )
615 }
616 _ => false,
617 };
618 let type_needs_assert = matches!(
619 a.assertion_type.as_str(),
620 "count_equals"
621 | "count_min"
622 | "count_max"
623 | "is_true"
624 | "is_false"
625 | "method_result"
626 | "min_length"
627 | "max_length"
628 | "matches_regex"
629 );
630 synthetic_field_needs_assert || type_needs_assert && field_valid
631 })
632 });
633
634 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
636 let needs_http = has_http_fixtures;
637 let needs_io = has_http_fixtures;
639
640 let needs_reflect = fixtures.iter().any(|f| {
643 if let Some(http) = &f.http {
644 let body_needs_reflect = http
645 .expected_response
646 .body
647 .as_ref()
648 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
649 let partial_needs_reflect = http.expected_response.body_partial.is_some();
650 body_needs_reflect || partial_needs_reflect
651 } else {
652 false
653 }
654 });
655
656 let _ = writeln!(out, "// E2e tests for category: {category}");
657 let _ = writeln!(out, "package e2e_test");
658 let _ = writeln!(out);
659 let _ = writeln!(out, "import (");
660 if needs_base64 {
661 let _ = writeln!(out, "\t\"encoding/base64\"");
662 }
663 if needs_json || needs_reflect {
664 let _ = writeln!(out, "\t\"encoding/json\"");
665 }
666 if needs_fmt {
667 let _ = writeln!(out, "\t\"fmt\"");
668 }
669 if needs_io {
670 let _ = writeln!(out, "\t\"io\"");
671 }
672 if needs_http {
673 let _ = writeln!(out, "\t\"net/http\"");
674 }
675 if needs_os {
676 let _ = writeln!(out, "\t\"os\"");
677 }
678 let _ = needs_filepath; if needs_reflect {
680 let _ = writeln!(out, "\t\"reflect\"");
681 }
682 if needs_strings {
683 let _ = writeln!(out, "\t\"strings\"");
684 }
685 let _ = writeln!(out, "\t\"testing\"");
686 if needs_assert {
687 let _ = writeln!(out);
688 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
689 }
690 if needs_pkg {
691 let _ = writeln!(out);
692 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
693 }
694 let _ = writeln!(out, ")");
695 let _ = writeln!(out);
696
697 for fixture in fixtures.iter() {
699 if let Some(visitor_spec) = &fixture.visitor {
700 let struct_name = visitor_struct_name(&fixture.id);
701 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
702 let _ = writeln!(out);
703 }
704 }
705
706 for (i, fixture) in fixtures.iter().enumerate() {
707 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
708 if i + 1 < fixtures.len() {
709 let _ = writeln!(out);
710 }
711 }
712
713 while out.ends_with("\n\n") {
715 out.pop();
716 }
717 if !out.ends_with('\n') {
718 out.push('\n');
719 }
720 out
721}
722
723fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
732 if fixture.is_http_test() {
734 return false;
735 }
736 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
737 if call_config.skip_languages.iter().any(|l| l == "go") {
740 return false;
741 }
742 let go_override = call_config
743 .overrides
744 .get("go")
745 .or_else(|| e2e_config.call.overrides.get("go"));
746 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
749 return true;
750 }
751 let fn_name = go_override
755 .and_then(|o| o.function.as_deref())
756 .filter(|s| !s.is_empty())
757 .unwrap_or(call_config.function.as_str());
758 !fn_name.is_empty()
759}
760
761fn render_test_function(
762 out: &mut String,
763 fixture: &Fixture,
764 import_alias: &str,
765 field_resolver: &FieldResolver,
766 e2e_config: &crate::config::E2eConfig,
767) {
768 let fn_name = fixture.id.to_upper_camel_case();
769 let description = &fixture.description;
770
771 if fixture.http.is_some() {
773 render_http_test_function(out, fixture);
774 return;
775 }
776
777 if !fixture_has_go_callable(fixture, e2e_config) {
782 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
783 let _ = writeln!(out, "\t// {description}");
784 let _ = writeln!(
785 out,
786 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
787 );
788 let _ = writeln!(out, "}}");
789 return;
790 }
791
792 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
794 let lang = "go";
795 let overrides = call_config.overrides.get(lang);
796
797 let base_function_name = overrides
801 .and_then(|o| o.function.as_deref())
802 .unwrap_or(&call_config.function);
803 let function_name = to_go_name(base_function_name);
804 let result_var = &call_config.result_var;
805 let args = &call_config.args;
806
807 let returns_result = overrides
810 .and_then(|o| o.returns_result)
811 .unwrap_or(call_config.returns_result);
812
813 let returns_void = call_config.returns_void;
816
817 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
820 if call_config.result_is_simple {
821 return true;
822 }
823 call_config
824 .overrides
825 .get("rust")
826 .map(|o| o.result_is_simple)
827 .unwrap_or(false)
828 });
829
830 let result_is_array = overrides
833 .map(|o| o.result_is_array)
834 .unwrap_or(call_config.result_is_array);
835
836 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
838 e2e_config
839 .call
840 .overrides
841 .get("go")
842 .and_then(|o| o.options_type.as_deref())
843 });
844
845 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
847 e2e_config
848 .call
849 .overrides
850 .get("go")
851 .map(|o| o.options_ptr)
852 .unwrap_or(false)
853 });
854
855 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
856
857 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
860 e2e_config
861 .call
862 .overrides
863 .get(lang)
864 .and_then(|o| o.client_factory.as_deref())
865 });
866
867 let (mut setup_lines, args_str) = build_args_and_setup(
868 &fixture.input,
869 args,
870 import_alias,
871 call_options_type,
872 &fixture.id,
873 call_options_ptr,
874 expects_error,
875 );
876
877 let mut visitor_opts_var: Option<String> = None;
880 if fixture.visitor.is_some() {
881 let struct_name = visitor_struct_name(&fixture.id);
882 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
883 let opts_type = call_options_type.unwrap_or("ConversionOptions");
885 let opts_var = "opts".to_string();
886 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
887 setup_lines.push("opts.Visitor = visitor".to_string());
888 visitor_opts_var = Some(opts_var);
889 }
890
891 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
892 let final_args = {
893 let mut parts: Vec<String> = Vec::new();
894 if !args_str.is_empty() {
895 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
897 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
898 } else {
899 args_str
900 };
901 parts.push(processed_args);
902 }
903 parts.extend(go_extra_args);
904 parts.join(", ")
905 };
906
907 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
908 let _ = writeln!(out, "\t// {description}");
909
910 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
914 if let Some(var) = api_key_var {
915 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
916 let _ = writeln!(out, "\tif apiKey == \"\" {{");
917 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
918 let _ = writeln!(out, "\t}}");
919 }
920
921 for line in &setup_lines {
922 let _ = writeln!(out, "\t{line}");
923 }
924
925 let call_prefix = if let Some(factory) = client_factory {
929 let factory_name = to_go_name(factory);
930 let fixture_id = &fixture.id;
931 let (api_key_expr, base_url_expr) = if api_key_var.is_some() {
935 ("apiKey".to_string(), "nil".to_string())
936 } else {
937 let _ = writeln!(
938 out,
939 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
940 );
941 ("\"test-key\"".to_string(), "&mockURL".to_string())
942 };
943 let _ = writeln!(
944 out,
945 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
946 );
947 let _ = writeln!(out, "\tif clientErr != nil {{");
948 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
949 let _ = writeln!(out, "\t}}");
950 "client".to_string()
951 } else {
952 import_alias.to_string()
953 };
954
955 let binding_returns_error_pre = args
960 .iter()
961 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
962 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
963
964 if expects_error {
965 if effective_returns_result_pre && !returns_void {
966 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
967 } else {
968 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
969 }
970 let _ = writeln!(out, "\tif err == nil {{");
971 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
972 let _ = writeln!(out, "\t}}");
973 let _ = writeln!(out, "}}");
974 return;
975 }
976
977 let has_usable_assertion = fixture.assertions.iter().any(|a| {
981 if a.assertion_type == "not_error" || a.assertion_type == "error" {
982 return false;
983 }
984 if a.assertion_type == "method_result" {
986 return true;
987 }
988 match &a.field {
989 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
990 _ => true,
991 }
992 });
993
994 let binding_returns_error = args
1001 .iter()
1002 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1003 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1005
1006 if !effective_returns_result && result_is_simple {
1012 let result_binding = if has_usable_assertion {
1014 result_var.to_string()
1015 } else {
1016 "_".to_string()
1017 };
1018 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1020 let _ = writeln!(
1021 out,
1022 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1023 );
1024 if has_usable_assertion && result_binding != "_" {
1025 if result_is_array {
1026 let _ = writeln!(out, "\tvalue := {result_var}");
1028 } else {
1029 let only_nil_assertions = fixture
1032 .assertions
1033 .iter()
1034 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1035 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1036 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1037
1038 if !only_nil_assertions {
1039 let _ = writeln!(out, "\tif {result_var} == nil {{");
1041 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1042 let _ = writeln!(out, "\t}}");
1043 let _ = writeln!(out, "\tvalue := *{result_var}");
1044 }
1045 }
1046 }
1047 } else if !effective_returns_result || returns_void {
1048 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1051 let _ = writeln!(out, "\tif err != nil {{");
1052 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1053 let _ = writeln!(out, "\t}}");
1054 let _ = writeln!(out, "}}");
1056 return;
1057 } else {
1058 let result_binding = if has_usable_assertion {
1060 result_var.to_string()
1061 } else {
1062 "_".to_string()
1063 };
1064 let _ = writeln!(
1065 out,
1066 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1067 );
1068 let _ = writeln!(out, "\tif err != nil {{");
1069 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1070 let _ = writeln!(out, "\t}}");
1071 if result_is_simple && has_usable_assertion && result_binding != "_" {
1072 if result_is_array {
1073 let _ = writeln!(out, "\tvalue := {}", result_var);
1075 } else {
1076 let only_nil_assertions = fixture
1079 .assertions
1080 .iter()
1081 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1082 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1083 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1084
1085 if !only_nil_assertions {
1086 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1088 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1089 let _ = writeln!(out, "\t}}");
1090 let _ = writeln!(out, "\tvalue := *{}", result_var);
1091 }
1092 }
1093 }
1094 }
1095
1096 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1099 let only_nil_assertions = fixture
1100 .assertions
1101 .iter()
1102 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1103 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1104 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1105 !only_nil_assertions
1106 } else {
1107 result_is_simple && has_usable_assertion
1108 };
1109
1110 let effective_result_var = if has_deref_value {
1111 "value".to_string()
1112 } else {
1113 result_var.to_string()
1114 };
1115
1116 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1121 for assertion in &fixture.assertions {
1122 if let Some(f) = &assertion.field {
1123 if !f.is_empty() {
1124 let resolved = field_resolver.resolve(f);
1125 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1126 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1131 let is_array_field = field_resolver.is_array(resolved);
1132 if !is_string_field || is_array_field {
1133 continue;
1136 }
1137 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1138 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1139 if field_resolver.has_map_access(f) {
1140 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1143 } else {
1144 let _ = writeln!(out, "\tvar {local_var} string");
1145 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1146 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1150 let _ = writeln!(out, "\t}}");
1151 }
1152 optional_locals.insert(f.clone(), local_var);
1153 }
1154 }
1155 }
1156 }
1157
1158 for assertion in &fixture.assertions {
1160 if let Some(f) = &assertion.field {
1161 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1162 let parts: Vec<&str> = f.split('.').collect();
1165 let mut guard_expr: Option<String> = None;
1166 for i in 1..parts.len() {
1167 let prefix = parts[..i].join(".");
1168 let resolved_prefix = field_resolver.resolve(&prefix);
1169 if field_resolver.is_optional(resolved_prefix) {
1170 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
1171 guard_expr = Some(accessor);
1172 break;
1173 }
1174 }
1175 if let Some(guard) = guard_expr {
1176 if field_resolver.is_valid_for_result(f) {
1179 let _ = writeln!(out, "\tif {guard} != nil {{");
1180 let mut nil_buf = String::new();
1183 render_assertion(
1184 &mut nil_buf,
1185 assertion,
1186 &effective_result_var,
1187 import_alias,
1188 field_resolver,
1189 &optional_locals,
1190 result_is_simple,
1191 result_is_array,
1192 );
1193 for line in nil_buf.lines() {
1194 let _ = writeln!(out, "\t{line}");
1195 }
1196 let _ = writeln!(out, "\t}}");
1197 } else {
1198 render_assertion(
1199 out,
1200 assertion,
1201 &effective_result_var,
1202 import_alias,
1203 field_resolver,
1204 &optional_locals,
1205 result_is_simple,
1206 result_is_array,
1207 );
1208 }
1209 continue;
1210 }
1211 }
1212 }
1213 render_assertion(
1214 out,
1215 assertion,
1216 &effective_result_var,
1217 import_alias,
1218 field_resolver,
1219 &optional_locals,
1220 result_is_simple,
1221 result_is_array,
1222 );
1223 }
1224
1225 let _ = writeln!(out, "}}");
1226}
1227
1228fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1234 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1235}
1236
1237struct GoTestClientRenderer;
1249
1250impl client::TestClientRenderer for GoTestClientRenderer {
1251 fn language_name(&self) -> &'static str {
1252 "go"
1253 }
1254
1255 fn sanitize_test_name(&self, id: &str) -> String {
1259 id.to_upper_camel_case()
1260 }
1261
1262 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1265 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1266 let _ = writeln!(out, "\t// {description}");
1267 if let Some(reason) = skip_reason {
1268 let escaped = go_string_literal(reason);
1269 let _ = writeln!(out, "\tt.Skip({escaped})");
1270 }
1271 }
1272
1273 fn render_test_close(&self, out: &mut String) {
1274 let _ = writeln!(out, "}}");
1275 }
1276
1277 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1283 let method = ctx.method.to_uppercase();
1284 let path = ctx.path;
1285
1286 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1287 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1288 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1289 let _ = writeln!(out, "\t}}");
1290
1291 let body_expr = if let Some(body) = ctx.body {
1293 let json = serde_json::to_string(body).unwrap_or_default();
1294 let escaped = go_string_literal(&json);
1295 format!("strings.NewReader({})", escaped)
1296 } else {
1297 "strings.NewReader(\"\")".to_string()
1298 };
1299
1300 let _ = writeln!(out, "\tbody := {body_expr}");
1301 let _ = writeln!(
1302 out,
1303 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1304 );
1305 let _ = writeln!(out, "\tif err != nil {{");
1306 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1307 let _ = writeln!(out, "\t}}");
1308
1309 if ctx.body.is_some() {
1311 let content_type = ctx.content_type.unwrap_or("application/json");
1312 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1313 }
1314
1315 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1317 header_names.sort();
1318 for name in header_names {
1319 let value = &ctx.headers[name];
1320 let escaped_name = go_string_literal(name);
1321 let escaped_value = go_string_literal(value);
1322 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1323 }
1324
1325 if !ctx.cookies.is_empty() {
1327 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1328 cookie_names.sort();
1329 for name in cookie_names {
1330 let value = &ctx.cookies[name];
1331 let escaped_name = go_string_literal(name);
1332 let escaped_value = go_string_literal(value);
1333 let _ = writeln!(
1334 out,
1335 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1336 );
1337 }
1338 }
1339
1340 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1342 let _ = writeln!(
1343 out,
1344 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1345 );
1346 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1347 let _ = writeln!(out, "\t\t}},");
1348 let _ = writeln!(out, "\t}}");
1349 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1350 let _ = writeln!(out, "\tif err != nil {{");
1351 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1352 let _ = writeln!(out, "\t}}");
1353 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1354
1355 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1359 let _ = writeln!(out, "\tif err != nil {{");
1360 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1361 let _ = writeln!(out, "\t}}");
1362 let _ = writeln!(out, "\t_ = bodyBytes");
1363 }
1364
1365 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1366 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1367 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1368 let _ = writeln!(out, "\t}}");
1369 }
1370
1371 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1374 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1376 return;
1377 }
1378 if name.eq_ignore_ascii_case("connection") {
1380 return;
1381 }
1382 let escaped_name = go_string_literal(name);
1383 let escaped_value = go_string_literal(expected);
1384 let _ = writeln!(
1385 out,
1386 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1387 );
1388 let _ = writeln!(
1389 out,
1390 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1391 );
1392 let _ = writeln!(out, "\t}}");
1393 }
1394
1395 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1400 match expected {
1401 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1402 let json_str = serde_json::to_string(expected).unwrap_or_default();
1403 let escaped = go_string_literal(&json_str);
1404 let _ = writeln!(out, "\tvar got any");
1405 let _ = writeln!(out, "\tvar want any");
1406 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1407 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1408 let _ = writeln!(out, "\t}}");
1409 let _ = writeln!(
1410 out,
1411 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1412 );
1413 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1414 let _ = writeln!(out, "\t}}");
1415 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1416 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1417 let _ = writeln!(out, "\t}}");
1418 }
1419 serde_json::Value::String(s) => {
1420 let escaped = go_string_literal(s);
1421 let _ = writeln!(out, "\twant := {escaped}");
1422 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1423 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1424 let _ = writeln!(out, "\t}}");
1425 }
1426 other => {
1427 let escaped = go_string_literal(&other.to_string());
1428 let _ = writeln!(out, "\twant := {escaped}");
1429 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1430 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1431 let _ = writeln!(out, "\t}}");
1432 }
1433 }
1434 }
1435
1436 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1439 if let Some(obj) = expected.as_object() {
1440 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1441 let _ = writeln!(
1442 out,
1443 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1444 );
1445 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1446 let _ = writeln!(out, "\t}}");
1447 for (key, val) in obj {
1448 let escaped_key = go_string_literal(key);
1449 let json_val = serde_json::to_string(val).unwrap_or_default();
1450 let escaped_val = go_string_literal(&json_val);
1451 let _ = writeln!(out, "\t{{");
1452 let _ = writeln!(out, "\t\tvar _wantVal any");
1453 let _ = writeln!(
1454 out,
1455 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1456 );
1457 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1458 let _ = writeln!(out, "\t\t}}");
1459 let _ = writeln!(
1460 out,
1461 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1462 );
1463 let _ = writeln!(
1464 out,
1465 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1466 );
1467 let _ = writeln!(out, "\t\t}}");
1468 let _ = writeln!(out, "\t}}");
1469 }
1470 }
1471 }
1472
1473 fn render_assert_validation_errors(
1478 &self,
1479 out: &mut String,
1480 _response_var: &str,
1481 errors: &[ValidationErrorExpectation],
1482 ) {
1483 let _ = writeln!(out, "\tvar _veBody map[string]any");
1484 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1485 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1486 let _ = writeln!(out, "\t}}");
1487 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1488 for ve in errors {
1489 let escaped_msg = go_string_literal(&ve.msg);
1490 let _ = writeln!(out, "\t{{");
1491 let _ = writeln!(out, "\t\t_found := false");
1492 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1493 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1494 let _ = writeln!(
1495 out,
1496 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1497 );
1498 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1499 let _ = writeln!(out, "\t\t\t\t\tbreak");
1500 let _ = writeln!(out, "\t\t\t\t}}");
1501 let _ = writeln!(out, "\t\t\t}}");
1502 let _ = writeln!(out, "\t\t}}");
1503 let _ = writeln!(out, "\t\tif !_found {{");
1504 let _ = writeln!(
1505 out,
1506 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1507 );
1508 let _ = writeln!(out, "\t\t}}");
1509 let _ = writeln!(out, "\t}}");
1510 }
1511 }
1512}
1513
1514fn build_args_and_setup(
1522 input: &serde_json::Value,
1523 args: &[crate::config::ArgMapping],
1524 import_alias: &str,
1525 options_type: Option<&str>,
1526 fixture_id: &str,
1527 options_ptr: bool,
1528 expects_error: bool,
1529) -> (Vec<String>, String) {
1530 use heck::ToUpperCamelCase;
1531
1532 if args.is_empty() {
1533 return (Vec::new(), String::new());
1534 }
1535
1536 let mut setup_lines: Vec<String> = Vec::new();
1537 let mut parts: Vec<String> = Vec::new();
1538
1539 for arg in args {
1540 if arg.arg_type == "mock_url" {
1541 setup_lines.push(format!(
1542 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1543 arg.name,
1544 ));
1545 parts.push(arg.name.clone());
1546 continue;
1547 }
1548
1549 if arg.arg_type == "handle" {
1550 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1552 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1553 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1554 let create_err_handler = if expects_error {
1558 "return".to_string()
1559 } else {
1560 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1561 };
1562 if config_value.is_null()
1563 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1564 {
1565 setup_lines.push(format!(
1566 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1567 name = arg.name,
1568 ));
1569 } else {
1570 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1571 let go_literal = go_string_literal(&json_str);
1572 let name = &arg.name;
1573 setup_lines.push(format!(
1574 "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}}"
1575 ));
1576 setup_lines.push(format!(
1577 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1578 ));
1579 }
1580 parts.push(arg.name.clone());
1581 continue;
1582 }
1583
1584 let val: Option<&serde_json::Value> = if arg.field == "input" {
1585 Some(input)
1586 } else {
1587 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1588 input.get(field)
1589 };
1590
1591 if arg.arg_type == "bytes" {
1598 let var_name = format!("{}Bytes", arg.name);
1599 match val {
1600 None | Some(serde_json::Value::Null) => {
1601 if arg.optional {
1602 parts.push("nil".to_string());
1603 } else {
1604 parts.push("[]byte{}".to_string());
1605 }
1606 }
1607 Some(serde_json::Value::String(s)) => {
1608 let go_path = go_string_literal(s);
1613 setup_lines.push(format!(
1614 "{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}}"
1615 ));
1616 parts.push(var_name);
1617 }
1618 Some(other) => {
1619 parts.push(format!("[]byte({})", json_to_go(other)));
1620 }
1621 }
1622 continue;
1623 }
1624
1625 match val {
1626 None | Some(serde_json::Value::Null) if arg.optional => {
1627 match arg.arg_type.as_str() {
1629 "string" => {
1630 parts.push("nil".to_string());
1632 }
1633 "json_object" => {
1634 if options_ptr {
1635 parts.push("nil".to_string());
1637 } else if let Some(opts_type) = options_type {
1638 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1640 } else {
1641 parts.push("nil".to_string());
1642 }
1643 }
1644 _ => {
1645 parts.push("nil".to_string());
1646 }
1647 }
1648 }
1649 None | Some(serde_json::Value::Null) => {
1650 let default_val = match arg.arg_type.as_str() {
1652 "string" => "\"\"".to_string(),
1653 "int" | "integer" | "i64" => "0".to_string(),
1654 "float" | "number" => "0.0".to_string(),
1655 "bool" | "boolean" => "false".to_string(),
1656 "json_object" => {
1657 if options_ptr {
1658 "nil".to_string()
1660 } else if let Some(opts_type) = options_type {
1661 format!("{import_alias}.{opts_type}{{}}")
1662 } else {
1663 "nil".to_string()
1664 }
1665 }
1666 _ => "nil".to_string(),
1667 };
1668 parts.push(default_val);
1669 }
1670 Some(v) => {
1671 match arg.arg_type.as_str() {
1672 "json_object" => {
1673 let is_array = v.is_array();
1676 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1677 if is_empty_obj {
1678 if options_ptr {
1679 parts.push("nil".to_string());
1681 } else if let Some(opts_type) = options_type {
1682 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1683 } else {
1684 parts.push("nil".to_string());
1685 }
1686 } else if is_array {
1687 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1692 if go_t.starts_with('[') {
1696 go_t.to_string()
1697 } else {
1698 let qualified = if go_t.contains('.') {
1700 go_t.to_string()
1701 } else {
1702 format!("{import_alias}.{go_t}")
1703 };
1704 format!("[]{qualified}")
1705 }
1706 } else {
1707 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1708 };
1709 let converted_v = convert_json_for_go(v.clone());
1711 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1712 let go_literal = go_string_literal(&json_str);
1713 let var_name = &arg.name;
1714 setup_lines.push(format!(
1715 "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}}"
1716 ));
1717 parts.push(var_name.to_string());
1718 } else if let Some(opts_type) = options_type {
1719 let remapped_v = if options_ptr {
1724 convert_json_for_go(v.clone())
1725 } else {
1726 v.clone()
1727 };
1728 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1729 let go_literal = go_string_literal(&json_str);
1730 let var_name = &arg.name;
1731 setup_lines.push(format!(
1732 "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}}"
1733 ));
1734 let arg_expr = if options_ptr {
1736 format!("&{var_name}")
1737 } else {
1738 var_name.to_string()
1739 };
1740 parts.push(arg_expr);
1741 } else {
1742 parts.push(json_to_go(v));
1743 }
1744 }
1745 "string" if arg.optional => {
1746 let var_name = format!("{}Val", arg.name);
1748 let go_val = json_to_go(v);
1749 setup_lines.push(format!("{var_name} := {go_val}"));
1750 parts.push(format!("&{var_name}"));
1751 }
1752 _ => {
1753 parts.push(json_to_go(v));
1754 }
1755 }
1756 }
1757 }
1758 }
1759
1760 (setup_lines, parts.join(", "))
1761}
1762
1763#[allow(clippy::too_many_arguments)]
1764fn render_assertion(
1765 out: &mut String,
1766 assertion: &Assertion,
1767 result_var: &str,
1768 import_alias: &str,
1769 field_resolver: &FieldResolver,
1770 optional_locals: &std::collections::HashMap<String, String>,
1771 result_is_simple: bool,
1772 result_is_array: bool,
1773) {
1774 if !result_is_simple {
1777 if let Some(f) = &assertion.field {
1778 let embed_deref = format!("(*{result_var})");
1781 match f.as_str() {
1782 "chunks_have_content" => {
1783 let pred = format!(
1784 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1785 );
1786 match assertion.assertion_type.as_str() {
1787 "is_true" => {
1788 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1789 }
1790 "is_false" => {
1791 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1792 }
1793 _ => {
1794 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1795 }
1796 }
1797 return;
1798 }
1799 "chunks_have_embeddings" => {
1800 let pred = format!(
1801 "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 }}()"
1802 );
1803 match assertion.assertion_type.as_str() {
1804 "is_true" => {
1805 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1806 }
1807 "is_false" => {
1808 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1809 }
1810 _ => {
1811 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1812 }
1813 }
1814 return;
1815 }
1816 "embeddings" => {
1817 match assertion.assertion_type.as_str() {
1818 "count_equals" => {
1819 if let Some(val) = &assertion.value {
1820 if let Some(n) = val.as_u64() {
1821 let _ = writeln!(
1822 out,
1823 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1824 );
1825 }
1826 }
1827 }
1828 "count_min" => {
1829 if let Some(val) = &assertion.value {
1830 if let Some(n) = val.as_u64() {
1831 let _ = writeln!(
1832 out,
1833 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1834 );
1835 }
1836 }
1837 }
1838 "not_empty" => {
1839 let _ = writeln!(
1840 out,
1841 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1842 );
1843 }
1844 "is_empty" => {
1845 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1846 }
1847 _ => {
1848 let _ = writeln!(
1849 out,
1850 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1851 );
1852 }
1853 }
1854 return;
1855 }
1856 "embedding_dimensions" => {
1857 let expr = format!(
1858 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1859 );
1860 match assertion.assertion_type.as_str() {
1861 "equals" => {
1862 if let Some(val) = &assertion.value {
1863 if let Some(n) = val.as_u64() {
1864 let _ = writeln!(
1865 out,
1866 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1867 );
1868 }
1869 }
1870 }
1871 "greater_than" => {
1872 if let Some(val) = &assertion.value {
1873 if let Some(n) = val.as_u64() {
1874 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1875 }
1876 }
1877 }
1878 _ => {
1879 let _ = writeln!(
1880 out,
1881 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1882 );
1883 }
1884 }
1885 return;
1886 }
1887 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1888 let pred = match f.as_str() {
1889 "embeddings_valid" => {
1890 format!(
1891 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1892 )
1893 }
1894 "embeddings_finite" => {
1895 format!(
1896 "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 }}()"
1897 )
1898 }
1899 "embeddings_non_zero" => {
1900 format!(
1901 "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 }}()"
1902 )
1903 }
1904 "embeddings_normalized" => {
1905 format!(
1906 "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 }}()"
1907 )
1908 }
1909 _ => unreachable!(),
1910 };
1911 match assertion.assertion_type.as_str() {
1912 "is_true" => {
1913 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1914 }
1915 "is_false" => {
1916 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1917 }
1918 _ => {
1919 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1920 }
1921 }
1922 return;
1923 }
1924 "keywords" | "keywords_count" => {
1927 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1928 return;
1929 }
1930 _ => {}
1931 }
1932 }
1933 }
1934
1935 if !result_is_simple {
1938 if let Some(f) = &assertion.field {
1939 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1940 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1941 return;
1942 }
1943 }
1944 }
1945
1946 let field_expr = if result_is_simple {
1947 result_var.to_string()
1949 } else {
1950 match &assertion.field {
1951 Some(f) if !f.is_empty() => {
1952 if let Some(local_var) = optional_locals.get(f.as_str()) {
1954 local_var.clone()
1955 } else {
1956 field_resolver.accessor(f, "go", result_var)
1957 }
1958 }
1959 _ => result_var.to_string(),
1960 }
1961 };
1962
1963 let is_optional = assertion
1967 .field
1968 .as_ref()
1969 .map(|f| {
1970 let resolved = field_resolver.resolve(f);
1971 let check_path = resolved
1972 .strip_suffix(".length")
1973 .or_else(|| resolved.strip_suffix(".count"))
1974 .or_else(|| resolved.strip_suffix(".size"))
1975 .unwrap_or(resolved);
1976 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1977 })
1978 .unwrap_or(false);
1979
1980 let field_is_array_for_len = assertion
1984 .field
1985 .as_ref()
1986 .map(|f| {
1987 let resolved = field_resolver.resolve(f);
1988 let check_path = resolved
1989 .strip_suffix(".length")
1990 .or_else(|| resolved.strip_suffix(".count"))
1991 .or_else(|| resolved.strip_suffix(".size"))
1992 .unwrap_or(resolved);
1993 field_resolver.is_array(check_path)
1994 })
1995 .unwrap_or(false);
1996 let field_expr =
1997 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
1998 let inner = &field_expr[4..field_expr.len() - 1];
1999 format!("len(*{inner})")
2000 } else {
2001 field_expr
2002 };
2003 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2005 Some(field_expr[5..field_expr.len() - 1].to_string())
2006 } else {
2007 None
2008 };
2009
2010 let field_is_slice = assertion
2014 .field
2015 .as_ref()
2016 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2017 .unwrap_or(false);
2018 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2019 format!("*{field_expr}")
2020 } else {
2021 field_expr.clone()
2022 };
2023
2024 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2029 let mut array_expr = field_expr[..idx].to_string();
2030 if let Some(stripped) = array_expr.strip_prefix("len(") {
2031 array_expr = stripped.to_string();
2032 }
2033 Some(array_expr)
2034 } else {
2035 None
2036 };
2037
2038 let mut assertion_buf = String::new();
2041 let out_ref = &mut assertion_buf;
2042
2043 match assertion.assertion_type.as_str() {
2044 "equals" => {
2045 if let Some(expected) = &assertion.value {
2046 let go_val = json_to_go(expected);
2047 if expected.is_string() {
2049 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2052 format!("strings.TrimSpace(string(*{field_expr}))")
2053 } else {
2054 format!("strings.TrimSpace(string({field_expr}))")
2055 };
2056 if is_optional && !field_expr.starts_with("len(") {
2057 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2058 } else {
2059 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2060 }
2061 } else if is_optional && !field_expr.starts_with("len(") {
2062 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2063 } else {
2064 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2065 }
2066 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2067 let _ = writeln!(out_ref, "\t}}");
2068 }
2069 }
2070 "contains" => {
2071 if let Some(expected) = &assertion.value {
2072 let go_val = json_to_go(expected);
2073 let resolved_field = assertion.field.as_deref().unwrap_or("");
2079 let resolved_name = field_resolver.resolve(resolved_field);
2080 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2081 let is_opt =
2082 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2083 let field_for_contains = if is_opt && field_is_array {
2084 format!("jsonString({field_expr})")
2086 } else if is_opt {
2087 format!("fmt.Sprint(*{field_expr})")
2088 } else if field_is_array {
2089 format!("jsonString({field_expr})")
2090 } else {
2091 format!("fmt.Sprint({field_expr})")
2092 };
2093 if is_opt {
2094 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2095 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2096 let _ = writeln!(
2097 out_ref,
2098 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2099 );
2100 let _ = writeln!(out_ref, "\t}}");
2101 let _ = writeln!(out_ref, "\t}}");
2102 } else {
2103 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2104 let _ = writeln!(
2105 out_ref,
2106 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2107 );
2108 let _ = writeln!(out_ref, "\t}}");
2109 }
2110 }
2111 }
2112 "contains_all" => {
2113 if let Some(values) = &assertion.values {
2114 let resolved_field = assertion.field.as_deref().unwrap_or("");
2115 let resolved_name = field_resolver.resolve(resolved_field);
2116 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2117 let is_opt =
2118 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2119 for val in values {
2120 let go_val = json_to_go(val);
2121 let field_for_contains = if is_opt && field_is_array {
2122 format!("jsonString({field_expr})")
2124 } else if is_opt {
2125 format!("fmt.Sprint(*{field_expr})")
2126 } else if field_is_array {
2127 format!("jsonString({field_expr})")
2128 } else {
2129 format!("fmt.Sprint({field_expr})")
2130 };
2131 if is_opt {
2132 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2133 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2134 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2135 let _ = writeln!(out_ref, "\t}}");
2136 let _ = writeln!(out_ref, "\t}}");
2137 } else {
2138 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2139 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2140 let _ = writeln!(out_ref, "\t}}");
2141 }
2142 }
2143 }
2144 }
2145 "not_contains" => {
2146 if let Some(expected) = &assertion.value {
2147 let go_val = json_to_go(expected);
2148 let resolved_field = assertion.field.as_deref().unwrap_or("");
2149 let resolved_name = field_resolver.resolve(resolved_field);
2150 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2151 let is_opt =
2152 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2153 let field_for_contains = if is_opt && field_is_array {
2154 format!("jsonString({field_expr})")
2156 } else if is_opt {
2157 format!("fmt.Sprint(*{field_expr})")
2158 } else if field_is_array {
2159 format!("jsonString({field_expr})")
2160 } else {
2161 format!("fmt.Sprint({field_expr})")
2162 };
2163 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2164 let _ = writeln!(
2165 out_ref,
2166 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2167 );
2168 let _ = writeln!(out_ref, "\t}}");
2169 }
2170 }
2171 "not_empty" => {
2172 let field_is_array = {
2175 let rf = assertion.field.as_deref().unwrap_or("");
2176 let rn = field_resolver.resolve(rf);
2177 field_resolver.is_array(rn)
2178 };
2179 if is_optional && !field_is_array {
2180 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2182 } else if is_optional && field_is_slice {
2183 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2185 } else if is_optional {
2186 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2188 } else if result_is_simple && result_is_array {
2189 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2191 } else {
2192 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2193 }
2194 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2195 let _ = writeln!(out_ref, "\t}}");
2196 }
2197 "is_empty" => {
2198 let field_is_array = {
2199 let rf = assertion.field.as_deref().unwrap_or("");
2200 let rn = field_resolver.resolve(rf);
2201 field_resolver.is_array(rn)
2202 };
2203 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2206 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2208 } else if is_optional && !field_is_array {
2209 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2211 } else if is_optional && field_is_slice {
2212 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2214 } else if is_optional {
2215 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2217 } else {
2218 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2219 }
2220 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2221 let _ = writeln!(out_ref, "\t}}");
2222 }
2223 "contains_any" => {
2224 if let Some(values) = &assertion.values {
2225 let resolved_field = assertion.field.as_deref().unwrap_or("");
2226 let resolved_name = field_resolver.resolve(resolved_field);
2227 let field_is_array = field_resolver.is_array(resolved_name);
2228 let is_opt =
2229 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2230 let field_for_contains = if is_opt && field_is_array {
2231 format!("jsonString({field_expr})")
2233 } else if is_opt {
2234 format!("fmt.Sprint(*{field_expr})")
2235 } else if field_is_array {
2236 format!("jsonString({field_expr})")
2237 } else {
2238 format!("fmt.Sprint({field_expr})")
2239 };
2240 let _ = writeln!(out_ref, "\t{{");
2241 let _ = writeln!(out_ref, "\t\tfound := false");
2242 for val in values {
2243 let go_val = json_to_go(val);
2244 let _ = writeln!(
2245 out_ref,
2246 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2247 );
2248 }
2249 let _ = writeln!(out_ref, "\t\tif !found {{");
2250 let _ = writeln!(
2251 out_ref,
2252 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2253 );
2254 let _ = writeln!(out_ref, "\t\t}}");
2255 let _ = writeln!(out_ref, "\t}}");
2256 }
2257 }
2258 "greater_than" => {
2259 if let Some(val) = &assertion.value {
2260 let go_val = json_to_go(val);
2261 if is_optional {
2265 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2266 if let Some(n) = val.as_u64() {
2267 let next = n + 1;
2268 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2269 } else {
2270 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2271 }
2272 let _ = writeln!(
2273 out_ref,
2274 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2275 );
2276 let _ = writeln!(out_ref, "\t\t}}");
2277 let _ = writeln!(out_ref, "\t}}");
2278 } else if let Some(n) = val.as_u64() {
2279 let next = n + 1;
2280 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2281 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2282 let _ = writeln!(out_ref, "\t}}");
2283 } else {
2284 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2285 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2286 let _ = writeln!(out_ref, "\t}}");
2287 }
2288 }
2289 }
2290 "less_than" => {
2291 if let Some(val) = &assertion.value {
2292 let go_val = json_to_go(val);
2293 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2294 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2295 let _ = writeln!(out_ref, "\t}}");
2296 }
2297 }
2298 "greater_than_or_equal" => {
2299 if let Some(val) = &assertion.value {
2300 let go_val = json_to_go(val);
2301 if let Some(ref guard) = nil_guard_expr {
2302 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2303 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2304 let _ = writeln!(
2305 out_ref,
2306 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2307 );
2308 let _ = writeln!(out_ref, "\t\t}}");
2309 let _ = writeln!(out_ref, "\t}}");
2310 } else if is_optional && !field_expr.starts_with("len(") {
2311 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2313 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2314 let _ = writeln!(
2315 out_ref,
2316 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2317 );
2318 let _ = writeln!(out_ref, "\t\t}}");
2319 let _ = writeln!(out_ref, "\t}}");
2320 } else {
2321 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2322 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2323 let _ = writeln!(out_ref, "\t}}");
2324 }
2325 }
2326 }
2327 "less_than_or_equal" => {
2328 if let Some(val) = &assertion.value {
2329 let go_val = json_to_go(val);
2330 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2331 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2332 let _ = writeln!(out_ref, "\t}}");
2333 }
2334 }
2335 "starts_with" => {
2336 if let Some(expected) = &assertion.value {
2337 let go_val = json_to_go(expected);
2338 let field_for_prefix = if is_optional
2339 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2340 {
2341 format!("string(*{field_expr})")
2342 } else {
2343 format!("string({field_expr})")
2344 };
2345 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2346 let _ = writeln!(
2347 out_ref,
2348 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2349 );
2350 let _ = writeln!(out_ref, "\t}}");
2351 }
2352 }
2353 "count_min" => {
2354 if let Some(val) = &assertion.value {
2355 if let Some(n) = val.as_u64() {
2356 if is_optional {
2357 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2358 let len_expr = if field_is_slice {
2360 format!("len({field_expr})")
2361 } else {
2362 format!("len(*{field_expr})")
2363 };
2364 let _ = writeln!(
2365 out_ref,
2366 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2367 );
2368 let _ = writeln!(out_ref, "\t}}");
2369 } else {
2370 let _ = writeln!(
2371 out_ref,
2372 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2373 );
2374 }
2375 }
2376 }
2377 }
2378 "count_equals" => {
2379 if let Some(val) = &assertion.value {
2380 if let Some(n) = val.as_u64() {
2381 if is_optional {
2382 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2383 let len_expr = if field_is_slice {
2385 format!("len({field_expr})")
2386 } else {
2387 format!("len(*{field_expr})")
2388 };
2389 let _ = writeln!(
2390 out_ref,
2391 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2392 );
2393 let _ = writeln!(out_ref, "\t}}");
2394 } else {
2395 let _ = writeln!(
2396 out_ref,
2397 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2398 );
2399 }
2400 }
2401 }
2402 }
2403 "is_true" => {
2404 if is_optional {
2405 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2406 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2407 let _ = writeln!(out_ref, "\t}}");
2408 } else {
2409 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2410 }
2411 }
2412 "is_false" => {
2413 if is_optional {
2414 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2415 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2416 let _ = writeln!(out_ref, "\t}}");
2417 } else {
2418 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2419 }
2420 }
2421 "method_result" => {
2422 if let Some(method_name) = &assertion.method {
2423 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2424 let check = assertion.check.as_deref().unwrap_or("is_true");
2425 let deref_expr = if info.is_pointer {
2428 format!("*{}", info.call_expr)
2429 } else {
2430 info.call_expr.clone()
2431 };
2432 match check {
2433 "equals" => {
2434 if let Some(val) = &assertion.value {
2435 if val.is_boolean() {
2436 if val.as_bool() == Some(true) {
2437 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2438 } else {
2439 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2440 }
2441 } else {
2442 let go_val = if let Some(cast) = info.value_cast {
2446 if val.is_number() {
2447 format!("{cast}({})", json_to_go(val))
2448 } else {
2449 json_to_go(val)
2450 }
2451 } else {
2452 json_to_go(val)
2453 };
2454 let _ = writeln!(
2455 out_ref,
2456 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2457 );
2458 }
2459 }
2460 }
2461 "is_true" => {
2462 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2463 }
2464 "is_false" => {
2465 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2466 }
2467 "greater_than_or_equal" => {
2468 if let Some(val) = &assertion.value {
2469 let n = val.as_u64().unwrap_or(0);
2470 let cast = info.value_cast.unwrap_or("uint");
2472 let _ = writeln!(
2473 out_ref,
2474 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2475 );
2476 }
2477 }
2478 "count_min" => {
2479 if let Some(val) = &assertion.value {
2480 let n = val.as_u64().unwrap_or(0);
2481 let _ = writeln!(
2482 out_ref,
2483 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2484 );
2485 }
2486 }
2487 "contains" => {
2488 if let Some(val) = &assertion.value {
2489 let go_val = json_to_go(val);
2490 let _ = writeln!(
2491 out_ref,
2492 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2493 );
2494 }
2495 }
2496 "is_error" => {
2497 let _ = writeln!(out_ref, "\t{{");
2498 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2499 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2500 let _ = writeln!(out_ref, "\t}}");
2501 }
2502 other_check => {
2503 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2504 }
2505 }
2506 } else {
2507 panic!("Go e2e generator: method_result assertion missing 'method' field");
2508 }
2509 }
2510 "min_length" => {
2511 if let Some(val) = &assertion.value {
2512 if let Some(n) = val.as_u64() {
2513 if is_optional {
2514 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2515 let _ = writeln!(
2516 out_ref,
2517 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2518 );
2519 let _ = writeln!(out_ref, "\t}}");
2520 } else if field_expr.starts_with("len(") {
2521 let _ = writeln!(
2522 out_ref,
2523 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2524 );
2525 } else {
2526 let _ = writeln!(
2527 out_ref,
2528 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2529 );
2530 }
2531 }
2532 }
2533 }
2534 "max_length" => {
2535 if let Some(val) = &assertion.value {
2536 if let Some(n) = val.as_u64() {
2537 if is_optional {
2538 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2539 let _ = writeln!(
2540 out_ref,
2541 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2542 );
2543 let _ = writeln!(out_ref, "\t}}");
2544 } else if field_expr.starts_with("len(") {
2545 let _ = writeln!(
2546 out_ref,
2547 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2548 );
2549 } else {
2550 let _ = writeln!(
2551 out_ref,
2552 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2553 );
2554 }
2555 }
2556 }
2557 }
2558 "ends_with" => {
2559 if let Some(expected) = &assertion.value {
2560 let go_val = json_to_go(expected);
2561 let field_for_suffix = if is_optional
2562 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2563 {
2564 format!("string(*{field_expr})")
2565 } else {
2566 format!("string({field_expr})")
2567 };
2568 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2569 let _ = writeln!(
2570 out_ref,
2571 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2572 );
2573 let _ = writeln!(out_ref, "\t}}");
2574 }
2575 }
2576 "matches_regex" => {
2577 if let Some(expected) = &assertion.value {
2578 let go_val = json_to_go(expected);
2579 let field_for_regex = if is_optional
2580 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2581 {
2582 format!("*{field_expr}")
2583 } else {
2584 field_expr.clone()
2585 };
2586 let _ = writeln!(
2587 out_ref,
2588 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2589 );
2590 }
2591 }
2592 "not_error" => {
2593 }
2595 "error" => {
2596 }
2598 other => {
2599 panic!("Go e2e generator: unsupported assertion type: {other}");
2600 }
2601 }
2602
2603 if let Some(ref arr) = array_guard {
2606 if !assertion_buf.is_empty() {
2607 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2608 for line in assertion_buf.lines() {
2610 let _ = writeln!(out, "\t{line}");
2611 }
2612 let _ = writeln!(out, "\t}}");
2613 }
2614 } else {
2615 out.push_str(&assertion_buf);
2616 }
2617}
2618
2619struct GoMethodCallInfo {
2621 call_expr: String,
2623 is_pointer: bool,
2625 value_cast: Option<&'static str>,
2628}
2629
2630fn build_go_method_call(
2645 result_var: &str,
2646 method_name: &str,
2647 args: Option<&serde_json::Value>,
2648 import_alias: &str,
2649) -> GoMethodCallInfo {
2650 match method_name {
2651 "root_node_type" => GoMethodCallInfo {
2652 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2653 is_pointer: false,
2654 value_cast: None,
2655 },
2656 "named_children_count" => GoMethodCallInfo {
2657 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2658 is_pointer: false,
2659 value_cast: Some("uint"),
2660 },
2661 "has_error_nodes" => GoMethodCallInfo {
2662 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2663 is_pointer: true,
2664 value_cast: None,
2665 },
2666 "error_count" | "tree_error_count" => GoMethodCallInfo {
2667 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2668 is_pointer: true,
2669 value_cast: Some("uint"),
2670 },
2671 "tree_to_sexp" => GoMethodCallInfo {
2672 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2673 is_pointer: true,
2674 value_cast: None,
2675 },
2676 "contains_node_type" => {
2677 let node_type = args
2678 .and_then(|a| a.get("node_type"))
2679 .and_then(|v| v.as_str())
2680 .unwrap_or("");
2681 GoMethodCallInfo {
2682 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2683 is_pointer: true,
2684 value_cast: None,
2685 }
2686 }
2687 "find_nodes_by_type" => {
2688 let node_type = args
2689 .and_then(|a| a.get("node_type"))
2690 .and_then(|v| v.as_str())
2691 .unwrap_or("");
2692 GoMethodCallInfo {
2693 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2694 is_pointer: true,
2695 value_cast: None,
2696 }
2697 }
2698 "run_query" => {
2699 let query_source = args
2700 .and_then(|a| a.get("query_source"))
2701 .and_then(|v| v.as_str())
2702 .unwrap_or("");
2703 let language = args
2704 .and_then(|a| a.get("language"))
2705 .and_then(|v| v.as_str())
2706 .unwrap_or("");
2707 let query_lit = go_string_literal(query_source);
2708 let lang_lit = go_string_literal(language);
2709 GoMethodCallInfo {
2711 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2712 is_pointer: false,
2713 value_cast: None,
2714 }
2715 }
2716 other => {
2717 let method_pascal = other.to_upper_camel_case();
2718 GoMethodCallInfo {
2719 call_expr: format!("{result_var}.{method_pascal}()"),
2720 is_pointer: false,
2721 value_cast: None,
2722 }
2723 }
2724 }
2725}
2726
2727fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2737 match value {
2738 serde_json::Value::Object(map) => {
2739 let new_map: serde_json::Map<String, serde_json::Value> = map
2740 .into_iter()
2741 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2742 .collect();
2743 serde_json::Value::Object(new_map)
2744 }
2745 serde_json::Value::Array(arr) => {
2746 if is_byte_array(&arr) {
2749 let bytes: Vec<u8> = arr
2750 .iter()
2751 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2752 .collect();
2753 let encoded = base64_encode(&bytes);
2755 serde_json::Value::String(encoded)
2756 } else {
2757 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2758 }
2759 }
2760 serde_json::Value::String(s) => {
2761 serde_json::Value::String(pascal_to_snake_case(&s))
2764 }
2765 other => other,
2766 }
2767}
2768
2769fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2771 if arr.is_empty() {
2772 return false;
2773 }
2774 arr.iter().all(|v| {
2775 if let serde_json::Value::Number(n) = v {
2776 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
2777 } else {
2778 false
2779 }
2780 })
2781}
2782
2783fn base64_encode(bytes: &[u8]) -> String {
2786 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2787 let mut result = String::new();
2788 let mut i = 0;
2789
2790 while i + 2 < bytes.len() {
2791 let b1 = bytes[i];
2792 let b2 = bytes[i + 1];
2793 let b3 = bytes[i + 2];
2794
2795 result.push(TABLE[(b1 >> 2) as usize] as char);
2796 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2797 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
2798 result.push(TABLE[(b3 & 0x3f) as usize] as char);
2799
2800 i += 3;
2801 }
2802
2803 if i < bytes.len() {
2805 let b1 = bytes[i];
2806 result.push(TABLE[(b1 >> 2) as usize] as char);
2807
2808 if i + 1 < bytes.len() {
2809 let b2 = bytes[i + 1];
2810 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2811 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
2812 result.push('=');
2813 } else {
2814 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
2815 result.push_str("==");
2816 }
2817 }
2818
2819 result
2820}
2821
2822fn camel_to_snake_case(s: &str) -> String {
2824 let mut result = String::new();
2825 let mut prev_upper = false;
2826 for (i, c) in s.char_indices() {
2827 if c.is_uppercase() {
2828 if i > 0 && !prev_upper {
2829 result.push('_');
2830 }
2831 result.push(c.to_lowercase().next().unwrap_or(c));
2832 prev_upper = true;
2833 } else {
2834 if prev_upper && i > 1 {
2835 }
2839 result.push(c);
2840 prev_upper = false;
2841 }
2842 }
2843 result
2844}
2845
2846fn pascal_to_snake_case(s: &str) -> String {
2851 let first_char = s.chars().next();
2853 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
2854 return s.to_string();
2855 }
2856 camel_to_snake_case(s)
2857}
2858
2859fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
2863 let elem = element_type.unwrap_or("String").trim();
2864 let go_elem = rust_type_to_go(elem, import_alias);
2865 format!("[]{go_elem}")
2866}
2867
2868fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
2871 let trimmed = rust.trim();
2872 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
2873 return format!("[]{}", rust_type_to_go(inner, import_alias));
2874 }
2875 match trimmed {
2876 "String" | "&str" | "str" => "string".to_string(),
2877 "bool" => "bool".to_string(),
2878 "f32" => "float32".to_string(),
2879 "f64" => "float64".to_string(),
2880 "i8" => "int8".to_string(),
2881 "i16" => "int16".to_string(),
2882 "i32" => "int32".to_string(),
2883 "i64" | "isize" => "int64".to_string(),
2884 "u8" => "uint8".to_string(),
2885 "u16" => "uint16".to_string(),
2886 "u32" => "uint32".to_string(),
2887 "u64" | "usize" => "uint64".to_string(),
2888 _ => format!("{import_alias}.{trimmed}"),
2889 }
2890}
2891
2892fn json_to_go(value: &serde_json::Value) -> String {
2893 match value {
2894 serde_json::Value::String(s) => go_string_literal(s),
2895 serde_json::Value::Bool(b) => b.to_string(),
2896 serde_json::Value::Number(n) => n.to_string(),
2897 serde_json::Value::Null => "nil".to_string(),
2898 other => go_string_literal(&other.to_string()),
2900 }
2901}
2902
2903fn visitor_struct_name(fixture_id: &str) -> String {
2912 use heck::ToUpperCamelCase;
2913 format!("testVisitor{}", fixture_id.to_upper_camel_case())
2915}
2916
2917fn emit_go_visitor_struct(
2922 out: &mut String,
2923 struct_name: &str,
2924 visitor_spec: &crate::fixture::VisitorSpec,
2925 import_alias: &str,
2926) {
2927 let _ = writeln!(out, "type {struct_name} struct{{");
2928 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
2929 let _ = writeln!(out, "}}");
2930 for (method_name, action) in &visitor_spec.callbacks {
2931 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
2932 }
2933}
2934
2935fn emit_go_visitor_method(
2937 out: &mut String,
2938 struct_name: &str,
2939 method_name: &str,
2940 action: &CallbackAction,
2941 import_alias: &str,
2942) {
2943 let camel_method = method_to_camel(method_name);
2944 let params = match method_name {
2947 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
2948 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
2949 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
2950 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
2951 "visit_code_inline"
2952 | "visit_strong"
2953 | "visit_emphasis"
2954 | "visit_strikethrough"
2955 | "visit_underline"
2956 | "visit_subscript"
2957 | "visit_superscript"
2958 | "visit_mark"
2959 | "visit_button"
2960 | "visit_summary"
2961 | "visit_figcaption"
2962 | "visit_definition_term"
2963 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
2964 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
2965 "visit_list_item" => {
2966 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
2967 }
2968 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
2969 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
2970 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
2971 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
2972 "visit_input" => {
2973 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
2974 }
2975 "visit_audio" | "visit_video" | "visit_iframe" => {
2976 format!("_ {import_alias}.NodeContext, src *string")
2977 }
2978 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
2979 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
2980 format!("_ {import_alias}.NodeContext, output string")
2981 }
2982 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
2983 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
2984 _ => format!("_ {import_alias}.NodeContext"),
2985 };
2986
2987 let _ = writeln!(
2988 out,
2989 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
2990 );
2991 match action {
2992 CallbackAction::Skip => {
2993 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
2994 }
2995 CallbackAction::Continue => {
2996 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
2997 }
2998 CallbackAction::PreserveHtml => {
2999 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3000 }
3001 CallbackAction::Custom { output } => {
3002 let escaped = go_string_literal(output);
3003 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3004 }
3005 CallbackAction::CustomTemplate { template } => {
3006 let ptr_params = go_visitor_ptr_params(method_name);
3013 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3014 let escaped_fmt = go_string_literal(&fmt_str);
3015 if fmt_args.is_empty() {
3016 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3017 } else {
3018 let args_str = fmt_args.join(", ");
3019 let _ = writeln!(
3020 out,
3021 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3022 );
3023 }
3024 }
3025 }
3026 let _ = writeln!(out, "}}");
3027}
3028
3029fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3032 match method_name {
3033 "visit_link" => ["title"].into(),
3034 "visit_image" => ["title"].into(),
3035 "visit_heading" => ["id"].into(),
3036 "visit_code_block" => ["lang"].into(),
3037 "visit_form" => ["action", "method"].into(),
3038 "visit_input" => ["name", "value"].into(),
3039 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3040 _ => std::collections::HashSet::new(),
3041 }
3042}
3043
3044fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3056 let mut fmt_str = String::new();
3057 let mut args: Vec<String> = Vec::new();
3058 let mut chars = template.chars().peekable();
3059 while let Some(c) = chars.next() {
3060 if c == '{' {
3061 let mut name = String::new();
3063 for inner in chars.by_ref() {
3064 if inner == '}' {
3065 break;
3066 }
3067 name.push(inner);
3068 }
3069 fmt_str.push_str("%s");
3070 let go_name = go_param_name(&name);
3072 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3074 format!("*{go_name}")
3075 } else {
3076 go_name
3077 };
3078 args.push(arg_expr);
3079 } else {
3080 fmt_str.push(c);
3081 }
3082 }
3083 (fmt_str, args)
3084}
3085
3086fn method_to_camel(snake: &str) -> String {
3088 use heck::ToUpperCamelCase;
3089 snake.to_upper_camel_case()
3090}
3091
3092#[cfg(test)]
3093mod tests {
3094 use super::*;
3095 use crate::config::{CallConfig, E2eConfig};
3096 use crate::field_access::FieldResolver;
3097 use crate::fixture::{Assertion, Fixture};
3098
3099 fn make_fixture(id: &str) -> Fixture {
3100 Fixture {
3101 id: id.to_string(),
3102 category: None,
3103 description: "test fixture".to_string(),
3104 tags: vec![],
3105 skip: None,
3106 env: None,
3107 call: None,
3108 input: serde_json::Value::Null,
3109 mock_response: Some(crate::fixture::MockResponse {
3110 status: 200,
3111 body: Some(serde_json::Value::Null),
3112 stream_chunks: None,
3113 headers: std::collections::HashMap::new(),
3114 }),
3115 source: String::new(),
3116 http: None,
3117 assertions: vec![Assertion {
3118 assertion_type: "not_error".to_string(),
3119 ..Default::default()
3120 }],
3121 visitor: None,
3122 }
3123 }
3124
3125 #[test]
3129 fn test_go_method_name_uses_go_casing() {
3130 let e2e_config = E2eConfig {
3131 call: CallConfig {
3132 function: "clean_extracted_text".to_string(),
3133 module: "github.com/example/mylib".to_string(),
3134 result_var: "result".to_string(),
3135 returns_result: true,
3136 ..CallConfig::default()
3137 },
3138 ..E2eConfig::default()
3139 };
3140
3141 let fixture = make_fixture("basic_text");
3142 let resolver = FieldResolver::new(
3143 &std::collections::HashMap::new(),
3144 &std::collections::HashSet::new(),
3145 &std::collections::HashSet::new(),
3146 &std::collections::HashSet::new(),
3147 &std::collections::HashSet::new(),
3148 );
3149 let mut out = String::new();
3150 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3151
3152 assert!(
3153 out.contains("kreuzberg.CleanExtractedText("),
3154 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3155 );
3156 assert!(
3157 !out.contains("kreuzberg.clean_extracted_text("),
3158 "must not emit raw snake_case method name, got:\n{out}"
3159 );
3160 }
3161}