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