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 let is_streaming_fixture = f.is_streaming_mock();
633 f.assertions.iter().any(|a| {
634 let field_is_streaming_virtual = a
635 .field
636 .as_deref()
637 .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
638 let field_valid = a
639 .field
640 .as_ref()
641 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
642 .unwrap_or(true)
643 || (is_streaming_fixture && field_is_streaming_virtual);
644 let synthetic_field_needs_assert = match a.field.as_deref() {
645 Some("chunks_have_content" | "chunks_have_embeddings") => {
646 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
647 }
648 Some("embeddings") => {
649 matches!(
650 a.assertion_type.as_str(),
651 "count_equals" | "count_min" | "not_empty" | "is_empty"
652 )
653 }
654 _ => false,
655 };
656 let type_needs_assert = matches!(
657 a.assertion_type.as_str(),
658 "count_equals"
659 | "count_min"
660 | "count_max"
661 | "is_true"
662 | "is_false"
663 | "method_result"
664 | "min_length"
665 | "max_length"
666 | "matches_regex"
667 );
668 synthetic_field_needs_assert || type_needs_assert && field_valid
669 })
670 });
671
672 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
674 let needs_http = has_http_fixtures;
675 let needs_io = has_http_fixtures;
677
678 let needs_reflect = fixtures.iter().any(|f| {
681 if let Some(http) = &f.http {
682 let body_needs_reflect = http
683 .expected_response
684 .body
685 .as_ref()
686 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
687 let partial_needs_reflect = http.expected_response.body_partial.is_some();
688 body_needs_reflect || partial_needs_reflect
689 } else {
690 false
691 }
692 });
693
694 let _ = writeln!(out, "// E2e tests for category: {category}");
695 let _ = writeln!(out, "package e2e_test");
696 let _ = writeln!(out);
697 let _ = writeln!(out, "import (");
698 if needs_base64 {
699 let _ = writeln!(out, "\t\"encoding/base64\"");
700 }
701 if needs_json || needs_reflect {
702 let _ = writeln!(out, "\t\"encoding/json\"");
703 }
704 if needs_fmt {
705 let _ = writeln!(out, "\t\"fmt\"");
706 }
707 if needs_io {
708 let _ = writeln!(out, "\t\"io\"");
709 }
710 if needs_http {
711 let _ = writeln!(out, "\t\"net/http\"");
712 }
713 if needs_os {
714 let _ = writeln!(out, "\t\"os\"");
715 }
716 let _ = needs_filepath; if needs_reflect {
718 let _ = writeln!(out, "\t\"reflect\"");
719 }
720 if needs_strings {
721 let _ = writeln!(out, "\t\"strings\"");
722 }
723 let _ = writeln!(out, "\t\"testing\"");
724 if needs_assert {
725 let _ = writeln!(out);
726 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
727 }
728 if needs_pkg {
729 let _ = writeln!(out);
730 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
731 }
732 let _ = writeln!(out, ")");
733 let _ = writeln!(out);
734
735 for fixture in fixtures.iter() {
737 if let Some(visitor_spec) = &fixture.visitor {
738 let struct_name = visitor_struct_name(&fixture.id);
739 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
740 let _ = writeln!(out);
741 }
742 }
743
744 for (i, fixture) in fixtures.iter().enumerate() {
745 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
746 if i + 1 < fixtures.len() {
747 let _ = writeln!(out);
748 }
749 }
750
751 while out.ends_with("\n\n") {
753 out.pop();
754 }
755 if !out.ends_with('\n') {
756 out.push('\n');
757 }
758 out
759}
760
761fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
770 if fixture.is_http_test() {
772 return false;
773 }
774 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
775 if call_config.skip_languages.iter().any(|l| l == "go") {
778 return false;
779 }
780 let go_override = call_config
781 .overrides
782 .get("go")
783 .or_else(|| e2e_config.call.overrides.get("go"));
784 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
787 return true;
788 }
789 let fn_name = go_override
793 .and_then(|o| o.function.as_deref())
794 .filter(|s| !s.is_empty())
795 .unwrap_or(call_config.function.as_str());
796 !fn_name.is_empty()
797}
798
799fn render_test_function(
800 out: &mut String,
801 fixture: &Fixture,
802 import_alias: &str,
803 field_resolver: &FieldResolver,
804 e2e_config: &crate::config::E2eConfig,
805) {
806 let fn_name = fixture.id.to_upper_camel_case();
807 let description = &fixture.description;
808
809 if fixture.http.is_some() {
811 render_http_test_function(out, fixture);
812 return;
813 }
814
815 if !fixture_has_go_callable(fixture, e2e_config) {
820 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
821 let _ = writeln!(out, "\t// {description}");
822 let _ = writeln!(
823 out,
824 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
825 );
826 let _ = writeln!(out, "}}");
827 return;
828 }
829
830 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
832 let lang = "go";
833 let overrides = call_config.overrides.get(lang);
834
835 let base_function_name = overrides
839 .and_then(|o| o.function.as_deref())
840 .unwrap_or(&call_config.function);
841 let function_name = to_go_name(base_function_name);
842 let result_var = &call_config.result_var;
843 let args = &call_config.args;
844
845 let returns_result = overrides
848 .and_then(|o| o.returns_result)
849 .unwrap_or(call_config.returns_result);
850
851 let returns_void = call_config.returns_void;
854
855 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
858 if call_config.result_is_simple {
859 return true;
860 }
861 call_config
862 .overrides
863 .get("rust")
864 .map(|o| o.result_is_simple)
865 .unwrap_or(false)
866 });
867
868 let result_is_array = overrides
871 .map(|o| o.result_is_array)
872 .unwrap_or(call_config.result_is_array);
873
874 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
876 e2e_config
877 .call
878 .overrides
879 .get("go")
880 .and_then(|o| o.options_type.as_deref())
881 });
882
883 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
885 e2e_config
886 .call
887 .overrides
888 .get("go")
889 .map(|o| o.options_ptr)
890 .unwrap_or(false)
891 });
892
893 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
894 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
898
899 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
902 e2e_config
903 .call
904 .overrides
905 .get(lang)
906 .and_then(|o| o.client_factory.as_deref())
907 });
908
909 let (mut setup_lines, args_str) = build_args_and_setup(
910 &fixture.input,
911 args,
912 import_alias,
913 call_options_type,
914 fixture,
915 call_options_ptr,
916 validation_creation_failure,
917 );
918
919 let mut visitor_opts_var: Option<String> = None;
922 if fixture.visitor.is_some() {
923 let struct_name = visitor_struct_name(&fixture.id);
924 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
925 let opts_type = call_options_type.unwrap_or("ConversionOptions");
927 let opts_var = "opts".to_string();
928 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
929 setup_lines.push("opts.Visitor = visitor".to_string());
930 visitor_opts_var = Some(opts_var);
931 }
932
933 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
934 let final_args = {
935 let mut parts: Vec<String> = Vec::new();
936 if !args_str.is_empty() {
937 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
939 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
940 } else {
941 args_str
942 };
943 parts.push(processed_args);
944 }
945 parts.extend(go_extra_args);
946 parts.join(", ")
947 };
948
949 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
950 let _ = writeln!(out, "\t// {description}");
951
952 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
956 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
957 if let Some(var) = api_key_var {
958 if has_mock {
959 let fixture_id = &fixture.id;
963 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
964 let _ = writeln!(out, "\tvar baseURL *string");
965 let _ = writeln!(out, "\tif apiKey != \"\" {{");
966 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
967 let _ = writeln!(out, "\t}} else {{");
968 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
969 let _ = writeln!(
970 out,
971 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
972 );
973 let _ = writeln!(out, "\t\tbaseURL = &u");
974 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
975 let _ = writeln!(out, "\t}}");
976 } else {
977 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
978 let _ = writeln!(out, "\tif apiKey == \"\" {{");
979 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
980 let _ = writeln!(out, "\t}}");
981 }
982 }
983
984 for line in &setup_lines {
985 let _ = writeln!(out, "\t{line}");
986 }
987
988 let call_prefix = if let Some(factory) = client_factory {
992 let factory_name = to_go_name(factory);
993 let fixture_id = &fixture.id;
994 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
997 ("apiKey".to_string(), "baseURL".to_string())
999 } else if api_key_var.is_some() {
1000 ("apiKey".to_string(), "nil".to_string())
1002 } else if fixture.has_host_root_route() {
1003 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1004 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1005 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1006 let _ = writeln!(
1007 out,
1008 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1009 );
1010 let _ = writeln!(out, "\t}}");
1011 ("\"test-key\"".to_string(), "&mockURL".to_string())
1012 } else {
1013 let _ = writeln!(
1014 out,
1015 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1016 );
1017 ("\"test-key\"".to_string(), "&mockURL".to_string())
1018 };
1019 let _ = writeln!(
1020 out,
1021 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1022 );
1023 let _ = writeln!(out, "\tif clientErr != nil {{");
1024 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1025 let _ = writeln!(out, "\t}}");
1026 "client".to_string()
1027 } else {
1028 import_alias.to_string()
1029 };
1030
1031 let binding_returns_error_pre = args
1036 .iter()
1037 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1038 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1039
1040 if expects_error {
1041 if effective_returns_result_pre && !returns_void {
1042 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1043 } else {
1044 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1045 }
1046 let _ = writeln!(out, "\tif err == nil {{");
1047 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1048 let _ = writeln!(out, "\t}}");
1049 let _ = writeln!(out, "}}");
1050 return;
1051 }
1052
1053 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1055
1056 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1061 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1062 return false;
1063 }
1064 if a.assertion_type == "method_result" {
1066 return true;
1067 }
1068 match &a.field {
1069 Some(f) if !f.is_empty() => {
1070 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1071 return true;
1072 }
1073 field_resolver.is_valid_for_result(f)
1074 }
1075 _ => true,
1076 }
1077 });
1078
1079 let binding_returns_error = args
1086 .iter()
1087 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1088 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1090
1091 if !effective_returns_result && result_is_simple {
1097 let result_binding = if has_usable_assertion {
1099 result_var.to_string()
1100 } else {
1101 "_".to_string()
1102 };
1103 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1105 let _ = writeln!(
1106 out,
1107 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1108 );
1109 if has_usable_assertion && result_binding != "_" {
1110 if result_is_array {
1111 let _ = writeln!(out, "\tvalue := {result_var}");
1113 } else {
1114 let only_nil_assertions = fixture
1117 .assertions
1118 .iter()
1119 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1120 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1121 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1122
1123 if !only_nil_assertions {
1124 let _ = writeln!(out, "\tif {result_var} == nil {{");
1126 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1127 let _ = writeln!(out, "\t}}");
1128 let _ = writeln!(out, "\tvalue := *{result_var}");
1129 }
1130 }
1131 }
1132 } else if !effective_returns_result || returns_void {
1133 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1136 let _ = writeln!(out, "\tif err != nil {{");
1137 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1138 let _ = writeln!(out, "\t}}");
1139 let _ = writeln!(out, "}}");
1141 return;
1142 } else {
1143 let result_binding = if is_streaming {
1146 "stream".to_string()
1147 } else if has_usable_assertion {
1148 result_var.to_string()
1149 } else {
1150 "_".to_string()
1151 };
1152 let _ = writeln!(
1153 out,
1154 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1155 );
1156 let _ = writeln!(out, "\tif err != nil {{");
1157 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1158 let _ = writeln!(out, "\t}}");
1159 if is_streaming {
1161 let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1162 let _ = writeln!(out, "\tfor chunk := range stream {{");
1163 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1164 let _ = writeln!(out, "\t}}");
1165 }
1166 if result_is_simple && has_usable_assertion && result_binding != "_" {
1167 if result_is_array {
1168 let _ = writeln!(out, "\tvalue := {}", result_var);
1170 } else {
1171 let only_nil_assertions = fixture
1174 .assertions
1175 .iter()
1176 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1177 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1178 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1179
1180 if !only_nil_assertions {
1181 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1183 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1184 let _ = writeln!(out, "\t}}");
1185 let _ = writeln!(out, "\tvalue := *{}", result_var);
1186 }
1187 }
1188 }
1189 }
1190
1191 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1194 let only_nil_assertions = fixture
1195 .assertions
1196 .iter()
1197 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1198 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1199 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1200 !only_nil_assertions
1201 } else {
1202 result_is_simple && has_usable_assertion
1203 };
1204
1205 let effective_result_var = if has_deref_value {
1206 "value".to_string()
1207 } else {
1208 result_var.to_string()
1209 };
1210
1211 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1216 for assertion in &fixture.assertions {
1217 if let Some(f) = &assertion.field {
1218 if !f.is_empty() {
1219 let resolved = field_resolver.resolve(f);
1220 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1221 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1226 let is_array_field = field_resolver.is_array(resolved);
1227 if !is_string_field || is_array_field {
1228 continue;
1231 }
1232 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1233 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1234 if field_resolver.has_map_access(f) {
1235 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1238 } else {
1239 let _ = writeln!(out, "\tvar {local_var} string");
1240 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1241 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1245 let _ = writeln!(out, "\t}}");
1246 }
1247 optional_locals.insert(f.clone(), local_var);
1248 }
1249 }
1250 }
1251 }
1252
1253 for assertion in &fixture.assertions {
1255 if let Some(f) = &assertion.field {
1256 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1257 let parts: Vec<&str> = f.split('.').collect();
1260 let mut guard_expr: Option<String> = None;
1261 for i in 1..parts.len() {
1262 let prefix = parts[..i].join(".");
1263 let resolved_prefix = field_resolver.resolve(&prefix);
1264 if field_resolver.is_optional(resolved_prefix) {
1265 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1271 let suffix = &resolved_prefix[bracket_pos + 1..];
1272 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1273 if is_numeric_index {
1274 &resolved_prefix[..bracket_pos]
1275 } else {
1276 resolved_prefix
1277 }
1278 } else {
1279 resolved_prefix
1280 };
1281 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1282 guard_expr = Some(accessor);
1283 break;
1284 }
1285 }
1286 if let Some(guard) = guard_expr {
1287 if field_resolver.is_valid_for_result(f) {
1290 let _ = writeln!(out, "\tif {guard} != nil {{");
1291 let mut nil_buf = String::new();
1294 render_assertion(
1295 &mut nil_buf,
1296 assertion,
1297 &effective_result_var,
1298 import_alias,
1299 field_resolver,
1300 &optional_locals,
1301 result_is_simple,
1302 result_is_array,
1303 is_streaming,
1304 );
1305 for line in nil_buf.lines() {
1306 let _ = writeln!(out, "\t{line}");
1307 }
1308 let _ = writeln!(out, "\t}}");
1309 } else {
1310 render_assertion(
1311 out,
1312 assertion,
1313 &effective_result_var,
1314 import_alias,
1315 field_resolver,
1316 &optional_locals,
1317 result_is_simple,
1318 result_is_array,
1319 is_streaming,
1320 );
1321 }
1322 continue;
1323 }
1324 }
1325 }
1326 render_assertion(
1327 out,
1328 assertion,
1329 &effective_result_var,
1330 import_alias,
1331 field_resolver,
1332 &optional_locals,
1333 result_is_simple,
1334 result_is_array,
1335 is_streaming,
1336 );
1337 }
1338
1339 let _ = writeln!(out, "}}");
1340}
1341
1342fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1348 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1349}
1350
1351struct GoTestClientRenderer;
1363
1364impl client::TestClientRenderer for GoTestClientRenderer {
1365 fn language_name(&self) -> &'static str {
1366 "go"
1367 }
1368
1369 fn sanitize_test_name(&self, id: &str) -> String {
1373 id.to_upper_camel_case()
1374 }
1375
1376 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1379 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1380 let _ = writeln!(out, "\t// {description}");
1381 if let Some(reason) = skip_reason {
1382 let escaped = go_string_literal(reason);
1383 let _ = writeln!(out, "\tt.Skip({escaped})");
1384 }
1385 }
1386
1387 fn render_test_close(&self, out: &mut String) {
1388 let _ = writeln!(out, "}}");
1389 }
1390
1391 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1397 let method = ctx.method.to_uppercase();
1398 let path = ctx.path;
1399
1400 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1401 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1402 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1403 let _ = writeln!(out, "\t}}");
1404
1405 let body_expr = if let Some(body) = ctx.body {
1407 let json = serde_json::to_string(body).unwrap_or_default();
1408 let escaped = go_string_literal(&json);
1409 format!("strings.NewReader({})", escaped)
1410 } else {
1411 "strings.NewReader(\"\")".to_string()
1412 };
1413
1414 let _ = writeln!(out, "\tbody := {body_expr}");
1415 let _ = writeln!(
1416 out,
1417 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1418 );
1419 let _ = writeln!(out, "\tif err != nil {{");
1420 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1421 let _ = writeln!(out, "\t}}");
1422
1423 if ctx.body.is_some() {
1425 let content_type = ctx.content_type.unwrap_or("application/json");
1426 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1427 }
1428
1429 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1431 header_names.sort();
1432 for name in header_names {
1433 let value = &ctx.headers[name];
1434 let escaped_name = go_string_literal(name);
1435 let escaped_value = go_string_literal(value);
1436 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1437 }
1438
1439 if !ctx.cookies.is_empty() {
1441 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1442 cookie_names.sort();
1443 for name in cookie_names {
1444 let value = &ctx.cookies[name];
1445 let escaped_name = go_string_literal(name);
1446 let escaped_value = go_string_literal(value);
1447 let _ = writeln!(
1448 out,
1449 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1450 );
1451 }
1452 }
1453
1454 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1456 let _ = writeln!(
1457 out,
1458 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1459 );
1460 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1461 let _ = writeln!(out, "\t\t}},");
1462 let _ = writeln!(out, "\t}}");
1463 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1464 let _ = writeln!(out, "\tif err != nil {{");
1465 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1466 let _ = writeln!(out, "\t}}");
1467 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1468
1469 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1473 let _ = writeln!(out, "\tif err != nil {{");
1474 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1475 let _ = writeln!(out, "\t}}");
1476 let _ = writeln!(out, "\t_ = bodyBytes");
1477 }
1478
1479 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1480 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1481 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1482 let _ = writeln!(out, "\t}}");
1483 }
1484
1485 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1488 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1490 return;
1491 }
1492 if name.eq_ignore_ascii_case("connection") {
1494 return;
1495 }
1496 let escaped_name = go_string_literal(name);
1497 let escaped_value = go_string_literal(expected);
1498 let _ = writeln!(
1499 out,
1500 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1501 );
1502 let _ = writeln!(
1503 out,
1504 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1505 );
1506 let _ = writeln!(out, "\t}}");
1507 }
1508
1509 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1514 match expected {
1515 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1516 let json_str = serde_json::to_string(expected).unwrap_or_default();
1517 let escaped = go_string_literal(&json_str);
1518 let _ = writeln!(out, "\tvar got any");
1519 let _ = writeln!(out, "\tvar want any");
1520 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1521 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1522 let _ = writeln!(out, "\t}}");
1523 let _ = writeln!(
1524 out,
1525 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1526 );
1527 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1528 let _ = writeln!(out, "\t}}");
1529 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1530 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1531 let _ = writeln!(out, "\t}}");
1532 }
1533 serde_json::Value::String(s) => {
1534 let escaped = go_string_literal(s);
1535 let _ = writeln!(out, "\twant := {escaped}");
1536 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1537 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1538 let _ = writeln!(out, "\t}}");
1539 }
1540 other => {
1541 let escaped = go_string_literal(&other.to_string());
1542 let _ = writeln!(out, "\twant := {escaped}");
1543 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1544 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1545 let _ = writeln!(out, "\t}}");
1546 }
1547 }
1548 }
1549
1550 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1553 if let Some(obj) = expected.as_object() {
1554 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1555 let _ = writeln!(
1556 out,
1557 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1558 );
1559 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1560 let _ = writeln!(out, "\t}}");
1561 for (key, val) in obj {
1562 let escaped_key = go_string_literal(key);
1563 let json_val = serde_json::to_string(val).unwrap_or_default();
1564 let escaped_val = go_string_literal(&json_val);
1565 let _ = writeln!(out, "\t{{");
1566 let _ = writeln!(out, "\t\tvar _wantVal any");
1567 let _ = writeln!(
1568 out,
1569 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1570 );
1571 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1572 let _ = writeln!(out, "\t\t}}");
1573 let _ = writeln!(
1574 out,
1575 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1576 );
1577 let _ = writeln!(
1578 out,
1579 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1580 );
1581 let _ = writeln!(out, "\t\t}}");
1582 let _ = writeln!(out, "\t}}");
1583 }
1584 }
1585 }
1586
1587 fn render_assert_validation_errors(
1592 &self,
1593 out: &mut String,
1594 _response_var: &str,
1595 errors: &[ValidationErrorExpectation],
1596 ) {
1597 let _ = writeln!(out, "\tvar _veBody map[string]any");
1598 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1599 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1600 let _ = writeln!(out, "\t}}");
1601 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1602 for ve in errors {
1603 let escaped_msg = go_string_literal(&ve.msg);
1604 let _ = writeln!(out, "\t{{");
1605 let _ = writeln!(out, "\t\t_found := false");
1606 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1607 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1608 let _ = writeln!(
1609 out,
1610 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1611 );
1612 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1613 let _ = writeln!(out, "\t\t\t\t\tbreak");
1614 let _ = writeln!(out, "\t\t\t\t}}");
1615 let _ = writeln!(out, "\t\t\t}}");
1616 let _ = writeln!(out, "\t\t}}");
1617 let _ = writeln!(out, "\t\tif !_found {{");
1618 let _ = writeln!(
1619 out,
1620 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1621 );
1622 let _ = writeln!(out, "\t\t}}");
1623 let _ = writeln!(out, "\t}}");
1624 }
1625 }
1626}
1627
1628fn build_args_and_setup(
1636 input: &serde_json::Value,
1637 args: &[crate::config::ArgMapping],
1638 import_alias: &str,
1639 options_type: Option<&str>,
1640 fixture: &crate::fixture::Fixture,
1641 options_ptr: bool,
1642 expects_error: bool,
1643) -> (Vec<String>, String) {
1644 let fixture_id = &fixture.id;
1645 use heck::ToUpperCamelCase;
1646
1647 if args.is_empty() {
1648 return (Vec::new(), String::new());
1649 }
1650
1651 let mut setup_lines: Vec<String> = Vec::new();
1652 let mut parts: Vec<String> = Vec::new();
1653
1654 for arg in args {
1655 if arg.arg_type == "mock_url" {
1656 if fixture.has_host_root_route() {
1657 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1658 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1659 setup_lines.push(format!(
1660 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1661 arg.name, arg.name
1662 ));
1663 } else {
1664 setup_lines.push(format!(
1665 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1666 arg.name,
1667 ));
1668 }
1669 parts.push(arg.name.clone());
1670 continue;
1671 }
1672
1673 if arg.arg_type == "handle" {
1674 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1676 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1677 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1678 let create_err_handler = if expects_error {
1682 "assert.Error(t, createErr)\n\t\treturn".to_string()
1683 } else {
1684 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1685 };
1686 if config_value.is_null()
1687 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1688 {
1689 setup_lines.push(format!(
1690 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1691 name = arg.name,
1692 ));
1693 } else {
1694 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1695 let go_literal = go_string_literal(&json_str);
1696 let name = &arg.name;
1697 setup_lines.push(format!(
1698 "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}}"
1699 ));
1700 setup_lines.push(format!(
1701 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1702 ));
1703 }
1704 parts.push(arg.name.clone());
1705 continue;
1706 }
1707
1708 let val: Option<&serde_json::Value> = if arg.field == "input" {
1709 Some(input)
1710 } else {
1711 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1712 input.get(field)
1713 };
1714
1715 if arg.arg_type == "bytes" {
1722 let var_name = format!("{}Bytes", arg.name);
1723 match val {
1724 None | Some(serde_json::Value::Null) => {
1725 if arg.optional {
1726 parts.push("nil".to_string());
1727 } else {
1728 parts.push("[]byte{}".to_string());
1729 }
1730 }
1731 Some(serde_json::Value::String(s)) => {
1732 let go_path = go_string_literal(s);
1737 setup_lines.push(format!(
1738 "{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}}"
1739 ));
1740 parts.push(var_name);
1741 }
1742 Some(other) => {
1743 parts.push(format!("[]byte({})", json_to_go(other)));
1744 }
1745 }
1746 continue;
1747 }
1748
1749 match val {
1750 None | Some(serde_json::Value::Null) if arg.optional => {
1751 match arg.arg_type.as_str() {
1753 "string" => {
1754 parts.push("nil".to_string());
1756 }
1757 "json_object" => {
1758 if options_ptr {
1759 parts.push("nil".to_string());
1761 } else if let Some(opts_type) = options_type {
1762 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1764 } else {
1765 parts.push("nil".to_string());
1766 }
1767 }
1768 _ => {
1769 parts.push("nil".to_string());
1770 }
1771 }
1772 }
1773 None | Some(serde_json::Value::Null) => {
1774 let default_val = match arg.arg_type.as_str() {
1776 "string" => "\"\"".to_string(),
1777 "int" | "integer" | "i64" => "0".to_string(),
1778 "float" | "number" => "0.0".to_string(),
1779 "bool" | "boolean" => "false".to_string(),
1780 "json_object" => {
1781 if options_ptr {
1782 "nil".to_string()
1784 } else if let Some(opts_type) = options_type {
1785 format!("{import_alias}.{opts_type}{{}}")
1786 } else {
1787 "nil".to_string()
1788 }
1789 }
1790 _ => "nil".to_string(),
1791 };
1792 parts.push(default_val);
1793 }
1794 Some(v) => {
1795 match arg.arg_type.as_str() {
1796 "json_object" => {
1797 let is_array = v.is_array();
1800 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1801 if is_empty_obj {
1802 if options_ptr {
1803 parts.push("nil".to_string());
1805 } else if let Some(opts_type) = options_type {
1806 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1807 } else {
1808 parts.push("nil".to_string());
1809 }
1810 } else if is_array {
1811 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1816 if go_t.starts_with('[') {
1820 go_t.to_string()
1821 } else {
1822 let qualified = if go_t.contains('.') {
1824 go_t.to_string()
1825 } else {
1826 format!("{import_alias}.{go_t}")
1827 };
1828 format!("[]{qualified}")
1829 }
1830 } else {
1831 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1832 };
1833 let converted_v = convert_json_for_go(v.clone());
1835 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1836 let go_literal = go_string_literal(&json_str);
1837 let var_name = &arg.name;
1838 setup_lines.push(format!(
1839 "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}}"
1840 ));
1841 parts.push(var_name.to_string());
1842 } else if let Some(opts_type) = options_type {
1843 let remapped_v = if options_ptr {
1848 convert_json_for_go(v.clone())
1849 } else {
1850 v.clone()
1851 };
1852 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1853 let go_literal = go_string_literal(&json_str);
1854 let var_name = &arg.name;
1855 setup_lines.push(format!(
1856 "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}}"
1857 ));
1858 let arg_expr = if options_ptr {
1860 format!("&{var_name}")
1861 } else {
1862 var_name.to_string()
1863 };
1864 parts.push(arg_expr);
1865 } else {
1866 parts.push(json_to_go(v));
1867 }
1868 }
1869 "string" if arg.optional => {
1870 let var_name = format!("{}Val", arg.name);
1872 let go_val = json_to_go(v);
1873 setup_lines.push(format!("{var_name} := {go_val}"));
1874 parts.push(format!("&{var_name}"));
1875 }
1876 _ => {
1877 parts.push(json_to_go(v));
1878 }
1879 }
1880 }
1881 }
1882 }
1883
1884 (setup_lines, parts.join(", "))
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888fn render_assertion(
1889 out: &mut String,
1890 assertion: &Assertion,
1891 result_var: &str,
1892 import_alias: &str,
1893 field_resolver: &FieldResolver,
1894 optional_locals: &std::collections::HashMap<String, String>,
1895 result_is_simple: bool,
1896 result_is_array: bool,
1897 is_streaming: bool,
1898) {
1899 if !result_is_simple {
1902 if let Some(f) = &assertion.field {
1903 let embed_deref = format!("(*{result_var})");
1906 match f.as_str() {
1907 "chunks_have_content" => {
1908 let pred = format!(
1909 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1910 );
1911 match assertion.assertion_type.as_str() {
1912 "is_true" => {
1913 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1914 }
1915 "is_false" => {
1916 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1917 }
1918 _ => {
1919 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1920 }
1921 }
1922 return;
1923 }
1924 "chunks_have_embeddings" => {
1925 let pred = format!(
1926 "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 }}()"
1927 );
1928 match assertion.assertion_type.as_str() {
1929 "is_true" => {
1930 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1931 }
1932 "is_false" => {
1933 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1934 }
1935 _ => {
1936 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1937 }
1938 }
1939 return;
1940 }
1941 "embeddings" => {
1942 match assertion.assertion_type.as_str() {
1943 "count_equals" => {
1944 if let Some(val) = &assertion.value {
1945 if let Some(n) = val.as_u64() {
1946 let _ = writeln!(
1947 out,
1948 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1949 );
1950 }
1951 }
1952 }
1953 "count_min" => {
1954 if let Some(val) = &assertion.value {
1955 if let Some(n) = val.as_u64() {
1956 let _ = writeln!(
1957 out,
1958 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1959 );
1960 }
1961 }
1962 }
1963 "not_empty" => {
1964 let _ = writeln!(
1965 out,
1966 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1967 );
1968 }
1969 "is_empty" => {
1970 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1971 }
1972 _ => {
1973 let _ = writeln!(
1974 out,
1975 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1976 );
1977 }
1978 }
1979 return;
1980 }
1981 "embedding_dimensions" => {
1982 let expr = format!(
1983 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1984 );
1985 match assertion.assertion_type.as_str() {
1986 "equals" => {
1987 if let Some(val) = &assertion.value {
1988 if let Some(n) = val.as_u64() {
1989 let _ = writeln!(
1990 out,
1991 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1992 );
1993 }
1994 }
1995 }
1996 "greater_than" => {
1997 if let Some(val) = &assertion.value {
1998 if let Some(n) = val.as_u64() {
1999 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2000 }
2001 }
2002 }
2003 _ => {
2004 let _ = writeln!(
2005 out,
2006 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2007 );
2008 }
2009 }
2010 return;
2011 }
2012 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2013 let pred = match f.as_str() {
2014 "embeddings_valid" => {
2015 format!(
2016 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2017 )
2018 }
2019 "embeddings_finite" => {
2020 format!(
2021 "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 }}()"
2022 )
2023 }
2024 "embeddings_non_zero" => {
2025 format!(
2026 "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 }}()"
2027 )
2028 }
2029 "embeddings_normalized" => {
2030 format!(
2031 "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 }}()"
2032 )
2033 }
2034 _ => unreachable!(),
2035 };
2036 match assertion.assertion_type.as_str() {
2037 "is_true" => {
2038 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2039 }
2040 "is_false" => {
2041 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2042 }
2043 _ => {
2044 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2045 }
2046 }
2047 return;
2048 }
2049 "keywords" | "keywords_count" => {
2052 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2053 return;
2054 }
2055 _ => {}
2056 }
2057 }
2058 }
2059
2060 if !result_is_simple && is_streaming {
2067 if let Some(f) = &assertion.field {
2068 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2069 if let Some(expr) =
2070 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2071 {
2072 match assertion.assertion_type.as_str() {
2073 "count_min" => {
2074 if let Some(val) = &assertion.value {
2075 if let Some(n) = val.as_u64() {
2076 let _ = writeln!(
2077 out,
2078 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2079 );
2080 }
2081 }
2082 }
2083 "count_equals" => {
2084 if let Some(val) = &assertion.value {
2085 if let Some(n) = val.as_u64() {
2086 let _ = writeln!(
2087 out,
2088 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2089 );
2090 }
2091 }
2092 }
2093 "equals" => {
2094 if let Some(serde_json::Value::String(s)) = &assertion.value {
2095 let escaped = crate::escape::go_string_literal(s);
2096 let is_deep_path = f.contains('.') || f.contains('[');
2101 let safe_expr = if is_deep_path {
2102 format!(
2103 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2104 )
2105 } else {
2106 expr.clone()
2107 };
2108 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2109 } else if let Some(val) = &assertion.value {
2110 if let Some(n) = val.as_u64() {
2111 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2112 }
2113 }
2114 }
2115 "not_empty" => {
2116 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2117 }
2118 "is_empty" => {
2119 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2120 }
2121 "is_true" => {
2122 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2123 }
2124 "is_false" => {
2125 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2126 }
2127 "greater_than" => {
2128 if let Some(val) = &assertion.value {
2129 if let Some(n) = val.as_u64() {
2130 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2131 }
2132 }
2133 }
2134 "greater_than_or_equal" => {
2135 if let Some(val) = &assertion.value {
2136 if let Some(n) = val.as_u64() {
2137 let _ =
2138 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2139 }
2140 }
2141 }
2142 "contains" => {
2143 if let Some(serde_json::Value::String(s)) = &assertion.value {
2144 let escaped = crate::escape::go_string_literal(s);
2145 let _ =
2146 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2147 }
2148 }
2149 _ => {
2150 let _ = writeln!(
2151 out,
2152 "\t// streaming field '{f}': assertion type '{}' not rendered",
2153 assertion.assertion_type
2154 );
2155 }
2156 }
2157 }
2158 return;
2159 }
2160 }
2161 }
2162
2163 if !result_is_simple {
2166 if let Some(f) = &assertion.field {
2167 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2168 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2169 return;
2170 }
2171 }
2172 }
2173
2174 let field_expr = if result_is_simple {
2175 result_var.to_string()
2177 } else {
2178 match &assertion.field {
2179 Some(f) if !f.is_empty() => {
2180 if let Some(local_var) = optional_locals.get(f.as_str()) {
2182 local_var.clone()
2183 } else {
2184 field_resolver.accessor(f, "go", result_var)
2185 }
2186 }
2187 _ => result_var.to_string(),
2188 }
2189 };
2190
2191 let is_optional = assertion
2195 .field
2196 .as_ref()
2197 .map(|f| {
2198 let resolved = field_resolver.resolve(f);
2199 let check_path = resolved
2200 .strip_suffix(".length")
2201 .or_else(|| resolved.strip_suffix(".count"))
2202 .or_else(|| resolved.strip_suffix(".size"))
2203 .unwrap_or(resolved);
2204 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2205 })
2206 .unwrap_or(false);
2207
2208 let field_is_array_for_len = assertion
2212 .field
2213 .as_ref()
2214 .map(|f| {
2215 let resolved = field_resolver.resolve(f);
2216 let check_path = resolved
2217 .strip_suffix(".length")
2218 .or_else(|| resolved.strip_suffix(".count"))
2219 .or_else(|| resolved.strip_suffix(".size"))
2220 .unwrap_or(resolved);
2221 field_resolver.is_array(check_path)
2222 })
2223 .unwrap_or(false);
2224 let field_expr =
2225 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2226 let inner = &field_expr[4..field_expr.len() - 1];
2227 format!("len(*{inner})")
2228 } else {
2229 field_expr
2230 };
2231 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2233 Some(field_expr[5..field_expr.len() - 1].to_string())
2234 } else {
2235 None
2236 };
2237
2238 let field_is_slice = assertion
2242 .field
2243 .as_ref()
2244 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2245 .unwrap_or(false);
2246 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2247 format!("*{field_expr}")
2248 } else {
2249 field_expr.clone()
2250 };
2251
2252 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2257 let mut array_expr = field_expr[..idx].to_string();
2258 if let Some(stripped) = array_expr.strip_prefix("len(") {
2259 array_expr = stripped.to_string();
2260 }
2261 Some(array_expr)
2262 } else {
2263 None
2264 };
2265
2266 let mut assertion_buf = String::new();
2269 let out_ref = &mut assertion_buf;
2270
2271 match assertion.assertion_type.as_str() {
2272 "equals" => {
2273 if let Some(expected) = &assertion.value {
2274 let go_val = json_to_go(expected);
2275 if expected.is_string() {
2277 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2280 format!("strings.TrimSpace(string(*{field_expr}))")
2281 } else {
2282 format!("strings.TrimSpace(string({field_expr}))")
2283 };
2284 if is_optional && !field_expr.starts_with("len(") {
2285 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2286 } else {
2287 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2288 }
2289 } else if is_optional && !field_expr.starts_with("len(") {
2290 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2291 } else {
2292 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2293 }
2294 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2295 let _ = writeln!(out_ref, "\t}}");
2296 }
2297 }
2298 "contains" => {
2299 if let Some(expected) = &assertion.value {
2300 let go_val = json_to_go(expected);
2301 let resolved_field = assertion.field.as_deref().unwrap_or("");
2307 let resolved_name = field_resolver.resolve(resolved_field);
2308 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2309 let is_opt =
2310 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2311 let field_for_contains = if is_opt && field_is_array {
2312 format!("jsonString({field_expr})")
2314 } else if is_opt {
2315 format!("fmt.Sprint(*{field_expr})")
2316 } else if field_is_array {
2317 format!("jsonString({field_expr})")
2318 } else {
2319 format!("fmt.Sprint({field_expr})")
2320 };
2321 if is_opt {
2322 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2323 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2324 let _ = writeln!(
2325 out_ref,
2326 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2327 );
2328 let _ = writeln!(out_ref, "\t}}");
2329 let _ = writeln!(out_ref, "\t}}");
2330 } else {
2331 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2332 let _ = writeln!(
2333 out_ref,
2334 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2335 );
2336 let _ = writeln!(out_ref, "\t}}");
2337 }
2338 }
2339 }
2340 "contains_all" => {
2341 if let Some(values) = &assertion.values {
2342 let resolved_field = assertion.field.as_deref().unwrap_or("");
2343 let resolved_name = field_resolver.resolve(resolved_field);
2344 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2345 let is_opt =
2346 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2347 for val in values {
2348 let go_val = json_to_go(val);
2349 let field_for_contains = if is_opt && field_is_array {
2350 format!("jsonString({field_expr})")
2352 } else if is_opt {
2353 format!("fmt.Sprint(*{field_expr})")
2354 } else if field_is_array {
2355 format!("jsonString({field_expr})")
2356 } else {
2357 format!("fmt.Sprint({field_expr})")
2358 };
2359 if is_opt {
2360 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2361 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2362 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2363 let _ = writeln!(out_ref, "\t}}");
2364 let _ = writeln!(out_ref, "\t}}");
2365 } else {
2366 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2367 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2368 let _ = writeln!(out_ref, "\t}}");
2369 }
2370 }
2371 }
2372 }
2373 "not_contains" => {
2374 if let Some(expected) = &assertion.value {
2375 let go_val = json_to_go(expected);
2376 let resolved_field = assertion.field.as_deref().unwrap_or("");
2377 let resolved_name = field_resolver.resolve(resolved_field);
2378 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2379 let is_opt =
2380 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2381 let field_for_contains = if is_opt && field_is_array {
2382 format!("jsonString({field_expr})")
2384 } else if is_opt {
2385 format!("fmt.Sprint(*{field_expr})")
2386 } else if field_is_array {
2387 format!("jsonString({field_expr})")
2388 } else {
2389 format!("fmt.Sprint({field_expr})")
2390 };
2391 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2392 let _ = writeln!(
2393 out_ref,
2394 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2395 );
2396 let _ = writeln!(out_ref, "\t}}");
2397 }
2398 }
2399 "not_empty" => {
2400 let field_is_array = {
2403 let rf = assertion.field.as_deref().unwrap_or("");
2404 let rn = field_resolver.resolve(rf);
2405 field_resolver.is_array(rn)
2406 };
2407 if is_optional && !field_is_array {
2408 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2410 } else if is_optional && field_is_slice {
2411 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2413 } else if is_optional {
2414 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2416 } else if result_is_simple && result_is_array {
2417 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2419 } else {
2420 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2421 }
2422 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2423 let _ = writeln!(out_ref, "\t}}");
2424 }
2425 "is_empty" => {
2426 let field_is_array = {
2427 let rf = assertion.field.as_deref().unwrap_or("");
2428 let rn = field_resolver.resolve(rf);
2429 field_resolver.is_array(rn)
2430 };
2431 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2434 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2436 } else if is_optional && !field_is_array {
2437 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2439 } else if is_optional && field_is_slice {
2440 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2442 } else if is_optional {
2443 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2445 } else {
2446 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2447 }
2448 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2449 let _ = writeln!(out_ref, "\t}}");
2450 }
2451 "contains_any" => {
2452 if let Some(values) = &assertion.values {
2453 let resolved_field = assertion.field.as_deref().unwrap_or("");
2454 let resolved_name = field_resolver.resolve(resolved_field);
2455 let field_is_array = field_resolver.is_array(resolved_name);
2456 let is_opt =
2457 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2458 let field_for_contains = if is_opt && field_is_array {
2459 format!("jsonString({field_expr})")
2461 } else if is_opt {
2462 format!("fmt.Sprint(*{field_expr})")
2463 } else if field_is_array {
2464 format!("jsonString({field_expr})")
2465 } else {
2466 format!("fmt.Sprint({field_expr})")
2467 };
2468 let _ = writeln!(out_ref, "\t{{");
2469 let _ = writeln!(out_ref, "\t\tfound := false");
2470 for val in values {
2471 let go_val = json_to_go(val);
2472 let _ = writeln!(
2473 out_ref,
2474 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2475 );
2476 }
2477 let _ = writeln!(out_ref, "\t\tif !found {{");
2478 let _ = writeln!(
2479 out_ref,
2480 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2481 );
2482 let _ = writeln!(out_ref, "\t\t}}");
2483 let _ = writeln!(out_ref, "\t}}");
2484 }
2485 }
2486 "greater_than" => {
2487 if let Some(val) = &assertion.value {
2488 let go_val = json_to_go(val);
2489 if is_optional {
2493 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2494 if let Some(n) = val.as_u64() {
2495 let next = n + 1;
2496 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2497 } else {
2498 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2499 }
2500 let _ = writeln!(
2501 out_ref,
2502 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2503 );
2504 let _ = writeln!(out_ref, "\t\t}}");
2505 let _ = writeln!(out_ref, "\t}}");
2506 } else if let Some(n) = val.as_u64() {
2507 let next = n + 1;
2508 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2509 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2510 let _ = writeln!(out_ref, "\t}}");
2511 } else {
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 }
2518 "less_than" => {
2519 if let Some(val) = &assertion.value {
2520 let go_val = json_to_go(val);
2521 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2522 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2523 let _ = writeln!(out_ref, "\t}}");
2524 }
2525 }
2526 "greater_than_or_equal" => {
2527 if let Some(val) = &assertion.value {
2528 let go_val = json_to_go(val);
2529 if let Some(ref guard) = nil_guard_expr {
2530 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2531 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2532 let _ = writeln!(
2533 out_ref,
2534 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2535 );
2536 let _ = writeln!(out_ref, "\t\t}}");
2537 let _ = writeln!(out_ref, "\t}}");
2538 } else if is_optional && !field_expr.starts_with("len(") {
2539 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2541 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2542 let _ = writeln!(
2543 out_ref,
2544 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2545 );
2546 let _ = writeln!(out_ref, "\t\t}}");
2547 let _ = writeln!(out_ref, "\t}}");
2548 } else {
2549 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2550 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2551 let _ = writeln!(out_ref, "\t}}");
2552 }
2553 }
2554 }
2555 "less_than_or_equal" => {
2556 if let Some(val) = &assertion.value {
2557 let go_val = json_to_go(val);
2558 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2559 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2560 let _ = writeln!(out_ref, "\t}}");
2561 }
2562 }
2563 "starts_with" => {
2564 if let Some(expected) = &assertion.value {
2565 let go_val = json_to_go(expected);
2566 let field_for_prefix = if is_optional
2567 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2568 {
2569 format!("string(*{field_expr})")
2570 } else {
2571 format!("string({field_expr})")
2572 };
2573 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2574 let _ = writeln!(
2575 out_ref,
2576 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2577 );
2578 let _ = writeln!(out_ref, "\t}}");
2579 }
2580 }
2581 "count_min" => {
2582 if let Some(val) = &assertion.value {
2583 if let Some(n) = val.as_u64() {
2584 if is_optional {
2585 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2586 let len_expr = if field_is_slice {
2588 format!("len({field_expr})")
2589 } else {
2590 format!("len(*{field_expr})")
2591 };
2592 let _ = writeln!(
2593 out_ref,
2594 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2595 );
2596 let _ = writeln!(out_ref, "\t}}");
2597 } else {
2598 let _ = writeln!(
2599 out_ref,
2600 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2601 );
2602 }
2603 }
2604 }
2605 }
2606 "count_equals" => {
2607 if let Some(val) = &assertion.value {
2608 if let Some(n) = val.as_u64() {
2609 if is_optional {
2610 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2611 let len_expr = if field_is_slice {
2613 format!("len({field_expr})")
2614 } else {
2615 format!("len(*{field_expr})")
2616 };
2617 let _ = writeln!(
2618 out_ref,
2619 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2620 );
2621 let _ = writeln!(out_ref, "\t}}");
2622 } else {
2623 let _ = writeln!(
2624 out_ref,
2625 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2626 );
2627 }
2628 }
2629 }
2630 }
2631 "is_true" => {
2632 if is_optional {
2633 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2634 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2635 let _ = writeln!(out_ref, "\t}}");
2636 } else {
2637 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2638 }
2639 }
2640 "is_false" => {
2641 if is_optional {
2642 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2643 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2644 let _ = writeln!(out_ref, "\t}}");
2645 } else {
2646 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2647 }
2648 }
2649 "method_result" => {
2650 if let Some(method_name) = &assertion.method {
2651 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2652 let check = assertion.check.as_deref().unwrap_or("is_true");
2653 let deref_expr = if info.is_pointer {
2656 format!("*{}", info.call_expr)
2657 } else {
2658 info.call_expr.clone()
2659 };
2660 match check {
2661 "equals" => {
2662 if let Some(val) = &assertion.value {
2663 if val.is_boolean() {
2664 if val.as_bool() == Some(true) {
2665 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2666 } else {
2667 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2668 }
2669 } else {
2670 let go_val = if let Some(cast) = info.value_cast {
2674 if val.is_number() {
2675 format!("{cast}({})", json_to_go(val))
2676 } else {
2677 json_to_go(val)
2678 }
2679 } else {
2680 json_to_go(val)
2681 };
2682 let _ = writeln!(
2683 out_ref,
2684 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2685 );
2686 }
2687 }
2688 }
2689 "is_true" => {
2690 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2691 }
2692 "is_false" => {
2693 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2694 }
2695 "greater_than_or_equal" => {
2696 if let Some(val) = &assertion.value {
2697 let n = val.as_u64().unwrap_or(0);
2698 let cast = info.value_cast.unwrap_or("uint");
2700 let _ = writeln!(
2701 out_ref,
2702 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2703 );
2704 }
2705 }
2706 "count_min" => {
2707 if let Some(val) = &assertion.value {
2708 let n = val.as_u64().unwrap_or(0);
2709 let _ = writeln!(
2710 out_ref,
2711 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2712 );
2713 }
2714 }
2715 "contains" => {
2716 if let Some(val) = &assertion.value {
2717 let go_val = json_to_go(val);
2718 let _ = writeln!(
2719 out_ref,
2720 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2721 );
2722 }
2723 }
2724 "is_error" => {
2725 let _ = writeln!(out_ref, "\t{{");
2726 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2727 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2728 let _ = writeln!(out_ref, "\t}}");
2729 }
2730 other_check => {
2731 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2732 }
2733 }
2734 } else {
2735 panic!("Go e2e generator: method_result assertion missing 'method' field");
2736 }
2737 }
2738 "min_length" => {
2739 if let Some(val) = &assertion.value {
2740 if let Some(n) = val.as_u64() {
2741 if is_optional {
2742 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2743 let _ = writeln!(
2744 out_ref,
2745 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2746 );
2747 let _ = writeln!(out_ref, "\t}}");
2748 } else if field_expr.starts_with("len(") {
2749 let _ = writeln!(
2750 out_ref,
2751 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2752 );
2753 } else {
2754 let _ = writeln!(
2755 out_ref,
2756 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2757 );
2758 }
2759 }
2760 }
2761 }
2762 "max_length" => {
2763 if let Some(val) = &assertion.value {
2764 if let Some(n) = val.as_u64() {
2765 if is_optional {
2766 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2767 let _ = writeln!(
2768 out_ref,
2769 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2770 );
2771 let _ = writeln!(out_ref, "\t}}");
2772 } else if field_expr.starts_with("len(") {
2773 let _ = writeln!(
2774 out_ref,
2775 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2776 );
2777 } else {
2778 let _ = writeln!(
2779 out_ref,
2780 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2781 );
2782 }
2783 }
2784 }
2785 }
2786 "ends_with" => {
2787 if let Some(expected) = &assertion.value {
2788 let go_val = json_to_go(expected);
2789 let field_for_suffix = if is_optional
2790 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2791 {
2792 format!("string(*{field_expr})")
2793 } else {
2794 format!("string({field_expr})")
2795 };
2796 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2797 let _ = writeln!(
2798 out_ref,
2799 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2800 );
2801 let _ = writeln!(out_ref, "\t}}");
2802 }
2803 }
2804 "matches_regex" => {
2805 if let Some(expected) = &assertion.value {
2806 let go_val = json_to_go(expected);
2807 let field_for_regex = if is_optional
2808 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2809 {
2810 format!("*{field_expr}")
2811 } else {
2812 field_expr.clone()
2813 };
2814 let _ = writeln!(
2815 out_ref,
2816 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2817 );
2818 }
2819 }
2820 "not_error" => {
2821 }
2823 "error" => {
2824 }
2826 other => {
2827 panic!("Go e2e generator: unsupported assertion type: {other}");
2828 }
2829 }
2830
2831 if let Some(ref arr) = array_guard {
2834 if !assertion_buf.is_empty() {
2835 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2836 for line in assertion_buf.lines() {
2838 let _ = writeln!(out, "\t{line}");
2839 }
2840 let _ = writeln!(out, "\t}}");
2841 }
2842 } else {
2843 out.push_str(&assertion_buf);
2844 }
2845}
2846
2847struct GoMethodCallInfo {
2849 call_expr: String,
2851 is_pointer: bool,
2853 value_cast: Option<&'static str>,
2856}
2857
2858fn build_go_method_call(
2873 result_var: &str,
2874 method_name: &str,
2875 args: Option<&serde_json::Value>,
2876 import_alias: &str,
2877) -> GoMethodCallInfo {
2878 match method_name {
2879 "root_node_type" => GoMethodCallInfo {
2880 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2881 is_pointer: false,
2882 value_cast: None,
2883 },
2884 "named_children_count" => GoMethodCallInfo {
2885 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2886 is_pointer: false,
2887 value_cast: Some("uint"),
2888 },
2889 "has_error_nodes" => GoMethodCallInfo {
2890 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2891 is_pointer: true,
2892 value_cast: None,
2893 },
2894 "error_count" | "tree_error_count" => GoMethodCallInfo {
2895 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2896 is_pointer: true,
2897 value_cast: Some("uint"),
2898 },
2899 "tree_to_sexp" => GoMethodCallInfo {
2900 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2901 is_pointer: true,
2902 value_cast: None,
2903 },
2904 "contains_node_type" => {
2905 let node_type = args
2906 .and_then(|a| a.get("node_type"))
2907 .and_then(|v| v.as_str())
2908 .unwrap_or("");
2909 GoMethodCallInfo {
2910 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2911 is_pointer: true,
2912 value_cast: None,
2913 }
2914 }
2915 "find_nodes_by_type" => {
2916 let node_type = args
2917 .and_then(|a| a.get("node_type"))
2918 .and_then(|v| v.as_str())
2919 .unwrap_or("");
2920 GoMethodCallInfo {
2921 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2922 is_pointer: true,
2923 value_cast: None,
2924 }
2925 }
2926 "run_query" => {
2927 let query_source = args
2928 .and_then(|a| a.get("query_source"))
2929 .and_then(|v| v.as_str())
2930 .unwrap_or("");
2931 let language = args
2932 .and_then(|a| a.get("language"))
2933 .and_then(|v| v.as_str())
2934 .unwrap_or("");
2935 let query_lit = go_string_literal(query_source);
2936 let lang_lit = go_string_literal(language);
2937 GoMethodCallInfo {
2939 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2940 is_pointer: false,
2941 value_cast: None,
2942 }
2943 }
2944 other => {
2945 let method_pascal = other.to_upper_camel_case();
2946 GoMethodCallInfo {
2947 call_expr: format!("{result_var}.{method_pascal}()"),
2948 is_pointer: false,
2949 value_cast: None,
2950 }
2951 }
2952 }
2953}
2954
2955fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2965 match value {
2966 serde_json::Value::Object(map) => {
2967 let new_map: serde_json::Map<String, serde_json::Value> = map
2968 .into_iter()
2969 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2970 .collect();
2971 serde_json::Value::Object(new_map)
2972 }
2973 serde_json::Value::Array(arr) => {
2974 if is_byte_array(&arr) {
2977 let bytes: Vec<u8> = arr
2978 .iter()
2979 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2980 .collect();
2981 let encoded = base64_encode(&bytes);
2983 serde_json::Value::String(encoded)
2984 } else {
2985 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2986 }
2987 }
2988 serde_json::Value::String(s) => {
2989 serde_json::Value::String(pascal_to_snake_case(&s))
2992 }
2993 other => other,
2994 }
2995}
2996
2997fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2999 if arr.is_empty() {
3000 return false;
3001 }
3002 arr.iter().all(|v| {
3003 if let serde_json::Value::Number(n) = v {
3004 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3005 } else {
3006 false
3007 }
3008 })
3009}
3010
3011fn base64_encode(bytes: &[u8]) -> String {
3014 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3015 let mut result = String::new();
3016 let mut i = 0;
3017
3018 while i + 2 < bytes.len() {
3019 let b1 = bytes[i];
3020 let b2 = bytes[i + 1];
3021 let b3 = bytes[i + 2];
3022
3023 result.push(TABLE[(b1 >> 2) as usize] as char);
3024 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3025 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3026 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3027
3028 i += 3;
3029 }
3030
3031 if i < bytes.len() {
3033 let b1 = bytes[i];
3034 result.push(TABLE[(b1 >> 2) as usize] as char);
3035
3036 if i + 1 < bytes.len() {
3037 let b2 = bytes[i + 1];
3038 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3039 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3040 result.push('=');
3041 } else {
3042 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3043 result.push_str("==");
3044 }
3045 }
3046
3047 result
3048}
3049
3050fn camel_to_snake_case(s: &str) -> String {
3052 let mut result = String::new();
3053 let mut prev_upper = false;
3054 for (i, c) in s.char_indices() {
3055 if c.is_uppercase() {
3056 if i > 0 && !prev_upper {
3057 result.push('_');
3058 }
3059 result.push(c.to_lowercase().next().unwrap_or(c));
3060 prev_upper = true;
3061 } else {
3062 if prev_upper && i > 1 {
3063 }
3067 result.push(c);
3068 prev_upper = false;
3069 }
3070 }
3071 result
3072}
3073
3074fn pascal_to_snake_case(s: &str) -> String {
3079 let first_char = s.chars().next();
3081 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3082 return s.to_string();
3083 }
3084 camel_to_snake_case(s)
3085}
3086
3087fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3091 let elem = element_type.unwrap_or("String").trim();
3092 let go_elem = rust_type_to_go(elem, import_alias);
3093 format!("[]{go_elem}")
3094}
3095
3096fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3099 let trimmed = rust.trim();
3100 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3101 return format!("[]{}", rust_type_to_go(inner, import_alias));
3102 }
3103 match trimmed {
3104 "String" | "&str" | "str" => "string".to_string(),
3105 "bool" => "bool".to_string(),
3106 "f32" => "float32".to_string(),
3107 "f64" => "float64".to_string(),
3108 "i8" => "int8".to_string(),
3109 "i16" => "int16".to_string(),
3110 "i32" => "int32".to_string(),
3111 "i64" | "isize" => "int64".to_string(),
3112 "u8" => "uint8".to_string(),
3113 "u16" => "uint16".to_string(),
3114 "u32" => "uint32".to_string(),
3115 "u64" | "usize" => "uint64".to_string(),
3116 _ => format!("{import_alias}.{trimmed}"),
3117 }
3118}
3119
3120fn json_to_go(value: &serde_json::Value) -> String {
3121 match value {
3122 serde_json::Value::String(s) => go_string_literal(s),
3123 serde_json::Value::Bool(b) => b.to_string(),
3124 serde_json::Value::Number(n) => n.to_string(),
3125 serde_json::Value::Null => "nil".to_string(),
3126 other => go_string_literal(&other.to_string()),
3128 }
3129}
3130
3131fn visitor_struct_name(fixture_id: &str) -> String {
3140 use heck::ToUpperCamelCase;
3141 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3143}
3144
3145fn emit_go_visitor_struct(
3150 out: &mut String,
3151 struct_name: &str,
3152 visitor_spec: &crate::fixture::VisitorSpec,
3153 import_alias: &str,
3154) {
3155 let _ = writeln!(out, "type {struct_name} struct{{");
3156 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3157 let _ = writeln!(out, "}}");
3158 for (method_name, action) in &visitor_spec.callbacks {
3159 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3160 }
3161}
3162
3163fn emit_go_visitor_method(
3165 out: &mut String,
3166 struct_name: &str,
3167 method_name: &str,
3168 action: &CallbackAction,
3169 import_alias: &str,
3170) {
3171 let camel_method = method_to_camel(method_name);
3172 let params = match method_name {
3175 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3176 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3177 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3178 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3179 "visit_code_inline"
3180 | "visit_strong"
3181 | "visit_emphasis"
3182 | "visit_strikethrough"
3183 | "visit_underline"
3184 | "visit_subscript"
3185 | "visit_superscript"
3186 | "visit_mark"
3187 | "visit_button"
3188 | "visit_summary"
3189 | "visit_figcaption"
3190 | "visit_definition_term"
3191 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3192 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3193 "visit_list_item" => {
3194 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3195 }
3196 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3197 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3198 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3199 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3200 "visit_input" => {
3201 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3202 }
3203 "visit_audio" | "visit_video" | "visit_iframe" => {
3204 format!("_ {import_alias}.NodeContext, src *string")
3205 }
3206 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3207 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3208 format!("_ {import_alias}.NodeContext, output string")
3209 }
3210 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3211 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3212 _ => format!("_ {import_alias}.NodeContext"),
3213 };
3214
3215 let _ = writeln!(
3216 out,
3217 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3218 );
3219 match action {
3220 CallbackAction::Skip => {
3221 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3222 }
3223 CallbackAction::Continue => {
3224 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3225 }
3226 CallbackAction::PreserveHtml => {
3227 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3228 }
3229 CallbackAction::Custom { output } => {
3230 let escaped = go_string_literal(output);
3231 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3232 }
3233 CallbackAction::CustomTemplate { template, .. } => {
3234 let ptr_params = go_visitor_ptr_params(method_name);
3241 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3242 let escaped_fmt = go_string_literal(&fmt_str);
3243 if fmt_args.is_empty() {
3244 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3245 } else {
3246 let args_str = fmt_args.join(", ");
3247 let _ = writeln!(
3248 out,
3249 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3250 );
3251 }
3252 }
3253 }
3254 let _ = writeln!(out, "}}");
3255}
3256
3257fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3260 match method_name {
3261 "visit_link" => ["title"].into(),
3262 "visit_image" => ["title"].into(),
3263 "visit_heading" => ["id"].into(),
3264 "visit_code_block" => ["lang"].into(),
3265 "visit_form" => ["action", "method"].into(),
3266 "visit_input" => ["name", "value"].into(),
3267 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3268 _ => std::collections::HashSet::new(),
3269 }
3270}
3271
3272fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3284 let mut fmt_str = String::new();
3285 let mut args: Vec<String> = Vec::new();
3286 let mut chars = template.chars().peekable();
3287 while let Some(c) = chars.next() {
3288 if c == '{' {
3289 let mut name = String::new();
3291 for inner in chars.by_ref() {
3292 if inner == '}' {
3293 break;
3294 }
3295 name.push(inner);
3296 }
3297 fmt_str.push_str("%s");
3298 let go_name = go_param_name(&name);
3300 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3302 format!("*{go_name}")
3303 } else {
3304 go_name
3305 };
3306 args.push(arg_expr);
3307 } else {
3308 fmt_str.push(c);
3309 }
3310 }
3311 (fmt_str, args)
3312}
3313
3314fn method_to_camel(snake: &str) -> String {
3316 use heck::ToUpperCamelCase;
3317 snake.to_upper_camel_case()
3318}
3319
3320#[cfg(test)]
3321mod tests {
3322 use super::*;
3323 use crate::config::{CallConfig, E2eConfig};
3324 use crate::field_access::FieldResolver;
3325 use crate::fixture::{Assertion, Fixture};
3326
3327 fn make_fixture(id: &str) -> Fixture {
3328 Fixture {
3329 id: id.to_string(),
3330 category: None,
3331 description: "test fixture".to_string(),
3332 tags: vec![],
3333 skip: None,
3334 env: None,
3335 call: None,
3336 input: serde_json::Value::Null,
3337 mock_response: Some(crate::fixture::MockResponse {
3338 status: 200,
3339 body: Some(serde_json::Value::Null),
3340 stream_chunks: None,
3341 headers: std::collections::HashMap::new(),
3342 }),
3343 source: String::new(),
3344 http: None,
3345 assertions: vec![Assertion {
3346 assertion_type: "not_error".to_string(),
3347 ..Default::default()
3348 }],
3349 visitor: None,
3350 }
3351 }
3352
3353 #[test]
3357 fn test_go_method_name_uses_go_casing() {
3358 let e2e_config = E2eConfig {
3359 call: CallConfig {
3360 function: "clean_extracted_text".to_string(),
3361 module: "github.com/example/mylib".to_string(),
3362 result_var: "result".to_string(),
3363 returns_result: true,
3364 ..CallConfig::default()
3365 },
3366 ..E2eConfig::default()
3367 };
3368
3369 let fixture = make_fixture("basic_text");
3370 let resolver = FieldResolver::new(
3371 &std::collections::HashMap::new(),
3372 &std::collections::HashSet::new(),
3373 &std::collections::HashSet::new(),
3374 &std::collections::HashSet::new(),
3375 &std::collections::HashSet::new(),
3376 );
3377 let mut out = String::new();
3378 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3379
3380 assert!(
3381 out.contains("kreuzberg.CleanExtractedText("),
3382 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3383 );
3384 assert!(
3385 !out.contains("kreuzberg.clean_extracted_text("),
3386 "must not emit raw snake_case method name, got:\n{out}"
3387 );
3388 }
3389
3390 #[test]
3391 fn test_streaming_fixture_emits_collect_snippet() {
3392 let streaming_fixture_json = r#"{
3394 "id": "basic_stream",
3395 "description": "basic streaming test",
3396 "call": "chat_stream",
3397 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3398 "mock_response": {
3399 "status": 200,
3400 "stream_chunks": [{"delta": "hello"}]
3401 },
3402 "assertions": [
3403 {"type": "count_min", "field": "chunks", "value": 1}
3404 ]
3405 }"#;
3406 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3407 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3408
3409 let e2e_config = E2eConfig {
3410 call: CallConfig {
3411 function: "chat_stream".to_string(),
3412 module: "github.com/example/mylib".to_string(),
3413 result_var: "result".to_string(),
3414 returns_result: true,
3415 r#async: true,
3416 ..CallConfig::default()
3417 },
3418 ..E2eConfig::default()
3419 };
3420
3421 let resolver = FieldResolver::new(
3422 &std::collections::HashMap::new(),
3423 &std::collections::HashSet::new(),
3424 &std::collections::HashSet::new(),
3425 &std::collections::HashSet::new(),
3426 &std::collections::HashSet::new(),
3427 );
3428
3429 let mut out = String::new();
3430 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3431
3432 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3433 assert!(
3434 out.contains("for chunk := range stream"),
3435 "should emit collect loop, got:\n{out}"
3436 );
3437 }
3438
3439 #[test]
3440 fn test_streaming_with_client_factory_and_json_arg() {
3441 use alef_core::config::e2e::{ArgMapping, CallOverride};
3445 let streaming_fixture_json = r#"{
3446 "id": "basic_stream_client",
3447 "description": "basic streaming test with client",
3448 "call": "chat_stream",
3449 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3450 "mock_response": {
3451 "status": 200,
3452 "stream_chunks": [{"delta": "hello"}]
3453 },
3454 "assertions": [
3455 {"type": "count_min", "field": "chunks", "value": 1}
3456 ]
3457 }"#;
3458 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3459 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3460
3461 let go_override = CallOverride {
3462 client_factory: Some("CreateClient".to_string()),
3463 ..Default::default()
3464 };
3465
3466 let mut call_overrides = std::collections::HashMap::new();
3467 call_overrides.insert("go".to_string(), go_override);
3468
3469 let e2e_config = E2eConfig {
3470 call: CallConfig {
3471 function: "chat_stream".to_string(),
3472 module: "github.com/example/mylib".to_string(),
3473 result_var: "result".to_string(),
3474 returns_result: false, r#async: true,
3476 args: vec![ArgMapping {
3477 name: "request".to_string(),
3478 field: "input".to_string(),
3479 arg_type: "json_object".to_string(),
3480 optional: false,
3481 owned: true,
3482 element_type: None,
3483 go_type: None,
3484 }],
3485 overrides: call_overrides,
3486 ..CallConfig::default()
3487 },
3488 ..E2eConfig::default()
3489 };
3490
3491 let resolver = FieldResolver::new(
3492 &std::collections::HashMap::new(),
3493 &std::collections::HashSet::new(),
3494 &std::collections::HashSet::new(),
3495 &std::collections::HashSet::new(),
3496 &std::collections::HashSet::new(),
3497 );
3498
3499 let mut out = String::new();
3500 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3501
3502 eprintln!("generated:\n{out}");
3503 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3504 assert!(
3505 out.contains("for chunk := range stream"),
3506 "should emit collect loop, got:\n{out}"
3507 );
3508 }
3509
3510 #[test]
3514 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3515 let e2e_config = E2eConfig {
3516 call: CallConfig {
3517 function: "transcribe".to_string(),
3518 module: "github.com/example/mylib".to_string(),
3519 result_var: "result".to_string(),
3520 returns_result: true,
3521 ..CallConfig::default()
3522 },
3523 ..E2eConfig::default()
3524 };
3525
3526 let fixture = Fixture {
3527 id: "edge_transcribe_with_timestamps".to_string(),
3528 category: None,
3529 description: "Transcription with timestamp segments".to_string(),
3530 tags: vec![],
3531 skip: None,
3532 env: None,
3533 call: None,
3534 input: serde_json::Value::Null,
3535 mock_response: Some(crate::fixture::MockResponse {
3536 status: 200,
3537 body: Some(serde_json::Value::Null),
3538 stream_chunks: None,
3539 headers: std::collections::HashMap::new(),
3540 }),
3541 source: String::new(),
3542 http: None,
3543 assertions: vec![
3544 Assertion {
3545 assertion_type: "not_error".to_string(),
3546 ..Default::default()
3547 },
3548 Assertion {
3549 assertion_type: "equals".to_string(),
3550 field: Some("segments[0].id".to_string()),
3551 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3552 ..Default::default()
3553 },
3554 ],
3555 visitor: None,
3556 };
3557
3558 let mut optional_fields = std::collections::HashSet::new();
3559 optional_fields.insert("segments".to_string());
3561
3562 let mut array_fields = std::collections::HashSet::new();
3563 array_fields.insert("segments".to_string());
3564
3565 let resolver = FieldResolver::new(
3566 &std::collections::HashMap::new(),
3567 &optional_fields,
3568 &array_fields,
3569 &std::collections::HashSet::new(),
3570 &std::collections::HashSet::new(),
3571 );
3572
3573 let mut out = String::new();
3574 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3575
3576 eprintln!("generated:\n{out}");
3577
3578 assert!(
3580 out.contains("result.Segments != nil"),
3581 "guard must be on Segments (the slice), not an element; got:\n{out}"
3582 );
3583 assert!(
3585 !out.contains("result.Segments[0] != nil"),
3586 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3587 );
3588 }
3589}