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\t// Keep a writable pipe to the mock-server's stdin so the");
302 let _ = writeln!(
303 out,
304 "\t\t// server does not see EOF and exit immediately. The mock-server"
305 );
306 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
307 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
308 let _ = writeln!(out, "\t\tif err != nil {{");
309 let _ = writeln!(out, "\t\t\tpanic(err)");
310 let _ = writeln!(out, "\t\t}}");
311 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
312 let _ = writeln!(out, "\t\t\tpanic(err)");
313 let _ = writeln!(out, "\t\t}}");
314 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
315 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
316 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
317 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
318 let _ = writeln!(
319 out,
320 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
321 );
322 let _ = writeln!(out, "\t\t\t\tbreak");
323 let _ = writeln!(out, "\t\t\t}}");
324 let _ = writeln!(out, "\t\t}}");
325 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
326 let _ = writeln!(out, "\t\tcode := m.Run()");
327 let _ = writeln!(out, "\t\t_ = stdin.Close()");
328 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
329 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
330 let _ = writeln!(out, "\t\tos.Exit(code)");
331 let _ = writeln!(out, "\t}} else {{");
332 let _ = writeln!(out, "\t\tcode := m.Run()");
333 let _ = writeln!(out, "\t\tos.Exit(code)");
334 let _ = writeln!(out, "\t}}");
335 let _ = writeln!(out, "}}");
336 out
337}
338
339fn render_helpers_test_go() -> String {
342 let mut out = String::new();
343 let _ = writeln!(out, "package e2e_test");
344 let _ = writeln!(out);
345 let _ = writeln!(out, "import \"encoding/json\"");
346 let _ = writeln!(out);
347 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
348 let _ = writeln!(
349 out,
350 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
351 );
352 let _ = writeln!(out, "func jsonString(value any) string {{");
353 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
354 let _ = writeln!(out, "\tif err != nil {{");
355 let _ = writeln!(out, "\t\treturn \"\"");
356 let _ = writeln!(out, "\t}}");
357 let _ = writeln!(out, "\treturn string(encoded)");
358 let _ = writeln!(out, "}}");
359 out
360}
361
362fn render_test_file(
363 category: &str,
364 fixtures: &[&Fixture],
365 go_module_path: &str,
366 import_alias: &str,
367 field_resolver: &FieldResolver,
368 e2e_config: &crate::config::E2eConfig,
369) -> String {
370 let mut out = String::new();
371 let emits_executable_test =
372 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
373
374 out.push_str(&hash::header(CommentStyle::DoubleSlash));
376 let _ = writeln!(out);
377
378 let needs_pkg = fixtures
387 .iter()
388 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
389
390 let needs_os = fixtures.iter().any(|f| {
393 if f.is_http_test() {
394 return true;
395 }
396 if !emits_executable_test(f) {
397 return false;
398 }
399 let call_config = e2e_config.resolve_call(f.call.as_deref());
400 let go_override = call_config
401 .overrides
402 .get("go")
403 .or_else(|| e2e_config.call.overrides.get("go"));
404 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
405 return true;
406 }
407 let call_args = &call_config.args;
408 if call_args.iter().any(|a| a.arg_type == "mock_url") {
411 return true;
412 }
413 call_args.iter().any(|a| {
414 if a.arg_type != "bytes" {
415 return false;
416 }
417 let mut current = &f.input;
420 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
421 for segment in path.split('.') {
422 match current.get(segment) {
423 Some(next) => current = next,
424 None => return false,
425 }
426 }
427 current.is_string()
428 })
429 });
430
431 let needs_filepath = false;
434
435 let _needs_json_stringify = fixtures.iter().any(|f| {
436 emits_executable_test(f)
437 && f.assertions.iter().any(|a| {
438 matches!(
439 a.assertion_type.as_str(),
440 "contains" | "contains_all" | "contains_any" | "not_contains"
441 ) && {
442 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
445 e2e_config.resolve_call(f.call.as_deref()).result_is_array
447 } else {
448 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
450 field_resolver.is_array(resolved_name)
451 }
452 }
453 })
454 });
455
456 let needs_json = fixtures.iter().any(|f| {
460 if let Some(http) = &f.http {
463 let body_needs_json = http
464 .expected_response
465 .body
466 .as_ref()
467 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
468 let partial_needs_json = http.expected_response.body_partial.is_some();
469 let ve_needs_json = http
470 .expected_response
471 .validation_errors
472 .as_ref()
473 .is_some_and(|v| !v.is_empty());
474 if body_needs_json || partial_needs_json || ve_needs_json {
475 return true;
476 }
477 }
478 if !emits_executable_test(f) {
479 return false;
480 }
481
482 let call = e2e_config.resolve_call(f.call.as_deref());
483 let call_args = &call.args;
484 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
486 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
487 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
488 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
489 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
490 })
491 };
492 let go_override = call.overrides.get("go");
494 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
495 e2e_config
496 .call
497 .overrides
498 .get("go")
499 .and_then(|o| o.options_type.as_deref())
500 });
501 let has_json_obj = call_args.iter().any(|a| {
502 if a.arg_type != "json_object" {
503 return false;
504 }
505 let v = if a.field == "input" {
506 &f.input
507 } else {
508 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
509 f.input.get(field).unwrap_or(&serde_json::Value::Null)
510 };
511 if v.is_array() {
512 return true;
513 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
515 });
516 has_handle || has_json_obj
517 });
518
519 let needs_base64 = false;
524
525 let needs_fmt = fixtures.iter().any(|f| {
530 f.visitor.as_ref().is_some_and(|v| {
531 v.callbacks.values().any(|action| {
532 if let CallbackAction::CustomTemplate { template } = action {
533 template.contains('{')
534 } else {
535 false
536 }
537 })
538 }) || (emits_executable_test(f)
539 && f.assertions.iter().any(|a| {
540 matches!(
541 a.assertion_type.as_str(),
542 "contains" | "contains_all" | "contains_any" | "not_contains"
543 ) && {
544 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
549 !e2e_config.resolve_call(f.call.as_deref()).result_is_array
551 } else {
552 let field = a.field.as_deref().unwrap_or("");
556 let resolved_name = field_resolver.resolve(field);
557 !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
558 }
559 }
560 }))
561 });
562
563 let needs_strings = fixtures.iter().any(|f| {
566 if !emits_executable_test(f) {
567 return false;
568 }
569 f.assertions.iter().any(|a| {
570 let type_needs_strings = if a.assertion_type == "equals" {
571 a.value.as_ref().is_some_and(|v| v.is_string())
573 } else {
574 matches!(
575 a.assertion_type.as_str(),
576 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
577 )
578 };
579 let field_valid = a
580 .field
581 .as_ref()
582 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
583 .unwrap_or(true);
584 type_needs_strings && field_valid
585 })
586 });
587
588 let needs_assert = fixtures.iter().any(|f| {
590 if !emits_executable_test(f) {
591 return false;
592 }
593 f.assertions.iter().any(|a| {
594 let field_valid = a
595 .field
596 .as_ref()
597 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
598 .unwrap_or(true);
599 let synthetic_field_needs_assert = match a.field.as_deref() {
600 Some("chunks_have_content" | "chunks_have_embeddings") => {
601 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
602 }
603 Some("embeddings") => {
604 matches!(
605 a.assertion_type.as_str(),
606 "count_equals" | "count_min" | "not_empty" | "is_empty"
607 )
608 }
609 _ => false,
610 };
611 let type_needs_assert = matches!(
612 a.assertion_type.as_str(),
613 "count_equals"
614 | "count_min"
615 | "count_max"
616 | "is_true"
617 | "is_false"
618 | "method_result"
619 | "min_length"
620 | "max_length"
621 | "matches_regex"
622 );
623 synthetic_field_needs_assert || type_needs_assert && field_valid
624 })
625 });
626
627 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
629 let needs_http = has_http_fixtures;
630 let needs_io = has_http_fixtures;
632
633 let needs_reflect = fixtures.iter().any(|f| {
636 if let Some(http) = &f.http {
637 let body_needs_reflect = http
638 .expected_response
639 .body
640 .as_ref()
641 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
642 let partial_needs_reflect = http.expected_response.body_partial.is_some();
643 body_needs_reflect || partial_needs_reflect
644 } else {
645 false
646 }
647 });
648
649 let _ = writeln!(out, "// E2e tests for category: {category}");
650 let _ = writeln!(out, "package e2e_test");
651 let _ = writeln!(out);
652 let _ = writeln!(out, "import (");
653 if needs_base64 {
654 let _ = writeln!(out, "\t\"encoding/base64\"");
655 }
656 if needs_json || needs_reflect {
657 let _ = writeln!(out, "\t\"encoding/json\"");
658 }
659 if needs_fmt {
660 let _ = writeln!(out, "\t\"fmt\"");
661 }
662 if needs_io {
663 let _ = writeln!(out, "\t\"io\"");
664 }
665 if needs_http {
666 let _ = writeln!(out, "\t\"net/http\"");
667 }
668 if needs_os {
669 let _ = writeln!(out, "\t\"os\"");
670 }
671 let _ = needs_filepath; if needs_reflect {
673 let _ = writeln!(out, "\t\"reflect\"");
674 }
675 if needs_strings {
676 let _ = writeln!(out, "\t\"strings\"");
677 }
678 let _ = writeln!(out, "\t\"testing\"");
679 if needs_assert {
680 let _ = writeln!(out);
681 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
682 }
683 if needs_pkg {
684 let _ = writeln!(out);
685 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
686 }
687 let _ = writeln!(out, ")");
688 let _ = writeln!(out);
689
690 for fixture in fixtures.iter() {
692 if let Some(visitor_spec) = &fixture.visitor {
693 let struct_name = visitor_struct_name(&fixture.id);
694 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
695 let _ = writeln!(out);
696 }
697 }
698
699 for (i, fixture) in fixtures.iter().enumerate() {
700 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
701 if i + 1 < fixtures.len() {
702 let _ = writeln!(out);
703 }
704 }
705
706 while out.ends_with("\n\n") {
708 out.pop();
709 }
710 if !out.ends_with('\n') {
711 out.push('\n');
712 }
713 out
714}
715
716fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
725 if fixture.is_http_test() {
727 return false;
728 }
729 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
730 if call_config.skip_languages.iter().any(|l| l == "go") {
733 return false;
734 }
735 let go_override = call_config
736 .overrides
737 .get("go")
738 .or_else(|| e2e_config.call.overrides.get("go"));
739 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
742 return true;
743 }
744 let fn_name = go_override
748 .and_then(|o| o.function.as_deref())
749 .filter(|s| !s.is_empty())
750 .unwrap_or(call_config.function.as_str());
751 !fn_name.is_empty()
752}
753
754fn render_test_function(
755 out: &mut String,
756 fixture: &Fixture,
757 import_alias: &str,
758 field_resolver: &FieldResolver,
759 e2e_config: &crate::config::E2eConfig,
760) {
761 let fn_name = fixture.id.to_upper_camel_case();
762 let description = &fixture.description;
763
764 if fixture.http.is_some() {
766 render_http_test_function(out, fixture);
767 return;
768 }
769
770 if !fixture_has_go_callable(fixture, e2e_config) {
775 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
776 let _ = writeln!(out, "\t// {description}");
777 let _ = writeln!(
778 out,
779 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
780 );
781 let _ = writeln!(out, "}}");
782 return;
783 }
784
785 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
787 let lang = "go";
788 let overrides = call_config.overrides.get(lang);
789
790 let base_function_name = overrides
794 .and_then(|o| o.function.as_deref())
795 .unwrap_or(&call_config.function);
796 let function_name = to_go_name(base_function_name);
797 let result_var = &call_config.result_var;
798 let args = &call_config.args;
799
800 let returns_result = overrides
803 .and_then(|o| o.returns_result)
804 .unwrap_or(call_config.returns_result);
805
806 let returns_void = call_config.returns_void;
809
810 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
813 if call_config.result_is_simple {
814 return true;
815 }
816 call_config
817 .overrides
818 .get("rust")
819 .map(|o| o.result_is_simple)
820 .unwrap_or(false)
821 });
822
823 let result_is_array = overrides
826 .map(|o| o.result_is_array)
827 .unwrap_or(call_config.result_is_array);
828
829 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
831 e2e_config
832 .call
833 .overrides
834 .get("go")
835 .and_then(|o| o.options_type.as_deref())
836 });
837
838 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
840 e2e_config
841 .call
842 .overrides
843 .get("go")
844 .map(|o| o.options_ptr)
845 .unwrap_or(false)
846 });
847
848 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
849
850 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
853 e2e_config
854 .call
855 .overrides
856 .get(lang)
857 .and_then(|o| o.client_factory.as_deref())
858 });
859
860 let (mut setup_lines, args_str) = build_args_and_setup(
861 &fixture.input,
862 args,
863 import_alias,
864 call_options_type,
865 &fixture.id,
866 call_options_ptr,
867 );
868
869 let mut visitor_opts_var: Option<String> = None;
872 if fixture.visitor.is_some() {
873 let struct_name = visitor_struct_name(&fixture.id);
874 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
875 let opts_type = call_options_type.unwrap_or("ConversionOptions");
877 let opts_var = "opts".to_string();
878 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
879 setup_lines.push("opts.Visitor = visitor".to_string());
880 visitor_opts_var = Some(opts_var);
881 }
882
883 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
884 let final_args = {
885 let mut parts: Vec<String> = Vec::new();
886 if !args_str.is_empty() {
887 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
889 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
890 } else {
891 args_str
892 };
893 parts.push(processed_args);
894 }
895 parts.extend(go_extra_args);
896 parts.join(", ")
897 };
898
899 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
900 let _ = writeln!(out, "\t// {description}");
901
902 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
906 if let Some(var) = api_key_var {
907 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
908 let _ = writeln!(out, "\tif apiKey == \"\" {{");
909 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
910 let _ = writeln!(out, "\t}}");
911 }
912
913 for line in &setup_lines {
914 let _ = writeln!(out, "\t{line}");
915 }
916
917 let call_prefix = if let Some(factory) = client_factory {
921 let factory_name = to_go_name(factory);
922 let fixture_id = &fixture.id;
923 let (api_key_expr, base_url_expr) = if api_key_var.is_some() {
927 ("apiKey".to_string(), "nil".to_string())
928 } else {
929 let _ = writeln!(
930 out,
931 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
932 );
933 ("\"test-key\"".to_string(), "&mockURL".to_string())
934 };
935 let _ = writeln!(
936 out,
937 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
938 );
939 let _ = writeln!(out, "\tif clientErr != nil {{");
940 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
941 let _ = writeln!(out, "\t}}");
942 "client".to_string()
943 } else {
944 import_alias.to_string()
945 };
946
947 let binding_returns_error_pre = args
952 .iter()
953 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
954 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
955
956 if expects_error {
957 if effective_returns_result_pre && !returns_void {
958 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
959 } else {
960 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
961 }
962 let _ = writeln!(out, "\tif err == nil {{");
963 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
964 let _ = writeln!(out, "\t}}");
965 let _ = writeln!(out, "}}");
966 return;
967 }
968
969 let has_usable_assertion = fixture.assertions.iter().any(|a| {
973 if a.assertion_type == "not_error" || a.assertion_type == "error" {
974 return false;
975 }
976 if a.assertion_type == "method_result" {
978 return true;
979 }
980 match &a.field {
981 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
982 _ => true,
983 }
984 });
985
986 let binding_returns_error = args
993 .iter()
994 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
995 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
997
998 if !effective_returns_result && result_is_simple {
1004 let result_binding = if has_usable_assertion {
1006 result_var.to_string()
1007 } else {
1008 "_".to_string()
1009 };
1010 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1012 let _ = writeln!(
1013 out,
1014 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1015 );
1016 if has_usable_assertion && result_binding != "_" {
1017 if result_is_array {
1018 let _ = writeln!(out, "\tvalue := {result_var}");
1020 } else {
1021 let only_nil_assertions = fixture
1024 .assertions
1025 .iter()
1026 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1027 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1028 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1029
1030 if !only_nil_assertions {
1031 let _ = writeln!(out, "\tif {result_var} == nil {{");
1033 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1034 let _ = writeln!(out, "\t}}");
1035 let _ = writeln!(out, "\tvalue := *{result_var}");
1036 }
1037 }
1038 }
1039 } else if !effective_returns_result || returns_void {
1040 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1043 let _ = writeln!(out, "\tif err != nil {{");
1044 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1045 let _ = writeln!(out, "\t}}");
1046 let _ = writeln!(out, "}}");
1048 return;
1049 } else {
1050 let result_binding = if has_usable_assertion {
1052 result_var.to_string()
1053 } else {
1054 "_".to_string()
1055 };
1056 let _ = writeln!(
1057 out,
1058 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1059 );
1060 let _ = writeln!(out, "\tif err != nil {{");
1061 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1062 let _ = writeln!(out, "\t}}");
1063 if result_is_simple && has_usable_assertion && result_binding != "_" {
1064 if result_is_array {
1065 let _ = writeln!(out, "\tvalue := {}", result_var);
1067 } else {
1068 let only_nil_assertions = fixture
1071 .assertions
1072 .iter()
1073 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1074 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1075 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1076
1077 if !only_nil_assertions {
1078 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1080 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1081 let _ = writeln!(out, "\t}}");
1082 let _ = writeln!(out, "\tvalue := *{}", result_var);
1083 }
1084 }
1085 }
1086 }
1087
1088 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1091 let only_nil_assertions = fixture
1092 .assertions
1093 .iter()
1094 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1095 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1096 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1097 !only_nil_assertions
1098 } else {
1099 result_is_simple && has_usable_assertion
1100 };
1101
1102 let effective_result_var = if has_deref_value {
1103 "value".to_string()
1104 } else {
1105 result_var.to_string()
1106 };
1107
1108 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1113 for assertion in &fixture.assertions {
1114 if let Some(f) = &assertion.field {
1115 if !f.is_empty() {
1116 let resolved = field_resolver.resolve(f);
1117 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1118 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1123 let is_array_field = field_resolver.is_array(resolved);
1124 if !is_string_field || is_array_field {
1125 continue;
1128 }
1129 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1130 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1131 if field_resolver.has_map_access(f) {
1132 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1135 } else {
1136 let _ = writeln!(out, "\tvar {local_var} string");
1137 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1138 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1142 let _ = writeln!(out, "\t}}");
1143 }
1144 optional_locals.insert(f.clone(), local_var);
1145 }
1146 }
1147 }
1148 }
1149
1150 for assertion in &fixture.assertions {
1152 if let Some(f) = &assertion.field {
1153 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1154 let parts: Vec<&str> = f.split('.').collect();
1157 let mut guard_expr: Option<String> = None;
1158 for i in 1..parts.len() {
1159 let prefix = parts[..i].join(".");
1160 let resolved_prefix = field_resolver.resolve(&prefix);
1161 if field_resolver.is_optional(resolved_prefix) {
1162 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
1163 guard_expr = Some(accessor);
1164 break;
1165 }
1166 }
1167 if let Some(guard) = guard_expr {
1168 if field_resolver.is_valid_for_result(f) {
1171 let _ = writeln!(out, "\tif {guard} != nil {{");
1172 let mut nil_buf = String::new();
1175 render_assertion(
1176 &mut nil_buf,
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 for line in nil_buf.lines() {
1186 let _ = writeln!(out, "\t{line}");
1187 }
1188 let _ = writeln!(out, "\t}}");
1189 } else {
1190 render_assertion(
1191 out,
1192 assertion,
1193 &effective_result_var,
1194 import_alias,
1195 field_resolver,
1196 &optional_locals,
1197 result_is_simple,
1198 result_is_array,
1199 );
1200 }
1201 continue;
1202 }
1203 }
1204 }
1205 render_assertion(
1206 out,
1207 assertion,
1208 &effective_result_var,
1209 import_alias,
1210 field_resolver,
1211 &optional_locals,
1212 result_is_simple,
1213 result_is_array,
1214 );
1215 }
1216
1217 let _ = writeln!(out, "}}");
1218}
1219
1220fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1226 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1227}
1228
1229struct GoTestClientRenderer;
1241
1242impl client::TestClientRenderer for GoTestClientRenderer {
1243 fn language_name(&self) -> &'static str {
1244 "go"
1245 }
1246
1247 fn sanitize_test_name(&self, id: &str) -> String {
1251 id.to_upper_camel_case()
1252 }
1253
1254 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1257 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1258 let _ = writeln!(out, "\t// {description}");
1259 if let Some(reason) = skip_reason {
1260 let escaped = go_string_literal(reason);
1261 let _ = writeln!(out, "\tt.Skip({escaped})");
1262 }
1263 }
1264
1265 fn render_test_close(&self, out: &mut String) {
1266 let _ = writeln!(out, "}}");
1267 }
1268
1269 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1275 let method = ctx.method.to_uppercase();
1276 let path = ctx.path;
1277
1278 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1279 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1280 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1281 let _ = writeln!(out, "\t}}");
1282
1283 let body_expr = if let Some(body) = ctx.body {
1285 let json = serde_json::to_string(body).unwrap_or_default();
1286 let escaped = go_string_literal(&json);
1287 format!("strings.NewReader({})", escaped)
1288 } else {
1289 "strings.NewReader(\"\")".to_string()
1290 };
1291
1292 let _ = writeln!(out, "\tbody := {body_expr}");
1293 let _ = writeln!(
1294 out,
1295 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1296 );
1297 let _ = writeln!(out, "\tif err != nil {{");
1298 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1299 let _ = writeln!(out, "\t}}");
1300
1301 if ctx.body.is_some() {
1303 let content_type = ctx.content_type.unwrap_or("application/json");
1304 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1305 }
1306
1307 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1309 header_names.sort();
1310 for name in header_names {
1311 let value = &ctx.headers[name];
1312 let escaped_name = go_string_literal(name);
1313 let escaped_value = go_string_literal(value);
1314 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1315 }
1316
1317 if !ctx.cookies.is_empty() {
1319 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1320 cookie_names.sort();
1321 for name in cookie_names {
1322 let value = &ctx.cookies[name];
1323 let escaped_name = go_string_literal(name);
1324 let escaped_value = go_string_literal(value);
1325 let _ = writeln!(
1326 out,
1327 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1328 );
1329 }
1330 }
1331
1332 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1334 let _ = writeln!(
1335 out,
1336 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1337 );
1338 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1339 let _ = writeln!(out, "\t\t}},");
1340 let _ = writeln!(out, "\t}}");
1341 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1342 let _ = writeln!(out, "\tif err != nil {{");
1343 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1344 let _ = writeln!(out, "\t}}");
1345 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1346
1347 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1351 let _ = writeln!(out, "\tif err != nil {{");
1352 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1353 let _ = writeln!(out, "\t}}");
1354 let _ = writeln!(out, "\t_ = bodyBytes");
1355 }
1356
1357 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1358 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1359 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1360 let _ = writeln!(out, "\t}}");
1361 }
1362
1363 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1366 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1368 return;
1369 }
1370 if name.eq_ignore_ascii_case("connection") {
1372 return;
1373 }
1374 let escaped_name = go_string_literal(name);
1375 let escaped_value = go_string_literal(expected);
1376 let _ = writeln!(
1377 out,
1378 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1379 );
1380 let _ = writeln!(
1381 out,
1382 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1383 );
1384 let _ = writeln!(out, "\t}}");
1385 }
1386
1387 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1392 match expected {
1393 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1394 let json_str = serde_json::to_string(expected).unwrap_or_default();
1395 let escaped = go_string_literal(&json_str);
1396 let _ = writeln!(out, "\tvar got any");
1397 let _ = writeln!(out, "\tvar want any");
1398 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1399 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1400 let _ = writeln!(out, "\t}}");
1401 let _ = writeln!(
1402 out,
1403 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1404 );
1405 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1406 let _ = writeln!(out, "\t}}");
1407 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1408 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1409 let _ = writeln!(out, "\t}}");
1410 }
1411 serde_json::Value::String(s) => {
1412 let escaped = go_string_literal(s);
1413 let _ = writeln!(out, "\twant := {escaped}");
1414 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1415 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1416 let _ = writeln!(out, "\t}}");
1417 }
1418 other => {
1419 let escaped = go_string_literal(&other.to_string());
1420 let _ = writeln!(out, "\twant := {escaped}");
1421 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1422 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1423 let _ = writeln!(out, "\t}}");
1424 }
1425 }
1426 }
1427
1428 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1431 if let Some(obj) = expected.as_object() {
1432 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1433 let _ = writeln!(
1434 out,
1435 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1436 );
1437 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1438 let _ = writeln!(out, "\t}}");
1439 for (key, val) in obj {
1440 let escaped_key = go_string_literal(key);
1441 let json_val = serde_json::to_string(val).unwrap_or_default();
1442 let escaped_val = go_string_literal(&json_val);
1443 let _ = writeln!(out, "\t{{");
1444 let _ = writeln!(out, "\t\tvar _wantVal any");
1445 let _ = writeln!(
1446 out,
1447 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1448 );
1449 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1450 let _ = writeln!(out, "\t\t}}");
1451 let _ = writeln!(
1452 out,
1453 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1454 );
1455 let _ = writeln!(
1456 out,
1457 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1458 );
1459 let _ = writeln!(out, "\t\t}}");
1460 let _ = writeln!(out, "\t}}");
1461 }
1462 }
1463 }
1464
1465 fn render_assert_validation_errors(
1470 &self,
1471 out: &mut String,
1472 _response_var: &str,
1473 errors: &[ValidationErrorExpectation],
1474 ) {
1475 let _ = writeln!(out, "\tvar _veBody map[string]any");
1476 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1477 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1478 let _ = writeln!(out, "\t}}");
1479 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1480 for ve in errors {
1481 let escaped_msg = go_string_literal(&ve.msg);
1482 let _ = writeln!(out, "\t{{");
1483 let _ = writeln!(out, "\t\t_found := false");
1484 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1485 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1486 let _ = writeln!(
1487 out,
1488 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1489 );
1490 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1491 let _ = writeln!(out, "\t\t\t\t\tbreak");
1492 let _ = writeln!(out, "\t\t\t\t}}");
1493 let _ = writeln!(out, "\t\t\t}}");
1494 let _ = writeln!(out, "\t\t}}");
1495 let _ = writeln!(out, "\t\tif !_found {{");
1496 let _ = writeln!(
1497 out,
1498 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1499 );
1500 let _ = writeln!(out, "\t\t}}");
1501 let _ = writeln!(out, "\t}}");
1502 }
1503 }
1504}
1505
1506fn build_args_and_setup(
1514 input: &serde_json::Value,
1515 args: &[crate::config::ArgMapping],
1516 import_alias: &str,
1517 options_type: Option<&str>,
1518 fixture_id: &str,
1519 options_ptr: bool,
1520) -> (Vec<String>, String) {
1521 use heck::ToUpperCamelCase;
1522
1523 if args.is_empty() {
1524 return (Vec::new(), String::new());
1525 }
1526
1527 let mut setup_lines: Vec<String> = Vec::new();
1528 let mut parts: Vec<String> = Vec::new();
1529
1530 for arg in args {
1531 if arg.arg_type == "mock_url" {
1532 setup_lines.push(format!(
1533 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1534 arg.name,
1535 ));
1536 parts.push(arg.name.clone());
1537 continue;
1538 }
1539
1540 if arg.arg_type == "handle" {
1541 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1543 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1544 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1545 if config_value.is_null()
1546 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1547 {
1548 setup_lines.push(format!(
1549 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
1550 name = arg.name,
1551 ));
1552 } else {
1553 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1554 let go_literal = go_string_literal(&json_str);
1555 let name = &arg.name;
1556 setup_lines.push(format!(
1557 "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}}"
1558 ));
1559 setup_lines.push(format!(
1560 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
1561 ));
1562 }
1563 parts.push(arg.name.clone());
1564 continue;
1565 }
1566
1567 let val: Option<&serde_json::Value> = if arg.field == "input" {
1568 Some(input)
1569 } else {
1570 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1571 input.get(field)
1572 };
1573
1574 if arg.arg_type == "bytes" {
1581 let var_name = format!("{}Bytes", arg.name);
1582 match val {
1583 None | Some(serde_json::Value::Null) => {
1584 if arg.optional {
1585 parts.push("nil".to_string());
1586 } else {
1587 parts.push("[]byte{}".to_string());
1588 }
1589 }
1590 Some(serde_json::Value::String(s)) => {
1591 let go_path = go_string_literal(s);
1596 setup_lines.push(format!(
1597 "{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}}"
1598 ));
1599 parts.push(var_name);
1600 }
1601 Some(other) => {
1602 parts.push(format!("[]byte({})", json_to_go(other)));
1603 }
1604 }
1605 continue;
1606 }
1607
1608 match val {
1609 None | Some(serde_json::Value::Null) if arg.optional => {
1610 match arg.arg_type.as_str() {
1612 "string" => {
1613 parts.push("nil".to_string());
1615 }
1616 "json_object" => {
1617 if options_ptr {
1618 parts.push("nil".to_string());
1620 } else if let Some(opts_type) = options_type {
1621 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1623 } else {
1624 parts.push("nil".to_string());
1625 }
1626 }
1627 _ => {
1628 parts.push("nil".to_string());
1629 }
1630 }
1631 }
1632 None | Some(serde_json::Value::Null) => {
1633 let default_val = match arg.arg_type.as_str() {
1635 "string" => "\"\"".to_string(),
1636 "int" | "integer" | "i64" => "0".to_string(),
1637 "float" | "number" => "0.0".to_string(),
1638 "bool" | "boolean" => "false".to_string(),
1639 "json_object" => {
1640 if options_ptr {
1641 "nil".to_string()
1643 } else if let Some(opts_type) = options_type {
1644 format!("{import_alias}.{opts_type}{{}}")
1645 } else {
1646 "nil".to_string()
1647 }
1648 }
1649 _ => "nil".to_string(),
1650 };
1651 parts.push(default_val);
1652 }
1653 Some(v) => {
1654 match arg.arg_type.as_str() {
1655 "json_object" => {
1656 let is_array = v.is_array();
1659 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1660 if is_empty_obj {
1661 if options_ptr {
1662 parts.push("nil".to_string());
1664 } else if let Some(opts_type) = options_type {
1665 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1666 } else {
1667 parts.push("nil".to_string());
1668 }
1669 } else if is_array {
1670 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1675 if go_t.starts_with('[') {
1679 go_t.to_string()
1680 } else {
1681 let qualified = if go_t.contains('.') {
1683 go_t.to_string()
1684 } else {
1685 format!("{import_alias}.{go_t}")
1686 };
1687 format!("[]{qualified}")
1688 }
1689 } else {
1690 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1691 };
1692 let converted_v = convert_json_for_go(v.clone());
1694 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1695 let go_literal = go_string_literal(&json_str);
1696 let var_name = &arg.name;
1697 setup_lines.push(format!(
1698 "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}}"
1699 ));
1700 parts.push(var_name.to_string());
1701 } else if let Some(opts_type) = options_type {
1702 let remapped_v = if options_ptr {
1707 convert_json_for_go(v.clone())
1708 } else {
1709 v.clone()
1710 };
1711 let json_str = serde_json::to_string(&remapped_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} {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}}"
1716 ));
1717 let arg_expr = if options_ptr {
1719 format!("&{var_name}")
1720 } else {
1721 var_name.to_string()
1722 };
1723 parts.push(arg_expr);
1724 } else {
1725 parts.push(json_to_go(v));
1726 }
1727 }
1728 "string" if arg.optional => {
1729 let var_name = format!("{}Val", arg.name);
1731 let go_val = json_to_go(v);
1732 setup_lines.push(format!("{var_name} := {go_val}"));
1733 parts.push(format!("&{var_name}"));
1734 }
1735 _ => {
1736 parts.push(json_to_go(v));
1737 }
1738 }
1739 }
1740 }
1741 }
1742
1743 (setup_lines, parts.join(", "))
1744}
1745
1746#[allow(clippy::too_many_arguments)]
1747fn render_assertion(
1748 out: &mut String,
1749 assertion: &Assertion,
1750 result_var: &str,
1751 import_alias: &str,
1752 field_resolver: &FieldResolver,
1753 optional_locals: &std::collections::HashMap<String, String>,
1754 result_is_simple: bool,
1755 result_is_array: bool,
1756) {
1757 if !result_is_simple {
1760 if let Some(f) = &assertion.field {
1761 let embed_deref = format!("(*{result_var})");
1764 match f.as_str() {
1765 "chunks_have_content" => {
1766 let pred = format!(
1767 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1768 );
1769 match assertion.assertion_type.as_str() {
1770 "is_true" => {
1771 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1772 }
1773 "is_false" => {
1774 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1775 }
1776 _ => {
1777 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1778 }
1779 }
1780 return;
1781 }
1782 "chunks_have_embeddings" => {
1783 let pred = format!(
1784 "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 }}()"
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 "embeddings" => {
1800 match assertion.assertion_type.as_str() {
1801 "count_equals" => {
1802 if let Some(val) = &assertion.value {
1803 if let Some(n) = val.as_u64() {
1804 let _ = writeln!(
1805 out,
1806 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1807 );
1808 }
1809 }
1810 }
1811 "count_min" => {
1812 if let Some(val) = &assertion.value {
1813 if let Some(n) = val.as_u64() {
1814 let _ = writeln!(
1815 out,
1816 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1817 );
1818 }
1819 }
1820 }
1821 "not_empty" => {
1822 let _ = writeln!(
1823 out,
1824 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1825 );
1826 }
1827 "is_empty" => {
1828 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1829 }
1830 _ => {
1831 let _ = writeln!(
1832 out,
1833 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1834 );
1835 }
1836 }
1837 return;
1838 }
1839 "embedding_dimensions" => {
1840 let expr = format!(
1841 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1842 );
1843 match assertion.assertion_type.as_str() {
1844 "equals" => {
1845 if let Some(val) = &assertion.value {
1846 if let Some(n) = val.as_u64() {
1847 let _ = writeln!(
1848 out,
1849 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1850 );
1851 }
1852 }
1853 }
1854 "greater_than" => {
1855 if let Some(val) = &assertion.value {
1856 if let Some(n) = val.as_u64() {
1857 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1858 }
1859 }
1860 }
1861 _ => {
1862 let _ = writeln!(
1863 out,
1864 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1865 );
1866 }
1867 }
1868 return;
1869 }
1870 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1871 let pred = match f.as_str() {
1872 "embeddings_valid" => {
1873 format!(
1874 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1875 )
1876 }
1877 "embeddings_finite" => {
1878 format!(
1879 "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 }}()"
1880 )
1881 }
1882 "embeddings_non_zero" => {
1883 format!(
1884 "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 }}()"
1885 )
1886 }
1887 "embeddings_normalized" => {
1888 format!(
1889 "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 }}()"
1890 )
1891 }
1892 _ => unreachable!(),
1893 };
1894 match assertion.assertion_type.as_str() {
1895 "is_true" => {
1896 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1897 }
1898 "is_false" => {
1899 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1900 }
1901 _ => {
1902 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1903 }
1904 }
1905 return;
1906 }
1907 "keywords" | "keywords_count" => {
1910 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1911 return;
1912 }
1913 _ => {}
1914 }
1915 }
1916 }
1917
1918 if !result_is_simple {
1921 if let Some(f) = &assertion.field {
1922 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1923 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1924 return;
1925 }
1926 }
1927 }
1928
1929 let field_expr = if result_is_simple {
1930 result_var.to_string()
1932 } else {
1933 match &assertion.field {
1934 Some(f) if !f.is_empty() => {
1935 if let Some(local_var) = optional_locals.get(f.as_str()) {
1937 local_var.clone()
1938 } else {
1939 field_resolver.accessor(f, "go", result_var)
1940 }
1941 }
1942 _ => result_var.to_string(),
1943 }
1944 };
1945
1946 let is_optional = assertion
1950 .field
1951 .as_ref()
1952 .map(|f| {
1953 let resolved = field_resolver.resolve(f);
1954 let check_path = resolved
1955 .strip_suffix(".length")
1956 .or_else(|| resolved.strip_suffix(".count"))
1957 .or_else(|| resolved.strip_suffix(".size"))
1958 .unwrap_or(resolved);
1959 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1960 })
1961 .unwrap_or(false);
1962
1963 let field_is_array_for_len = 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_array(check_path)
1977 })
1978 .unwrap_or(false);
1979 let field_expr =
1980 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
1981 let inner = &field_expr[4..field_expr.len() - 1];
1982 format!("len(*{inner})")
1983 } else {
1984 field_expr
1985 };
1986 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1988 Some(field_expr[5..field_expr.len() - 1].to_string())
1989 } else {
1990 None
1991 };
1992
1993 let field_is_slice = assertion
1997 .field
1998 .as_ref()
1999 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2000 .unwrap_or(false);
2001 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2002 format!("*{field_expr}")
2003 } else {
2004 field_expr.clone()
2005 };
2006
2007 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2012 let mut array_expr = field_expr[..idx].to_string();
2013 if let Some(stripped) = array_expr.strip_prefix("len(") {
2014 array_expr = stripped.to_string();
2015 }
2016 Some(array_expr)
2017 } else {
2018 None
2019 };
2020
2021 let mut assertion_buf = String::new();
2024 let out_ref = &mut assertion_buf;
2025
2026 match assertion.assertion_type.as_str() {
2027 "equals" => {
2028 if let Some(expected) = &assertion.value {
2029 let go_val = json_to_go(expected);
2030 if expected.is_string() {
2032 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2035 format!("strings.TrimSpace(string(*{field_expr}))")
2036 } else {
2037 format!("strings.TrimSpace(string({field_expr}))")
2038 };
2039 if is_optional && !field_expr.starts_with("len(") {
2040 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2041 } else {
2042 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2043 }
2044 } else if is_optional && !field_expr.starts_with("len(") {
2045 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2046 } else {
2047 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2048 }
2049 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2050 let _ = writeln!(out_ref, "\t}}");
2051 }
2052 }
2053 "contains" => {
2054 if let Some(expected) = &assertion.value {
2055 let go_val = json_to_go(expected);
2056 let resolved_field = assertion.field.as_deref().unwrap_or("");
2062 let resolved_name = field_resolver.resolve(resolved_field);
2063 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2064 let is_opt =
2065 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2066 let field_for_contains = if is_opt && field_is_array {
2067 format!("jsonString({field_expr})")
2069 } else if is_opt {
2070 format!("fmt.Sprint(*{field_expr})")
2071 } else if field_is_array {
2072 format!("jsonString({field_expr})")
2073 } else {
2074 format!("fmt.Sprint({field_expr})")
2075 };
2076 if is_opt {
2077 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2078 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2079 let _ = writeln!(
2080 out_ref,
2081 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2082 );
2083 let _ = writeln!(out_ref, "\t}}");
2084 let _ = writeln!(out_ref, "\t}}");
2085 } else {
2086 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2087 let _ = writeln!(
2088 out_ref,
2089 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2090 );
2091 let _ = writeln!(out_ref, "\t}}");
2092 }
2093 }
2094 }
2095 "contains_all" => {
2096 if let Some(values) = &assertion.values {
2097 let resolved_field = assertion.field.as_deref().unwrap_or("");
2098 let resolved_name = field_resolver.resolve(resolved_field);
2099 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2100 let is_opt =
2101 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2102 for val in values {
2103 let go_val = json_to_go(val);
2104 let field_for_contains = if is_opt && field_is_array {
2105 format!("jsonString({field_expr})")
2107 } else if is_opt {
2108 format!("fmt.Sprint(*{field_expr})")
2109 } else if field_is_array {
2110 format!("jsonString({field_expr})")
2111 } else {
2112 format!("fmt.Sprint({field_expr})")
2113 };
2114 if is_opt {
2115 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2116 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2117 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2118 let _ = writeln!(out_ref, "\t}}");
2119 let _ = writeln!(out_ref, "\t}}");
2120 } else {
2121 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2122 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2123 let _ = writeln!(out_ref, "\t}}");
2124 }
2125 }
2126 }
2127 }
2128 "not_contains" => {
2129 if let Some(expected) = &assertion.value {
2130 let go_val = json_to_go(expected);
2131 let resolved_field = assertion.field.as_deref().unwrap_or("");
2132 let resolved_name = field_resolver.resolve(resolved_field);
2133 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2134 let is_opt =
2135 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2136 let field_for_contains = if is_opt && field_is_array {
2137 format!("jsonString({field_expr})")
2139 } else if is_opt {
2140 format!("fmt.Sprint(*{field_expr})")
2141 } else if field_is_array {
2142 format!("jsonString({field_expr})")
2143 } else {
2144 format!("fmt.Sprint({field_expr})")
2145 };
2146 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2147 let _ = writeln!(
2148 out_ref,
2149 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2150 );
2151 let _ = writeln!(out_ref, "\t}}");
2152 }
2153 }
2154 "not_empty" => {
2155 let field_is_array = {
2158 let rf = assertion.field.as_deref().unwrap_or("");
2159 let rn = field_resolver.resolve(rf);
2160 field_resolver.is_array(rn)
2161 };
2162 if is_optional && !field_is_array {
2163 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2165 } else if is_optional && field_is_slice {
2166 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2168 } else if is_optional {
2169 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2171 } else if result_is_simple && result_is_array {
2172 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2174 } else {
2175 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2176 }
2177 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2178 let _ = writeln!(out_ref, "\t}}");
2179 }
2180 "is_empty" => {
2181 let field_is_array = {
2182 let rf = assertion.field.as_deref().unwrap_or("");
2183 let rn = field_resolver.resolve(rf);
2184 field_resolver.is_array(rn)
2185 };
2186 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2189 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2191 } else if is_optional && !field_is_array {
2192 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2194 } else if is_optional && field_is_slice {
2195 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2197 } else if is_optional {
2198 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2200 } else {
2201 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2202 }
2203 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2204 let _ = writeln!(out_ref, "\t}}");
2205 }
2206 "contains_any" => {
2207 if let Some(values) = &assertion.values {
2208 let resolved_field = assertion.field.as_deref().unwrap_or("");
2209 let resolved_name = field_resolver.resolve(resolved_field);
2210 let field_is_array = field_resolver.is_array(resolved_name);
2211 let is_opt =
2212 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2213 let field_for_contains = if is_opt && field_is_array {
2214 format!("jsonString({field_expr})")
2216 } else if is_opt {
2217 format!("fmt.Sprint(*{field_expr})")
2218 } else if field_is_array {
2219 format!("jsonString({field_expr})")
2220 } else {
2221 format!("fmt.Sprint({field_expr})")
2222 };
2223 let _ = writeln!(out_ref, "\t{{");
2224 let _ = writeln!(out_ref, "\t\tfound := false");
2225 for val in values {
2226 let go_val = json_to_go(val);
2227 let _ = writeln!(
2228 out_ref,
2229 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2230 );
2231 }
2232 let _ = writeln!(out_ref, "\t\tif !found {{");
2233 let _ = writeln!(
2234 out_ref,
2235 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2236 );
2237 let _ = writeln!(out_ref, "\t\t}}");
2238 let _ = writeln!(out_ref, "\t}}");
2239 }
2240 }
2241 "greater_than" => {
2242 if let Some(val) = &assertion.value {
2243 let go_val = json_to_go(val);
2244 if is_optional {
2248 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2249 if let Some(n) = val.as_u64() {
2250 let next = n + 1;
2251 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2252 } else {
2253 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2254 }
2255 let _ = writeln!(
2256 out_ref,
2257 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2258 );
2259 let _ = writeln!(out_ref, "\t\t}}");
2260 let _ = writeln!(out_ref, "\t}}");
2261 } else if let Some(n) = val.as_u64() {
2262 let next = n + 1;
2263 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2264 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2265 let _ = writeln!(out_ref, "\t}}");
2266 } else {
2267 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2268 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2269 let _ = writeln!(out_ref, "\t}}");
2270 }
2271 }
2272 }
2273 "less_than" => {
2274 if let Some(val) = &assertion.value {
2275 let go_val = json_to_go(val);
2276 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2277 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2278 let _ = writeln!(out_ref, "\t}}");
2279 }
2280 }
2281 "greater_than_or_equal" => {
2282 if let Some(val) = &assertion.value {
2283 let go_val = json_to_go(val);
2284 if let Some(ref guard) = nil_guard_expr {
2285 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2286 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2287 let _ = writeln!(
2288 out_ref,
2289 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2290 );
2291 let _ = writeln!(out_ref, "\t\t}}");
2292 let _ = writeln!(out_ref, "\t}}");
2293 } else if is_optional && !field_expr.starts_with("len(") {
2294 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2296 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2297 let _ = writeln!(
2298 out_ref,
2299 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2300 );
2301 let _ = writeln!(out_ref, "\t\t}}");
2302 let _ = writeln!(out_ref, "\t}}");
2303 } else {
2304 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2305 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2306 let _ = writeln!(out_ref, "\t}}");
2307 }
2308 }
2309 }
2310 "less_than_or_equal" => {
2311 if let Some(val) = &assertion.value {
2312 let go_val = json_to_go(val);
2313 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2314 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2315 let _ = writeln!(out_ref, "\t}}");
2316 }
2317 }
2318 "starts_with" => {
2319 if let Some(expected) = &assertion.value {
2320 let go_val = json_to_go(expected);
2321 let field_for_prefix = if is_optional
2322 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2323 {
2324 format!("string(*{field_expr})")
2325 } else {
2326 format!("string({field_expr})")
2327 };
2328 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2329 let _ = writeln!(
2330 out_ref,
2331 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2332 );
2333 let _ = writeln!(out_ref, "\t}}");
2334 }
2335 }
2336 "count_min" => {
2337 if let Some(val) = &assertion.value {
2338 if let Some(n) = val.as_u64() {
2339 if is_optional {
2340 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2341 let len_expr = if field_is_slice {
2343 format!("len({field_expr})")
2344 } else {
2345 format!("len(*{field_expr})")
2346 };
2347 let _ = writeln!(
2348 out_ref,
2349 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2350 );
2351 let _ = writeln!(out_ref, "\t}}");
2352 } else {
2353 let _ = writeln!(
2354 out_ref,
2355 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2356 );
2357 }
2358 }
2359 }
2360 }
2361 "count_equals" => {
2362 if let Some(val) = &assertion.value {
2363 if let Some(n) = val.as_u64() {
2364 if is_optional {
2365 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2366 let len_expr = if field_is_slice {
2368 format!("len({field_expr})")
2369 } else {
2370 format!("len(*{field_expr})")
2371 };
2372 let _ = writeln!(
2373 out_ref,
2374 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2375 );
2376 let _ = writeln!(out_ref, "\t}}");
2377 } else {
2378 let _ = writeln!(
2379 out_ref,
2380 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2381 );
2382 }
2383 }
2384 }
2385 }
2386 "is_true" => {
2387 if is_optional {
2388 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2389 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2390 let _ = writeln!(out_ref, "\t}}");
2391 } else {
2392 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2393 }
2394 }
2395 "is_false" => {
2396 if is_optional {
2397 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2398 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2399 let _ = writeln!(out_ref, "\t}}");
2400 } else {
2401 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2402 }
2403 }
2404 "method_result" => {
2405 if let Some(method_name) = &assertion.method {
2406 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2407 let check = assertion.check.as_deref().unwrap_or("is_true");
2408 let deref_expr = if info.is_pointer {
2411 format!("*{}", info.call_expr)
2412 } else {
2413 info.call_expr.clone()
2414 };
2415 match check {
2416 "equals" => {
2417 if let Some(val) = &assertion.value {
2418 if val.is_boolean() {
2419 if val.as_bool() == Some(true) {
2420 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2421 } else {
2422 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2423 }
2424 } else {
2425 let go_val = if let Some(cast) = info.value_cast {
2429 if val.is_number() {
2430 format!("{cast}({})", json_to_go(val))
2431 } else {
2432 json_to_go(val)
2433 }
2434 } else {
2435 json_to_go(val)
2436 };
2437 let _ = writeln!(
2438 out_ref,
2439 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2440 );
2441 }
2442 }
2443 }
2444 "is_true" => {
2445 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2446 }
2447 "is_false" => {
2448 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2449 }
2450 "greater_than_or_equal" => {
2451 if let Some(val) = &assertion.value {
2452 let n = val.as_u64().unwrap_or(0);
2453 let cast = info.value_cast.unwrap_or("uint");
2455 let _ = writeln!(
2456 out_ref,
2457 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2458 );
2459 }
2460 }
2461 "count_min" => {
2462 if let Some(val) = &assertion.value {
2463 let n = val.as_u64().unwrap_or(0);
2464 let _ = writeln!(
2465 out_ref,
2466 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2467 );
2468 }
2469 }
2470 "contains" => {
2471 if let Some(val) = &assertion.value {
2472 let go_val = json_to_go(val);
2473 let _ = writeln!(
2474 out_ref,
2475 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2476 );
2477 }
2478 }
2479 "is_error" => {
2480 let _ = writeln!(out_ref, "\t{{");
2481 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2482 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2483 let _ = writeln!(out_ref, "\t}}");
2484 }
2485 other_check => {
2486 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2487 }
2488 }
2489 } else {
2490 panic!("Go e2e generator: method_result assertion missing 'method' field");
2491 }
2492 }
2493 "min_length" => {
2494 if let Some(val) = &assertion.value {
2495 if let Some(n) = val.as_u64() {
2496 if is_optional {
2497 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2498 let _ = writeln!(
2499 out_ref,
2500 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2501 );
2502 let _ = writeln!(out_ref, "\t}}");
2503 } else if field_expr.starts_with("len(") {
2504 let _ = writeln!(
2505 out_ref,
2506 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2507 );
2508 } else {
2509 let _ = writeln!(
2510 out_ref,
2511 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2512 );
2513 }
2514 }
2515 }
2516 }
2517 "max_length" => {
2518 if let Some(val) = &assertion.value {
2519 if let Some(n) = val.as_u64() {
2520 if is_optional {
2521 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2522 let _ = writeln!(
2523 out_ref,
2524 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2525 );
2526 let _ = writeln!(out_ref, "\t}}");
2527 } else if field_expr.starts_with("len(") {
2528 let _ = writeln!(
2529 out_ref,
2530 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2531 );
2532 } else {
2533 let _ = writeln!(
2534 out_ref,
2535 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2536 );
2537 }
2538 }
2539 }
2540 }
2541 "ends_with" => {
2542 if let Some(expected) = &assertion.value {
2543 let go_val = json_to_go(expected);
2544 let field_for_suffix = if is_optional
2545 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2546 {
2547 format!("string(*{field_expr})")
2548 } else {
2549 format!("string({field_expr})")
2550 };
2551 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2552 let _ = writeln!(
2553 out_ref,
2554 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2555 );
2556 let _ = writeln!(out_ref, "\t}}");
2557 }
2558 }
2559 "matches_regex" => {
2560 if let Some(expected) = &assertion.value {
2561 let go_val = json_to_go(expected);
2562 let field_for_regex = if is_optional
2563 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2564 {
2565 format!("*{field_expr}")
2566 } else {
2567 field_expr.clone()
2568 };
2569 let _ = writeln!(
2570 out_ref,
2571 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2572 );
2573 }
2574 }
2575 "not_error" => {
2576 }
2578 "error" => {
2579 }
2581 other => {
2582 panic!("Go e2e generator: unsupported assertion type: {other}");
2583 }
2584 }
2585
2586 if let Some(ref arr) = array_guard {
2589 if !assertion_buf.is_empty() {
2590 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2591 for line in assertion_buf.lines() {
2593 let _ = writeln!(out, "\t{line}");
2594 }
2595 let _ = writeln!(out, "\t}}");
2596 }
2597 } else {
2598 out.push_str(&assertion_buf);
2599 }
2600}
2601
2602struct GoMethodCallInfo {
2604 call_expr: String,
2606 is_pointer: bool,
2608 value_cast: Option<&'static str>,
2611}
2612
2613fn build_go_method_call(
2628 result_var: &str,
2629 method_name: &str,
2630 args: Option<&serde_json::Value>,
2631 import_alias: &str,
2632) -> GoMethodCallInfo {
2633 match method_name {
2634 "root_node_type" => GoMethodCallInfo {
2635 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2636 is_pointer: false,
2637 value_cast: None,
2638 },
2639 "named_children_count" => GoMethodCallInfo {
2640 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2641 is_pointer: false,
2642 value_cast: Some("uint"),
2643 },
2644 "has_error_nodes" => GoMethodCallInfo {
2645 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2646 is_pointer: true,
2647 value_cast: None,
2648 },
2649 "error_count" | "tree_error_count" => GoMethodCallInfo {
2650 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2651 is_pointer: true,
2652 value_cast: Some("uint"),
2653 },
2654 "tree_to_sexp" => GoMethodCallInfo {
2655 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2656 is_pointer: true,
2657 value_cast: None,
2658 },
2659 "contains_node_type" => {
2660 let node_type = args
2661 .and_then(|a| a.get("node_type"))
2662 .and_then(|v| v.as_str())
2663 .unwrap_or("");
2664 GoMethodCallInfo {
2665 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2666 is_pointer: true,
2667 value_cast: None,
2668 }
2669 }
2670 "find_nodes_by_type" => {
2671 let node_type = args
2672 .and_then(|a| a.get("node_type"))
2673 .and_then(|v| v.as_str())
2674 .unwrap_or("");
2675 GoMethodCallInfo {
2676 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2677 is_pointer: true,
2678 value_cast: None,
2679 }
2680 }
2681 "run_query" => {
2682 let query_source = args
2683 .and_then(|a| a.get("query_source"))
2684 .and_then(|v| v.as_str())
2685 .unwrap_or("");
2686 let language = args
2687 .and_then(|a| a.get("language"))
2688 .and_then(|v| v.as_str())
2689 .unwrap_or("");
2690 let query_lit = go_string_literal(query_source);
2691 let lang_lit = go_string_literal(language);
2692 GoMethodCallInfo {
2694 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2695 is_pointer: false,
2696 value_cast: None,
2697 }
2698 }
2699 other => {
2700 let method_pascal = other.to_upper_camel_case();
2701 GoMethodCallInfo {
2702 call_expr: format!("{result_var}.{method_pascal}()"),
2703 is_pointer: false,
2704 value_cast: None,
2705 }
2706 }
2707 }
2708}
2709
2710fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2720 match value {
2721 serde_json::Value::Object(map) => {
2722 let new_map: serde_json::Map<String, serde_json::Value> = map
2723 .into_iter()
2724 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2725 .collect();
2726 serde_json::Value::Object(new_map)
2727 }
2728 serde_json::Value::Array(arr) => {
2729 if is_byte_array(&arr) {
2732 let bytes: Vec<u8> = arr
2733 .iter()
2734 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2735 .collect();
2736 let encoded = base64_encode(&bytes);
2738 serde_json::Value::String(encoded)
2739 } else {
2740 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2741 }
2742 }
2743 serde_json::Value::String(s) => {
2744 serde_json::Value::String(pascal_to_snake_case(&s))
2747 }
2748 other => other,
2749 }
2750}
2751
2752fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2754 if arr.is_empty() {
2755 return false;
2756 }
2757 arr.iter().all(|v| {
2758 if let serde_json::Value::Number(n) = v {
2759 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
2760 } else {
2761 false
2762 }
2763 })
2764}
2765
2766fn base64_encode(bytes: &[u8]) -> String {
2769 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2770 let mut result = String::new();
2771 let mut i = 0;
2772
2773 while i + 2 < bytes.len() {
2774 let b1 = bytes[i];
2775 let b2 = bytes[i + 1];
2776 let b3 = bytes[i + 2];
2777
2778 result.push(TABLE[(b1 >> 2) as usize] as char);
2779 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2780 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
2781 result.push(TABLE[(b3 & 0x3f) as usize] as char);
2782
2783 i += 3;
2784 }
2785
2786 if i < bytes.len() {
2788 let b1 = bytes[i];
2789 result.push(TABLE[(b1 >> 2) as usize] as char);
2790
2791 if i + 1 < bytes.len() {
2792 let b2 = bytes[i + 1];
2793 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2794 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
2795 result.push('=');
2796 } else {
2797 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
2798 result.push_str("==");
2799 }
2800 }
2801
2802 result
2803}
2804
2805fn camel_to_snake_case(s: &str) -> String {
2807 let mut result = String::new();
2808 let mut prev_upper = false;
2809 for (i, c) in s.char_indices() {
2810 if c.is_uppercase() {
2811 if i > 0 && !prev_upper {
2812 result.push('_');
2813 }
2814 result.push(c.to_lowercase().next().unwrap_or(c));
2815 prev_upper = true;
2816 } else {
2817 if prev_upper && i > 1 {
2818 }
2822 result.push(c);
2823 prev_upper = false;
2824 }
2825 }
2826 result
2827}
2828
2829fn pascal_to_snake_case(s: &str) -> String {
2834 let first_char = s.chars().next();
2836 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
2837 return s.to_string();
2838 }
2839 camel_to_snake_case(s)
2840}
2841
2842fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
2846 let elem = element_type.unwrap_or("String").trim();
2847 let go_elem = rust_type_to_go(elem, import_alias);
2848 format!("[]{go_elem}")
2849}
2850
2851fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
2854 let trimmed = rust.trim();
2855 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
2856 return format!("[]{}", rust_type_to_go(inner, import_alias));
2857 }
2858 match trimmed {
2859 "String" | "&str" | "str" => "string".to_string(),
2860 "bool" => "bool".to_string(),
2861 "f32" => "float32".to_string(),
2862 "f64" => "float64".to_string(),
2863 "i8" => "int8".to_string(),
2864 "i16" => "int16".to_string(),
2865 "i32" => "int32".to_string(),
2866 "i64" | "isize" => "int64".to_string(),
2867 "u8" => "uint8".to_string(),
2868 "u16" => "uint16".to_string(),
2869 "u32" => "uint32".to_string(),
2870 "u64" | "usize" => "uint64".to_string(),
2871 _ => format!("{import_alias}.{trimmed}"),
2872 }
2873}
2874
2875fn json_to_go(value: &serde_json::Value) -> String {
2876 match value {
2877 serde_json::Value::String(s) => go_string_literal(s),
2878 serde_json::Value::Bool(b) => b.to_string(),
2879 serde_json::Value::Number(n) => n.to_string(),
2880 serde_json::Value::Null => "nil".to_string(),
2881 other => go_string_literal(&other.to_string()),
2883 }
2884}
2885
2886fn visitor_struct_name(fixture_id: &str) -> String {
2895 use heck::ToUpperCamelCase;
2896 format!("testVisitor{}", fixture_id.to_upper_camel_case())
2898}
2899
2900fn emit_go_visitor_struct(
2905 out: &mut String,
2906 struct_name: &str,
2907 visitor_spec: &crate::fixture::VisitorSpec,
2908 import_alias: &str,
2909) {
2910 let _ = writeln!(out, "type {struct_name} struct{{");
2911 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
2912 let _ = writeln!(out, "}}");
2913 for (method_name, action) in &visitor_spec.callbacks {
2914 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
2915 }
2916}
2917
2918fn emit_go_visitor_method(
2920 out: &mut String,
2921 struct_name: &str,
2922 method_name: &str,
2923 action: &CallbackAction,
2924 import_alias: &str,
2925) {
2926 let camel_method = method_to_camel(method_name);
2927 let params = match method_name {
2930 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
2931 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
2932 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
2933 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
2934 "visit_code_inline"
2935 | "visit_strong"
2936 | "visit_emphasis"
2937 | "visit_strikethrough"
2938 | "visit_underline"
2939 | "visit_subscript"
2940 | "visit_superscript"
2941 | "visit_mark"
2942 | "visit_button"
2943 | "visit_summary"
2944 | "visit_figcaption"
2945 | "visit_definition_term"
2946 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
2947 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
2948 "visit_list_item" => {
2949 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
2950 }
2951 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
2952 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
2953 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
2954 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
2955 "visit_input" => {
2956 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
2957 }
2958 "visit_audio" | "visit_video" | "visit_iframe" => {
2959 format!("_ {import_alias}.NodeContext, src *string")
2960 }
2961 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
2962 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
2963 format!("_ {import_alias}.NodeContext, output string")
2964 }
2965 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
2966 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
2967 _ => format!("_ {import_alias}.NodeContext"),
2968 };
2969
2970 let _ = writeln!(
2971 out,
2972 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
2973 );
2974 match action {
2975 CallbackAction::Skip => {
2976 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
2977 }
2978 CallbackAction::Continue => {
2979 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
2980 }
2981 CallbackAction::PreserveHtml => {
2982 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
2983 }
2984 CallbackAction::Custom { output } => {
2985 let escaped = go_string_literal(output);
2986 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
2987 }
2988 CallbackAction::CustomTemplate { template } => {
2989 let ptr_params = go_visitor_ptr_params(method_name);
2996 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
2997 let escaped_fmt = go_string_literal(&fmt_str);
2998 if fmt_args.is_empty() {
2999 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3000 } else {
3001 let args_str = fmt_args.join(", ");
3002 let _ = writeln!(
3003 out,
3004 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3005 );
3006 }
3007 }
3008 }
3009 let _ = writeln!(out, "}}");
3010}
3011
3012fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3015 match method_name {
3016 "visit_link" => ["title"].into(),
3017 "visit_image" => ["title"].into(),
3018 "visit_heading" => ["id"].into(),
3019 "visit_code_block" => ["lang"].into(),
3020 "visit_form" => ["action", "method"].into(),
3021 "visit_input" => ["name", "value"].into(),
3022 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3023 _ => std::collections::HashSet::new(),
3024 }
3025}
3026
3027fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3039 let mut fmt_str = String::new();
3040 let mut args: Vec<String> = Vec::new();
3041 let mut chars = template.chars().peekable();
3042 while let Some(c) = chars.next() {
3043 if c == '{' {
3044 let mut name = String::new();
3046 for inner in chars.by_ref() {
3047 if inner == '}' {
3048 break;
3049 }
3050 name.push(inner);
3051 }
3052 fmt_str.push_str("%s");
3053 let go_name = go_param_name(&name);
3055 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3057 format!("*{go_name}")
3058 } else {
3059 go_name
3060 };
3061 args.push(arg_expr);
3062 } else {
3063 fmt_str.push(c);
3064 }
3065 }
3066 (fmt_str, args)
3067}
3068
3069fn method_to_camel(snake: &str) -> String {
3071 use heck::ToUpperCamelCase;
3072 snake.to_upper_camel_case()
3073}
3074
3075#[cfg(test)]
3076mod tests {
3077 use super::*;
3078 use crate::config::{CallConfig, E2eConfig};
3079 use crate::field_access::FieldResolver;
3080 use crate::fixture::{Assertion, Fixture};
3081
3082 fn make_fixture(id: &str) -> Fixture {
3083 Fixture {
3084 id: id.to_string(),
3085 category: None,
3086 description: "test fixture".to_string(),
3087 tags: vec![],
3088 skip: None,
3089 env: None,
3090 call: None,
3091 input: serde_json::Value::Null,
3092 mock_response: Some(crate::fixture::MockResponse {
3093 status: 200,
3094 body: Some(serde_json::Value::Null),
3095 stream_chunks: None,
3096 headers: std::collections::HashMap::new(),
3097 }),
3098 source: String::new(),
3099 http: None,
3100 assertions: vec![Assertion {
3101 assertion_type: "not_error".to_string(),
3102 ..Default::default()
3103 }],
3104 visitor: None,
3105 }
3106 }
3107
3108 #[test]
3112 fn test_go_method_name_uses_go_casing() {
3113 let e2e_config = E2eConfig {
3114 call: CallConfig {
3115 function: "clean_extracted_text".to_string(),
3116 module: "github.com/example/mylib".to_string(),
3117 result_var: "result".to_string(),
3118 returns_result: true,
3119 ..CallConfig::default()
3120 },
3121 ..E2eConfig::default()
3122 };
3123
3124 let fixture = make_fixture("basic_text");
3125 let resolver = FieldResolver::new(
3126 &std::collections::HashMap::new(),
3127 &std::collections::HashSet::new(),
3128 &std::collections::HashSet::new(),
3129 &std::collections::HashSet::new(),
3130 &std::collections::HashSet::new(),
3131 );
3132 let mut out = String::new();
3133 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3134
3135 assert!(
3136 out.contains("kreuzberg.CleanExtractedText("),
3137 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3138 );
3139 assert!(
3140 !out.contains("kreuzberg.clean_extracted_text("),
3141 "must not emit raw snake_case method name, got:\n{out}"
3142 );
3143 }
3144}