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(&e2e_config.test_documents_dir),
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(test_documents_dir: &str) -> 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\"encoding/json\"");
260 let _ = writeln!(out, "\t\"io\"");
261 let _ = writeln!(out, "\t\"os\"");
262 let _ = writeln!(out, "\t\"os/exec\"");
263 let _ = writeln!(out, "\t\"path/filepath\"");
264 let _ = writeln!(out, "\t\"runtime\"");
265 let _ = writeln!(out, "\t\"strings\"");
266 let _ = writeln!(out, "\t\"testing\"");
267 let _ = writeln!(out, ")");
268 let _ = writeln!(out);
269 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
270 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
271 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
272 let _ = writeln!(out);
273 let _ = writeln!(
274 out,
275 "\t// Change to the configured test-documents directory so that fixture file paths like"
276 );
277 let _ = writeln!(
278 out,
279 "\t// \"pdf/fake_memo.pdf\" resolve correctly when running go test from e2e/go/."
280 );
281 let _ = writeln!(
282 out,
283 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
284 );
285 let _ = writeln!(out, "\tif err := os.Chdir(testDocumentsDir); err != nil {{");
286 let _ = writeln!(out, "\t\tpanic(err)");
287 let _ = writeln!(out, "\t}}");
288 let _ = writeln!(out);
289 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
290 let _ = writeln!(
291 out,
292 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
293 );
294 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
295 let _ = writeln!(
296 out,
297 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
298 );
299 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
300 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
301 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
302 let _ = writeln!(out, "\t\tif err != nil {{");
303 let _ = writeln!(out, "\t\t\tpanic(err)");
304 let _ = writeln!(out, "\t\t}}");
305 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
306 let _ = writeln!(
307 out,
308 "\t\t// server does not see EOF and exit immediately. The mock-server"
309 );
310 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
311 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
312 let _ = writeln!(out, "\t\tif err != nil {{");
313 let _ = writeln!(out, "\t\t\tpanic(err)");
314 let _ = writeln!(out, "\t\t}}");
315 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
316 let _ = writeln!(out, "\t\t\tpanic(err)");
317 let _ = writeln!(out, "\t\t}}");
318 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
319 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
320 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
321 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
322 let _ = writeln!(
323 out,
324 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
325 );
326 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
327 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
328 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
329 let _ = writeln!(
330 out,
331 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
332 );
333 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
334 let _ = writeln!(
335 out,
336 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
337 );
338 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
339 let _ = writeln!(
340 out,
341 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
342 );
343 let _ = writeln!(out, "\t\t\t\t\t}}");
344 let _ = writeln!(out, "\t\t\t\t}}");
345 let _ = writeln!(out, "\t\t\t\tbreak");
346 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
347 let _ = writeln!(out, "\t\t\t\tbreak");
348 let _ = writeln!(out, "\t\t\t}}");
349 let _ = writeln!(out, "\t\t}}");
350 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
351 let _ = writeln!(out, "\t\tcode := m.Run()");
352 let _ = writeln!(out, "\t\t_ = stdin.Close()");
353 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
354 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
355 let _ = writeln!(out, "\t\tos.Exit(code)");
356 let _ = writeln!(out, "\t}} else {{");
357 let _ = writeln!(out, "\t\tcode := m.Run()");
358 let _ = writeln!(out, "\t\tos.Exit(code)");
359 let _ = writeln!(out, "\t}}");
360 let _ = writeln!(out, "}}");
361 out
362}
363
364fn render_helpers_test_go() -> String {
367 let mut out = String::new();
368 let _ = writeln!(out, "package e2e_test");
369 let _ = writeln!(out);
370 let _ = writeln!(out, "import \"encoding/json\"");
371 let _ = writeln!(out);
372 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
373 let _ = writeln!(
374 out,
375 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
376 );
377 let _ = writeln!(out, "func jsonString(value any) string {{");
378 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
379 let _ = writeln!(out, "\tif err != nil {{");
380 let _ = writeln!(out, "\t\treturn \"\"");
381 let _ = writeln!(out, "\t}}");
382 let _ = writeln!(out, "\treturn string(encoded)");
383 let _ = writeln!(out, "}}");
384 out
385}
386
387fn render_test_file(
388 category: &str,
389 fixtures: &[&Fixture],
390 go_module_path: &str,
391 import_alias: &str,
392 field_resolver: &FieldResolver,
393 e2e_config: &crate::config::E2eConfig,
394) -> String {
395 let mut out = String::new();
396 let emits_executable_test =
397 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
398
399 out.push_str(&hash::header(CommentStyle::DoubleSlash));
401 let _ = writeln!(out);
402
403 let needs_pkg = fixtures
412 .iter()
413 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
414
415 let needs_os = fixtures.iter().any(|f| {
418 if f.is_http_test() {
419 return true;
420 }
421 if !emits_executable_test(f) {
422 return false;
423 }
424 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
425 let go_override = call_config
426 .overrides
427 .get("go")
428 .or_else(|| e2e_config.call.overrides.get("go"));
429 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
430 return true;
431 }
432 let call_args = &call_config.args;
433 if call_args.iter().any(|a| a.arg_type == "mock_url") {
436 return true;
437 }
438 call_args.iter().any(|a| {
439 if a.arg_type != "bytes" {
440 return false;
441 }
442 let mut current = &f.input;
445 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
446 for segment in path.split('.') {
447 match current.get(segment) {
448 Some(next) => current = next,
449 None => return false,
450 }
451 }
452 current.is_string()
453 })
454 });
455
456 let needs_filepath = false;
459
460 let _needs_json_stringify = fixtures.iter().any(|f| {
461 emits_executable_test(f)
462 && f.assertions.iter().any(|a| {
463 matches!(
464 a.assertion_type.as_str(),
465 "contains" | "contains_all" | "contains_any" | "not_contains"
466 ) && {
467 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
470 e2e_config
472 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
473 .result_is_array
474 } else {
475 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
477 field_resolver.is_array(resolved_name)
478 }
479 }
480 })
481 });
482
483 let needs_json = fixtures.iter().any(|f| {
487 if let Some(http) = &f.http {
490 let body_needs_json = http
491 .expected_response
492 .body
493 .as_ref()
494 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
495 let partial_needs_json = http.expected_response.body_partial.is_some();
496 let ve_needs_json = http
497 .expected_response
498 .validation_errors
499 .as_ref()
500 .is_some_and(|v| !v.is_empty());
501 if body_needs_json || partial_needs_json || ve_needs_json {
502 return true;
503 }
504 }
505 if !emits_executable_test(f) {
506 return false;
507 }
508
509 let call = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
510 let call_args = &call.args;
511 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
513 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
514 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
515 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
516 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
517 })
518 };
519 let go_override = call.overrides.get("go");
521 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
522 e2e_config
523 .call
524 .overrides
525 .get("go")
526 .and_then(|o| o.options_type.as_deref())
527 });
528 let has_json_obj = call_args.iter().any(|a| {
529 if a.arg_type != "json_object" {
530 return false;
531 }
532 let v = if a.field == "input" {
533 &f.input
534 } else {
535 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
536 f.input.get(field).unwrap_or(&serde_json::Value::Null)
537 };
538 if v.is_array() {
539 return true;
540 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
542 });
543 has_handle || has_json_obj
544 });
545
546 let needs_base64 = false;
551
552 let needs_fmt = fixtures.iter().any(|f| {
557 f.visitor.as_ref().is_some_and(|v| {
558 v.callbacks.values().any(|action| {
559 if let CallbackAction::CustomTemplate { template, .. } = action {
560 template.contains('{')
561 } else {
562 false
563 }
564 })
565 }) || (emits_executable_test(f)
566 && f.assertions.iter().any(|a| {
567 matches!(
568 a.assertion_type.as_str(),
569 "contains" | "contains_all" | "contains_any" | "not_contains"
570 ) && {
571 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
576 !e2e_config
578 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
579 .result_is_array
580 } else {
581 let field = a.field.as_deref().unwrap_or("");
585 let resolved_name = field_resolver.resolve(field);
586 !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
587 }
588 }
589 }))
590 });
591
592 let needs_strings = fixtures.iter().any(|f| {
595 if !emits_executable_test(f) {
596 return false;
597 }
598 f.assertions.iter().any(|a| {
599 let type_needs_strings = if a.assertion_type == "equals" {
600 a.value.as_ref().is_some_and(|v| v.is_string())
602 } else {
603 matches!(
604 a.assertion_type.as_str(),
605 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
606 )
607 };
608 let field_valid = a
609 .field
610 .as_ref()
611 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
612 .unwrap_or(true);
613 type_needs_strings && field_valid
614 })
615 });
616
617 let needs_assert = fixtures.iter().any(|f| {
619 if !emits_executable_test(f) {
620 return false;
621 }
622 if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
626 return true;
627 }
628 f.assertions.iter().any(|a| {
629 let field_valid = a
630 .field
631 .as_ref()
632 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
633 .unwrap_or(true);
634 let synthetic_field_needs_assert = match a.field.as_deref() {
635 Some("chunks_have_content" | "chunks_have_embeddings") => {
636 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
637 }
638 Some("embeddings") => {
639 matches!(
640 a.assertion_type.as_str(),
641 "count_equals" | "count_min" | "not_empty" | "is_empty"
642 )
643 }
644 _ => false,
645 };
646 let type_needs_assert = matches!(
647 a.assertion_type.as_str(),
648 "count_equals"
649 | "count_min"
650 | "count_max"
651 | "is_true"
652 | "is_false"
653 | "method_result"
654 | "min_length"
655 | "max_length"
656 | "matches_regex"
657 );
658 synthetic_field_needs_assert || type_needs_assert && field_valid
659 })
660 });
661
662 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
664 let needs_http = has_http_fixtures;
665 let needs_io = has_http_fixtures;
667
668 let needs_reflect = fixtures.iter().any(|f| {
671 if let Some(http) = &f.http {
672 let body_needs_reflect = http
673 .expected_response
674 .body
675 .as_ref()
676 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
677 let partial_needs_reflect = http.expected_response.body_partial.is_some();
678 body_needs_reflect || partial_needs_reflect
679 } else {
680 false
681 }
682 });
683
684 let _ = writeln!(out, "// E2e tests for category: {category}");
685 let _ = writeln!(out, "package e2e_test");
686 let _ = writeln!(out);
687 let _ = writeln!(out, "import (");
688 if needs_base64 {
689 let _ = writeln!(out, "\t\"encoding/base64\"");
690 }
691 if needs_json || needs_reflect {
692 let _ = writeln!(out, "\t\"encoding/json\"");
693 }
694 if needs_fmt {
695 let _ = writeln!(out, "\t\"fmt\"");
696 }
697 if needs_io {
698 let _ = writeln!(out, "\t\"io\"");
699 }
700 if needs_http {
701 let _ = writeln!(out, "\t\"net/http\"");
702 }
703 if needs_os {
704 let _ = writeln!(out, "\t\"os\"");
705 }
706 let _ = needs_filepath; if needs_reflect {
708 let _ = writeln!(out, "\t\"reflect\"");
709 }
710 if needs_strings {
711 let _ = writeln!(out, "\t\"strings\"");
712 }
713 let _ = writeln!(out, "\t\"testing\"");
714 if needs_assert {
715 let _ = writeln!(out);
716 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
717 }
718 if needs_pkg {
719 let _ = writeln!(out);
720 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
721 }
722 let _ = writeln!(out, ")");
723 let _ = writeln!(out);
724
725 for fixture in fixtures.iter() {
727 if let Some(visitor_spec) = &fixture.visitor {
728 let struct_name = visitor_struct_name(&fixture.id);
729 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
730 let _ = writeln!(out);
731 }
732 }
733
734 for (i, fixture) in fixtures.iter().enumerate() {
735 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
736 if i + 1 < fixtures.len() {
737 let _ = writeln!(out);
738 }
739 }
740
741 while out.ends_with("\n\n") {
743 out.pop();
744 }
745 if !out.ends_with('\n') {
746 out.push('\n');
747 }
748 out
749}
750
751fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
760 if fixture.is_http_test() {
762 return false;
763 }
764 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
765 if call_config.skip_languages.iter().any(|l| l == "go") {
768 return false;
769 }
770 let go_override = call_config
771 .overrides
772 .get("go")
773 .or_else(|| e2e_config.call.overrides.get("go"));
774 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
777 return true;
778 }
779 let fn_name = go_override
783 .and_then(|o| o.function.as_deref())
784 .filter(|s| !s.is_empty())
785 .unwrap_or(call_config.function.as_str());
786 !fn_name.is_empty()
787}
788
789fn render_test_function(
790 out: &mut String,
791 fixture: &Fixture,
792 import_alias: &str,
793 field_resolver: &FieldResolver,
794 e2e_config: &crate::config::E2eConfig,
795) {
796 let fn_name = fixture.id.to_upper_camel_case();
797 let description = &fixture.description;
798
799 if fixture.http.is_some() {
801 render_http_test_function(out, fixture);
802 return;
803 }
804
805 if !fixture_has_go_callable(fixture, e2e_config) {
810 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
811 let _ = writeln!(out, "\t// {description}");
812 let _ = writeln!(
813 out,
814 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
815 );
816 let _ = writeln!(out, "}}");
817 return;
818 }
819
820 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
822 let lang = "go";
823 let overrides = call_config.overrides.get(lang);
824
825 let base_function_name = overrides
829 .and_then(|o| o.function.as_deref())
830 .unwrap_or(&call_config.function);
831 let function_name = to_go_name(base_function_name);
832 let result_var = &call_config.result_var;
833 let args = &call_config.args;
834
835 let returns_result = overrides
838 .and_then(|o| o.returns_result)
839 .unwrap_or(call_config.returns_result);
840
841 let returns_void = call_config.returns_void;
844
845 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
848 if call_config.result_is_simple {
849 return true;
850 }
851 call_config
852 .overrides
853 .get("rust")
854 .map(|o| o.result_is_simple)
855 .unwrap_or(false)
856 });
857
858 let result_is_array = overrides
861 .map(|o| o.result_is_array)
862 .unwrap_or(call_config.result_is_array);
863
864 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
866 e2e_config
867 .call
868 .overrides
869 .get("go")
870 .and_then(|o| o.options_type.as_deref())
871 });
872
873 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
875 e2e_config
876 .call
877 .overrides
878 .get("go")
879 .map(|o| o.options_ptr)
880 .unwrap_or(false)
881 });
882
883 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
884 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
888
889 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
892 e2e_config
893 .call
894 .overrides
895 .get(lang)
896 .and_then(|o| o.client_factory.as_deref())
897 });
898
899 let (mut setup_lines, args_str) = build_args_and_setup(
900 &fixture.input,
901 args,
902 import_alias,
903 call_options_type,
904 fixture,
905 call_options_ptr,
906 validation_creation_failure,
907 );
908
909 let mut visitor_opts_var: Option<String> = None;
912 if fixture.visitor.is_some() {
913 let struct_name = visitor_struct_name(&fixture.id);
914 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
915 let opts_type = call_options_type.unwrap_or("ConversionOptions");
917 let opts_var = "opts".to_string();
918 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
919 setup_lines.push("opts.Visitor = visitor".to_string());
920 visitor_opts_var = Some(opts_var);
921 }
922
923 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
924 let final_args = {
925 let mut parts: Vec<String> = Vec::new();
926 if !args_str.is_empty() {
927 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
929 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
930 } else {
931 args_str
932 };
933 parts.push(processed_args);
934 }
935 parts.extend(go_extra_args);
936 parts.join(", ")
937 };
938
939 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
940 let _ = writeln!(out, "\t// {description}");
941
942 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
946 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
947 if let Some(var) = api_key_var {
948 if has_mock {
949 let fixture_id = &fixture.id;
953 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
954 let _ = writeln!(out, "\tvar baseURL *string");
955 let _ = writeln!(out, "\tif apiKey != \"\" {{");
956 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
957 let _ = writeln!(out, "\t}} else {{");
958 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
959 let _ = writeln!(
960 out,
961 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
962 );
963 let _ = writeln!(out, "\t\tbaseURL = &u");
964 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
965 let _ = writeln!(out, "\t}}");
966 } else {
967 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
968 let _ = writeln!(out, "\tif apiKey == \"\" {{");
969 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
970 let _ = writeln!(out, "\t}}");
971 }
972 }
973
974 for line in &setup_lines {
975 let _ = writeln!(out, "\t{line}");
976 }
977
978 let call_prefix = if let Some(factory) = client_factory {
982 let factory_name = to_go_name(factory);
983 let fixture_id = &fixture.id;
984 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
987 ("apiKey".to_string(), "baseURL".to_string())
989 } else if api_key_var.is_some() {
990 ("apiKey".to_string(), "nil".to_string())
992 } else if fixture.has_host_root_route() {
993 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
994 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
995 let _ = writeln!(out, "\tif mockURL == \"\" {{");
996 let _ = writeln!(
997 out,
998 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
999 );
1000 let _ = writeln!(out, "\t}}");
1001 ("\"test-key\"".to_string(), "&mockURL".to_string())
1002 } else {
1003 let _ = writeln!(
1004 out,
1005 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1006 );
1007 ("\"test-key\"".to_string(), "&mockURL".to_string())
1008 };
1009 let _ = writeln!(
1010 out,
1011 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1012 );
1013 let _ = writeln!(out, "\tif clientErr != nil {{");
1014 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1015 let _ = writeln!(out, "\t}}");
1016 "client".to_string()
1017 } else {
1018 import_alias.to_string()
1019 };
1020
1021 let binding_returns_error_pre = args
1026 .iter()
1027 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1028 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1029
1030 if expects_error {
1031 if effective_returns_result_pre && !returns_void {
1032 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1033 } else {
1034 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1035 }
1036 let _ = writeln!(out, "\tif err == nil {{");
1037 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1038 let _ = writeln!(out, "\t}}");
1039 let _ = writeln!(out, "}}");
1040 return;
1041 }
1042
1043 let is_streaming = fixture.is_streaming_mock();
1045
1046 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1051 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1052 return false;
1053 }
1054 if a.assertion_type == "method_result" {
1056 return true;
1057 }
1058 match &a.field {
1059 Some(f) if !f.is_empty() => {
1060 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1061 return true;
1062 }
1063 field_resolver.is_valid_for_result(f)
1064 }
1065 _ => true,
1066 }
1067 });
1068
1069 let binding_returns_error = args
1076 .iter()
1077 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1078 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1080
1081 if !effective_returns_result && result_is_simple {
1087 let result_binding = if has_usable_assertion {
1089 result_var.to_string()
1090 } else {
1091 "_".to_string()
1092 };
1093 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1095 let _ = writeln!(
1096 out,
1097 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1098 );
1099 if has_usable_assertion && result_binding != "_" {
1100 if result_is_array {
1101 let _ = writeln!(out, "\tvalue := {result_var}");
1103 } else {
1104 let only_nil_assertions = fixture
1107 .assertions
1108 .iter()
1109 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1110 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1111 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1112
1113 if !only_nil_assertions {
1114 let _ = writeln!(out, "\tif {result_var} == nil {{");
1116 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1117 let _ = writeln!(out, "\t}}");
1118 let _ = writeln!(out, "\tvalue := *{result_var}");
1119 }
1120 }
1121 }
1122 } else if !effective_returns_result || returns_void {
1123 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1126 let _ = writeln!(out, "\tif err != nil {{");
1127 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1128 let _ = writeln!(out, "\t}}");
1129 let _ = writeln!(out, "}}");
1131 return;
1132 } else {
1133 let result_binding = if is_streaming {
1136 "stream".to_string()
1137 } else if has_usable_assertion {
1138 result_var.to_string()
1139 } else {
1140 "_".to_string()
1141 };
1142 let _ = writeln!(
1143 out,
1144 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1145 );
1146 let _ = writeln!(out, "\tif err != nil {{");
1147 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1148 let _ = writeln!(out, "\t}}");
1149 if is_streaming {
1151 let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1152 let _ = writeln!(out, "\tfor chunk := range stream {{");
1153 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1154 let _ = writeln!(out, "\t}}");
1155 }
1156 if result_is_simple && has_usable_assertion && result_binding != "_" {
1157 if result_is_array {
1158 let _ = writeln!(out, "\tvalue := {}", result_var);
1160 } else {
1161 let only_nil_assertions = fixture
1164 .assertions
1165 .iter()
1166 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1167 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1168 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1169
1170 if !only_nil_assertions {
1171 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1173 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1174 let _ = writeln!(out, "\t}}");
1175 let _ = writeln!(out, "\tvalue := *{}", result_var);
1176 }
1177 }
1178 }
1179 }
1180
1181 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1184 let only_nil_assertions = fixture
1185 .assertions
1186 .iter()
1187 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1188 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1189 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1190 !only_nil_assertions
1191 } else {
1192 result_is_simple && has_usable_assertion
1193 };
1194
1195 let effective_result_var = if has_deref_value {
1196 "value".to_string()
1197 } else {
1198 result_var.to_string()
1199 };
1200
1201 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1206 for assertion in &fixture.assertions {
1207 if let Some(f) = &assertion.field {
1208 if !f.is_empty() {
1209 let resolved = field_resolver.resolve(f);
1210 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1211 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1216 let is_array_field = field_resolver.is_array(resolved);
1217 if !is_string_field || is_array_field {
1218 continue;
1221 }
1222 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1223 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1224 if field_resolver.has_map_access(f) {
1225 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1228 } else {
1229 let _ = writeln!(out, "\tvar {local_var} string");
1230 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1231 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1235 let _ = writeln!(out, "\t}}");
1236 }
1237 optional_locals.insert(f.clone(), local_var);
1238 }
1239 }
1240 }
1241 }
1242
1243 for assertion in &fixture.assertions {
1245 if let Some(f) = &assertion.field {
1246 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1247 let parts: Vec<&str> = f.split('.').collect();
1250 let mut guard_expr: Option<String> = None;
1251 for i in 1..parts.len() {
1252 let prefix = parts[..i].join(".");
1253 let resolved_prefix = field_resolver.resolve(&prefix);
1254 if field_resolver.is_optional(resolved_prefix) {
1255 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
1256 guard_expr = Some(accessor);
1257 break;
1258 }
1259 }
1260 if let Some(guard) = guard_expr {
1261 if field_resolver.is_valid_for_result(f) {
1264 let _ = writeln!(out, "\tif {guard} != nil {{");
1265 let mut nil_buf = String::new();
1268 render_assertion(
1269 &mut nil_buf,
1270 assertion,
1271 &effective_result_var,
1272 import_alias,
1273 field_resolver,
1274 &optional_locals,
1275 result_is_simple,
1276 result_is_array,
1277 );
1278 for line in nil_buf.lines() {
1279 let _ = writeln!(out, "\t{line}");
1280 }
1281 let _ = writeln!(out, "\t}}");
1282 } else {
1283 render_assertion(
1284 out,
1285 assertion,
1286 &effective_result_var,
1287 import_alias,
1288 field_resolver,
1289 &optional_locals,
1290 result_is_simple,
1291 result_is_array,
1292 );
1293 }
1294 continue;
1295 }
1296 }
1297 }
1298 render_assertion(
1299 out,
1300 assertion,
1301 &effective_result_var,
1302 import_alias,
1303 field_resolver,
1304 &optional_locals,
1305 result_is_simple,
1306 result_is_array,
1307 );
1308 }
1309
1310 let _ = writeln!(out, "}}");
1311}
1312
1313fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1319 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1320}
1321
1322struct GoTestClientRenderer;
1334
1335impl client::TestClientRenderer for GoTestClientRenderer {
1336 fn language_name(&self) -> &'static str {
1337 "go"
1338 }
1339
1340 fn sanitize_test_name(&self, id: &str) -> String {
1344 id.to_upper_camel_case()
1345 }
1346
1347 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1350 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1351 let _ = writeln!(out, "\t// {description}");
1352 if let Some(reason) = skip_reason {
1353 let escaped = go_string_literal(reason);
1354 let _ = writeln!(out, "\tt.Skip({escaped})");
1355 }
1356 }
1357
1358 fn render_test_close(&self, out: &mut String) {
1359 let _ = writeln!(out, "}}");
1360 }
1361
1362 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1368 let method = ctx.method.to_uppercase();
1369 let path = ctx.path;
1370
1371 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1372 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1373 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1374 let _ = writeln!(out, "\t}}");
1375
1376 let body_expr = if let Some(body) = ctx.body {
1378 let json = serde_json::to_string(body).unwrap_or_default();
1379 let escaped = go_string_literal(&json);
1380 format!("strings.NewReader({})", escaped)
1381 } else {
1382 "strings.NewReader(\"\")".to_string()
1383 };
1384
1385 let _ = writeln!(out, "\tbody := {body_expr}");
1386 let _ = writeln!(
1387 out,
1388 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1389 );
1390 let _ = writeln!(out, "\tif err != nil {{");
1391 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1392 let _ = writeln!(out, "\t}}");
1393
1394 if ctx.body.is_some() {
1396 let content_type = ctx.content_type.unwrap_or("application/json");
1397 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1398 }
1399
1400 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1402 header_names.sort();
1403 for name in header_names {
1404 let value = &ctx.headers[name];
1405 let escaped_name = go_string_literal(name);
1406 let escaped_value = go_string_literal(value);
1407 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1408 }
1409
1410 if !ctx.cookies.is_empty() {
1412 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1413 cookie_names.sort();
1414 for name in cookie_names {
1415 let value = &ctx.cookies[name];
1416 let escaped_name = go_string_literal(name);
1417 let escaped_value = go_string_literal(value);
1418 let _ = writeln!(
1419 out,
1420 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1421 );
1422 }
1423 }
1424
1425 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1427 let _ = writeln!(
1428 out,
1429 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1430 );
1431 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1432 let _ = writeln!(out, "\t\t}},");
1433 let _ = writeln!(out, "\t}}");
1434 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1435 let _ = writeln!(out, "\tif err != nil {{");
1436 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1437 let _ = writeln!(out, "\t}}");
1438 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1439
1440 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1444 let _ = writeln!(out, "\tif err != nil {{");
1445 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1446 let _ = writeln!(out, "\t}}");
1447 let _ = writeln!(out, "\t_ = bodyBytes");
1448 }
1449
1450 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1451 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1452 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1453 let _ = writeln!(out, "\t}}");
1454 }
1455
1456 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1459 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1461 return;
1462 }
1463 if name.eq_ignore_ascii_case("connection") {
1465 return;
1466 }
1467 let escaped_name = go_string_literal(name);
1468 let escaped_value = go_string_literal(expected);
1469 let _ = writeln!(
1470 out,
1471 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1472 );
1473 let _ = writeln!(
1474 out,
1475 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1476 );
1477 let _ = writeln!(out, "\t}}");
1478 }
1479
1480 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1485 match expected {
1486 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1487 let json_str = serde_json::to_string(expected).unwrap_or_default();
1488 let escaped = go_string_literal(&json_str);
1489 let _ = writeln!(out, "\tvar got any");
1490 let _ = writeln!(out, "\tvar want any");
1491 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1492 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1493 let _ = writeln!(out, "\t}}");
1494 let _ = writeln!(
1495 out,
1496 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1497 );
1498 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1499 let _ = writeln!(out, "\t}}");
1500 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1501 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1502 let _ = writeln!(out, "\t}}");
1503 }
1504 serde_json::Value::String(s) => {
1505 let escaped = go_string_literal(s);
1506 let _ = writeln!(out, "\twant := {escaped}");
1507 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1508 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1509 let _ = writeln!(out, "\t}}");
1510 }
1511 other => {
1512 let escaped = go_string_literal(&other.to_string());
1513 let _ = writeln!(out, "\twant := {escaped}");
1514 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1515 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1516 let _ = writeln!(out, "\t}}");
1517 }
1518 }
1519 }
1520
1521 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1524 if let Some(obj) = expected.as_object() {
1525 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1526 let _ = writeln!(
1527 out,
1528 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1529 );
1530 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1531 let _ = writeln!(out, "\t}}");
1532 for (key, val) in obj {
1533 let escaped_key = go_string_literal(key);
1534 let json_val = serde_json::to_string(val).unwrap_or_default();
1535 let escaped_val = go_string_literal(&json_val);
1536 let _ = writeln!(out, "\t{{");
1537 let _ = writeln!(out, "\t\tvar _wantVal any");
1538 let _ = writeln!(
1539 out,
1540 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1541 );
1542 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1543 let _ = writeln!(out, "\t\t}}");
1544 let _ = writeln!(
1545 out,
1546 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1547 );
1548 let _ = writeln!(
1549 out,
1550 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1551 );
1552 let _ = writeln!(out, "\t\t}}");
1553 let _ = writeln!(out, "\t}}");
1554 }
1555 }
1556 }
1557
1558 fn render_assert_validation_errors(
1563 &self,
1564 out: &mut String,
1565 _response_var: &str,
1566 errors: &[ValidationErrorExpectation],
1567 ) {
1568 let _ = writeln!(out, "\tvar _veBody map[string]any");
1569 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1570 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1571 let _ = writeln!(out, "\t}}");
1572 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1573 for ve in errors {
1574 let escaped_msg = go_string_literal(&ve.msg);
1575 let _ = writeln!(out, "\t{{");
1576 let _ = writeln!(out, "\t\t_found := false");
1577 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1578 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1579 let _ = writeln!(
1580 out,
1581 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1582 );
1583 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1584 let _ = writeln!(out, "\t\t\t\t\tbreak");
1585 let _ = writeln!(out, "\t\t\t\t}}");
1586 let _ = writeln!(out, "\t\t\t}}");
1587 let _ = writeln!(out, "\t\t}}");
1588 let _ = writeln!(out, "\t\tif !_found {{");
1589 let _ = writeln!(
1590 out,
1591 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1592 );
1593 let _ = writeln!(out, "\t\t}}");
1594 let _ = writeln!(out, "\t}}");
1595 }
1596 }
1597}
1598
1599fn build_args_and_setup(
1607 input: &serde_json::Value,
1608 args: &[crate::config::ArgMapping],
1609 import_alias: &str,
1610 options_type: Option<&str>,
1611 fixture: &crate::fixture::Fixture,
1612 options_ptr: bool,
1613 expects_error: bool,
1614) -> (Vec<String>, String) {
1615 let fixture_id = &fixture.id;
1616 use heck::ToUpperCamelCase;
1617
1618 if args.is_empty() {
1619 return (Vec::new(), String::new());
1620 }
1621
1622 let mut setup_lines: Vec<String> = Vec::new();
1623 let mut parts: Vec<String> = Vec::new();
1624
1625 for arg in args {
1626 if arg.arg_type == "mock_url" {
1627 if fixture.has_host_root_route() {
1628 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1629 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1630 setup_lines.push(format!(
1631 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1632 arg.name, arg.name
1633 ));
1634 } else {
1635 setup_lines.push(format!(
1636 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1637 arg.name,
1638 ));
1639 }
1640 parts.push(arg.name.clone());
1641 continue;
1642 }
1643
1644 if arg.arg_type == "handle" {
1645 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1647 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1648 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1649 let create_err_handler = if expects_error {
1653 "assert.Error(t, createErr)\n\t\treturn".to_string()
1654 } else {
1655 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1656 };
1657 if config_value.is_null()
1658 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1659 {
1660 setup_lines.push(format!(
1661 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1662 name = arg.name,
1663 ));
1664 } else {
1665 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1666 let go_literal = go_string_literal(&json_str);
1667 let name = &arg.name;
1668 setup_lines.push(format!(
1669 "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}}"
1670 ));
1671 setup_lines.push(format!(
1672 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1673 ));
1674 }
1675 parts.push(arg.name.clone());
1676 continue;
1677 }
1678
1679 let val: Option<&serde_json::Value> = if arg.field == "input" {
1680 Some(input)
1681 } else {
1682 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1683 input.get(field)
1684 };
1685
1686 if arg.arg_type == "bytes" {
1693 let var_name = format!("{}Bytes", arg.name);
1694 match val {
1695 None | Some(serde_json::Value::Null) => {
1696 if arg.optional {
1697 parts.push("nil".to_string());
1698 } else {
1699 parts.push("[]byte{}".to_string());
1700 }
1701 }
1702 Some(serde_json::Value::String(s)) => {
1703 let go_path = go_string_literal(s);
1708 setup_lines.push(format!(
1709 "{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}}"
1710 ));
1711 parts.push(var_name);
1712 }
1713 Some(other) => {
1714 parts.push(format!("[]byte({})", json_to_go(other)));
1715 }
1716 }
1717 continue;
1718 }
1719
1720 match val {
1721 None | Some(serde_json::Value::Null) if arg.optional => {
1722 match arg.arg_type.as_str() {
1724 "string" => {
1725 parts.push("nil".to_string());
1727 }
1728 "json_object" => {
1729 if options_ptr {
1730 parts.push("nil".to_string());
1732 } else if let Some(opts_type) = options_type {
1733 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1735 } else {
1736 parts.push("nil".to_string());
1737 }
1738 }
1739 _ => {
1740 parts.push("nil".to_string());
1741 }
1742 }
1743 }
1744 None | Some(serde_json::Value::Null) => {
1745 let default_val = match arg.arg_type.as_str() {
1747 "string" => "\"\"".to_string(),
1748 "int" | "integer" | "i64" => "0".to_string(),
1749 "float" | "number" => "0.0".to_string(),
1750 "bool" | "boolean" => "false".to_string(),
1751 "json_object" => {
1752 if options_ptr {
1753 "nil".to_string()
1755 } else if let Some(opts_type) = options_type {
1756 format!("{import_alias}.{opts_type}{{}}")
1757 } else {
1758 "nil".to_string()
1759 }
1760 }
1761 _ => "nil".to_string(),
1762 };
1763 parts.push(default_val);
1764 }
1765 Some(v) => {
1766 match arg.arg_type.as_str() {
1767 "json_object" => {
1768 let is_array = v.is_array();
1771 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1772 if is_empty_obj {
1773 if options_ptr {
1774 parts.push("nil".to_string());
1776 } else if let Some(opts_type) = options_type {
1777 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1778 } else {
1779 parts.push("nil".to_string());
1780 }
1781 } else if is_array {
1782 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1787 if go_t.starts_with('[') {
1791 go_t.to_string()
1792 } else {
1793 let qualified = if go_t.contains('.') {
1795 go_t.to_string()
1796 } else {
1797 format!("{import_alias}.{go_t}")
1798 };
1799 format!("[]{qualified}")
1800 }
1801 } else {
1802 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1803 };
1804 let converted_v = convert_json_for_go(v.clone());
1806 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1807 let go_literal = go_string_literal(&json_str);
1808 let var_name = &arg.name;
1809 setup_lines.push(format!(
1810 "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}}"
1811 ));
1812 parts.push(var_name.to_string());
1813 } else if let Some(opts_type) = options_type {
1814 let remapped_v = if options_ptr {
1819 convert_json_for_go(v.clone())
1820 } else {
1821 v.clone()
1822 };
1823 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1824 let go_literal = go_string_literal(&json_str);
1825 let var_name = &arg.name;
1826 setup_lines.push(format!(
1827 "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}}"
1828 ));
1829 let arg_expr = if options_ptr {
1831 format!("&{var_name}")
1832 } else {
1833 var_name.to_string()
1834 };
1835 parts.push(arg_expr);
1836 } else {
1837 parts.push(json_to_go(v));
1838 }
1839 }
1840 "string" if arg.optional => {
1841 let var_name = format!("{}Val", arg.name);
1843 let go_val = json_to_go(v);
1844 setup_lines.push(format!("{var_name} := {go_val}"));
1845 parts.push(format!("&{var_name}"));
1846 }
1847 _ => {
1848 parts.push(json_to_go(v));
1849 }
1850 }
1851 }
1852 }
1853 }
1854
1855 (setup_lines, parts.join(", "))
1856}
1857
1858#[allow(clippy::too_many_arguments)]
1859fn render_assertion(
1860 out: &mut String,
1861 assertion: &Assertion,
1862 result_var: &str,
1863 import_alias: &str,
1864 field_resolver: &FieldResolver,
1865 optional_locals: &std::collections::HashMap<String, String>,
1866 result_is_simple: bool,
1867 result_is_array: bool,
1868) {
1869 if !result_is_simple {
1872 if let Some(f) = &assertion.field {
1873 let embed_deref = format!("(*{result_var})");
1876 match f.as_str() {
1877 "chunks_have_content" => {
1878 let pred = format!(
1879 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1880 );
1881 match assertion.assertion_type.as_str() {
1882 "is_true" => {
1883 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1884 }
1885 "is_false" => {
1886 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1887 }
1888 _ => {
1889 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1890 }
1891 }
1892 return;
1893 }
1894 "chunks_have_embeddings" => {
1895 let pred = format!(
1896 "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 }}()"
1897 );
1898 match assertion.assertion_type.as_str() {
1899 "is_true" => {
1900 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1901 }
1902 "is_false" => {
1903 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1904 }
1905 _ => {
1906 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1907 }
1908 }
1909 return;
1910 }
1911 "embeddings" => {
1912 match assertion.assertion_type.as_str() {
1913 "count_equals" => {
1914 if let Some(val) = &assertion.value {
1915 if let Some(n) = val.as_u64() {
1916 let _ = writeln!(
1917 out,
1918 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1919 );
1920 }
1921 }
1922 }
1923 "count_min" => {
1924 if let Some(val) = &assertion.value {
1925 if let Some(n) = val.as_u64() {
1926 let _ = writeln!(
1927 out,
1928 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1929 );
1930 }
1931 }
1932 }
1933 "not_empty" => {
1934 let _ = writeln!(
1935 out,
1936 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1937 );
1938 }
1939 "is_empty" => {
1940 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1941 }
1942 _ => {
1943 let _ = writeln!(
1944 out,
1945 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1946 );
1947 }
1948 }
1949 return;
1950 }
1951 "embedding_dimensions" => {
1952 let expr = format!(
1953 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1954 );
1955 match assertion.assertion_type.as_str() {
1956 "equals" => {
1957 if let Some(val) = &assertion.value {
1958 if let Some(n) = val.as_u64() {
1959 let _ = writeln!(
1960 out,
1961 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1962 );
1963 }
1964 }
1965 }
1966 "greater_than" => {
1967 if let Some(val) = &assertion.value {
1968 if let Some(n) = val.as_u64() {
1969 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1970 }
1971 }
1972 }
1973 _ => {
1974 let _ = writeln!(
1975 out,
1976 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1977 );
1978 }
1979 }
1980 return;
1981 }
1982 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1983 let pred = match f.as_str() {
1984 "embeddings_valid" => {
1985 format!(
1986 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1987 )
1988 }
1989 "embeddings_finite" => {
1990 format!(
1991 "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 }}()"
1992 )
1993 }
1994 "embeddings_non_zero" => {
1995 format!(
1996 "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 }}()"
1997 )
1998 }
1999 "embeddings_normalized" => {
2000 format!(
2001 "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 }}()"
2002 )
2003 }
2004 _ => unreachable!(),
2005 };
2006 match assertion.assertion_type.as_str() {
2007 "is_true" => {
2008 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2009 }
2010 "is_false" => {
2011 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2012 }
2013 _ => {
2014 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2015 }
2016 }
2017 return;
2018 }
2019 "keywords" | "keywords_count" => {
2022 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2023 return;
2024 }
2025 _ => {}
2026 }
2027 }
2028 }
2029
2030 if !result_is_simple {
2033 if let Some(f) = &assertion.field {
2034 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2035 if let Some(expr) =
2036 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2037 {
2038 match assertion.assertion_type.as_str() {
2039 "count_min" => {
2040 if let Some(val) = &assertion.value {
2041 if let Some(n) = val.as_u64() {
2042 let _ = writeln!(
2043 out,
2044 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2045 );
2046 }
2047 }
2048 }
2049 "count_equals" => {
2050 if let Some(val) = &assertion.value {
2051 if let Some(n) = val.as_u64() {
2052 let _ = writeln!(
2053 out,
2054 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2055 );
2056 }
2057 }
2058 }
2059 "equals" => {
2060 if let Some(serde_json::Value::String(s)) = &assertion.value {
2061 let escaped = crate::escape::go_string_literal(s);
2062 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {expr})");
2063 } else if let Some(val) = &assertion.value {
2064 if let Some(n) = val.as_u64() {
2065 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2066 }
2067 }
2068 }
2069 "not_empty" => {
2070 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2071 }
2072 "is_empty" => {
2073 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2074 }
2075 "is_true" => {
2076 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2077 }
2078 "is_false" => {
2079 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2080 }
2081 "greater_than" => {
2082 if let Some(val) = &assertion.value {
2083 if let Some(n) = val.as_u64() {
2084 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2085 }
2086 }
2087 }
2088 "greater_than_or_equal" => {
2089 if let Some(val) = &assertion.value {
2090 if let Some(n) = val.as_u64() {
2091 let _ =
2092 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2093 }
2094 }
2095 }
2096 "contains" => {
2097 if let Some(serde_json::Value::String(s)) = &assertion.value {
2098 let escaped = crate::escape::go_string_literal(s);
2099 let _ =
2100 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2101 }
2102 }
2103 _ => {
2104 let _ = writeln!(
2105 out,
2106 "\t// streaming field '{f}': assertion type '{}' not rendered",
2107 assertion.assertion_type
2108 );
2109 }
2110 }
2111 }
2112 return;
2113 }
2114 }
2115 }
2116
2117 if !result_is_simple {
2120 if let Some(f) = &assertion.field {
2121 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2122 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2123 return;
2124 }
2125 }
2126 }
2127
2128 let field_expr = if result_is_simple {
2129 result_var.to_string()
2131 } else {
2132 match &assertion.field {
2133 Some(f) if !f.is_empty() => {
2134 if let Some(local_var) = optional_locals.get(f.as_str()) {
2136 local_var.clone()
2137 } else {
2138 field_resolver.accessor(f, "go", result_var)
2139 }
2140 }
2141 _ => result_var.to_string(),
2142 }
2143 };
2144
2145 let is_optional = assertion
2149 .field
2150 .as_ref()
2151 .map(|f| {
2152 let resolved = field_resolver.resolve(f);
2153 let check_path = resolved
2154 .strip_suffix(".length")
2155 .or_else(|| resolved.strip_suffix(".count"))
2156 .or_else(|| resolved.strip_suffix(".size"))
2157 .unwrap_or(resolved);
2158 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2159 })
2160 .unwrap_or(false);
2161
2162 let field_is_array_for_len = assertion
2166 .field
2167 .as_ref()
2168 .map(|f| {
2169 let resolved = field_resolver.resolve(f);
2170 let check_path = resolved
2171 .strip_suffix(".length")
2172 .or_else(|| resolved.strip_suffix(".count"))
2173 .or_else(|| resolved.strip_suffix(".size"))
2174 .unwrap_or(resolved);
2175 field_resolver.is_array(check_path)
2176 })
2177 .unwrap_or(false);
2178 let field_expr =
2179 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2180 let inner = &field_expr[4..field_expr.len() - 1];
2181 format!("len(*{inner})")
2182 } else {
2183 field_expr
2184 };
2185 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2187 Some(field_expr[5..field_expr.len() - 1].to_string())
2188 } else {
2189 None
2190 };
2191
2192 let field_is_slice = assertion
2196 .field
2197 .as_ref()
2198 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2199 .unwrap_or(false);
2200 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2201 format!("*{field_expr}")
2202 } else {
2203 field_expr.clone()
2204 };
2205
2206 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2211 let mut array_expr = field_expr[..idx].to_string();
2212 if let Some(stripped) = array_expr.strip_prefix("len(") {
2213 array_expr = stripped.to_string();
2214 }
2215 Some(array_expr)
2216 } else {
2217 None
2218 };
2219
2220 let mut assertion_buf = String::new();
2223 let out_ref = &mut assertion_buf;
2224
2225 match assertion.assertion_type.as_str() {
2226 "equals" => {
2227 if let Some(expected) = &assertion.value {
2228 let go_val = json_to_go(expected);
2229 if expected.is_string() {
2231 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2234 format!("strings.TrimSpace(string(*{field_expr}))")
2235 } else {
2236 format!("strings.TrimSpace(string({field_expr}))")
2237 };
2238 if is_optional && !field_expr.starts_with("len(") {
2239 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2240 } else {
2241 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2242 }
2243 } else if is_optional && !field_expr.starts_with("len(") {
2244 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2245 } else {
2246 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2247 }
2248 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2249 let _ = writeln!(out_ref, "\t}}");
2250 }
2251 }
2252 "contains" => {
2253 if let Some(expected) = &assertion.value {
2254 let go_val = json_to_go(expected);
2255 let resolved_field = assertion.field.as_deref().unwrap_or("");
2261 let resolved_name = field_resolver.resolve(resolved_field);
2262 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2263 let is_opt =
2264 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2265 let field_for_contains = if is_opt && field_is_array {
2266 format!("jsonString({field_expr})")
2268 } else if is_opt {
2269 format!("fmt.Sprint(*{field_expr})")
2270 } else if field_is_array {
2271 format!("jsonString({field_expr})")
2272 } else {
2273 format!("fmt.Sprint({field_expr})")
2274 };
2275 if is_opt {
2276 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2277 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2278 let _ = writeln!(
2279 out_ref,
2280 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2281 );
2282 let _ = writeln!(out_ref, "\t}}");
2283 let _ = writeln!(out_ref, "\t}}");
2284 } else {
2285 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2286 let _ = writeln!(
2287 out_ref,
2288 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2289 );
2290 let _ = writeln!(out_ref, "\t}}");
2291 }
2292 }
2293 }
2294 "contains_all" => {
2295 if let Some(values) = &assertion.values {
2296 let resolved_field = assertion.field.as_deref().unwrap_or("");
2297 let resolved_name = field_resolver.resolve(resolved_field);
2298 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2299 let is_opt =
2300 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2301 for val in values {
2302 let go_val = json_to_go(val);
2303 let field_for_contains = if is_opt && field_is_array {
2304 format!("jsonString({field_expr})")
2306 } else if is_opt {
2307 format!("fmt.Sprint(*{field_expr})")
2308 } else if field_is_array {
2309 format!("jsonString({field_expr})")
2310 } else {
2311 format!("fmt.Sprint({field_expr})")
2312 };
2313 if is_opt {
2314 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2315 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2316 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2317 let _ = writeln!(out_ref, "\t}}");
2318 let _ = writeln!(out_ref, "\t}}");
2319 } else {
2320 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2321 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2322 let _ = writeln!(out_ref, "\t}}");
2323 }
2324 }
2325 }
2326 }
2327 "not_contains" => {
2328 if let Some(expected) = &assertion.value {
2329 let go_val = json_to_go(expected);
2330 let resolved_field = assertion.field.as_deref().unwrap_or("");
2331 let resolved_name = field_resolver.resolve(resolved_field);
2332 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2333 let is_opt =
2334 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2335 let field_for_contains = if is_opt && field_is_array {
2336 format!("jsonString({field_expr})")
2338 } else if is_opt {
2339 format!("fmt.Sprint(*{field_expr})")
2340 } else if field_is_array {
2341 format!("jsonString({field_expr})")
2342 } else {
2343 format!("fmt.Sprint({field_expr})")
2344 };
2345 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2346 let _ = writeln!(
2347 out_ref,
2348 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2349 );
2350 let _ = writeln!(out_ref, "\t}}");
2351 }
2352 }
2353 "not_empty" => {
2354 let field_is_array = {
2357 let rf = assertion.field.as_deref().unwrap_or("");
2358 let rn = field_resolver.resolve(rf);
2359 field_resolver.is_array(rn)
2360 };
2361 if is_optional && !field_is_array {
2362 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2364 } else if is_optional && field_is_slice {
2365 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2367 } else if is_optional {
2368 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2370 } else if result_is_simple && result_is_array {
2371 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2373 } else {
2374 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2375 }
2376 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2377 let _ = writeln!(out_ref, "\t}}");
2378 }
2379 "is_empty" => {
2380 let field_is_array = {
2381 let rf = assertion.field.as_deref().unwrap_or("");
2382 let rn = field_resolver.resolve(rf);
2383 field_resolver.is_array(rn)
2384 };
2385 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2388 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2390 } else if is_optional && !field_is_array {
2391 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2393 } else if is_optional && field_is_slice {
2394 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2396 } else if is_optional {
2397 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2399 } else {
2400 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2401 }
2402 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2403 let _ = writeln!(out_ref, "\t}}");
2404 }
2405 "contains_any" => {
2406 if let Some(values) = &assertion.values {
2407 let resolved_field = assertion.field.as_deref().unwrap_or("");
2408 let resolved_name = field_resolver.resolve(resolved_field);
2409 let field_is_array = field_resolver.is_array(resolved_name);
2410 let is_opt =
2411 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2412 let field_for_contains = if is_opt && field_is_array {
2413 format!("jsonString({field_expr})")
2415 } else if is_opt {
2416 format!("fmt.Sprint(*{field_expr})")
2417 } else if field_is_array {
2418 format!("jsonString({field_expr})")
2419 } else {
2420 format!("fmt.Sprint({field_expr})")
2421 };
2422 let _ = writeln!(out_ref, "\t{{");
2423 let _ = writeln!(out_ref, "\t\tfound := false");
2424 for val in values {
2425 let go_val = json_to_go(val);
2426 let _ = writeln!(
2427 out_ref,
2428 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2429 );
2430 }
2431 let _ = writeln!(out_ref, "\t\tif !found {{");
2432 let _ = writeln!(
2433 out_ref,
2434 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2435 );
2436 let _ = writeln!(out_ref, "\t\t}}");
2437 let _ = writeln!(out_ref, "\t}}");
2438 }
2439 }
2440 "greater_than" => {
2441 if let Some(val) = &assertion.value {
2442 let go_val = json_to_go(val);
2443 if is_optional {
2447 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2448 if let Some(n) = val.as_u64() {
2449 let next = n + 1;
2450 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2451 } else {
2452 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2453 }
2454 let _ = writeln!(
2455 out_ref,
2456 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2457 );
2458 let _ = writeln!(out_ref, "\t\t}}");
2459 let _ = writeln!(out_ref, "\t}}");
2460 } else if let Some(n) = val.as_u64() {
2461 let next = n + 1;
2462 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2463 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2464 let _ = writeln!(out_ref, "\t}}");
2465 } else {
2466 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2467 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2468 let _ = writeln!(out_ref, "\t}}");
2469 }
2470 }
2471 }
2472 "less_than" => {
2473 if let Some(val) = &assertion.value {
2474 let go_val = json_to_go(val);
2475 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2476 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2477 let _ = writeln!(out_ref, "\t}}");
2478 }
2479 }
2480 "greater_than_or_equal" => {
2481 if let Some(val) = &assertion.value {
2482 let go_val = json_to_go(val);
2483 if let Some(ref guard) = nil_guard_expr {
2484 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2485 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2486 let _ = writeln!(
2487 out_ref,
2488 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2489 );
2490 let _ = writeln!(out_ref, "\t\t}}");
2491 let _ = writeln!(out_ref, "\t}}");
2492 } else if is_optional && !field_expr.starts_with("len(") {
2493 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2495 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2496 let _ = writeln!(
2497 out_ref,
2498 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2499 );
2500 let _ = writeln!(out_ref, "\t\t}}");
2501 let _ = writeln!(out_ref, "\t}}");
2502 } else {
2503 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2504 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2505 let _ = writeln!(out_ref, "\t}}");
2506 }
2507 }
2508 }
2509 "less_than_or_equal" => {
2510 if let Some(val) = &assertion.value {
2511 let go_val = json_to_go(val);
2512 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2513 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2514 let _ = writeln!(out_ref, "\t}}");
2515 }
2516 }
2517 "starts_with" => {
2518 if let Some(expected) = &assertion.value {
2519 let go_val = json_to_go(expected);
2520 let field_for_prefix = if is_optional
2521 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2522 {
2523 format!("string(*{field_expr})")
2524 } else {
2525 format!("string({field_expr})")
2526 };
2527 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2528 let _ = writeln!(
2529 out_ref,
2530 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2531 );
2532 let _ = writeln!(out_ref, "\t}}");
2533 }
2534 }
2535 "count_min" => {
2536 if let Some(val) = &assertion.value {
2537 if let Some(n) = val.as_u64() {
2538 if is_optional {
2539 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2540 let len_expr = if field_is_slice {
2542 format!("len({field_expr})")
2543 } else {
2544 format!("len(*{field_expr})")
2545 };
2546 let _ = writeln!(
2547 out_ref,
2548 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2549 );
2550 let _ = writeln!(out_ref, "\t}}");
2551 } else {
2552 let _ = writeln!(
2553 out_ref,
2554 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2555 );
2556 }
2557 }
2558 }
2559 }
2560 "count_equals" => {
2561 if let Some(val) = &assertion.value {
2562 if let Some(n) = val.as_u64() {
2563 if is_optional {
2564 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2565 let len_expr = if field_is_slice {
2567 format!("len({field_expr})")
2568 } else {
2569 format!("len(*{field_expr})")
2570 };
2571 let _ = writeln!(
2572 out_ref,
2573 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2574 );
2575 let _ = writeln!(out_ref, "\t}}");
2576 } else {
2577 let _ = writeln!(
2578 out_ref,
2579 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2580 );
2581 }
2582 }
2583 }
2584 }
2585 "is_true" => {
2586 if is_optional {
2587 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2588 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2589 let _ = writeln!(out_ref, "\t}}");
2590 } else {
2591 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2592 }
2593 }
2594 "is_false" => {
2595 if is_optional {
2596 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2597 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2598 let _ = writeln!(out_ref, "\t}}");
2599 } else {
2600 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2601 }
2602 }
2603 "method_result" => {
2604 if let Some(method_name) = &assertion.method {
2605 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2606 let check = assertion.check.as_deref().unwrap_or("is_true");
2607 let deref_expr = if info.is_pointer {
2610 format!("*{}", info.call_expr)
2611 } else {
2612 info.call_expr.clone()
2613 };
2614 match check {
2615 "equals" => {
2616 if let Some(val) = &assertion.value {
2617 if val.is_boolean() {
2618 if val.as_bool() == Some(true) {
2619 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2620 } else {
2621 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2622 }
2623 } else {
2624 let go_val = if let Some(cast) = info.value_cast {
2628 if val.is_number() {
2629 format!("{cast}({})", json_to_go(val))
2630 } else {
2631 json_to_go(val)
2632 }
2633 } else {
2634 json_to_go(val)
2635 };
2636 let _ = writeln!(
2637 out_ref,
2638 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2639 );
2640 }
2641 }
2642 }
2643 "is_true" => {
2644 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2645 }
2646 "is_false" => {
2647 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2648 }
2649 "greater_than_or_equal" => {
2650 if let Some(val) = &assertion.value {
2651 let n = val.as_u64().unwrap_or(0);
2652 let cast = info.value_cast.unwrap_or("uint");
2654 let _ = writeln!(
2655 out_ref,
2656 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2657 );
2658 }
2659 }
2660 "count_min" => {
2661 if let Some(val) = &assertion.value {
2662 let n = val.as_u64().unwrap_or(0);
2663 let _ = writeln!(
2664 out_ref,
2665 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2666 );
2667 }
2668 }
2669 "contains" => {
2670 if let Some(val) = &assertion.value {
2671 let go_val = json_to_go(val);
2672 let _ = writeln!(
2673 out_ref,
2674 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2675 );
2676 }
2677 }
2678 "is_error" => {
2679 let _ = writeln!(out_ref, "\t{{");
2680 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2681 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2682 let _ = writeln!(out_ref, "\t}}");
2683 }
2684 other_check => {
2685 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2686 }
2687 }
2688 } else {
2689 panic!("Go e2e generator: method_result assertion missing 'method' field");
2690 }
2691 }
2692 "min_length" => {
2693 if let Some(val) = &assertion.value {
2694 if let Some(n) = val.as_u64() {
2695 if is_optional {
2696 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2697 let _ = writeln!(
2698 out_ref,
2699 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2700 );
2701 let _ = writeln!(out_ref, "\t}}");
2702 } else if field_expr.starts_with("len(") {
2703 let _ = writeln!(
2704 out_ref,
2705 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2706 );
2707 } else {
2708 let _ = writeln!(
2709 out_ref,
2710 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2711 );
2712 }
2713 }
2714 }
2715 }
2716 "max_length" => {
2717 if let Some(val) = &assertion.value {
2718 if let Some(n) = val.as_u64() {
2719 if is_optional {
2720 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2721 let _ = writeln!(
2722 out_ref,
2723 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2724 );
2725 let _ = writeln!(out_ref, "\t}}");
2726 } else if field_expr.starts_with("len(") {
2727 let _ = writeln!(
2728 out_ref,
2729 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2730 );
2731 } else {
2732 let _ = writeln!(
2733 out_ref,
2734 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2735 );
2736 }
2737 }
2738 }
2739 }
2740 "ends_with" => {
2741 if let Some(expected) = &assertion.value {
2742 let go_val = json_to_go(expected);
2743 let field_for_suffix = if is_optional
2744 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2745 {
2746 format!("string(*{field_expr})")
2747 } else {
2748 format!("string({field_expr})")
2749 };
2750 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2751 let _ = writeln!(
2752 out_ref,
2753 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2754 );
2755 let _ = writeln!(out_ref, "\t}}");
2756 }
2757 }
2758 "matches_regex" => {
2759 if let Some(expected) = &assertion.value {
2760 let go_val = json_to_go(expected);
2761 let field_for_regex = if is_optional
2762 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2763 {
2764 format!("*{field_expr}")
2765 } else {
2766 field_expr.clone()
2767 };
2768 let _ = writeln!(
2769 out_ref,
2770 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2771 );
2772 }
2773 }
2774 "not_error" => {
2775 }
2777 "error" => {
2778 }
2780 other => {
2781 panic!("Go e2e generator: unsupported assertion type: {other}");
2782 }
2783 }
2784
2785 if let Some(ref arr) = array_guard {
2788 if !assertion_buf.is_empty() {
2789 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2790 for line in assertion_buf.lines() {
2792 let _ = writeln!(out, "\t{line}");
2793 }
2794 let _ = writeln!(out, "\t}}");
2795 }
2796 } else {
2797 out.push_str(&assertion_buf);
2798 }
2799}
2800
2801struct GoMethodCallInfo {
2803 call_expr: String,
2805 is_pointer: bool,
2807 value_cast: Option<&'static str>,
2810}
2811
2812fn build_go_method_call(
2827 result_var: &str,
2828 method_name: &str,
2829 args: Option<&serde_json::Value>,
2830 import_alias: &str,
2831) -> GoMethodCallInfo {
2832 match method_name {
2833 "root_node_type" => GoMethodCallInfo {
2834 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2835 is_pointer: false,
2836 value_cast: None,
2837 },
2838 "named_children_count" => GoMethodCallInfo {
2839 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2840 is_pointer: false,
2841 value_cast: Some("uint"),
2842 },
2843 "has_error_nodes" => GoMethodCallInfo {
2844 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2845 is_pointer: true,
2846 value_cast: None,
2847 },
2848 "error_count" | "tree_error_count" => GoMethodCallInfo {
2849 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2850 is_pointer: true,
2851 value_cast: Some("uint"),
2852 },
2853 "tree_to_sexp" => GoMethodCallInfo {
2854 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2855 is_pointer: true,
2856 value_cast: None,
2857 },
2858 "contains_node_type" => {
2859 let node_type = args
2860 .and_then(|a| a.get("node_type"))
2861 .and_then(|v| v.as_str())
2862 .unwrap_or("");
2863 GoMethodCallInfo {
2864 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2865 is_pointer: true,
2866 value_cast: None,
2867 }
2868 }
2869 "find_nodes_by_type" => {
2870 let node_type = args
2871 .and_then(|a| a.get("node_type"))
2872 .and_then(|v| v.as_str())
2873 .unwrap_or("");
2874 GoMethodCallInfo {
2875 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2876 is_pointer: true,
2877 value_cast: None,
2878 }
2879 }
2880 "run_query" => {
2881 let query_source = args
2882 .and_then(|a| a.get("query_source"))
2883 .and_then(|v| v.as_str())
2884 .unwrap_or("");
2885 let language = args
2886 .and_then(|a| a.get("language"))
2887 .and_then(|v| v.as_str())
2888 .unwrap_or("");
2889 let query_lit = go_string_literal(query_source);
2890 let lang_lit = go_string_literal(language);
2891 GoMethodCallInfo {
2893 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2894 is_pointer: false,
2895 value_cast: None,
2896 }
2897 }
2898 other => {
2899 let method_pascal = other.to_upper_camel_case();
2900 GoMethodCallInfo {
2901 call_expr: format!("{result_var}.{method_pascal}()"),
2902 is_pointer: false,
2903 value_cast: None,
2904 }
2905 }
2906 }
2907}
2908
2909fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2919 match value {
2920 serde_json::Value::Object(map) => {
2921 let new_map: serde_json::Map<String, serde_json::Value> = map
2922 .into_iter()
2923 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2924 .collect();
2925 serde_json::Value::Object(new_map)
2926 }
2927 serde_json::Value::Array(arr) => {
2928 if is_byte_array(&arr) {
2931 let bytes: Vec<u8> = arr
2932 .iter()
2933 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2934 .collect();
2935 let encoded = base64_encode(&bytes);
2937 serde_json::Value::String(encoded)
2938 } else {
2939 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2940 }
2941 }
2942 serde_json::Value::String(s) => {
2943 serde_json::Value::String(pascal_to_snake_case(&s))
2946 }
2947 other => other,
2948 }
2949}
2950
2951fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2953 if arr.is_empty() {
2954 return false;
2955 }
2956 arr.iter().all(|v| {
2957 if let serde_json::Value::Number(n) = v {
2958 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
2959 } else {
2960 false
2961 }
2962 })
2963}
2964
2965fn base64_encode(bytes: &[u8]) -> String {
2968 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2969 let mut result = String::new();
2970 let mut i = 0;
2971
2972 while i + 2 < bytes.len() {
2973 let b1 = bytes[i];
2974 let b2 = bytes[i + 1];
2975 let b3 = bytes[i + 2];
2976
2977 result.push(TABLE[(b1 >> 2) as usize] as char);
2978 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2979 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
2980 result.push(TABLE[(b3 & 0x3f) as usize] as char);
2981
2982 i += 3;
2983 }
2984
2985 if i < bytes.len() {
2987 let b1 = bytes[i];
2988 result.push(TABLE[(b1 >> 2) as usize] as char);
2989
2990 if i + 1 < bytes.len() {
2991 let b2 = bytes[i + 1];
2992 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
2993 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
2994 result.push('=');
2995 } else {
2996 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
2997 result.push_str("==");
2998 }
2999 }
3000
3001 result
3002}
3003
3004fn camel_to_snake_case(s: &str) -> String {
3006 let mut result = String::new();
3007 let mut prev_upper = false;
3008 for (i, c) in s.char_indices() {
3009 if c.is_uppercase() {
3010 if i > 0 && !prev_upper {
3011 result.push('_');
3012 }
3013 result.push(c.to_lowercase().next().unwrap_or(c));
3014 prev_upper = true;
3015 } else {
3016 if prev_upper && i > 1 {
3017 }
3021 result.push(c);
3022 prev_upper = false;
3023 }
3024 }
3025 result
3026}
3027
3028fn pascal_to_snake_case(s: &str) -> String {
3033 let first_char = s.chars().next();
3035 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3036 return s.to_string();
3037 }
3038 camel_to_snake_case(s)
3039}
3040
3041fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3045 let elem = element_type.unwrap_or("String").trim();
3046 let go_elem = rust_type_to_go(elem, import_alias);
3047 format!("[]{go_elem}")
3048}
3049
3050fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3053 let trimmed = rust.trim();
3054 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3055 return format!("[]{}", rust_type_to_go(inner, import_alias));
3056 }
3057 match trimmed {
3058 "String" | "&str" | "str" => "string".to_string(),
3059 "bool" => "bool".to_string(),
3060 "f32" => "float32".to_string(),
3061 "f64" => "float64".to_string(),
3062 "i8" => "int8".to_string(),
3063 "i16" => "int16".to_string(),
3064 "i32" => "int32".to_string(),
3065 "i64" | "isize" => "int64".to_string(),
3066 "u8" => "uint8".to_string(),
3067 "u16" => "uint16".to_string(),
3068 "u32" => "uint32".to_string(),
3069 "u64" | "usize" => "uint64".to_string(),
3070 _ => format!("{import_alias}.{trimmed}"),
3071 }
3072}
3073
3074fn json_to_go(value: &serde_json::Value) -> String {
3075 match value {
3076 serde_json::Value::String(s) => go_string_literal(s),
3077 serde_json::Value::Bool(b) => b.to_string(),
3078 serde_json::Value::Number(n) => n.to_string(),
3079 serde_json::Value::Null => "nil".to_string(),
3080 other => go_string_literal(&other.to_string()),
3082 }
3083}
3084
3085fn visitor_struct_name(fixture_id: &str) -> String {
3094 use heck::ToUpperCamelCase;
3095 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3097}
3098
3099fn emit_go_visitor_struct(
3104 out: &mut String,
3105 struct_name: &str,
3106 visitor_spec: &crate::fixture::VisitorSpec,
3107 import_alias: &str,
3108) {
3109 let _ = writeln!(out, "type {struct_name} struct{{");
3110 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3111 let _ = writeln!(out, "}}");
3112 for (method_name, action) in &visitor_spec.callbacks {
3113 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3114 }
3115}
3116
3117fn emit_go_visitor_method(
3119 out: &mut String,
3120 struct_name: &str,
3121 method_name: &str,
3122 action: &CallbackAction,
3123 import_alias: &str,
3124) {
3125 let camel_method = method_to_camel(method_name);
3126 let params = match method_name {
3129 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3130 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3131 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3132 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3133 "visit_code_inline"
3134 | "visit_strong"
3135 | "visit_emphasis"
3136 | "visit_strikethrough"
3137 | "visit_underline"
3138 | "visit_subscript"
3139 | "visit_superscript"
3140 | "visit_mark"
3141 | "visit_button"
3142 | "visit_summary"
3143 | "visit_figcaption"
3144 | "visit_definition_term"
3145 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3146 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3147 "visit_list_item" => {
3148 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3149 }
3150 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3151 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3152 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3153 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3154 "visit_input" => {
3155 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3156 }
3157 "visit_audio" | "visit_video" | "visit_iframe" => {
3158 format!("_ {import_alias}.NodeContext, src *string")
3159 }
3160 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3161 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3162 format!("_ {import_alias}.NodeContext, output string")
3163 }
3164 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3165 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3166 _ => format!("_ {import_alias}.NodeContext"),
3167 };
3168
3169 let _ = writeln!(
3170 out,
3171 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3172 );
3173 match action {
3174 CallbackAction::Skip => {
3175 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3176 }
3177 CallbackAction::Continue => {
3178 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3179 }
3180 CallbackAction::PreserveHtml => {
3181 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3182 }
3183 CallbackAction::Custom { output } => {
3184 let escaped = go_string_literal(output);
3185 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3186 }
3187 CallbackAction::CustomTemplate { template, .. } => {
3188 let ptr_params = go_visitor_ptr_params(method_name);
3195 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3196 let escaped_fmt = go_string_literal(&fmt_str);
3197 if fmt_args.is_empty() {
3198 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3199 } else {
3200 let args_str = fmt_args.join(", ");
3201 let _ = writeln!(
3202 out,
3203 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3204 );
3205 }
3206 }
3207 }
3208 let _ = writeln!(out, "}}");
3209}
3210
3211fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3214 match method_name {
3215 "visit_link" => ["title"].into(),
3216 "visit_image" => ["title"].into(),
3217 "visit_heading" => ["id"].into(),
3218 "visit_code_block" => ["lang"].into(),
3219 "visit_form" => ["action", "method"].into(),
3220 "visit_input" => ["name", "value"].into(),
3221 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3222 _ => std::collections::HashSet::new(),
3223 }
3224}
3225
3226fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3238 let mut fmt_str = String::new();
3239 let mut args: Vec<String> = Vec::new();
3240 let mut chars = template.chars().peekable();
3241 while let Some(c) = chars.next() {
3242 if c == '{' {
3243 let mut name = String::new();
3245 for inner in chars.by_ref() {
3246 if inner == '}' {
3247 break;
3248 }
3249 name.push(inner);
3250 }
3251 fmt_str.push_str("%s");
3252 let go_name = go_param_name(&name);
3254 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3256 format!("*{go_name}")
3257 } else {
3258 go_name
3259 };
3260 args.push(arg_expr);
3261 } else {
3262 fmt_str.push(c);
3263 }
3264 }
3265 (fmt_str, args)
3266}
3267
3268fn method_to_camel(snake: &str) -> String {
3270 use heck::ToUpperCamelCase;
3271 snake.to_upper_camel_case()
3272}
3273
3274#[cfg(test)]
3275mod tests {
3276 use super::*;
3277 use crate::config::{CallConfig, E2eConfig};
3278 use crate::field_access::FieldResolver;
3279 use crate::fixture::{Assertion, Fixture};
3280
3281 fn make_fixture(id: &str) -> Fixture {
3282 Fixture {
3283 id: id.to_string(),
3284 category: None,
3285 description: "test fixture".to_string(),
3286 tags: vec![],
3287 skip: None,
3288 env: None,
3289 call: None,
3290 input: serde_json::Value::Null,
3291 mock_response: Some(crate::fixture::MockResponse {
3292 status: 200,
3293 body: Some(serde_json::Value::Null),
3294 stream_chunks: None,
3295 headers: std::collections::HashMap::new(),
3296 }),
3297 source: String::new(),
3298 http: None,
3299 assertions: vec![Assertion {
3300 assertion_type: "not_error".to_string(),
3301 ..Default::default()
3302 }],
3303 visitor: None,
3304 }
3305 }
3306
3307 #[test]
3311 fn test_go_method_name_uses_go_casing() {
3312 let e2e_config = E2eConfig {
3313 call: CallConfig {
3314 function: "clean_extracted_text".to_string(),
3315 module: "github.com/example/mylib".to_string(),
3316 result_var: "result".to_string(),
3317 returns_result: true,
3318 ..CallConfig::default()
3319 },
3320 ..E2eConfig::default()
3321 };
3322
3323 let fixture = make_fixture("basic_text");
3324 let resolver = FieldResolver::new(
3325 &std::collections::HashMap::new(),
3326 &std::collections::HashSet::new(),
3327 &std::collections::HashSet::new(),
3328 &std::collections::HashSet::new(),
3329 &std::collections::HashSet::new(),
3330 );
3331 let mut out = String::new();
3332 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3333
3334 assert!(
3335 out.contains("kreuzberg.CleanExtractedText("),
3336 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3337 );
3338 assert!(
3339 !out.contains("kreuzberg.clean_extracted_text("),
3340 "must not emit raw snake_case method name, got:\n{out}"
3341 );
3342 }
3343
3344 #[test]
3345 fn test_streaming_fixture_emits_collect_snippet() {
3346 let streaming_fixture_json = r#"{
3348 "id": "basic_stream",
3349 "description": "basic streaming test",
3350 "call": "chat_stream",
3351 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3352 "mock_response": {
3353 "status": 200,
3354 "stream_chunks": [{"delta": "hello"}]
3355 },
3356 "assertions": [
3357 {"type": "count_min", "field": "chunks", "value": 1}
3358 ]
3359 }"#;
3360 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3361 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3362
3363 let e2e_config = E2eConfig {
3364 call: CallConfig {
3365 function: "chat_stream".to_string(),
3366 module: "github.com/example/mylib".to_string(),
3367 result_var: "result".to_string(),
3368 returns_result: true,
3369 r#async: true,
3370 ..CallConfig::default()
3371 },
3372 ..E2eConfig::default()
3373 };
3374
3375 let resolver = FieldResolver::new(
3376 &std::collections::HashMap::new(),
3377 &std::collections::HashSet::new(),
3378 &std::collections::HashSet::new(),
3379 &std::collections::HashSet::new(),
3380 &std::collections::HashSet::new(),
3381 );
3382
3383 let mut out = String::new();
3384 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3385
3386 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3387 assert!(
3388 out.contains("for chunk := range stream"),
3389 "should emit collect loop, got:\n{out}"
3390 );
3391 }
3392
3393 #[test]
3394 fn test_streaming_with_client_factory_and_json_arg() {
3395 use alef_core::config::e2e::{ArgMapping, CallOverride};
3399 let streaming_fixture_json = r#"{
3400 "id": "basic_stream_client",
3401 "description": "basic streaming test with client",
3402 "call": "chat_stream",
3403 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3404 "mock_response": {
3405 "status": 200,
3406 "stream_chunks": [{"delta": "hello"}]
3407 },
3408 "assertions": [
3409 {"type": "count_min", "field": "chunks", "value": 1}
3410 ]
3411 }"#;
3412 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3413 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3414
3415 let mut go_override = CallOverride::default();
3416 go_override.client_factory = Some("CreateClient".to_string());
3417
3418 let mut call_overrides = std::collections::HashMap::new();
3419 call_overrides.insert("go".to_string(), go_override);
3420
3421 let e2e_config = E2eConfig {
3422 call: CallConfig {
3423 function: "chat_stream".to_string(),
3424 module: "github.com/example/mylib".to_string(),
3425 result_var: "result".to_string(),
3426 returns_result: false, r#async: true,
3428 args: vec![ArgMapping {
3429 name: "request".to_string(),
3430 field: "input".to_string(),
3431 arg_type: "json_object".to_string(),
3432 optional: false,
3433 owned: true,
3434 element_type: None,
3435 go_type: None,
3436 }],
3437 overrides: call_overrides,
3438 ..CallConfig::default()
3439 },
3440 ..E2eConfig::default()
3441 };
3442
3443 let resolver = FieldResolver::new(
3444 &std::collections::HashMap::new(),
3445 &std::collections::HashSet::new(),
3446 &std::collections::HashSet::new(),
3447 &std::collections::HashSet::new(),
3448 &std::collections::HashSet::new(),
3449 );
3450
3451 let mut out = String::new();
3452 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3453
3454 eprintln!("generated:\n{out}");
3455 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3456 assert!(
3457 out.contains("for chunk := range stream"),
3458 "should emit collect loop, got:\n{out}"
3459 );
3460 }
3461}