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 (if it exists) so that fixture"
276 );
277 let _ = writeln!(
278 out,
279 "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
280 );
281 let _ = writeln!(
282 out,
283 "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
284 );
285 let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
286 let _ = writeln!(
287 out,
288 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
289 );
290 let _ = writeln!(
291 out,
292 "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
293 );
294 let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
295 let _ = writeln!(out, "\t\t\tpanic(err)");
296 let _ = writeln!(out, "\t\t}}");
297 let _ = writeln!(out, "\t}}");
298 let _ = writeln!(out);
299 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
300 let _ = writeln!(
301 out,
302 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
303 );
304 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
305 let _ = writeln!(
306 out,
307 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
308 );
309 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
310 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
311 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
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\t// Keep a writable pipe to the mock-server's stdin so the");
316 let _ = writeln!(
317 out,
318 "\t\t// server does not see EOF and exit immediately. The mock-server"
319 );
320 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
321 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
322 let _ = writeln!(out, "\t\tif err != nil {{");
323 let _ = writeln!(out, "\t\t\tpanic(err)");
324 let _ = writeln!(out, "\t\t}}");
325 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
326 let _ = writeln!(out, "\t\t\tpanic(err)");
327 let _ = writeln!(out, "\t\t}}");
328 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
329 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
330 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
331 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
332 let _ = writeln!(
333 out,
334 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
335 );
336 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
337 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
338 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
339 let _ = writeln!(
340 out,
341 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
342 );
343 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
344 let _ = writeln!(
345 out,
346 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
347 );
348 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
349 let _ = writeln!(
350 out,
351 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
352 );
353 let _ = writeln!(out, "\t\t\t\t\t}}");
354 let _ = writeln!(out, "\t\t\t\t}}");
355 let _ = writeln!(out, "\t\t\t\tbreak");
356 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
357 let _ = writeln!(out, "\t\t\t\tbreak");
358 let _ = writeln!(out, "\t\t\t}}");
359 let _ = writeln!(out, "\t\t}}");
360 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
361 let _ = writeln!(out, "\t\tcode := m.Run()");
362 let _ = writeln!(out, "\t\t_ = stdin.Close()");
363 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
364 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
365 let _ = writeln!(out, "\t\tos.Exit(code)");
366 let _ = writeln!(out, "\t}} else {{");
367 let _ = writeln!(out, "\t\tcode := m.Run()");
368 let _ = writeln!(out, "\t\tos.Exit(code)");
369 let _ = writeln!(out, "\t}}");
370 let _ = writeln!(out, "}}");
371 out
372}
373
374fn render_helpers_test_go() -> String {
377 let mut out = String::new();
378 let _ = writeln!(out, "package e2e_test");
379 let _ = writeln!(out);
380 let _ = writeln!(out, "import \"encoding/json\"");
381 let _ = writeln!(out);
382 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
383 let _ = writeln!(
384 out,
385 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
386 );
387 let _ = writeln!(out, "func jsonString(value any) string {{");
388 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
389 let _ = writeln!(out, "\tif err != nil {{");
390 let _ = writeln!(out, "\t\treturn \"\"");
391 let _ = writeln!(out, "\t}}");
392 let _ = writeln!(out, "\treturn string(encoded)");
393 let _ = writeln!(out, "}}");
394 out
395}
396
397fn render_test_file(
398 category: &str,
399 fixtures: &[&Fixture],
400 go_module_path: &str,
401 import_alias: &str,
402 field_resolver: &FieldResolver,
403 e2e_config: &crate::config::E2eConfig,
404) -> String {
405 let mut out = String::new();
406 let emits_executable_test =
407 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
408
409 out.push_str(&hash::header(CommentStyle::DoubleSlash));
411 let _ = writeln!(out);
412
413 let needs_pkg = fixtures
422 .iter()
423 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
424
425 let needs_os = fixtures.iter().any(|f| {
428 if f.is_http_test() {
429 return true;
430 }
431 if !emits_executable_test(f) {
432 return false;
433 }
434 let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
435 let go_override = call_config
436 .overrides
437 .get("go")
438 .or_else(|| e2e_config.call.overrides.get("go"));
439 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
440 return true;
441 }
442 let call_args = &call_config.args;
443 if call_args.iter().any(|a| a.arg_type == "mock_url") {
446 return true;
447 }
448 call_args.iter().any(|a| {
449 if a.arg_type != "bytes" {
450 return false;
451 }
452 let mut current = &f.input;
455 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
456 for segment in path.split('.') {
457 match current.get(segment) {
458 Some(next) => current = next,
459 None => return false,
460 }
461 }
462 current.is_string()
463 })
464 });
465
466 let needs_filepath = false;
469
470 let _needs_json_stringify = fixtures.iter().any(|f| {
471 emits_executable_test(f)
472 && f.assertions.iter().any(|a| {
473 matches!(
474 a.assertion_type.as_str(),
475 "contains" | "contains_all" | "contains_any" | "not_contains"
476 ) && {
477 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
480 e2e_config
482 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
483 .result_is_array
484 } else {
485 let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
487 field_resolver.is_array(resolved_name)
488 }
489 }
490 })
491 });
492
493 let needs_json = fixtures.iter().any(|f| {
497 if let Some(http) = &f.http {
500 let body_needs_json = http
501 .expected_response
502 .body
503 .as_ref()
504 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
505 let partial_needs_json = http.expected_response.body_partial.is_some();
506 let ve_needs_json = http
507 .expected_response
508 .validation_errors
509 .as_ref()
510 .is_some_and(|v| !v.is_empty());
511 if body_needs_json || partial_needs_json || ve_needs_json {
512 return true;
513 }
514 }
515 if !emits_executable_test(f) {
516 return false;
517 }
518
519 let call = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
520 let call_args = &call.args;
521 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
523 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
524 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
525 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
526 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
527 })
528 };
529 let go_override = call.overrides.get("go");
531 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
532 e2e_config
533 .call
534 .overrides
535 .get("go")
536 .and_then(|o| o.options_type.as_deref())
537 });
538 let has_json_obj = call_args.iter().any(|a| {
539 if a.arg_type != "json_object" {
540 return false;
541 }
542 let v = if a.field == "input" {
543 &f.input
544 } else {
545 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
546 f.input.get(field).unwrap_or(&serde_json::Value::Null)
547 };
548 if v.is_array() {
549 return true;
550 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
552 });
553 has_handle || has_json_obj
554 });
555
556 let needs_base64 = false;
561
562 let needs_fmt = fixtures.iter().any(|f| {
567 f.visitor.as_ref().is_some_and(|v| {
568 v.callbacks.values().any(|action| {
569 if let CallbackAction::CustomTemplate { template, .. } = action {
570 template.contains('{')
571 } else {
572 false
573 }
574 })
575 }) || (emits_executable_test(f)
576 && f.assertions.iter().any(|a| {
577 matches!(
578 a.assertion_type.as_str(),
579 "contains" | "contains_all" | "contains_any" | "not_contains"
580 ) && {
581 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
586 !e2e_config
588 .resolve_call_for_fixture(f.call.as_deref(), &f.input)
589 .result_is_array
590 } else {
591 let field = a.field.as_deref().unwrap_or("");
595 let resolved_name = field_resolver.resolve(field);
596 !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
597 }
598 }
599 }))
600 });
601
602 let needs_strings = fixtures.iter().any(|f| {
605 if !emits_executable_test(f) {
606 return false;
607 }
608 f.assertions.iter().any(|a| {
609 let type_needs_strings = if a.assertion_type == "equals" {
610 a.value.as_ref().is_some_and(|v| v.is_string())
612 } else {
613 matches!(
614 a.assertion_type.as_str(),
615 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
616 )
617 };
618 let field_valid = a
619 .field
620 .as_ref()
621 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
622 .unwrap_or(true);
623 type_needs_strings && field_valid
624 })
625 });
626
627 let needs_assert = fixtures.iter().any(|f| {
629 if !emits_executable_test(f) {
630 return false;
631 }
632 if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
636 return true;
637 }
638 let is_streaming_fixture = f.is_streaming_mock();
643 f.assertions.iter().any(|a| {
644 let field_is_streaming_virtual = a
645 .field
646 .as_deref()
647 .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
648 let field_valid = a
649 .field
650 .as_ref()
651 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
652 .unwrap_or(true)
653 || (is_streaming_fixture && field_is_streaming_virtual);
654 let synthetic_field_needs_assert = match a.field.as_deref() {
655 Some("chunks_have_content" | "chunks_have_embeddings") => {
656 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
657 }
658 Some("embeddings") => {
659 matches!(
660 a.assertion_type.as_str(),
661 "count_equals" | "count_min" | "not_empty" | "is_empty"
662 )
663 }
664 _ => false,
665 };
666 let type_needs_assert = matches!(
667 a.assertion_type.as_str(),
668 "count_equals"
669 | "count_min"
670 | "count_max"
671 | "is_true"
672 | "is_false"
673 | "method_result"
674 | "min_length"
675 | "max_length"
676 | "matches_regex"
677 );
678 synthetic_field_needs_assert || type_needs_assert && field_valid
679 })
680 });
681
682 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
684 let needs_http = has_http_fixtures;
685 let needs_io = has_http_fixtures;
687
688 let needs_reflect = fixtures.iter().any(|f| {
691 if let Some(http) = &f.http {
692 let body_needs_reflect = http
693 .expected_response
694 .body
695 .as_ref()
696 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
697 let partial_needs_reflect = http.expected_response.body_partial.is_some();
698 body_needs_reflect || partial_needs_reflect
699 } else {
700 false
701 }
702 });
703
704 let _ = writeln!(out, "// E2e tests for category: {category}");
705 let _ = writeln!(out, "package e2e_test");
706 let _ = writeln!(out);
707 let _ = writeln!(out, "import (");
708 if needs_base64 {
709 let _ = writeln!(out, "\t\"encoding/base64\"");
710 }
711 if needs_json || needs_reflect {
712 let _ = writeln!(out, "\t\"encoding/json\"");
713 }
714 if needs_fmt {
715 let _ = writeln!(out, "\t\"fmt\"");
716 }
717 if needs_io {
718 let _ = writeln!(out, "\t\"io\"");
719 }
720 if needs_http {
721 let _ = writeln!(out, "\t\"net/http\"");
722 }
723 if needs_os {
724 let _ = writeln!(out, "\t\"os\"");
725 }
726 let _ = needs_filepath; if needs_reflect {
728 let _ = writeln!(out, "\t\"reflect\"");
729 }
730 if needs_strings {
731 let _ = writeln!(out, "\t\"strings\"");
732 }
733 let _ = writeln!(out, "\t\"testing\"");
734 if needs_assert {
735 let _ = writeln!(out);
736 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
737 }
738 if needs_pkg {
739 let _ = writeln!(out);
740 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
741 }
742 let _ = writeln!(out, ")");
743 let _ = writeln!(out);
744
745 for fixture in fixtures.iter() {
747 if let Some(visitor_spec) = &fixture.visitor {
748 let struct_name = visitor_struct_name(&fixture.id);
749 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
750 let _ = writeln!(out);
751 }
752 }
753
754 for (i, fixture) in fixtures.iter().enumerate() {
755 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
756 if i + 1 < fixtures.len() {
757 let _ = writeln!(out);
758 }
759 }
760
761 while out.ends_with("\n\n") {
763 out.pop();
764 }
765 if !out.ends_with('\n') {
766 out.push('\n');
767 }
768 out
769}
770
771fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
780 if fixture.is_http_test() {
782 return false;
783 }
784 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
785 if call_config.skip_languages.iter().any(|l| l == "go") {
788 return false;
789 }
790 let go_override = call_config
791 .overrides
792 .get("go")
793 .or_else(|| e2e_config.call.overrides.get("go"));
794 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
797 return true;
798 }
799 let fn_name = go_override
803 .and_then(|o| o.function.as_deref())
804 .filter(|s| !s.is_empty())
805 .unwrap_or(call_config.function.as_str());
806 !fn_name.is_empty()
807}
808
809fn render_test_function(
810 out: &mut String,
811 fixture: &Fixture,
812 import_alias: &str,
813 field_resolver: &FieldResolver,
814 e2e_config: &crate::config::E2eConfig,
815) {
816 let fn_name = fixture.id.to_upper_camel_case();
817 let description = &fixture.description;
818
819 if fixture.http.is_some() {
821 render_http_test_function(out, fixture);
822 return;
823 }
824
825 if !fixture_has_go_callable(fixture, e2e_config) {
830 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
831 let _ = writeln!(out, "\t// {description}");
832 let _ = writeln!(
833 out,
834 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
835 );
836 let _ = writeln!(out, "}}");
837 return;
838 }
839
840 let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
842 let lang = "go";
843 let overrides = call_config.overrides.get(lang);
844
845 let base_function_name = overrides
849 .and_then(|o| o.function.as_deref())
850 .unwrap_or(&call_config.function);
851 let function_name = to_go_name(base_function_name);
852 let result_var = &call_config.result_var;
853 let args = &call_config.args;
854
855 let returns_result = overrides
858 .and_then(|o| o.returns_result)
859 .unwrap_or(call_config.returns_result);
860
861 let returns_void = call_config.returns_void;
864
865 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
868 if call_config.result_is_simple {
869 return true;
870 }
871 call_config
872 .overrides
873 .get("rust")
874 .map(|o| o.result_is_simple)
875 .unwrap_or(false)
876 });
877
878 let result_is_array = overrides
881 .map(|o| o.result_is_array)
882 .unwrap_or(call_config.result_is_array);
883
884 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
886 e2e_config
887 .call
888 .overrides
889 .get("go")
890 .and_then(|o| o.options_type.as_deref())
891 });
892
893 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
895 e2e_config
896 .call
897 .overrides
898 .get("go")
899 .map(|o| o.options_ptr)
900 .unwrap_or(false)
901 });
902
903 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
904 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
908
909 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
912 e2e_config
913 .call
914 .overrides
915 .get(lang)
916 .and_then(|o| o.client_factory.as_deref())
917 });
918
919 let (mut setup_lines, args_str) = build_args_and_setup(
920 &fixture.input,
921 args,
922 import_alias,
923 call_options_type,
924 fixture,
925 call_options_ptr,
926 validation_creation_failure,
927 );
928
929 let mut visitor_opts_var: Option<String> = None;
932 if fixture.visitor.is_some() {
933 let struct_name = visitor_struct_name(&fixture.id);
934 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
935 let opts_type = call_options_type.unwrap_or("ConversionOptions");
937 let opts_var = "opts".to_string();
938 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
939 setup_lines.push("opts.Visitor = visitor".to_string());
940 visitor_opts_var = Some(opts_var);
941 }
942
943 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
944 let final_args = {
945 let mut parts: Vec<String> = Vec::new();
946 if !args_str.is_empty() {
947 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
949 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
950 } else {
951 args_str
952 };
953 parts.push(processed_args);
954 }
955 parts.extend(go_extra_args);
956 parts.join(", ")
957 };
958
959 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
960 let _ = writeln!(out, "\t// {description}");
961
962 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
966 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
967 if let Some(var) = api_key_var {
968 if has_mock {
969 let fixture_id = &fixture.id;
973 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
974 let _ = writeln!(out, "\tvar baseURL *string");
975 let _ = writeln!(out, "\tif apiKey != \"\" {{");
976 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
977 let _ = writeln!(out, "\t}} else {{");
978 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
979 let _ = writeln!(
980 out,
981 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
982 );
983 let _ = writeln!(out, "\t\tbaseURL = &u");
984 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
985 let _ = writeln!(out, "\t}}");
986 } else {
987 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
988 let _ = writeln!(out, "\tif apiKey == \"\" {{");
989 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
990 let _ = writeln!(out, "\t}}");
991 }
992 }
993
994 for line in &setup_lines {
995 let _ = writeln!(out, "\t{line}");
996 }
997
998 let call_prefix = if let Some(factory) = client_factory {
1002 let factory_name = to_go_name(factory);
1003 let fixture_id = &fixture.id;
1004 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1007 ("apiKey".to_string(), "baseURL".to_string())
1009 } else if api_key_var.is_some() {
1010 ("apiKey".to_string(), "nil".to_string())
1012 } else if fixture.has_host_root_route() {
1013 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1014 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1015 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1016 let _ = writeln!(
1017 out,
1018 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1019 );
1020 let _ = writeln!(out, "\t}}");
1021 ("\"test-key\"".to_string(), "&mockURL".to_string())
1022 } else {
1023 let _ = writeln!(
1024 out,
1025 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1026 );
1027 ("\"test-key\"".to_string(), "&mockURL".to_string())
1028 };
1029 let _ = writeln!(
1030 out,
1031 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1032 );
1033 let _ = writeln!(out, "\tif clientErr != nil {{");
1034 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1035 let _ = writeln!(out, "\t}}");
1036 "client".to_string()
1037 } else {
1038 import_alias.to_string()
1039 };
1040
1041 let binding_returns_error_pre = args
1046 .iter()
1047 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1048 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1049
1050 if expects_error {
1051 if effective_returns_result_pre && !returns_void {
1052 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1053 } else {
1054 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1055 }
1056 let _ = writeln!(out, "\tif err == nil {{");
1057 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1058 let _ = writeln!(out, "\t}}");
1059 let _ = writeln!(out, "}}");
1060 return;
1061 }
1062
1063 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1065
1066 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1071 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1072 return false;
1073 }
1074 if a.assertion_type == "method_result" {
1076 return true;
1077 }
1078 match &a.field {
1079 Some(f) if !f.is_empty() => {
1080 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1081 return true;
1082 }
1083 field_resolver.is_valid_for_result(f)
1084 }
1085 _ => true,
1086 }
1087 });
1088
1089 let binding_returns_error = args
1096 .iter()
1097 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1098 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1100
1101 if !effective_returns_result && result_is_simple {
1107 let result_binding = if has_usable_assertion {
1109 result_var.to_string()
1110 } else {
1111 "_".to_string()
1112 };
1113 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1115 let _ = writeln!(
1116 out,
1117 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1118 );
1119 if has_usable_assertion && result_binding != "_" {
1120 if result_is_array {
1121 let _ = writeln!(out, "\tvalue := {result_var}");
1123 } else {
1124 let only_nil_assertions = fixture
1127 .assertions
1128 .iter()
1129 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1130 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1131 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1132
1133 if !only_nil_assertions {
1134 let _ = writeln!(out, "\tif {result_var} == nil {{");
1136 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1137 let _ = writeln!(out, "\t}}");
1138 let _ = writeln!(out, "\tvalue := *{result_var}");
1139 }
1140 }
1141 }
1142 } else if !effective_returns_result || returns_void {
1143 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1146 let _ = writeln!(out, "\tif err != nil {{");
1147 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1148 let _ = writeln!(out, "\t}}");
1149 let _ = writeln!(out, "}}");
1151 return;
1152 } else {
1153 let result_binding = if is_streaming {
1156 "stream".to_string()
1157 } else if has_usable_assertion {
1158 result_var.to_string()
1159 } else {
1160 "_".to_string()
1161 };
1162 let _ = writeln!(
1163 out,
1164 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1165 );
1166 let _ = writeln!(out, "\tif err != nil {{");
1167 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1168 let _ = writeln!(out, "\t}}");
1169 if is_streaming {
1171 let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1172 let _ = writeln!(out, "\tfor chunk := range stream {{");
1173 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1174 let _ = writeln!(out, "\t}}");
1175 }
1176 if result_is_simple && has_usable_assertion && result_binding != "_" {
1177 if result_is_array {
1178 let _ = writeln!(out, "\tvalue := {}", result_var);
1180 } else {
1181 let only_nil_assertions = fixture
1184 .assertions
1185 .iter()
1186 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1187 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1188 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1189
1190 if !only_nil_assertions {
1191 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1193 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1194 let _ = writeln!(out, "\t}}");
1195 let _ = writeln!(out, "\tvalue := *{}", result_var);
1196 }
1197 }
1198 }
1199 }
1200
1201 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1204 let only_nil_assertions = fixture
1205 .assertions
1206 .iter()
1207 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1208 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1209 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1210 !only_nil_assertions
1211 } else {
1212 result_is_simple && has_usable_assertion
1213 };
1214
1215 let effective_result_var = if has_deref_value {
1216 "value".to_string()
1217 } else {
1218 result_var.to_string()
1219 };
1220
1221 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1226 for assertion in &fixture.assertions {
1227 if let Some(f) = &assertion.field {
1228 if !f.is_empty() {
1229 let resolved = field_resolver.resolve(f);
1230 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1231 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1236 let is_array_field = field_resolver.is_array(resolved);
1237 if !is_string_field || is_array_field {
1238 continue;
1241 }
1242 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1243 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1244 if field_resolver.has_map_access(f) {
1245 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1248 } else {
1249 let _ = writeln!(out, "\tvar {local_var} string");
1250 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1251 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1255 let _ = writeln!(out, "\t}}");
1256 }
1257 optional_locals.insert(f.clone(), local_var);
1258 }
1259 }
1260 }
1261 }
1262
1263 for assertion in &fixture.assertions {
1265 if let Some(f) = &assertion.field {
1266 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1267 let parts: Vec<&str> = f.split('.').collect();
1270 let mut guard_expr: Option<String> = None;
1271 for i in 1..parts.len() {
1272 let prefix = parts[..i].join(".");
1273 let resolved_prefix = field_resolver.resolve(&prefix);
1274 if field_resolver.is_optional(resolved_prefix) {
1275 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1281 let suffix = &resolved_prefix[bracket_pos + 1..];
1282 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1283 if is_numeric_index {
1284 &resolved_prefix[..bracket_pos]
1285 } else {
1286 resolved_prefix
1287 }
1288 } else {
1289 resolved_prefix
1290 };
1291 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1292 guard_expr = Some(accessor);
1293 break;
1294 }
1295 }
1296 if let Some(guard) = guard_expr {
1297 if field_resolver.is_valid_for_result(f) {
1300 let _ = writeln!(out, "\tif {guard} != nil {{");
1301 let mut nil_buf = String::new();
1304 render_assertion(
1305 &mut nil_buf,
1306 assertion,
1307 &effective_result_var,
1308 import_alias,
1309 field_resolver,
1310 &optional_locals,
1311 result_is_simple,
1312 result_is_array,
1313 is_streaming,
1314 );
1315 for line in nil_buf.lines() {
1316 let _ = writeln!(out, "\t{line}");
1317 }
1318 let _ = writeln!(out, "\t}}");
1319 } else {
1320 render_assertion(
1321 out,
1322 assertion,
1323 &effective_result_var,
1324 import_alias,
1325 field_resolver,
1326 &optional_locals,
1327 result_is_simple,
1328 result_is_array,
1329 is_streaming,
1330 );
1331 }
1332 continue;
1333 }
1334 }
1335 }
1336 render_assertion(
1337 out,
1338 assertion,
1339 &effective_result_var,
1340 import_alias,
1341 field_resolver,
1342 &optional_locals,
1343 result_is_simple,
1344 result_is_array,
1345 is_streaming,
1346 );
1347 }
1348
1349 let _ = writeln!(out, "}}");
1350}
1351
1352fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1358 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1359}
1360
1361struct GoTestClientRenderer;
1373
1374impl client::TestClientRenderer for GoTestClientRenderer {
1375 fn language_name(&self) -> &'static str {
1376 "go"
1377 }
1378
1379 fn sanitize_test_name(&self, id: &str) -> String {
1383 id.to_upper_camel_case()
1384 }
1385
1386 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1389 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1390 let _ = writeln!(out, "\t// {description}");
1391 if let Some(reason) = skip_reason {
1392 let escaped = go_string_literal(reason);
1393 let _ = writeln!(out, "\tt.Skip({escaped})");
1394 }
1395 }
1396
1397 fn render_test_close(&self, out: &mut String) {
1398 let _ = writeln!(out, "}}");
1399 }
1400
1401 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1407 let method = ctx.method.to_uppercase();
1408 let path = ctx.path;
1409
1410 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1411 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1412 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1413 let _ = writeln!(out, "\t}}");
1414
1415 let body_expr = if let Some(body) = ctx.body {
1417 let json = serde_json::to_string(body).unwrap_or_default();
1418 let escaped = go_string_literal(&json);
1419 format!("strings.NewReader({})", escaped)
1420 } else {
1421 "strings.NewReader(\"\")".to_string()
1422 };
1423
1424 let _ = writeln!(out, "\tbody := {body_expr}");
1425 let _ = writeln!(
1426 out,
1427 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1428 );
1429 let _ = writeln!(out, "\tif err != nil {{");
1430 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1431 let _ = writeln!(out, "\t}}");
1432
1433 if ctx.body.is_some() {
1435 let content_type = ctx.content_type.unwrap_or("application/json");
1436 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1437 }
1438
1439 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1441 header_names.sort();
1442 for name in header_names {
1443 let value = &ctx.headers[name];
1444 let escaped_name = go_string_literal(name);
1445 let escaped_value = go_string_literal(value);
1446 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1447 }
1448
1449 if !ctx.cookies.is_empty() {
1451 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1452 cookie_names.sort();
1453 for name in cookie_names {
1454 let value = &ctx.cookies[name];
1455 let escaped_name = go_string_literal(name);
1456 let escaped_value = go_string_literal(value);
1457 let _ = writeln!(
1458 out,
1459 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1460 );
1461 }
1462 }
1463
1464 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1466 let _ = writeln!(
1467 out,
1468 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1469 );
1470 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1471 let _ = writeln!(out, "\t\t}},");
1472 let _ = writeln!(out, "\t}}");
1473 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1474 let _ = writeln!(out, "\tif err != nil {{");
1475 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1476 let _ = writeln!(out, "\t}}");
1477 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1478
1479 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1483 let _ = writeln!(out, "\tif err != nil {{");
1484 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1485 let _ = writeln!(out, "\t}}");
1486 let _ = writeln!(out, "\t_ = bodyBytes");
1487 }
1488
1489 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1490 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1491 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1492 let _ = writeln!(out, "\t}}");
1493 }
1494
1495 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1498 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1500 return;
1501 }
1502 if name.eq_ignore_ascii_case("connection") {
1504 return;
1505 }
1506 let escaped_name = go_string_literal(name);
1507 let escaped_value = go_string_literal(expected);
1508 let _ = writeln!(
1509 out,
1510 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1511 );
1512 let _ = writeln!(
1513 out,
1514 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1515 );
1516 let _ = writeln!(out, "\t}}");
1517 }
1518
1519 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1524 match expected {
1525 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1526 let json_str = serde_json::to_string(expected).unwrap_or_default();
1527 let escaped = go_string_literal(&json_str);
1528 let _ = writeln!(out, "\tvar got any");
1529 let _ = writeln!(out, "\tvar want any");
1530 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1531 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1532 let _ = writeln!(out, "\t}}");
1533 let _ = writeln!(
1534 out,
1535 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1536 );
1537 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1538 let _ = writeln!(out, "\t}}");
1539 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1540 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1541 let _ = writeln!(out, "\t}}");
1542 }
1543 serde_json::Value::String(s) => {
1544 let escaped = go_string_literal(s);
1545 let _ = writeln!(out, "\twant := {escaped}");
1546 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1547 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1548 let _ = writeln!(out, "\t}}");
1549 }
1550 other => {
1551 let escaped = go_string_literal(&other.to_string());
1552 let _ = writeln!(out, "\twant := {escaped}");
1553 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1554 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1555 let _ = writeln!(out, "\t}}");
1556 }
1557 }
1558 }
1559
1560 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1563 if let Some(obj) = expected.as_object() {
1564 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1565 let _ = writeln!(
1566 out,
1567 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1568 );
1569 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1570 let _ = writeln!(out, "\t}}");
1571 for (key, val) in obj {
1572 let escaped_key = go_string_literal(key);
1573 let json_val = serde_json::to_string(val).unwrap_or_default();
1574 let escaped_val = go_string_literal(&json_val);
1575 let _ = writeln!(out, "\t{{");
1576 let _ = writeln!(out, "\t\tvar _wantVal any");
1577 let _ = writeln!(
1578 out,
1579 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1580 );
1581 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1582 let _ = writeln!(out, "\t\t}}");
1583 let _ = writeln!(
1584 out,
1585 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1586 );
1587 let _ = writeln!(
1588 out,
1589 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1590 );
1591 let _ = writeln!(out, "\t\t}}");
1592 let _ = writeln!(out, "\t}}");
1593 }
1594 }
1595 }
1596
1597 fn render_assert_validation_errors(
1602 &self,
1603 out: &mut String,
1604 _response_var: &str,
1605 errors: &[ValidationErrorExpectation],
1606 ) {
1607 let _ = writeln!(out, "\tvar _veBody map[string]any");
1608 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1609 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1610 let _ = writeln!(out, "\t}}");
1611 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1612 for ve in errors {
1613 let escaped_msg = go_string_literal(&ve.msg);
1614 let _ = writeln!(out, "\t{{");
1615 let _ = writeln!(out, "\t\t_found := false");
1616 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1617 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1618 let _ = writeln!(
1619 out,
1620 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1621 );
1622 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1623 let _ = writeln!(out, "\t\t\t\t\tbreak");
1624 let _ = writeln!(out, "\t\t\t\t}}");
1625 let _ = writeln!(out, "\t\t\t}}");
1626 let _ = writeln!(out, "\t\t}}");
1627 let _ = writeln!(out, "\t\tif !_found {{");
1628 let _ = writeln!(
1629 out,
1630 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1631 );
1632 let _ = writeln!(out, "\t\t}}");
1633 let _ = writeln!(out, "\t}}");
1634 }
1635 }
1636}
1637
1638fn build_args_and_setup(
1646 input: &serde_json::Value,
1647 args: &[crate::config::ArgMapping],
1648 import_alias: &str,
1649 options_type: Option<&str>,
1650 fixture: &crate::fixture::Fixture,
1651 options_ptr: bool,
1652 expects_error: bool,
1653) -> (Vec<String>, String) {
1654 let fixture_id = &fixture.id;
1655 use heck::ToUpperCamelCase;
1656
1657 if args.is_empty() {
1658 return (Vec::new(), String::new());
1659 }
1660
1661 let mut setup_lines: Vec<String> = Vec::new();
1662 let mut parts: Vec<String> = Vec::new();
1663
1664 for arg in args {
1665 if arg.arg_type == "mock_url" {
1666 if fixture.has_host_root_route() {
1667 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1668 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1669 setup_lines.push(format!(
1670 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1671 arg.name, arg.name
1672 ));
1673 } else {
1674 setup_lines.push(format!(
1675 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1676 arg.name,
1677 ));
1678 }
1679 parts.push(arg.name.clone());
1680 continue;
1681 }
1682
1683 if arg.arg_type == "handle" {
1684 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1686 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1687 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1688 let create_err_handler = if expects_error {
1692 "assert.Error(t, createErr)\n\t\treturn".to_string()
1693 } else {
1694 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1695 };
1696 if config_value.is_null()
1697 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1698 {
1699 setup_lines.push(format!(
1700 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1701 name = arg.name,
1702 ));
1703 } else {
1704 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1705 let go_literal = go_string_literal(&json_str);
1706 let name = &arg.name;
1707 setup_lines.push(format!(
1708 "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}}"
1709 ));
1710 setup_lines.push(format!(
1711 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1712 ));
1713 }
1714 parts.push(arg.name.clone());
1715 continue;
1716 }
1717
1718 let val: Option<&serde_json::Value> = if arg.field == "input" {
1719 Some(input)
1720 } else {
1721 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1722 input.get(field)
1723 };
1724
1725 if arg.arg_type == "bytes" {
1732 let var_name = format!("{}Bytes", arg.name);
1733 match val {
1734 None | Some(serde_json::Value::Null) => {
1735 if arg.optional {
1736 parts.push("nil".to_string());
1737 } else {
1738 parts.push("[]byte{}".to_string());
1739 }
1740 }
1741 Some(serde_json::Value::String(s)) => {
1742 let go_path = go_string_literal(s);
1747 setup_lines.push(format!(
1748 "{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}}"
1749 ));
1750 parts.push(var_name);
1751 }
1752 Some(other) => {
1753 parts.push(format!("[]byte({})", json_to_go(other)));
1754 }
1755 }
1756 continue;
1757 }
1758
1759 match val {
1760 None | Some(serde_json::Value::Null) if arg.optional => {
1761 match arg.arg_type.as_str() {
1763 "string" => {
1764 parts.push("nil".to_string());
1766 }
1767 "json_object" => {
1768 if options_ptr {
1769 parts.push("nil".to_string());
1771 } else if let Some(opts_type) = options_type {
1772 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1774 } else {
1775 parts.push("nil".to_string());
1776 }
1777 }
1778 _ => {
1779 parts.push("nil".to_string());
1780 }
1781 }
1782 }
1783 None | Some(serde_json::Value::Null) => {
1784 let default_val = match arg.arg_type.as_str() {
1786 "string" => "\"\"".to_string(),
1787 "int" | "integer" | "i64" => "0".to_string(),
1788 "float" | "number" => "0.0".to_string(),
1789 "bool" | "boolean" => "false".to_string(),
1790 "json_object" => {
1791 if options_ptr {
1792 "nil".to_string()
1794 } else if let Some(opts_type) = options_type {
1795 format!("{import_alias}.{opts_type}{{}}")
1796 } else {
1797 "nil".to_string()
1798 }
1799 }
1800 _ => "nil".to_string(),
1801 };
1802 parts.push(default_val);
1803 }
1804 Some(v) => {
1805 match arg.arg_type.as_str() {
1806 "json_object" => {
1807 let is_array = v.is_array();
1810 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1811 if is_empty_obj {
1812 if options_ptr {
1813 parts.push("nil".to_string());
1815 } else if let Some(opts_type) = options_type {
1816 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1817 } else {
1818 parts.push("nil".to_string());
1819 }
1820 } else if is_array {
1821 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1826 if go_t.starts_with('[') {
1830 go_t.to_string()
1831 } else {
1832 let qualified = if go_t.contains('.') {
1834 go_t.to_string()
1835 } else {
1836 format!("{import_alias}.{go_t}")
1837 };
1838 format!("[]{qualified}")
1839 }
1840 } else {
1841 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1842 };
1843 let converted_v = convert_json_for_go(v.clone());
1845 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1846 let go_literal = go_string_literal(&json_str);
1847 let var_name = &arg.name;
1848 setup_lines.push(format!(
1849 "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}}"
1850 ));
1851 parts.push(var_name.to_string());
1852 } else if let Some(opts_type) = options_type {
1853 let remapped_v = if options_ptr {
1858 convert_json_for_go(v.clone())
1859 } else {
1860 v.clone()
1861 };
1862 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1863 let go_literal = go_string_literal(&json_str);
1864 let var_name = &arg.name;
1865 setup_lines.push(format!(
1866 "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}}"
1867 ));
1868 let arg_expr = if options_ptr {
1870 format!("&{var_name}")
1871 } else {
1872 var_name.to_string()
1873 };
1874 parts.push(arg_expr);
1875 } else {
1876 parts.push(json_to_go(v));
1877 }
1878 }
1879 "string" if arg.optional => {
1880 let var_name = format!("{}Val", arg.name);
1882 let go_val = json_to_go(v);
1883 setup_lines.push(format!("{var_name} := {go_val}"));
1884 parts.push(format!("&{var_name}"));
1885 }
1886 _ => {
1887 parts.push(json_to_go(v));
1888 }
1889 }
1890 }
1891 }
1892 }
1893
1894 (setup_lines, parts.join(", "))
1895}
1896
1897#[allow(clippy::too_many_arguments)]
1898fn render_assertion(
1899 out: &mut String,
1900 assertion: &Assertion,
1901 result_var: &str,
1902 import_alias: &str,
1903 field_resolver: &FieldResolver,
1904 optional_locals: &std::collections::HashMap<String, String>,
1905 result_is_simple: bool,
1906 result_is_array: bool,
1907 is_streaming: bool,
1908) {
1909 if !result_is_simple {
1912 if let Some(f) = &assertion.field {
1913 let embed_deref = format!("(*{result_var})");
1916 match f.as_str() {
1917 "chunks_have_content" => {
1918 let pred = format!(
1919 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1920 );
1921 match assertion.assertion_type.as_str() {
1922 "is_true" => {
1923 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1924 }
1925 "is_false" => {
1926 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1927 }
1928 _ => {
1929 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1930 }
1931 }
1932 return;
1933 }
1934 "chunks_have_embeddings" => {
1935 let pred = format!(
1936 "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 }}()"
1937 );
1938 match assertion.assertion_type.as_str() {
1939 "is_true" => {
1940 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1941 }
1942 "is_false" => {
1943 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1944 }
1945 _ => {
1946 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1947 }
1948 }
1949 return;
1950 }
1951 "embeddings" => {
1952 match assertion.assertion_type.as_str() {
1953 "count_equals" => {
1954 if let Some(val) = &assertion.value {
1955 if let Some(n) = val.as_u64() {
1956 let _ = writeln!(
1957 out,
1958 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1959 );
1960 }
1961 }
1962 }
1963 "count_min" => {
1964 if let Some(val) = &assertion.value {
1965 if let Some(n) = val.as_u64() {
1966 let _ = writeln!(
1967 out,
1968 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1969 );
1970 }
1971 }
1972 }
1973 "not_empty" => {
1974 let _ = writeln!(
1975 out,
1976 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1977 );
1978 }
1979 "is_empty" => {
1980 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1981 }
1982 _ => {
1983 let _ = writeln!(
1984 out,
1985 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1986 );
1987 }
1988 }
1989 return;
1990 }
1991 "embedding_dimensions" => {
1992 let expr = format!(
1993 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1994 );
1995 match assertion.assertion_type.as_str() {
1996 "equals" => {
1997 if let Some(val) = &assertion.value {
1998 if let Some(n) = val.as_u64() {
1999 let _ = writeln!(
2000 out,
2001 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2002 );
2003 }
2004 }
2005 }
2006 "greater_than" => {
2007 if let Some(val) = &assertion.value {
2008 if let Some(n) = val.as_u64() {
2009 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2010 }
2011 }
2012 }
2013 _ => {
2014 let _ = writeln!(
2015 out,
2016 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2017 );
2018 }
2019 }
2020 return;
2021 }
2022 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2023 let pred = match f.as_str() {
2024 "embeddings_valid" => {
2025 format!(
2026 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2027 )
2028 }
2029 "embeddings_finite" => {
2030 format!(
2031 "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 }}()"
2032 )
2033 }
2034 "embeddings_non_zero" => {
2035 format!(
2036 "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 }}()"
2037 )
2038 }
2039 "embeddings_normalized" => {
2040 format!(
2041 "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 }}()"
2042 )
2043 }
2044 _ => unreachable!(),
2045 };
2046 match assertion.assertion_type.as_str() {
2047 "is_true" => {
2048 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2049 }
2050 "is_false" => {
2051 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2052 }
2053 _ => {
2054 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2055 }
2056 }
2057 return;
2058 }
2059 "keywords" | "keywords_count" => {
2062 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2063 return;
2064 }
2065 _ => {}
2066 }
2067 }
2068 }
2069
2070 if !result_is_simple && is_streaming {
2077 if let Some(f) = &assertion.field {
2078 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2079 if let Some(expr) =
2080 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2081 {
2082 match assertion.assertion_type.as_str() {
2083 "count_min" => {
2084 if let Some(val) = &assertion.value {
2085 if let Some(n) = val.as_u64() {
2086 let _ = writeln!(
2087 out,
2088 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2089 );
2090 }
2091 }
2092 }
2093 "count_equals" => {
2094 if let Some(val) = &assertion.value {
2095 if let Some(n) = val.as_u64() {
2096 let _ = writeln!(
2097 out,
2098 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2099 );
2100 }
2101 }
2102 }
2103 "equals" => {
2104 if let Some(serde_json::Value::String(s)) = &assertion.value {
2105 let escaped = crate::escape::go_string_literal(s);
2106 let is_deep_path = f.contains('.') || f.contains('[');
2111 let safe_expr = if is_deep_path {
2112 format!(
2113 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2114 )
2115 } else {
2116 expr.clone()
2117 };
2118 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2119 } else if let Some(val) = &assertion.value {
2120 if let Some(n) = val.as_u64() {
2121 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2122 }
2123 }
2124 }
2125 "not_empty" => {
2126 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2127 }
2128 "is_empty" => {
2129 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2130 }
2131 "is_true" => {
2132 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2133 }
2134 "is_false" => {
2135 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2136 }
2137 "greater_than" => {
2138 if let Some(val) = &assertion.value {
2139 if let Some(n) = val.as_u64() {
2140 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2141 }
2142 }
2143 }
2144 "greater_than_or_equal" => {
2145 if let Some(val) = &assertion.value {
2146 if let Some(n) = val.as_u64() {
2147 let _ =
2148 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2149 }
2150 }
2151 }
2152 "contains" => {
2153 if let Some(serde_json::Value::String(s)) = &assertion.value {
2154 let escaped = crate::escape::go_string_literal(s);
2155 let _ =
2156 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2157 }
2158 }
2159 _ => {
2160 let _ = writeln!(
2161 out,
2162 "\t// streaming field '{f}': assertion type '{}' not rendered",
2163 assertion.assertion_type
2164 );
2165 }
2166 }
2167 }
2168 return;
2169 }
2170 }
2171 }
2172
2173 if !result_is_simple {
2176 if let Some(f) = &assertion.field {
2177 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2178 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2179 return;
2180 }
2181 }
2182 }
2183
2184 let field_expr = if result_is_simple {
2185 result_var.to_string()
2187 } else {
2188 match &assertion.field {
2189 Some(f) if !f.is_empty() => {
2190 if let Some(local_var) = optional_locals.get(f.as_str()) {
2192 local_var.clone()
2193 } else {
2194 field_resolver.accessor(f, "go", result_var)
2195 }
2196 }
2197 _ => result_var.to_string(),
2198 }
2199 };
2200
2201 let is_optional = assertion
2205 .field
2206 .as_ref()
2207 .map(|f| {
2208 let resolved = field_resolver.resolve(f);
2209 let check_path = resolved
2210 .strip_suffix(".length")
2211 .or_else(|| resolved.strip_suffix(".count"))
2212 .or_else(|| resolved.strip_suffix(".size"))
2213 .unwrap_or(resolved);
2214 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2215 })
2216 .unwrap_or(false);
2217
2218 let field_is_array_for_len = assertion
2222 .field
2223 .as_ref()
2224 .map(|f| {
2225 let resolved = field_resolver.resolve(f);
2226 let check_path = resolved
2227 .strip_suffix(".length")
2228 .or_else(|| resolved.strip_suffix(".count"))
2229 .or_else(|| resolved.strip_suffix(".size"))
2230 .unwrap_or(resolved);
2231 field_resolver.is_array(check_path)
2232 })
2233 .unwrap_or(false);
2234 let field_expr =
2235 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2236 let inner = &field_expr[4..field_expr.len() - 1];
2237 format!("len(*{inner})")
2238 } else {
2239 field_expr
2240 };
2241 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2243 Some(field_expr[5..field_expr.len() - 1].to_string())
2244 } else {
2245 None
2246 };
2247
2248 let field_is_slice = assertion
2252 .field
2253 .as_ref()
2254 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2255 .unwrap_or(false);
2256 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2257 format!("*{field_expr}")
2258 } else {
2259 field_expr.clone()
2260 };
2261
2262 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2267 let mut array_expr = field_expr[..idx].to_string();
2268 if let Some(stripped) = array_expr.strip_prefix("len(") {
2269 array_expr = stripped.to_string();
2270 }
2271 Some(array_expr)
2272 } else {
2273 None
2274 };
2275
2276 let mut assertion_buf = String::new();
2279 let out_ref = &mut assertion_buf;
2280
2281 match assertion.assertion_type.as_str() {
2282 "equals" => {
2283 if let Some(expected) = &assertion.value {
2284 let go_val = json_to_go(expected);
2285 if expected.is_string() {
2287 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2290 format!("strings.TrimSpace(string(*{field_expr}))")
2291 } else {
2292 format!("strings.TrimSpace(string({field_expr}))")
2293 };
2294 if is_optional && !field_expr.starts_with("len(") {
2295 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2296 } else {
2297 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2298 }
2299 } else if is_optional && !field_expr.starts_with("len(") {
2300 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2301 } else {
2302 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2303 }
2304 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2305 let _ = writeln!(out_ref, "\t}}");
2306 }
2307 }
2308 "contains" => {
2309 if let Some(expected) = &assertion.value {
2310 let go_val = json_to_go(expected);
2311 let resolved_field = assertion.field.as_deref().unwrap_or("");
2317 let resolved_name = field_resolver.resolve(resolved_field);
2318 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2319 let is_opt =
2320 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2321 let field_for_contains = if is_opt && field_is_array {
2322 format!("jsonString({field_expr})")
2324 } else if is_opt {
2325 format!("fmt.Sprint(*{field_expr})")
2326 } else if field_is_array {
2327 format!("jsonString({field_expr})")
2328 } else {
2329 format!("fmt.Sprint({field_expr})")
2330 };
2331 if is_opt {
2332 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2333 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2334 let _ = writeln!(
2335 out_ref,
2336 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2337 );
2338 let _ = writeln!(out_ref, "\t}}");
2339 let _ = writeln!(out_ref, "\t}}");
2340 } else {
2341 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2342 let _ = writeln!(
2343 out_ref,
2344 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2345 );
2346 let _ = writeln!(out_ref, "\t}}");
2347 }
2348 }
2349 }
2350 "contains_all" => {
2351 if let Some(values) = &assertion.values {
2352 let resolved_field = assertion.field.as_deref().unwrap_or("");
2353 let resolved_name = field_resolver.resolve(resolved_field);
2354 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2355 let is_opt =
2356 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2357 for val in values {
2358 let go_val = json_to_go(val);
2359 let field_for_contains = if is_opt && field_is_array {
2360 format!("jsonString({field_expr})")
2362 } else if is_opt {
2363 format!("fmt.Sprint(*{field_expr})")
2364 } else if field_is_array {
2365 format!("jsonString({field_expr})")
2366 } else {
2367 format!("fmt.Sprint({field_expr})")
2368 };
2369 if is_opt {
2370 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2371 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2372 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2373 let _ = writeln!(out_ref, "\t}}");
2374 let _ = writeln!(out_ref, "\t}}");
2375 } else {
2376 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2377 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2378 let _ = writeln!(out_ref, "\t}}");
2379 }
2380 }
2381 }
2382 }
2383 "not_contains" => {
2384 if let Some(expected) = &assertion.value {
2385 let go_val = json_to_go(expected);
2386 let resolved_field = assertion.field.as_deref().unwrap_or("");
2387 let resolved_name = field_resolver.resolve(resolved_field);
2388 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2389 let is_opt =
2390 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2391 let field_for_contains = if is_opt && field_is_array {
2392 format!("jsonString({field_expr})")
2394 } else if is_opt {
2395 format!("fmt.Sprint(*{field_expr})")
2396 } else if field_is_array {
2397 format!("jsonString({field_expr})")
2398 } else {
2399 format!("fmt.Sprint({field_expr})")
2400 };
2401 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2402 let _ = writeln!(
2403 out_ref,
2404 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2405 );
2406 let _ = writeln!(out_ref, "\t}}");
2407 }
2408 }
2409 "not_empty" => {
2410 let field_is_array = {
2413 let rf = assertion.field.as_deref().unwrap_or("");
2414 let rn = field_resolver.resolve(rf);
2415 field_resolver.is_array(rn)
2416 };
2417 if is_optional && !field_is_array {
2418 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2420 } else if is_optional && field_is_slice {
2421 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2423 } else if is_optional {
2424 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2426 } else if result_is_simple && result_is_array {
2427 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2429 } else {
2430 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2431 }
2432 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2433 let _ = writeln!(out_ref, "\t}}");
2434 }
2435 "is_empty" => {
2436 let field_is_array = {
2437 let rf = assertion.field.as_deref().unwrap_or("");
2438 let rn = field_resolver.resolve(rf);
2439 field_resolver.is_array(rn)
2440 };
2441 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2444 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2446 } else if is_optional && !field_is_array {
2447 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2449 } else if is_optional && field_is_slice {
2450 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2452 } else if is_optional {
2453 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2455 } else {
2456 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2457 }
2458 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2459 let _ = writeln!(out_ref, "\t}}");
2460 }
2461 "contains_any" => {
2462 if let Some(values) = &assertion.values {
2463 let resolved_field = assertion.field.as_deref().unwrap_or("");
2464 let resolved_name = field_resolver.resolve(resolved_field);
2465 let field_is_array = field_resolver.is_array(resolved_name);
2466 let is_opt =
2467 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2468 let field_for_contains = if is_opt && field_is_array {
2469 format!("jsonString({field_expr})")
2471 } else if is_opt {
2472 format!("fmt.Sprint(*{field_expr})")
2473 } else if field_is_array {
2474 format!("jsonString({field_expr})")
2475 } else {
2476 format!("fmt.Sprint({field_expr})")
2477 };
2478 let _ = writeln!(out_ref, "\t{{");
2479 let _ = writeln!(out_ref, "\t\tfound := false");
2480 for val in values {
2481 let go_val = json_to_go(val);
2482 let _ = writeln!(
2483 out_ref,
2484 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2485 );
2486 }
2487 let _ = writeln!(out_ref, "\t\tif !found {{");
2488 let _ = writeln!(
2489 out_ref,
2490 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2491 );
2492 let _ = writeln!(out_ref, "\t\t}}");
2493 let _ = writeln!(out_ref, "\t}}");
2494 }
2495 }
2496 "greater_than" => {
2497 if let Some(val) = &assertion.value {
2498 let go_val = json_to_go(val);
2499 if is_optional {
2503 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2504 if let Some(n) = val.as_u64() {
2505 let next = n + 1;
2506 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2507 } else {
2508 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2509 }
2510 let _ = writeln!(
2511 out_ref,
2512 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2513 );
2514 let _ = writeln!(out_ref, "\t\t}}");
2515 let _ = writeln!(out_ref, "\t}}");
2516 } else if let Some(n) = val.as_u64() {
2517 let next = n + 1;
2518 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2519 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2520 let _ = writeln!(out_ref, "\t}}");
2521 } else {
2522 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2523 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2524 let _ = writeln!(out_ref, "\t}}");
2525 }
2526 }
2527 }
2528 "less_than" => {
2529 if let Some(val) = &assertion.value {
2530 let go_val = json_to_go(val);
2531 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2532 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2533 let _ = writeln!(out_ref, "\t}}");
2534 }
2535 }
2536 "greater_than_or_equal" => {
2537 if let Some(val) = &assertion.value {
2538 let go_val = json_to_go(val);
2539 if let Some(ref guard) = nil_guard_expr {
2540 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2541 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2542 let _ = writeln!(
2543 out_ref,
2544 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2545 );
2546 let _ = writeln!(out_ref, "\t\t}}");
2547 let _ = writeln!(out_ref, "\t}}");
2548 } else if is_optional && !field_expr.starts_with("len(") {
2549 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2551 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2552 let _ = writeln!(
2553 out_ref,
2554 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2555 );
2556 let _ = writeln!(out_ref, "\t\t}}");
2557 let _ = writeln!(out_ref, "\t}}");
2558 } else {
2559 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2560 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2561 let _ = writeln!(out_ref, "\t}}");
2562 }
2563 }
2564 }
2565 "less_than_or_equal" => {
2566 if let Some(val) = &assertion.value {
2567 let go_val = json_to_go(val);
2568 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2569 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2570 let _ = writeln!(out_ref, "\t}}");
2571 }
2572 }
2573 "starts_with" => {
2574 if let Some(expected) = &assertion.value {
2575 let go_val = json_to_go(expected);
2576 let field_for_prefix = if is_optional
2577 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2578 {
2579 format!("string(*{field_expr})")
2580 } else {
2581 format!("string({field_expr})")
2582 };
2583 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2584 let _ = writeln!(
2585 out_ref,
2586 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2587 );
2588 let _ = writeln!(out_ref, "\t}}");
2589 }
2590 }
2591 "count_min" => {
2592 if let Some(val) = &assertion.value {
2593 if let Some(n) = val.as_u64() {
2594 if is_optional {
2595 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2596 let len_expr = if field_is_slice {
2598 format!("len({field_expr})")
2599 } else {
2600 format!("len(*{field_expr})")
2601 };
2602 let _ = writeln!(
2603 out_ref,
2604 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2605 );
2606 let _ = writeln!(out_ref, "\t}}");
2607 } else {
2608 let _ = writeln!(
2609 out_ref,
2610 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2611 );
2612 }
2613 }
2614 }
2615 }
2616 "count_equals" => {
2617 if let Some(val) = &assertion.value {
2618 if let Some(n) = val.as_u64() {
2619 if is_optional {
2620 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2621 let len_expr = if field_is_slice {
2623 format!("len({field_expr})")
2624 } else {
2625 format!("len(*{field_expr})")
2626 };
2627 let _ = writeln!(
2628 out_ref,
2629 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2630 );
2631 let _ = writeln!(out_ref, "\t}}");
2632 } else {
2633 let _ = writeln!(
2634 out_ref,
2635 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2636 );
2637 }
2638 }
2639 }
2640 }
2641 "is_true" => {
2642 if is_optional {
2643 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2644 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2645 let _ = writeln!(out_ref, "\t}}");
2646 } else {
2647 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2648 }
2649 }
2650 "is_false" => {
2651 if is_optional {
2652 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2653 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2654 let _ = writeln!(out_ref, "\t}}");
2655 } else {
2656 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2657 }
2658 }
2659 "method_result" => {
2660 if let Some(method_name) = &assertion.method {
2661 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2662 let check = assertion.check.as_deref().unwrap_or("is_true");
2663 let deref_expr = if info.is_pointer {
2666 format!("*{}", info.call_expr)
2667 } else {
2668 info.call_expr.clone()
2669 };
2670 match check {
2671 "equals" => {
2672 if let Some(val) = &assertion.value {
2673 if val.is_boolean() {
2674 if val.as_bool() == Some(true) {
2675 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2676 } else {
2677 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2678 }
2679 } else {
2680 let go_val = if let Some(cast) = info.value_cast {
2684 if val.is_number() {
2685 format!("{cast}({})", json_to_go(val))
2686 } else {
2687 json_to_go(val)
2688 }
2689 } else {
2690 json_to_go(val)
2691 };
2692 let _ = writeln!(
2693 out_ref,
2694 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2695 );
2696 }
2697 }
2698 }
2699 "is_true" => {
2700 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2701 }
2702 "is_false" => {
2703 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2704 }
2705 "greater_than_or_equal" => {
2706 if let Some(val) = &assertion.value {
2707 let n = val.as_u64().unwrap_or(0);
2708 let cast = info.value_cast.unwrap_or("uint");
2710 let _ = writeln!(
2711 out_ref,
2712 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2713 );
2714 }
2715 }
2716 "count_min" => {
2717 if let Some(val) = &assertion.value {
2718 let n = val.as_u64().unwrap_or(0);
2719 let _ = writeln!(
2720 out_ref,
2721 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2722 );
2723 }
2724 }
2725 "contains" => {
2726 if let Some(val) = &assertion.value {
2727 let go_val = json_to_go(val);
2728 let _ = writeln!(
2729 out_ref,
2730 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2731 );
2732 }
2733 }
2734 "is_error" => {
2735 let _ = writeln!(out_ref, "\t{{");
2736 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2737 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2738 let _ = writeln!(out_ref, "\t}}");
2739 }
2740 other_check => {
2741 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2742 }
2743 }
2744 } else {
2745 panic!("Go e2e generator: method_result assertion missing 'method' field");
2746 }
2747 }
2748 "min_length" => {
2749 if let Some(val) = &assertion.value {
2750 if let Some(n) = val.as_u64() {
2751 if is_optional {
2752 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2753 let _ = writeln!(
2754 out_ref,
2755 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2756 );
2757 let _ = writeln!(out_ref, "\t}}");
2758 } else if field_expr.starts_with("len(") {
2759 let _ = writeln!(
2760 out_ref,
2761 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2762 );
2763 } else {
2764 let _ = writeln!(
2765 out_ref,
2766 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2767 );
2768 }
2769 }
2770 }
2771 }
2772 "max_length" => {
2773 if let Some(val) = &assertion.value {
2774 if let Some(n) = val.as_u64() {
2775 if is_optional {
2776 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2777 let _ = writeln!(
2778 out_ref,
2779 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2780 );
2781 let _ = writeln!(out_ref, "\t}}");
2782 } else if field_expr.starts_with("len(") {
2783 let _ = writeln!(
2784 out_ref,
2785 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2786 );
2787 } else {
2788 let _ = writeln!(
2789 out_ref,
2790 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2791 );
2792 }
2793 }
2794 }
2795 }
2796 "ends_with" => {
2797 if let Some(expected) = &assertion.value {
2798 let go_val = json_to_go(expected);
2799 let field_for_suffix = if is_optional
2800 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2801 {
2802 format!("string(*{field_expr})")
2803 } else {
2804 format!("string({field_expr})")
2805 };
2806 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2807 let _ = writeln!(
2808 out_ref,
2809 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2810 );
2811 let _ = writeln!(out_ref, "\t}}");
2812 }
2813 }
2814 "matches_regex" => {
2815 if let Some(expected) = &assertion.value {
2816 let go_val = json_to_go(expected);
2817 let field_for_regex = if is_optional
2818 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2819 {
2820 format!("*{field_expr}")
2821 } else {
2822 field_expr.clone()
2823 };
2824 let _ = writeln!(
2825 out_ref,
2826 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2827 );
2828 }
2829 }
2830 "not_error" => {
2831 }
2833 "error" => {
2834 }
2836 other => {
2837 panic!("Go e2e generator: unsupported assertion type: {other}");
2838 }
2839 }
2840
2841 if let Some(ref arr) = array_guard {
2844 if !assertion_buf.is_empty() {
2845 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2846 for line in assertion_buf.lines() {
2848 let _ = writeln!(out, "\t{line}");
2849 }
2850 let _ = writeln!(out, "\t}}");
2851 }
2852 } else {
2853 out.push_str(&assertion_buf);
2854 }
2855}
2856
2857struct GoMethodCallInfo {
2859 call_expr: String,
2861 is_pointer: bool,
2863 value_cast: Option<&'static str>,
2866}
2867
2868fn build_go_method_call(
2883 result_var: &str,
2884 method_name: &str,
2885 args: Option<&serde_json::Value>,
2886 import_alias: &str,
2887) -> GoMethodCallInfo {
2888 match method_name {
2889 "root_node_type" => GoMethodCallInfo {
2890 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2891 is_pointer: false,
2892 value_cast: None,
2893 },
2894 "named_children_count" => GoMethodCallInfo {
2895 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2896 is_pointer: false,
2897 value_cast: Some("uint"),
2898 },
2899 "has_error_nodes" => GoMethodCallInfo {
2900 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2901 is_pointer: true,
2902 value_cast: None,
2903 },
2904 "error_count" | "tree_error_count" => GoMethodCallInfo {
2905 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2906 is_pointer: true,
2907 value_cast: Some("uint"),
2908 },
2909 "tree_to_sexp" => GoMethodCallInfo {
2910 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2911 is_pointer: true,
2912 value_cast: None,
2913 },
2914 "contains_node_type" => {
2915 let node_type = args
2916 .and_then(|a| a.get("node_type"))
2917 .and_then(|v| v.as_str())
2918 .unwrap_or("");
2919 GoMethodCallInfo {
2920 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2921 is_pointer: true,
2922 value_cast: None,
2923 }
2924 }
2925 "find_nodes_by_type" => {
2926 let node_type = args
2927 .and_then(|a| a.get("node_type"))
2928 .and_then(|v| v.as_str())
2929 .unwrap_or("");
2930 GoMethodCallInfo {
2931 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2932 is_pointer: true,
2933 value_cast: None,
2934 }
2935 }
2936 "run_query" => {
2937 let query_source = args
2938 .and_then(|a| a.get("query_source"))
2939 .and_then(|v| v.as_str())
2940 .unwrap_or("");
2941 let language = args
2942 .and_then(|a| a.get("language"))
2943 .and_then(|v| v.as_str())
2944 .unwrap_or("");
2945 let query_lit = go_string_literal(query_source);
2946 let lang_lit = go_string_literal(language);
2947 GoMethodCallInfo {
2949 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2950 is_pointer: false,
2951 value_cast: None,
2952 }
2953 }
2954 other => {
2955 let method_pascal = other.to_upper_camel_case();
2956 GoMethodCallInfo {
2957 call_expr: format!("{result_var}.{method_pascal}()"),
2958 is_pointer: false,
2959 value_cast: None,
2960 }
2961 }
2962 }
2963}
2964
2965fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2975 match value {
2976 serde_json::Value::Object(map) => {
2977 let new_map: serde_json::Map<String, serde_json::Value> = map
2978 .into_iter()
2979 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2980 .collect();
2981 serde_json::Value::Object(new_map)
2982 }
2983 serde_json::Value::Array(arr) => {
2984 if is_byte_array(&arr) {
2987 let bytes: Vec<u8> = arr
2988 .iter()
2989 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2990 .collect();
2991 let encoded = base64_encode(&bytes);
2993 serde_json::Value::String(encoded)
2994 } else {
2995 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2996 }
2997 }
2998 serde_json::Value::String(s) => {
2999 serde_json::Value::String(pascal_to_snake_case(&s))
3002 }
3003 other => other,
3004 }
3005}
3006
3007fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3009 if arr.is_empty() {
3010 return false;
3011 }
3012 arr.iter().all(|v| {
3013 if let serde_json::Value::Number(n) = v {
3014 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3015 } else {
3016 false
3017 }
3018 })
3019}
3020
3021fn base64_encode(bytes: &[u8]) -> String {
3024 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3025 let mut result = String::new();
3026 let mut i = 0;
3027
3028 while i + 2 < bytes.len() {
3029 let b1 = bytes[i];
3030 let b2 = bytes[i + 1];
3031 let b3 = bytes[i + 2];
3032
3033 result.push(TABLE[(b1 >> 2) as usize] as char);
3034 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3035 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3036 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3037
3038 i += 3;
3039 }
3040
3041 if i < bytes.len() {
3043 let b1 = bytes[i];
3044 result.push(TABLE[(b1 >> 2) as usize] as char);
3045
3046 if i + 1 < bytes.len() {
3047 let b2 = bytes[i + 1];
3048 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3049 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3050 result.push('=');
3051 } else {
3052 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3053 result.push_str("==");
3054 }
3055 }
3056
3057 result
3058}
3059
3060fn camel_to_snake_case(s: &str) -> String {
3062 let mut result = String::new();
3063 let mut prev_upper = false;
3064 for (i, c) in s.char_indices() {
3065 if c.is_uppercase() {
3066 if i > 0 && !prev_upper {
3067 result.push('_');
3068 }
3069 result.push(c.to_lowercase().next().unwrap_or(c));
3070 prev_upper = true;
3071 } else {
3072 if prev_upper && i > 1 {
3073 }
3077 result.push(c);
3078 prev_upper = false;
3079 }
3080 }
3081 result
3082}
3083
3084fn pascal_to_snake_case(s: &str) -> String {
3089 let first_char = s.chars().next();
3091 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3092 return s.to_string();
3093 }
3094 camel_to_snake_case(s)
3095}
3096
3097fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3101 let elem = element_type.unwrap_or("String").trim();
3102 let go_elem = rust_type_to_go(elem, import_alias);
3103 format!("[]{go_elem}")
3104}
3105
3106fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3109 let trimmed = rust.trim();
3110 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3111 return format!("[]{}", rust_type_to_go(inner, import_alias));
3112 }
3113 match trimmed {
3114 "String" | "&str" | "str" => "string".to_string(),
3115 "bool" => "bool".to_string(),
3116 "f32" => "float32".to_string(),
3117 "f64" => "float64".to_string(),
3118 "i8" => "int8".to_string(),
3119 "i16" => "int16".to_string(),
3120 "i32" => "int32".to_string(),
3121 "i64" | "isize" => "int64".to_string(),
3122 "u8" => "uint8".to_string(),
3123 "u16" => "uint16".to_string(),
3124 "u32" => "uint32".to_string(),
3125 "u64" | "usize" => "uint64".to_string(),
3126 _ => format!("{import_alias}.{trimmed}"),
3127 }
3128}
3129
3130fn json_to_go(value: &serde_json::Value) -> String {
3131 match value {
3132 serde_json::Value::String(s) => go_string_literal(s),
3133 serde_json::Value::Bool(b) => b.to_string(),
3134 serde_json::Value::Number(n) => n.to_string(),
3135 serde_json::Value::Null => "nil".to_string(),
3136 other => go_string_literal(&other.to_string()),
3138 }
3139}
3140
3141fn visitor_struct_name(fixture_id: &str) -> String {
3150 use heck::ToUpperCamelCase;
3151 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3153}
3154
3155fn emit_go_visitor_struct(
3160 out: &mut String,
3161 struct_name: &str,
3162 visitor_spec: &crate::fixture::VisitorSpec,
3163 import_alias: &str,
3164) {
3165 let _ = writeln!(out, "type {struct_name} struct{{");
3166 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3167 let _ = writeln!(out, "}}");
3168 for (method_name, action) in &visitor_spec.callbacks {
3169 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3170 }
3171}
3172
3173fn emit_go_visitor_method(
3175 out: &mut String,
3176 struct_name: &str,
3177 method_name: &str,
3178 action: &CallbackAction,
3179 import_alias: &str,
3180) {
3181 let camel_method = method_to_camel(method_name);
3182 let params = match method_name {
3185 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3186 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3187 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3188 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3189 "visit_code_inline"
3190 | "visit_strong"
3191 | "visit_emphasis"
3192 | "visit_strikethrough"
3193 | "visit_underline"
3194 | "visit_subscript"
3195 | "visit_superscript"
3196 | "visit_mark"
3197 | "visit_button"
3198 | "visit_summary"
3199 | "visit_figcaption"
3200 | "visit_definition_term"
3201 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3202 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3203 "visit_list_item" => {
3204 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3205 }
3206 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3207 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3208 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3209 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3210 "visit_input" => {
3211 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3212 }
3213 "visit_audio" | "visit_video" | "visit_iframe" => {
3214 format!("_ {import_alias}.NodeContext, src *string")
3215 }
3216 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3217 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3218 format!("_ {import_alias}.NodeContext, output string")
3219 }
3220 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3221 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3222 _ => format!("_ {import_alias}.NodeContext"),
3223 };
3224
3225 let _ = writeln!(
3226 out,
3227 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3228 );
3229 match action {
3230 CallbackAction::Skip => {
3231 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3232 }
3233 CallbackAction::Continue => {
3234 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3235 }
3236 CallbackAction::PreserveHtml => {
3237 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3238 }
3239 CallbackAction::Custom { output } => {
3240 let escaped = go_string_literal(output);
3241 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3242 }
3243 CallbackAction::CustomTemplate { template, .. } => {
3244 let ptr_params = go_visitor_ptr_params(method_name);
3251 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3252 let escaped_fmt = go_string_literal(&fmt_str);
3253 if fmt_args.is_empty() {
3254 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3255 } else {
3256 let args_str = fmt_args.join(", ");
3257 let _ = writeln!(
3258 out,
3259 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3260 );
3261 }
3262 }
3263 }
3264 let _ = writeln!(out, "}}");
3265}
3266
3267fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3270 match method_name {
3271 "visit_link" => ["title"].into(),
3272 "visit_image" => ["title"].into(),
3273 "visit_heading" => ["id"].into(),
3274 "visit_code_block" => ["lang"].into(),
3275 "visit_form" => ["action", "method"].into(),
3276 "visit_input" => ["name", "value"].into(),
3277 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3278 _ => std::collections::HashSet::new(),
3279 }
3280}
3281
3282fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3294 let mut fmt_str = String::new();
3295 let mut args: Vec<String> = Vec::new();
3296 let mut chars = template.chars().peekable();
3297 while let Some(c) = chars.next() {
3298 if c == '{' {
3299 let mut name = String::new();
3301 for inner in chars.by_ref() {
3302 if inner == '}' {
3303 break;
3304 }
3305 name.push(inner);
3306 }
3307 fmt_str.push_str("%s");
3308 let go_name = go_param_name(&name);
3310 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3312 format!("*{go_name}")
3313 } else {
3314 go_name
3315 };
3316 args.push(arg_expr);
3317 } else {
3318 fmt_str.push(c);
3319 }
3320 }
3321 (fmt_str, args)
3322}
3323
3324fn method_to_camel(snake: &str) -> String {
3326 use heck::ToUpperCamelCase;
3327 snake.to_upper_camel_case()
3328}
3329
3330#[cfg(test)]
3331mod tests {
3332 use super::*;
3333 use crate::config::{CallConfig, E2eConfig};
3334 use crate::field_access::FieldResolver;
3335 use crate::fixture::{Assertion, Fixture};
3336
3337 fn make_fixture(id: &str) -> Fixture {
3338 Fixture {
3339 id: id.to_string(),
3340 category: None,
3341 description: "test fixture".to_string(),
3342 tags: vec![],
3343 skip: None,
3344 env: None,
3345 call: None,
3346 input: serde_json::Value::Null,
3347 mock_response: Some(crate::fixture::MockResponse {
3348 status: 200,
3349 body: Some(serde_json::Value::Null),
3350 stream_chunks: None,
3351 headers: std::collections::HashMap::new(),
3352 }),
3353 source: String::new(),
3354 http: None,
3355 assertions: vec![Assertion {
3356 assertion_type: "not_error".to_string(),
3357 ..Default::default()
3358 }],
3359 visitor: None,
3360 }
3361 }
3362
3363 #[test]
3367 fn test_go_method_name_uses_go_casing() {
3368 let e2e_config = E2eConfig {
3369 call: CallConfig {
3370 function: "clean_extracted_text".to_string(),
3371 module: "github.com/example/mylib".to_string(),
3372 result_var: "result".to_string(),
3373 returns_result: true,
3374 ..CallConfig::default()
3375 },
3376 ..E2eConfig::default()
3377 };
3378
3379 let fixture = make_fixture("basic_text");
3380 let resolver = FieldResolver::new(
3381 &std::collections::HashMap::new(),
3382 &std::collections::HashSet::new(),
3383 &std::collections::HashSet::new(),
3384 &std::collections::HashSet::new(),
3385 &std::collections::HashSet::new(),
3386 );
3387 let mut out = String::new();
3388 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3389
3390 assert!(
3391 out.contains("kreuzberg.CleanExtractedText("),
3392 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3393 );
3394 assert!(
3395 !out.contains("kreuzberg.clean_extracted_text("),
3396 "must not emit raw snake_case method name, got:\n{out}"
3397 );
3398 }
3399
3400 #[test]
3401 fn test_streaming_fixture_emits_collect_snippet() {
3402 let streaming_fixture_json = r#"{
3404 "id": "basic_stream",
3405 "description": "basic streaming test",
3406 "call": "chat_stream",
3407 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3408 "mock_response": {
3409 "status": 200,
3410 "stream_chunks": [{"delta": "hello"}]
3411 },
3412 "assertions": [
3413 {"type": "count_min", "field": "chunks", "value": 1}
3414 ]
3415 }"#;
3416 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3417 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3418
3419 let e2e_config = E2eConfig {
3420 call: CallConfig {
3421 function: "chat_stream".to_string(),
3422 module: "github.com/example/mylib".to_string(),
3423 result_var: "result".to_string(),
3424 returns_result: true,
3425 r#async: true,
3426 ..CallConfig::default()
3427 },
3428 ..E2eConfig::default()
3429 };
3430
3431 let resolver = FieldResolver::new(
3432 &std::collections::HashMap::new(),
3433 &std::collections::HashSet::new(),
3434 &std::collections::HashSet::new(),
3435 &std::collections::HashSet::new(),
3436 &std::collections::HashSet::new(),
3437 );
3438
3439 let mut out = String::new();
3440 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3441
3442 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3443 assert!(
3444 out.contains("for chunk := range stream"),
3445 "should emit collect loop, got:\n{out}"
3446 );
3447 }
3448
3449 #[test]
3450 fn test_streaming_with_client_factory_and_json_arg() {
3451 use alef_core::config::e2e::{ArgMapping, CallOverride};
3455 let streaming_fixture_json = r#"{
3456 "id": "basic_stream_client",
3457 "description": "basic streaming test with client",
3458 "call": "chat_stream",
3459 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3460 "mock_response": {
3461 "status": 200,
3462 "stream_chunks": [{"delta": "hello"}]
3463 },
3464 "assertions": [
3465 {"type": "count_min", "field": "chunks", "value": 1}
3466 ]
3467 }"#;
3468 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3469 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3470
3471 let go_override = CallOverride {
3472 client_factory: Some("CreateClient".to_string()),
3473 ..Default::default()
3474 };
3475
3476 let mut call_overrides = std::collections::HashMap::new();
3477 call_overrides.insert("go".to_string(), go_override);
3478
3479 let e2e_config = E2eConfig {
3480 call: CallConfig {
3481 function: "chat_stream".to_string(),
3482 module: "github.com/example/mylib".to_string(),
3483 result_var: "result".to_string(),
3484 returns_result: false, r#async: true,
3486 args: vec![ArgMapping {
3487 name: "request".to_string(),
3488 field: "input".to_string(),
3489 arg_type: "json_object".to_string(),
3490 optional: false,
3491 owned: true,
3492 element_type: None,
3493 go_type: None,
3494 }],
3495 overrides: call_overrides,
3496 ..CallConfig::default()
3497 },
3498 ..E2eConfig::default()
3499 };
3500
3501 let resolver = FieldResolver::new(
3502 &std::collections::HashMap::new(),
3503 &std::collections::HashSet::new(),
3504 &std::collections::HashSet::new(),
3505 &std::collections::HashSet::new(),
3506 &std::collections::HashSet::new(),
3507 );
3508
3509 let mut out = String::new();
3510 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3511
3512 eprintln!("generated:\n{out}");
3513 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3514 assert!(
3515 out.contains("for chunk := range stream"),
3516 "should emit collect loop, got:\n{out}"
3517 );
3518 }
3519
3520 #[test]
3524 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3525 let e2e_config = E2eConfig {
3526 call: CallConfig {
3527 function: "transcribe".to_string(),
3528 module: "github.com/example/mylib".to_string(),
3529 result_var: "result".to_string(),
3530 returns_result: true,
3531 ..CallConfig::default()
3532 },
3533 ..E2eConfig::default()
3534 };
3535
3536 let fixture = Fixture {
3537 id: "edge_transcribe_with_timestamps".to_string(),
3538 category: None,
3539 description: "Transcription with timestamp segments".to_string(),
3540 tags: vec![],
3541 skip: None,
3542 env: None,
3543 call: None,
3544 input: serde_json::Value::Null,
3545 mock_response: Some(crate::fixture::MockResponse {
3546 status: 200,
3547 body: Some(serde_json::Value::Null),
3548 stream_chunks: None,
3549 headers: std::collections::HashMap::new(),
3550 }),
3551 source: String::new(),
3552 http: None,
3553 assertions: vec![
3554 Assertion {
3555 assertion_type: "not_error".to_string(),
3556 ..Default::default()
3557 },
3558 Assertion {
3559 assertion_type: "equals".to_string(),
3560 field: Some("segments[0].id".to_string()),
3561 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3562 ..Default::default()
3563 },
3564 ],
3565 visitor: None,
3566 };
3567
3568 let mut optional_fields = std::collections::HashSet::new();
3569 optional_fields.insert("segments".to_string());
3571
3572 let mut array_fields = std::collections::HashSet::new();
3573 array_fields.insert("segments".to_string());
3574
3575 let resolver = FieldResolver::new(
3576 &std::collections::HashMap::new(),
3577 &optional_fields,
3578 &array_fields,
3579 &std::collections::HashSet::new(),
3580 &std::collections::HashSet::new(),
3581 );
3582
3583 let mut out = String::new();
3584 render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3585
3586 eprintln!("generated:\n{out}");
3587
3588 assert!(
3590 out.contains("result.Segments != nil"),
3591 "guard must be on Segments (the slice), not an element; got:\n{out}"
3592 );
3593 assert!(
3595 !out.contains("result.Segments[0] != nil"),
3596 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3597 );
3598 }
3599}