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 effective_replace = match e2e_config.dep_mode {
77 crate::config::DependencyMode::Registry => None,
78 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
79 };
80 let effective_go_version = if effective_replace.is_some() {
86 fix_go_major_version(&go_module_path, &go_version)
87 } else {
88 go_version.clone()
89 };
90 files.push(GeneratedFile {
91 path: output_base.join("go.mod"),
92 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
93 generated_header: false,
94 });
95
96 let emits_executable_test =
98 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
99 let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
100 emits_executable_test(f)
101 && f.assertions.iter().any(|a| {
102 matches!(
103 a.assertion_type.as_str(),
104 "contains" | "contains_all" | "contains_any" | "not_contains"
105 ) && {
106 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
107 e2e_config
108 .resolve_call_for_fixture(
109 f.call.as_deref(),
110 &f.id,
111 &f.resolved_category(),
112 &f.tags,
113 &f.input,
114 )
115 .result_is_array
116 } else {
117 let cc = e2e_config.resolve_call_for_fixture(
118 f.call.as_deref(),
119 &f.id,
120 &f.resolved_category(),
121 &f.tags,
122 &f.input,
123 );
124 let per_call_resolver = FieldResolver::new(
125 e2e_config.effective_fields(cc),
126 e2e_config.effective_fields_optional(cc),
127 e2e_config.effective_result_fields(cc),
128 e2e_config.effective_fields_array(cc),
129 &std::collections::HashSet::new(),
130 );
131 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
132 per_call_resolver.is_array(resolved_name)
133 }
134 }
135 })
136 });
137
138 if needs_json_stringify {
140 files.push(GeneratedFile {
141 path: output_base.join("helpers_test.go"),
142 content: render_helpers_test_go(),
143 generated_header: true,
144 });
145 }
146
147 let has_file_fixtures = groups
155 .iter()
156 .flat_map(|g| g.fixtures.iter())
157 .any(|f| f.http.is_none() && !f.needs_mock_server());
158
159 let needs_main_test = has_file_fixtures
160 || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
161 if f.needs_mock_server() {
162 return true;
163 }
164 let cc = e2e_config.resolve_call_for_fixture(
165 f.call.as_deref(),
166 &f.id,
167 &f.resolved_category(),
168 &f.tags,
169 &f.input,
170 );
171 let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
172 go_override.and_then(|o| o.client_factory.as_deref()).is_some()
173 });
174
175 if needs_main_test {
176 files.push(GeneratedFile {
177 path: output_base.join("main_test.go"),
178 content: render_main_test_go(&e2e_config.test_documents_dir),
179 generated_header: true,
180 });
181 }
182
183 for group in groups {
185 let active: Vec<&Fixture> = group
186 .fixtures
187 .iter()
188 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
189 .collect();
190
191 if active.is_empty() {
192 continue;
193 }
194
195 let filename = format!("{}_test.go", sanitize_filename(&group.category));
196 let content = render_test_file(&group.category, &active, &module_path, &import_alias, e2e_config);
197 files.push(GeneratedFile {
198 path: output_base.join(filename),
199 content,
200 generated_header: true,
201 });
202 }
203
204 Ok(files)
205 }
206
207 fn language_name(&self) -> &'static str {
208 "go"
209 }
210}
211
212fn fix_go_major_version(module_path: &str, version: &str) -> String {
219 let major = module_path
221 .rsplit('/')
222 .next()
223 .and_then(|seg| seg.strip_prefix('v'))
224 .and_then(|n| n.parse::<u64>().ok())
225 .filter(|&n| n >= 2);
226
227 let Some(n) = major else {
228 return version.to_string();
229 };
230
231 let expected_prefix = format!("v{n}.");
233 if version.starts_with(&expected_prefix) {
234 return version.to_string();
235 }
236
237 format!("v{n}.0.0")
238}
239
240fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
241 let mut out = String::new();
242 let _ = writeln!(out, "module e2e_go");
243 let _ = writeln!(out);
244 let _ = writeln!(out, "go 1.26");
245 let _ = writeln!(out);
246 let _ = writeln!(out, "require (");
247 let _ = writeln!(out, "\t{go_module_path} {version}");
248 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
249 let _ = writeln!(out, ")");
250
251 if let Some(path) = replace_path {
252 let _ = writeln!(out);
253 let _ = writeln!(out, "replace {go_module_path} => {path}");
254 }
255
256 out
257}
258
259fn render_main_test_go(test_documents_dir: &str) -> String {
265 let mut out = String::new();
267 let _ = writeln!(out, "package e2e_test");
268 let _ = writeln!(out);
269 let _ = writeln!(out, "import (");
270 let _ = writeln!(out, "\t\"bufio\"");
271 let _ = writeln!(out, "\t\"encoding/json\"");
272 let _ = writeln!(out, "\t\"io\"");
273 let _ = writeln!(out, "\t\"os\"");
274 let _ = writeln!(out, "\t\"os/exec\"");
275 let _ = writeln!(out, "\t\"path/filepath\"");
276 let _ = writeln!(out, "\t\"runtime\"");
277 let _ = writeln!(out, "\t\"strings\"");
278 let _ = writeln!(out, "\t\"testing\"");
279 let _ = writeln!(out, ")");
280 let _ = writeln!(out);
281 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
282 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
283 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
284 let _ = writeln!(out);
285 let _ = writeln!(
286 out,
287 "\t// Change to the configured test-documents directory (if it exists) so that fixture"
288 );
289 let _ = writeln!(
290 out,
291 "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
292 );
293 let _ = writeln!(
294 out,
295 "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
296 );
297 let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
298 let _ = writeln!(
299 out,
300 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
301 );
302 let _ = writeln!(
303 out,
304 "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
305 );
306 let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
307 let _ = writeln!(out, "\t\t\tpanic(err)");
308 let _ = writeln!(out, "\t\t}}");
309 let _ = writeln!(out, "\t}}");
310 let _ = writeln!(out);
311 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
312 let _ = writeln!(
313 out,
314 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
315 );
316 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
317 let _ = writeln!(
318 out,
319 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
320 );
321 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
322 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
323 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
324 let _ = writeln!(out, "\t\tif err != nil {{");
325 let _ = writeln!(out, "\t\t\tpanic(err)");
326 let _ = writeln!(out, "\t\t}}");
327 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
328 let _ = writeln!(
329 out,
330 "\t\t// server does not see EOF and exit immediately. The mock-server"
331 );
332 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
333 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
334 let _ = writeln!(out, "\t\tif err != nil {{");
335 let _ = writeln!(out, "\t\t\tpanic(err)");
336 let _ = writeln!(out, "\t\t}}");
337 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
338 let _ = writeln!(out, "\t\t\tpanic(err)");
339 let _ = writeln!(out, "\t\t}}");
340 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
341 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
342 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
343 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
344 let _ = writeln!(
345 out,
346 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
347 );
348 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
349 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
350 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
351 let _ = writeln!(
352 out,
353 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
354 );
355 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
356 let _ = writeln!(
357 out,
358 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
359 );
360 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
361 let _ = writeln!(
362 out,
363 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
364 );
365 let _ = writeln!(out, "\t\t\t\t\t}}");
366 let _ = writeln!(out, "\t\t\t\t}}");
367 let _ = writeln!(out, "\t\t\t\tbreak");
368 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
369 let _ = writeln!(out, "\t\t\t\tbreak");
370 let _ = writeln!(out, "\t\t\t}}");
371 let _ = writeln!(out, "\t\t}}");
372 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
373 let _ = writeln!(out, "\t\tcode := m.Run()");
374 let _ = writeln!(out, "\t\t_ = stdin.Close()");
375 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
376 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
377 let _ = writeln!(out, "\t\tos.Exit(code)");
378 let _ = writeln!(out, "\t}} else {{");
379 let _ = writeln!(out, "\t\tcode := m.Run()");
380 let _ = writeln!(out, "\t\tos.Exit(code)");
381 let _ = writeln!(out, "\t}}");
382 let _ = writeln!(out, "}}");
383 out
384}
385
386fn render_helpers_test_go() -> String {
389 let mut out = String::new();
390 let _ = writeln!(out, "package e2e_test");
391 let _ = writeln!(out);
392 let _ = writeln!(out, "import \"encoding/json\"");
393 let _ = writeln!(out);
394 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
395 let _ = writeln!(
396 out,
397 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
398 );
399 let _ = writeln!(out, "func jsonString(value any) string {{");
400 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
401 let _ = writeln!(out, "\tif err != nil {{");
402 let _ = writeln!(out, "\t\treturn \"\"");
403 let _ = writeln!(out, "\t}}");
404 let _ = writeln!(out, "\treturn string(encoded)");
405 let _ = writeln!(out, "}}");
406 out
407}
408
409fn render_test_file(
410 category: &str,
411 fixtures: &[&Fixture],
412 go_module_path: &str,
413 import_alias: &str,
414 e2e_config: &crate::config::E2eConfig,
415) -> String {
416 let mut out = String::new();
417 let emits_executable_test =
418 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
419
420 out.push_str(&hash::header(CommentStyle::DoubleSlash));
422 let _ = writeln!(out);
423
424 let needs_pkg = fixtures
433 .iter()
434 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
435
436 let needs_os = fixtures.iter().any(|f| {
439 if f.is_http_test() {
440 return true;
441 }
442 if !emits_executable_test(f) {
443 return false;
444 }
445 let call_config =
446 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
447 let go_override = call_config
448 .overrides
449 .get("go")
450 .or_else(|| e2e_config.call.overrides.get("go"));
451 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
452 return true;
453 }
454 let call_args = &call_config.args;
455 if call_args.iter().any(|a| a.arg_type == "mock_url") {
458 return true;
459 }
460 call_args.iter().any(|a| {
461 if a.arg_type != "bytes" {
462 return false;
463 }
464 let mut current = &f.input;
467 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
468 for segment in path.split('.') {
469 match current.get(segment) {
470 Some(next) => current = next,
471 None => return false,
472 }
473 }
474 current.is_string()
475 })
476 });
477
478 let needs_filepath = false;
481
482 let _needs_json_stringify = fixtures.iter().any(|f| {
483 emits_executable_test(f)
484 && f.assertions.iter().any(|a| {
485 matches!(
486 a.assertion_type.as_str(),
487 "contains" | "contains_all" | "contains_any" | "not_contains"
488 ) && {
489 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
492 e2e_config
494 .resolve_call_for_fixture(
495 f.call.as_deref(),
496 &f.id,
497 &f.resolved_category(),
498 &f.tags,
499 &f.input,
500 )
501 .result_is_array
502 } else {
503 let cc = e2e_config.resolve_call_for_fixture(
505 f.call.as_deref(),
506 &f.id,
507 &f.resolved_category(),
508 &f.tags,
509 &f.input,
510 );
511 let per_call_resolver = FieldResolver::new(
512 e2e_config.effective_fields(cc),
513 e2e_config.effective_fields_optional(cc),
514 e2e_config.effective_result_fields(cc),
515 e2e_config.effective_fields_array(cc),
516 &std::collections::HashSet::new(),
517 );
518 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
519 per_call_resolver.is_array(resolved_name)
520 }
521 }
522 })
523 });
524
525 let needs_json = fixtures.iter().any(|f| {
529 if let Some(http) = &f.http {
532 let body_needs_json = http
533 .expected_response
534 .body
535 .as_ref()
536 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
537 let partial_needs_json = http.expected_response.body_partial.is_some();
538 let ve_needs_json = http
539 .expected_response
540 .validation_errors
541 .as_ref()
542 .is_some_and(|v| !v.is_empty());
543 if body_needs_json || partial_needs_json || ve_needs_json {
544 return true;
545 }
546 }
547 if !emits_executable_test(f) {
548 return false;
549 }
550
551 let call =
552 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
553 let call_args = &call.args;
554 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
556 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
557 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
558 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
559 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
560 })
561 };
562 let go_override = call.overrides.get("go");
564 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
565 e2e_config
566 .call
567 .overrides
568 .get("go")
569 .and_then(|o| o.options_type.as_deref())
570 });
571 let has_json_obj = call_args.iter().any(|a| {
572 if a.arg_type != "json_object" {
573 return false;
574 }
575 let v = if a.field == "input" {
576 &f.input
577 } else {
578 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
579 f.input.get(field).unwrap_or(&serde_json::Value::Null)
580 };
581 if v.is_array() {
582 return true;
583 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
585 });
586 has_handle || has_json_obj
587 });
588
589 let needs_base64 = false;
594
595 let needs_fmt = fixtures.iter().any(|f| {
600 f.visitor.as_ref().is_some_and(|v| {
601 v.callbacks.values().any(|action| {
602 if let CallbackAction::CustomTemplate { template, .. } = action {
603 template.contains('{')
604 } else {
605 false
606 }
607 })
608 }) || (emits_executable_test(f)
609 && f.assertions.iter().any(|a| {
610 matches!(
611 a.assertion_type.as_str(),
612 "contains" | "contains_all" | "contains_any" | "not_contains"
613 ) && {
614 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
619 !e2e_config
621 .resolve_call_for_fixture(
622 f.call.as_deref(),
623 &f.id,
624 &f.resolved_category(),
625 &f.tags,
626 &f.input,
627 )
628 .result_is_array
629 } else {
630 let field = a.field.as_deref().unwrap_or("");
634 let cc = e2e_config.resolve_call_for_fixture(
635 f.call.as_deref(),
636 &f.id,
637 &f.resolved_category(),
638 &f.tags,
639 &f.input,
640 );
641 let per_call_resolver = FieldResolver::new(
642 e2e_config.effective_fields(cc),
643 e2e_config.effective_fields_optional(cc),
644 e2e_config.effective_result_fields(cc),
645 e2e_config.effective_fields_array(cc),
646 &std::collections::HashSet::new(),
647 );
648 let resolved_name = per_call_resolver.resolve(field);
649 !per_call_resolver.is_array(resolved_name) && per_call_resolver.is_valid_for_result(field)
650 }
651 }
652 }))
653 });
654
655 let needs_strings = fixtures.iter().any(|f| {
658 if !emits_executable_test(f) {
659 return false;
660 }
661 let cc =
662 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
663 let per_call_resolver = FieldResolver::new(
664 e2e_config.effective_fields(cc),
665 e2e_config.effective_fields_optional(cc),
666 e2e_config.effective_result_fields(cc),
667 e2e_config.effective_fields_array(cc),
668 &std::collections::HashSet::new(),
669 );
670 f.assertions.iter().any(|a| {
671 let type_needs_strings = if a.assertion_type == "equals" {
672 a.value.as_ref().is_some_and(|v| v.is_string())
674 } else {
675 matches!(
676 a.assertion_type.as_str(),
677 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
678 )
679 };
680 let field_valid = a
681 .field
682 .as_ref()
683 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
684 .unwrap_or(true);
685 type_needs_strings && field_valid
686 })
687 });
688
689 let needs_assert = fixtures.iter().any(|f| {
691 if !emits_executable_test(f) {
692 return false;
693 }
694 if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
698 return true;
699 }
700 let is_streaming_fixture = f.is_streaming_mock();
705 let cc =
706 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
707 let per_call_resolver = FieldResolver::new(
708 e2e_config.effective_fields(cc),
709 e2e_config.effective_fields_optional(cc),
710 e2e_config.effective_result_fields(cc),
711 e2e_config.effective_fields_array(cc),
712 &std::collections::HashSet::new(),
713 );
714 f.assertions.iter().any(|a| {
715 let field_is_streaming_virtual = a
716 .field
717 .as_deref()
718 .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
719 let field_valid = a
720 .field
721 .as_ref()
722 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
723 .unwrap_or(true)
724 || (is_streaming_fixture && field_is_streaming_virtual);
725 let synthetic_field_needs_assert = match a.field.as_deref() {
726 Some("chunks_have_content" | "chunks_have_embeddings") => {
727 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
728 }
729 Some("embeddings") => {
730 matches!(
731 a.assertion_type.as_str(),
732 "count_equals" | "count_min" | "not_empty" | "is_empty"
733 )
734 }
735 _ => false,
736 };
737 let type_needs_assert = matches!(
738 a.assertion_type.as_str(),
739 "count_equals"
740 | "count_min"
741 | "count_max"
742 | "is_true"
743 | "is_false"
744 | "method_result"
745 | "min_length"
746 | "max_length"
747 | "matches_regex"
748 );
749 synthetic_field_needs_assert || type_needs_assert && field_valid
750 })
751 });
752
753 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
755 let needs_http = has_http_fixtures;
756 let needs_io = has_http_fixtures;
758
759 let needs_reflect = fixtures.iter().any(|f| {
762 if let Some(http) = &f.http {
763 let body_needs_reflect = http
764 .expected_response
765 .body
766 .as_ref()
767 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
768 let partial_needs_reflect = http.expected_response.body_partial.is_some();
769 body_needs_reflect || partial_needs_reflect
770 } else {
771 false
772 }
773 });
774
775 let _ = writeln!(out, "// E2e tests for category: {category}");
776 let _ = writeln!(out, "package e2e_test");
777 let _ = writeln!(out);
778 let _ = writeln!(out, "import (");
779 if needs_base64 {
780 let _ = writeln!(out, "\t\"encoding/base64\"");
781 }
782 if needs_json || needs_reflect {
783 let _ = writeln!(out, "\t\"encoding/json\"");
784 }
785 if needs_fmt {
786 let _ = writeln!(out, "\t\"fmt\"");
787 }
788 if needs_io {
789 let _ = writeln!(out, "\t\"io\"");
790 }
791 if needs_http {
792 let _ = writeln!(out, "\t\"net/http\"");
793 }
794 if needs_os {
795 let _ = writeln!(out, "\t\"os\"");
796 }
797 let _ = needs_filepath; if needs_reflect {
799 let _ = writeln!(out, "\t\"reflect\"");
800 }
801 if needs_strings {
802 let _ = writeln!(out, "\t\"strings\"");
803 }
804 let _ = writeln!(out, "\t\"testing\"");
805 if needs_assert {
806 let _ = writeln!(out);
807 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
808 }
809 if needs_pkg {
810 let _ = writeln!(out);
811 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
812 }
813 let _ = writeln!(out, ")");
814 let _ = writeln!(out);
815
816 for fixture in fixtures.iter() {
818 if let Some(visitor_spec) = &fixture.visitor {
819 let struct_name = visitor_struct_name(&fixture.id);
820 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
821 let _ = writeln!(out);
822 }
823 }
824
825 for (i, fixture) in fixtures.iter().enumerate() {
826 render_test_function(&mut out, fixture, import_alias, e2e_config);
827 if i + 1 < fixtures.len() {
828 let _ = writeln!(out);
829 }
830 }
831
832 while out.ends_with("\n\n") {
834 out.pop();
835 }
836 if !out.ends_with('\n') {
837 out.push('\n');
838 }
839 out
840}
841
842fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
851 if fixture.is_http_test() {
853 return false;
854 }
855 let call_config = e2e_config.resolve_call_for_fixture(
856 fixture.call.as_deref(),
857 &fixture.id,
858 &fixture.resolved_category(),
859 &fixture.tags,
860 &fixture.input,
861 );
862 if call_config.skip_languages.iter().any(|l| l == "go") {
865 return false;
866 }
867 let go_override = call_config
868 .overrides
869 .get("go")
870 .or_else(|| e2e_config.call.overrides.get("go"));
871 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
874 return true;
875 }
876 let fn_name = go_override
880 .and_then(|o| o.function.as_deref())
881 .filter(|s| !s.is_empty())
882 .unwrap_or(call_config.function.as_str());
883 !fn_name.is_empty()
884}
885
886fn render_test_function(
887 out: &mut String,
888 fixture: &Fixture,
889 import_alias: &str,
890 e2e_config: &crate::config::E2eConfig,
891) {
892 let fn_name = fixture.id.to_upper_camel_case();
893 let description = &fixture.description;
894
895 if fixture.http.is_some() {
897 render_http_test_function(out, fixture);
898 return;
899 }
900
901 if !fixture_has_go_callable(fixture, e2e_config) {
906 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
907 let _ = writeln!(out, "\t// {description}");
908 let _ = writeln!(
909 out,
910 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
911 );
912 let _ = writeln!(out, "}}");
913 return;
914 }
915
916 let call_config = e2e_config.resolve_call_for_fixture(
918 fixture.call.as_deref(),
919 &fixture.id,
920 &fixture.resolved_category(),
921 &fixture.tags,
922 &fixture.input,
923 );
924 let call_field_resolver = FieldResolver::new(
926 e2e_config.effective_fields(call_config),
927 e2e_config.effective_fields_optional(call_config),
928 e2e_config.effective_result_fields(call_config),
929 e2e_config.effective_fields_array(call_config),
930 &std::collections::HashSet::new(),
931 );
932 let field_resolver = &call_field_resolver;
933 let lang = "go";
934 let overrides = call_config.overrides.get(lang);
935
936 let base_function_name = overrides
940 .and_then(|o| o.function.as_deref())
941 .unwrap_or(&call_config.function);
942 let function_name = to_go_name(base_function_name);
943 let result_var = &call_config.result_var;
944 let args = &call_config.args;
945
946 let returns_result = overrides
949 .and_then(|o| o.returns_result)
950 .unwrap_or(call_config.returns_result);
951
952 let returns_void = call_config.returns_void;
955
956 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
962 || call_config.result_is_simple
963 || call_config
964 .overrides
965 .get("rust")
966 .map(|o| o.result_is_simple)
967 .unwrap_or(false);
968
969 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
975
976 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
978 e2e_config
979 .call
980 .overrides
981 .get("go")
982 .and_then(|o| o.options_type.as_deref())
983 });
984
985 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
987 e2e_config
988 .call
989 .overrides
990 .get("go")
991 .map(|o| o.options_ptr)
992 .unwrap_or(false)
993 });
994
995 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
996 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1000
1001 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1004 e2e_config
1005 .call
1006 .overrides
1007 .get(lang)
1008 .and_then(|o| o.client_factory.as_deref())
1009 });
1010
1011 let (mut setup_lines, args_str) = build_args_and_setup(
1012 &fixture.input,
1013 args,
1014 import_alias,
1015 call_options_type,
1016 fixture,
1017 call_options_ptr,
1018 validation_creation_failure,
1019 );
1020
1021 let mut visitor_opts_var: Option<String> = None;
1024 if fixture.visitor.is_some() {
1025 let struct_name = visitor_struct_name(&fixture.id);
1026 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1027 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1029 let opts_var = "opts".to_string();
1030 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1031 setup_lines.push("opts.Visitor = visitor".to_string());
1032 visitor_opts_var = Some(opts_var);
1033 }
1034
1035 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1036 let final_args = {
1037 let mut parts: Vec<String> = Vec::new();
1038 if !args_str.is_empty() {
1039 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1041 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1042 } else {
1043 args_str
1044 };
1045 parts.push(processed_args);
1046 }
1047 parts.extend(go_extra_args);
1048 parts.join(", ")
1049 };
1050
1051 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1052 let _ = writeln!(out, "\t// {description}");
1053
1054 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1058 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1059 if let Some(var) = api_key_var {
1060 if has_mock {
1061 let fixture_id = &fixture.id;
1065 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1066 let _ = writeln!(out, "\tvar baseURL *string");
1067 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1068 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1069 let _ = writeln!(out, "\t}} else {{");
1070 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1071 let _ = writeln!(
1072 out,
1073 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1074 );
1075 let _ = writeln!(out, "\t\tbaseURL = &u");
1076 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1077 let _ = writeln!(out, "\t}}");
1078 } else {
1079 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1080 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1081 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1082 let _ = writeln!(out, "\t}}");
1083 }
1084 }
1085
1086 for line in &setup_lines {
1087 let _ = writeln!(out, "\t{line}");
1088 }
1089
1090 let call_prefix = if let Some(factory) = client_factory {
1094 let factory_name = to_go_name(factory);
1095 let fixture_id = &fixture.id;
1096 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1099 ("apiKey".to_string(), "baseURL".to_string())
1101 } else if api_key_var.is_some() {
1102 ("apiKey".to_string(), "nil".to_string())
1104 } else if fixture.has_host_root_route() {
1105 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1106 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1107 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1108 let _ = writeln!(
1109 out,
1110 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1111 );
1112 let _ = writeln!(out, "\t}}");
1113 ("\"test-key\"".to_string(), "&mockURL".to_string())
1114 } else {
1115 let _ = writeln!(
1116 out,
1117 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1118 );
1119 ("\"test-key\"".to_string(), "&mockURL".to_string())
1120 };
1121 let _ = writeln!(
1122 out,
1123 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1124 );
1125 let _ = writeln!(out, "\tif clientErr != nil {{");
1126 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1127 let _ = writeln!(out, "\t}}");
1128 "client".to_string()
1129 } else {
1130 import_alias.to_string()
1131 };
1132
1133 let binding_returns_error_pre = args
1138 .iter()
1139 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1140 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1141
1142 if expects_error {
1143 if effective_returns_result_pre && !returns_void {
1144 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1145 } else {
1146 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1147 }
1148 let _ = writeln!(out, "\tif err == nil {{");
1149 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1150 let _ = writeln!(out, "\t}}");
1151 let _ = writeln!(out, "}}");
1152 return;
1153 }
1154
1155 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1157
1158 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1163 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1164 return false;
1165 }
1166 if a.assertion_type == "method_result" {
1168 return true;
1169 }
1170 match &a.field {
1171 Some(f) if !f.is_empty() => {
1172 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1173 return true;
1174 }
1175 field_resolver.is_valid_for_result(f)
1176 }
1177 _ => true,
1178 }
1179 });
1180
1181 let binding_returns_error = args
1188 .iter()
1189 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1190 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1192
1193 if !effective_returns_result && result_is_simple {
1199 let result_binding = if has_usable_assertion {
1201 result_var.to_string()
1202 } else {
1203 "_".to_string()
1204 };
1205 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1207 let _ = writeln!(
1208 out,
1209 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1210 );
1211 if has_usable_assertion && result_binding != "_" {
1212 if result_is_array {
1213 let _ = writeln!(out, "\tvalue := {result_var}");
1215 } else {
1216 let only_nil_assertions = fixture
1219 .assertions
1220 .iter()
1221 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1222 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1223 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1224
1225 if !only_nil_assertions {
1226 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1229 if result_is_ptr {
1230 let _ = writeln!(out, "\tif {result_var} == nil {{");
1231 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1232 let _ = writeln!(out, "\t}}");
1233 let _ = writeln!(out, "\tvalue := *{result_var}");
1234 } else {
1235 let _ = writeln!(out, "\tvalue := {result_var}");
1237 }
1238 }
1239 }
1240 }
1241 } else if !effective_returns_result || returns_void {
1242 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1245 let _ = writeln!(out, "\tif err != nil {{");
1246 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1247 let _ = writeln!(out, "\t}}");
1248 let _ = writeln!(out, "}}");
1250 return;
1251 } else {
1252 let result_binding = if is_streaming {
1255 "stream".to_string()
1256 } else if has_usable_assertion {
1257 result_var.to_string()
1258 } else {
1259 "_".to_string()
1260 };
1261 let _ = writeln!(
1262 out,
1263 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1264 );
1265 let _ = writeln!(out, "\tif err != nil {{");
1266 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1267 let _ = writeln!(out, "\t}}");
1268 if is_streaming {
1270 let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1271 let _ = writeln!(out, "\tfor chunk := range stream {{");
1272 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1273 let _ = writeln!(out, "\t}}");
1274 }
1275 if result_is_simple && has_usable_assertion && result_binding != "_" {
1276 if result_is_array {
1277 let _ = writeln!(out, "\tvalue := {}", result_var);
1279 } else {
1280 let only_nil_assertions = fixture
1283 .assertions
1284 .iter()
1285 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1286 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1287 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1288
1289 if !only_nil_assertions {
1290 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1293 if result_is_ptr {
1294 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1295 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1296 let _ = writeln!(out, "\t}}");
1297 let _ = writeln!(out, "\tvalue := *{}", result_var);
1298 } else {
1299 let _ = writeln!(out, "\tvalue := {}", result_var);
1301 }
1302 }
1303 }
1304 }
1305 }
1306
1307 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1311 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1312 let only_nil_assertions = fixture
1313 .assertions
1314 .iter()
1315 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1316 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1317 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1318 !only_nil_assertions
1319 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1320 true
1321 } else {
1322 result_is_simple && has_usable_assertion
1323 };
1324
1325 let effective_result_var = if has_deref_value {
1326 "value".to_string()
1327 } else {
1328 result_var.to_string()
1329 };
1330
1331 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1336 for assertion in &fixture.assertions {
1337 if let Some(f) = &assertion.field {
1338 if !f.is_empty() {
1339 let resolved = field_resolver.resolve(f);
1340 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1341 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1346 let is_array_field = field_resolver.is_array(resolved);
1347 if !is_string_field || is_array_field {
1348 continue;
1351 }
1352 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1353 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1354 if field_resolver.has_map_access(f) {
1355 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1358 } else {
1359 let _ = writeln!(out, "\tvar {local_var} string");
1360 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1361 let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1365 let _ = writeln!(out, "\t}}");
1366 }
1367 optional_locals.insert(f.clone(), local_var);
1368 }
1369 }
1370 }
1371 }
1372
1373 for assertion in &fixture.assertions {
1375 if let Some(f) = &assertion.field {
1376 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1377 let parts: Vec<&str> = f.split('.').collect();
1380 let mut guard_expr: Option<String> = None;
1381 for i in 1..parts.len() {
1382 let prefix = parts[..i].join(".");
1383 let resolved_prefix = field_resolver.resolve(&prefix);
1384 if field_resolver.is_optional(resolved_prefix) {
1385 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1391 let suffix = &resolved_prefix[bracket_pos + 1..];
1392 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1393 if is_numeric_index {
1394 &resolved_prefix[..bracket_pos]
1395 } else {
1396 resolved_prefix
1397 }
1398 } else {
1399 resolved_prefix
1400 };
1401 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1402 guard_expr = Some(accessor);
1403 break;
1404 }
1405 }
1406 if let Some(guard) = guard_expr {
1407 if field_resolver.is_valid_for_result(f) {
1410 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1416 if is_struct_value {
1417 render_assertion(
1420 out,
1421 assertion,
1422 &effective_result_var,
1423 import_alias,
1424 field_resolver,
1425 &optional_locals,
1426 result_is_simple,
1427 result_is_array,
1428 is_streaming,
1429 );
1430 continue;
1431 }
1432 let _ = writeln!(out, "\tif {guard} != nil {{");
1433 let mut nil_buf = String::new();
1436 render_assertion(
1437 &mut nil_buf,
1438 assertion,
1439 &effective_result_var,
1440 import_alias,
1441 field_resolver,
1442 &optional_locals,
1443 result_is_simple,
1444 result_is_array,
1445 is_streaming,
1446 );
1447 for line in nil_buf.lines() {
1448 let _ = writeln!(out, "\t{line}");
1449 }
1450 let _ = writeln!(out, "\t}}");
1451 } else {
1452 render_assertion(
1453 out,
1454 assertion,
1455 &effective_result_var,
1456 import_alias,
1457 field_resolver,
1458 &optional_locals,
1459 result_is_simple,
1460 result_is_array,
1461 is_streaming,
1462 );
1463 }
1464 continue;
1465 }
1466 }
1467 }
1468 render_assertion(
1469 out,
1470 assertion,
1471 &effective_result_var,
1472 import_alias,
1473 field_resolver,
1474 &optional_locals,
1475 result_is_simple,
1476 result_is_array,
1477 is_streaming,
1478 );
1479 }
1480
1481 let _ = writeln!(out, "}}");
1482}
1483
1484fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1490 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1491}
1492
1493struct GoTestClientRenderer;
1505
1506impl client::TestClientRenderer for GoTestClientRenderer {
1507 fn language_name(&self) -> &'static str {
1508 "go"
1509 }
1510
1511 fn sanitize_test_name(&self, id: &str) -> String {
1515 id.to_upper_camel_case()
1516 }
1517
1518 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1521 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1522 let _ = writeln!(out, "\t// {description}");
1523 if let Some(reason) = skip_reason {
1524 let escaped = go_string_literal(reason);
1525 let _ = writeln!(out, "\tt.Skip({escaped})");
1526 }
1527 }
1528
1529 fn render_test_close(&self, out: &mut String) {
1530 let _ = writeln!(out, "}}");
1531 }
1532
1533 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1539 let method = ctx.method.to_uppercase();
1540 let path = ctx.path;
1541
1542 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1543 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1544 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1545 let _ = writeln!(out, "\t}}");
1546
1547 let body_expr = if let Some(body) = ctx.body {
1549 let json = serde_json::to_string(body).unwrap_or_default();
1550 let escaped = go_string_literal(&json);
1551 format!("strings.NewReader({})", escaped)
1552 } else {
1553 "strings.NewReader(\"\")".to_string()
1554 };
1555
1556 let _ = writeln!(out, "\tbody := {body_expr}");
1557 let _ = writeln!(
1558 out,
1559 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1560 );
1561 let _ = writeln!(out, "\tif err != nil {{");
1562 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1563 let _ = writeln!(out, "\t}}");
1564
1565 if ctx.body.is_some() {
1567 let content_type = ctx.content_type.unwrap_or("application/json");
1568 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1569 }
1570
1571 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1573 header_names.sort();
1574 for name in header_names {
1575 let value = &ctx.headers[name];
1576 let escaped_name = go_string_literal(name);
1577 let escaped_value = go_string_literal(value);
1578 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1579 }
1580
1581 if !ctx.cookies.is_empty() {
1583 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1584 cookie_names.sort();
1585 for name in cookie_names {
1586 let value = &ctx.cookies[name];
1587 let escaped_name = go_string_literal(name);
1588 let escaped_value = go_string_literal(value);
1589 let _ = writeln!(
1590 out,
1591 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1592 );
1593 }
1594 }
1595
1596 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1598 let _ = writeln!(
1599 out,
1600 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1601 );
1602 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1603 let _ = writeln!(out, "\t\t}},");
1604 let _ = writeln!(out, "\t}}");
1605 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1606 let _ = writeln!(out, "\tif err != nil {{");
1607 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1608 let _ = writeln!(out, "\t}}");
1609 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1610
1611 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1615 let _ = writeln!(out, "\tif err != nil {{");
1616 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1617 let _ = writeln!(out, "\t}}");
1618 let _ = writeln!(out, "\t_ = bodyBytes");
1619 }
1620
1621 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1622 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1623 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1624 let _ = writeln!(out, "\t}}");
1625 }
1626
1627 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1630 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1632 return;
1633 }
1634 if name.eq_ignore_ascii_case("connection") {
1636 return;
1637 }
1638 let escaped_name = go_string_literal(name);
1639 let escaped_value = go_string_literal(expected);
1640 let _ = writeln!(
1641 out,
1642 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1643 );
1644 let _ = writeln!(
1645 out,
1646 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1647 );
1648 let _ = writeln!(out, "\t}}");
1649 }
1650
1651 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1656 match expected {
1657 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1658 let json_str = serde_json::to_string(expected).unwrap_or_default();
1659 let escaped = go_string_literal(&json_str);
1660 let _ = writeln!(out, "\tvar got any");
1661 let _ = writeln!(out, "\tvar want any");
1662 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1663 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1664 let _ = writeln!(out, "\t}}");
1665 let _ = writeln!(
1666 out,
1667 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1668 );
1669 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1670 let _ = writeln!(out, "\t}}");
1671 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1672 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1673 let _ = writeln!(out, "\t}}");
1674 }
1675 serde_json::Value::String(s) => {
1676 let escaped = go_string_literal(s);
1677 let _ = writeln!(out, "\twant := {escaped}");
1678 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1679 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1680 let _ = writeln!(out, "\t}}");
1681 }
1682 other => {
1683 let escaped = go_string_literal(&other.to_string());
1684 let _ = writeln!(out, "\twant := {escaped}");
1685 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1686 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1687 let _ = writeln!(out, "\t}}");
1688 }
1689 }
1690 }
1691
1692 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1695 if let Some(obj) = expected.as_object() {
1696 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1697 let _ = writeln!(
1698 out,
1699 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1700 );
1701 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1702 let _ = writeln!(out, "\t}}");
1703 for (key, val) in obj {
1704 let escaped_key = go_string_literal(key);
1705 let json_val = serde_json::to_string(val).unwrap_or_default();
1706 let escaped_val = go_string_literal(&json_val);
1707 let _ = writeln!(out, "\t{{");
1708 let _ = writeln!(out, "\t\tvar _wantVal any");
1709 let _ = writeln!(
1710 out,
1711 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1712 );
1713 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1714 let _ = writeln!(out, "\t\t}}");
1715 let _ = writeln!(
1716 out,
1717 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1718 );
1719 let _ = writeln!(
1720 out,
1721 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1722 );
1723 let _ = writeln!(out, "\t\t}}");
1724 let _ = writeln!(out, "\t}}");
1725 }
1726 }
1727 }
1728
1729 fn render_assert_validation_errors(
1734 &self,
1735 out: &mut String,
1736 _response_var: &str,
1737 errors: &[ValidationErrorExpectation],
1738 ) {
1739 let _ = writeln!(out, "\tvar _veBody map[string]any");
1740 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1741 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1742 let _ = writeln!(out, "\t}}");
1743 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1744 for ve in errors {
1745 let escaped_msg = go_string_literal(&ve.msg);
1746 let _ = writeln!(out, "\t{{");
1747 let _ = writeln!(out, "\t\t_found := false");
1748 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1749 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1750 let _ = writeln!(
1751 out,
1752 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1753 );
1754 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1755 let _ = writeln!(out, "\t\t\t\t\tbreak");
1756 let _ = writeln!(out, "\t\t\t\t}}");
1757 let _ = writeln!(out, "\t\t\t}}");
1758 let _ = writeln!(out, "\t\t}}");
1759 let _ = writeln!(out, "\t\tif !_found {{");
1760 let _ = writeln!(
1761 out,
1762 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1763 );
1764 let _ = writeln!(out, "\t\t}}");
1765 let _ = writeln!(out, "\t}}");
1766 }
1767 }
1768}
1769
1770fn build_args_and_setup(
1778 input: &serde_json::Value,
1779 args: &[crate::config::ArgMapping],
1780 import_alias: &str,
1781 options_type: Option<&str>,
1782 fixture: &crate::fixture::Fixture,
1783 options_ptr: bool,
1784 expects_error: bool,
1785) -> (Vec<String>, String) {
1786 let fixture_id = &fixture.id;
1787 use heck::ToUpperCamelCase;
1788
1789 if args.is_empty() {
1790 return (Vec::new(), String::new());
1791 }
1792
1793 let mut setup_lines: Vec<String> = Vec::new();
1794 let mut parts: Vec<String> = Vec::new();
1795
1796 for arg in args {
1797 if arg.arg_type == "mock_url" {
1798 if fixture.has_host_root_route() {
1799 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1800 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1801 setup_lines.push(format!(
1802 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1803 arg.name, arg.name
1804 ));
1805 } else {
1806 setup_lines.push(format!(
1807 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1808 arg.name,
1809 ));
1810 }
1811 parts.push(arg.name.clone());
1812 continue;
1813 }
1814
1815 if arg.arg_type == "handle" {
1816 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1818 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1819 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1820 let create_err_handler = if expects_error {
1824 "assert.Error(t, createErr)\n\t\treturn".to_string()
1825 } else {
1826 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1827 };
1828 if config_value.is_null()
1829 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1830 {
1831 setup_lines.push(format!(
1832 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1833 name = arg.name,
1834 ));
1835 } else {
1836 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1837 let go_literal = go_string_literal(&json_str);
1838 let name = &arg.name;
1839 setup_lines.push(format!(
1840 "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}}"
1841 ));
1842 setup_lines.push(format!(
1843 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1844 ));
1845 }
1846 parts.push(arg.name.clone());
1847 continue;
1848 }
1849
1850 let val: Option<&serde_json::Value> = if arg.field == "input" {
1851 Some(input)
1852 } else {
1853 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1854 input.get(field)
1855 };
1856
1857 if arg.arg_type == "bytes" {
1864 let var_name = format!("{}Bytes", arg.name);
1865 match val {
1866 None | Some(serde_json::Value::Null) => {
1867 if arg.optional {
1868 parts.push("nil".to_string());
1869 } else {
1870 parts.push("[]byte{}".to_string());
1871 }
1872 }
1873 Some(serde_json::Value::String(s)) => {
1874 let go_path = go_string_literal(s);
1879 setup_lines.push(format!(
1880 "{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}}"
1881 ));
1882 parts.push(var_name);
1883 }
1884 Some(other) => {
1885 parts.push(format!("[]byte({})", json_to_go(other)));
1886 }
1887 }
1888 continue;
1889 }
1890
1891 match val {
1892 None | Some(serde_json::Value::Null) if arg.optional => {
1893 match arg.arg_type.as_str() {
1895 "string" => {
1896 parts.push("nil".to_string());
1898 }
1899 "json_object" => {
1900 if options_ptr {
1901 parts.push("nil".to_string());
1903 } else if let Some(opts_type) = options_type {
1904 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1906 } else {
1907 parts.push("nil".to_string());
1908 }
1909 }
1910 _ => {
1911 parts.push("nil".to_string());
1912 }
1913 }
1914 }
1915 None | Some(serde_json::Value::Null) => {
1916 let default_val = match arg.arg_type.as_str() {
1918 "string" => "\"\"".to_string(),
1919 "int" | "integer" | "i64" => "0".to_string(),
1920 "float" | "number" => "0.0".to_string(),
1921 "bool" | "boolean" => "false".to_string(),
1922 "json_object" => {
1923 if options_ptr {
1924 "nil".to_string()
1926 } else if let Some(opts_type) = options_type {
1927 format!("{import_alias}.{opts_type}{{}}")
1928 } else {
1929 "nil".to_string()
1930 }
1931 }
1932 _ => "nil".to_string(),
1933 };
1934 parts.push(default_val);
1935 }
1936 Some(v) => {
1937 match arg.arg_type.as_str() {
1938 "json_object" => {
1939 let is_array = v.is_array();
1942 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1943 if is_empty_obj {
1944 if options_ptr {
1945 parts.push("nil".to_string());
1947 } else if let Some(opts_type) = options_type {
1948 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1949 } else {
1950 parts.push("nil".to_string());
1951 }
1952 } else if is_array {
1953 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1958 if go_t.starts_with('[') {
1962 go_t.to_string()
1963 } else {
1964 let qualified = if go_t.contains('.') {
1966 go_t.to_string()
1967 } else {
1968 format!("{import_alias}.{go_t}")
1969 };
1970 format!("[]{qualified}")
1971 }
1972 } else {
1973 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1974 };
1975 let converted_v = convert_json_for_go(v.clone());
1977 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1978 let go_literal = go_string_literal(&json_str);
1979 let var_name = &arg.name;
1980 setup_lines.push(format!(
1981 "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}}"
1982 ));
1983 parts.push(var_name.to_string());
1984 } else if let Some(opts_type) = options_type {
1985 let remapped_v = if options_ptr {
1990 convert_json_for_go(v.clone())
1991 } else {
1992 v.clone()
1993 };
1994 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1995 let go_literal = go_string_literal(&json_str);
1996 let var_name = &arg.name;
1997 setup_lines.push(format!(
1998 "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}}"
1999 ));
2000 let arg_expr = if options_ptr {
2002 format!("&{var_name}")
2003 } else {
2004 var_name.to_string()
2005 };
2006 parts.push(arg_expr);
2007 } else {
2008 parts.push(json_to_go(v));
2009 }
2010 }
2011 "string" if arg.optional => {
2012 let var_name = format!("{}Val", arg.name);
2014 let go_val = json_to_go(v);
2015 setup_lines.push(format!("{var_name} := {go_val}"));
2016 parts.push(format!("&{var_name}"));
2017 }
2018 _ => {
2019 parts.push(json_to_go(v));
2020 }
2021 }
2022 }
2023 }
2024 }
2025
2026 (setup_lines, parts.join(", "))
2027}
2028
2029#[allow(clippy::too_many_arguments)]
2030fn render_assertion(
2031 out: &mut String,
2032 assertion: &Assertion,
2033 result_var: &str,
2034 import_alias: &str,
2035 field_resolver: &FieldResolver,
2036 optional_locals: &std::collections::HashMap<String, String>,
2037 result_is_simple: bool,
2038 result_is_array: bool,
2039 is_streaming: bool,
2040) {
2041 if !result_is_simple {
2044 if let Some(f) = &assertion.field {
2045 let embed_deref = format!("(*{result_var})");
2048 match f.as_str() {
2049 "chunks_have_content" => {
2050 let pred = format!(
2051 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2052 );
2053 match assertion.assertion_type.as_str() {
2054 "is_true" => {
2055 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2056 }
2057 "is_false" => {
2058 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2059 }
2060 _ => {
2061 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2062 }
2063 }
2064 return;
2065 }
2066 "chunks_have_embeddings" => {
2067 let pred = format!(
2068 "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 }}()"
2069 );
2070 match assertion.assertion_type.as_str() {
2071 "is_true" => {
2072 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2073 }
2074 "is_false" => {
2075 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2076 }
2077 _ => {
2078 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2079 }
2080 }
2081 return;
2082 }
2083 "embeddings" => {
2084 match assertion.assertion_type.as_str() {
2085 "count_equals" => {
2086 if let Some(val) = &assertion.value {
2087 if let Some(n) = val.as_u64() {
2088 let _ = writeln!(
2089 out,
2090 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2091 );
2092 }
2093 }
2094 }
2095 "count_min" => {
2096 if let Some(val) = &assertion.value {
2097 if let Some(n) = val.as_u64() {
2098 let _ = writeln!(
2099 out,
2100 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2101 );
2102 }
2103 }
2104 }
2105 "not_empty" => {
2106 let _ = writeln!(
2107 out,
2108 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2109 );
2110 }
2111 "is_empty" => {
2112 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2113 }
2114 _ => {
2115 let _ = writeln!(
2116 out,
2117 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2118 );
2119 }
2120 }
2121 return;
2122 }
2123 "embedding_dimensions" => {
2124 let expr = format!(
2125 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2126 );
2127 match assertion.assertion_type.as_str() {
2128 "equals" => {
2129 if let Some(val) = &assertion.value {
2130 if let Some(n) = val.as_u64() {
2131 let _ = writeln!(
2132 out,
2133 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2134 );
2135 }
2136 }
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 _ => {
2146 let _ = writeln!(
2147 out,
2148 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2149 );
2150 }
2151 }
2152 return;
2153 }
2154 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2155 let pred = match f.as_str() {
2156 "embeddings_valid" => {
2157 format!(
2158 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2159 )
2160 }
2161 "embeddings_finite" => {
2162 format!(
2163 "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 }}()"
2164 )
2165 }
2166 "embeddings_non_zero" => {
2167 format!(
2168 "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 }}()"
2169 )
2170 }
2171 "embeddings_normalized" => {
2172 format!(
2173 "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 }}()"
2174 )
2175 }
2176 _ => unreachable!(),
2177 };
2178 match assertion.assertion_type.as_str() {
2179 "is_true" => {
2180 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2181 }
2182 "is_false" => {
2183 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2184 }
2185 _ => {
2186 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2187 }
2188 }
2189 return;
2190 }
2191 "keywords" | "keywords_count" => {
2194 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2195 return;
2196 }
2197 _ => {}
2198 }
2199 }
2200 }
2201
2202 if !result_is_simple && is_streaming {
2209 if let Some(f) = &assertion.field {
2210 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2211 if let Some(expr) =
2212 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2213 {
2214 match assertion.assertion_type.as_str() {
2215 "count_min" => {
2216 if let Some(val) = &assertion.value {
2217 if let Some(n) = val.as_u64() {
2218 let _ = writeln!(
2219 out,
2220 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2221 );
2222 }
2223 }
2224 }
2225 "count_equals" => {
2226 if let Some(val) = &assertion.value {
2227 if let Some(n) = val.as_u64() {
2228 let _ = writeln!(
2229 out,
2230 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2231 );
2232 }
2233 }
2234 }
2235 "equals" => {
2236 if let Some(serde_json::Value::String(s)) = &assertion.value {
2237 let escaped = crate::escape::go_string_literal(s);
2238 let is_deep_path = f.contains('.') || f.contains('[');
2243 let safe_expr = if is_deep_path {
2244 format!(
2245 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2246 )
2247 } else {
2248 expr.clone()
2249 };
2250 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2251 } else if let Some(val) = &assertion.value {
2252 if let Some(n) = val.as_u64() {
2253 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2254 }
2255 }
2256 }
2257 "not_empty" => {
2258 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2259 }
2260 "is_empty" => {
2261 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2262 }
2263 "is_true" => {
2264 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2265 }
2266 "is_false" => {
2267 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2268 }
2269 "greater_than" => {
2270 if let Some(val) = &assertion.value {
2271 if let Some(n) = val.as_u64() {
2272 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2273 }
2274 }
2275 }
2276 "greater_than_or_equal" => {
2277 if let Some(val) = &assertion.value {
2278 if let Some(n) = val.as_u64() {
2279 let _ =
2280 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2281 }
2282 }
2283 }
2284 "contains" => {
2285 if let Some(serde_json::Value::String(s)) = &assertion.value {
2286 let escaped = crate::escape::go_string_literal(s);
2287 let _ =
2288 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2289 }
2290 }
2291 _ => {
2292 let _ = writeln!(
2293 out,
2294 "\t// streaming field '{f}': assertion type '{}' not rendered",
2295 assertion.assertion_type
2296 );
2297 }
2298 }
2299 }
2300 return;
2301 }
2302 }
2303 }
2304
2305 if !result_is_simple {
2308 if let Some(f) = &assertion.field {
2309 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2310 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2311 return;
2312 }
2313 }
2314 }
2315
2316 let field_expr = if result_is_simple {
2317 result_var.to_string()
2319 } else {
2320 match &assertion.field {
2321 Some(f) if !f.is_empty() => {
2322 if let Some(local_var) = optional_locals.get(f.as_str()) {
2324 local_var.clone()
2325 } else {
2326 field_resolver.accessor(f, "go", result_var)
2327 }
2328 }
2329 _ => result_var.to_string(),
2330 }
2331 };
2332
2333 let is_optional = assertion
2337 .field
2338 .as_ref()
2339 .map(|f| {
2340 let resolved = field_resolver.resolve(f);
2341 let check_path = resolved
2342 .strip_suffix(".length")
2343 .or_else(|| resolved.strip_suffix(".count"))
2344 .or_else(|| resolved.strip_suffix(".size"))
2345 .unwrap_or(resolved);
2346 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2347 })
2348 .unwrap_or(false);
2349
2350 let field_is_array_for_len = assertion
2354 .field
2355 .as_ref()
2356 .map(|f| {
2357 let resolved = field_resolver.resolve(f);
2358 let check_path = resolved
2359 .strip_suffix(".length")
2360 .or_else(|| resolved.strip_suffix(".count"))
2361 .or_else(|| resolved.strip_suffix(".size"))
2362 .unwrap_or(resolved);
2363 field_resolver.is_array(check_path)
2364 })
2365 .unwrap_or(false);
2366 let field_expr =
2367 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2368 let inner = &field_expr[4..field_expr.len() - 1];
2369 format!("len(*{inner})")
2370 } else {
2371 field_expr
2372 };
2373 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2375 Some(field_expr[5..field_expr.len() - 1].to_string())
2376 } else {
2377 None
2378 };
2379
2380 let field_is_slice = assertion
2384 .field
2385 .as_ref()
2386 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2387 .unwrap_or(false);
2388 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2389 format!("*{field_expr}")
2390 } else {
2391 field_expr.clone()
2392 };
2393
2394 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2399 let mut array_expr = field_expr[..idx].to_string();
2400 if let Some(stripped) = array_expr.strip_prefix("len(") {
2401 array_expr = stripped.to_string();
2402 }
2403 Some(array_expr)
2404 } else {
2405 None
2406 };
2407
2408 let mut assertion_buf = String::new();
2411 let out_ref = &mut assertion_buf;
2412
2413 match assertion.assertion_type.as_str() {
2414 "equals" => {
2415 if let Some(expected) = &assertion.value {
2416 let go_val = json_to_go(expected);
2417 if expected.is_string() {
2419 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2422 format!("strings.TrimSpace(string(*{field_expr}))")
2423 } else {
2424 format!("strings.TrimSpace(string({field_expr}))")
2425 };
2426 if is_optional && !field_expr.starts_with("len(") {
2427 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2428 } else {
2429 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2430 }
2431 } else if is_optional && !field_expr.starts_with("len(") {
2432 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2433 } else {
2434 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2435 }
2436 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2437 let _ = writeln!(out_ref, "\t}}");
2438 }
2439 }
2440 "contains" => {
2441 if let Some(expected) = &assertion.value {
2442 let go_val = json_to_go(expected);
2443 let resolved_field = assertion.field.as_deref().unwrap_or("");
2449 let resolved_name = field_resolver.resolve(resolved_field);
2450 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2451 let is_opt =
2452 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2453 let field_for_contains = if is_opt && field_is_array {
2454 format!("jsonString({field_expr})")
2456 } else if is_opt {
2457 format!("fmt.Sprint(*{field_expr})")
2458 } else if field_is_array {
2459 format!("jsonString({field_expr})")
2460 } else {
2461 format!("fmt.Sprint({field_expr})")
2462 };
2463 if is_opt {
2464 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2465 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2466 let _ = writeln!(
2467 out_ref,
2468 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2469 );
2470 let _ = writeln!(out_ref, "\t}}");
2471 let _ = writeln!(out_ref, "\t}}");
2472 } else {
2473 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2474 let _ = writeln!(
2475 out_ref,
2476 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2477 );
2478 let _ = writeln!(out_ref, "\t}}");
2479 }
2480 }
2481 }
2482 "contains_all" => {
2483 if let Some(values) = &assertion.values {
2484 let resolved_field = assertion.field.as_deref().unwrap_or("");
2485 let resolved_name = field_resolver.resolve(resolved_field);
2486 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2487 let is_opt =
2488 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2489 for val in values {
2490 let go_val = json_to_go(val);
2491 let field_for_contains = if is_opt && field_is_array {
2492 format!("jsonString({field_expr})")
2494 } else if is_opt {
2495 format!("fmt.Sprint(*{field_expr})")
2496 } else if field_is_array {
2497 format!("jsonString({field_expr})")
2498 } else {
2499 format!("fmt.Sprint({field_expr})")
2500 };
2501 if is_opt {
2502 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2503 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2504 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2505 let _ = writeln!(out_ref, "\t}}");
2506 let _ = writeln!(out_ref, "\t}}");
2507 } else {
2508 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2509 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2510 let _ = writeln!(out_ref, "\t}}");
2511 }
2512 }
2513 }
2514 }
2515 "not_contains" => {
2516 if let Some(expected) = &assertion.value {
2517 let go_val = json_to_go(expected);
2518 let resolved_field = assertion.field.as_deref().unwrap_or("");
2519 let resolved_name = field_resolver.resolve(resolved_field);
2520 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2521 let is_opt =
2522 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2523 let field_for_contains = if is_opt && field_is_array {
2524 format!("jsonString({field_expr})")
2526 } else if is_opt {
2527 format!("fmt.Sprint(*{field_expr})")
2528 } else if field_is_array {
2529 format!("jsonString({field_expr})")
2530 } else {
2531 format!("fmt.Sprint({field_expr})")
2532 };
2533 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2534 let _ = writeln!(
2535 out_ref,
2536 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2537 );
2538 let _ = writeln!(out_ref, "\t}}");
2539 }
2540 }
2541 "not_empty" => {
2542 let field_is_array = {
2545 let rf = assertion.field.as_deref().unwrap_or("");
2546 let rn = field_resolver.resolve(rf);
2547 field_resolver.is_array(rn)
2548 };
2549 if is_optional && !field_is_array {
2550 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2552 } else if is_optional && field_is_slice {
2553 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2555 } else if is_optional {
2556 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2558 } else if result_is_simple && result_is_array {
2559 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2561 } else {
2562 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2563 }
2564 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2565 let _ = writeln!(out_ref, "\t}}");
2566 }
2567 "is_empty" => {
2568 let field_is_array = {
2569 let rf = assertion.field.as_deref().unwrap_or("");
2570 let rn = field_resolver.resolve(rf);
2571 field_resolver.is_array(rn)
2572 };
2573 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2576 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2578 } else if is_optional && !field_is_array {
2579 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2581 } else if is_optional && field_is_slice {
2582 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2584 } else if is_optional {
2585 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2587 } else {
2588 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2589 }
2590 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2591 let _ = writeln!(out_ref, "\t}}");
2592 }
2593 "contains_any" => {
2594 if let Some(values) = &assertion.values {
2595 let resolved_field = assertion.field.as_deref().unwrap_or("");
2596 let resolved_name = field_resolver.resolve(resolved_field);
2597 let field_is_array = field_resolver.is_array(resolved_name);
2598 let is_opt =
2599 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2600 let field_for_contains = if is_opt && field_is_array {
2601 format!("jsonString({field_expr})")
2603 } else if is_opt {
2604 format!("fmt.Sprint(*{field_expr})")
2605 } else if field_is_array {
2606 format!("jsonString({field_expr})")
2607 } else {
2608 format!("fmt.Sprint({field_expr})")
2609 };
2610 let _ = writeln!(out_ref, "\t{{");
2611 let _ = writeln!(out_ref, "\t\tfound := false");
2612 for val in values {
2613 let go_val = json_to_go(val);
2614 let _ = writeln!(
2615 out_ref,
2616 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2617 );
2618 }
2619 let _ = writeln!(out_ref, "\t\tif !found {{");
2620 let _ = writeln!(
2621 out_ref,
2622 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2623 );
2624 let _ = writeln!(out_ref, "\t\t}}");
2625 let _ = writeln!(out_ref, "\t}}");
2626 }
2627 }
2628 "greater_than" => {
2629 if let Some(val) = &assertion.value {
2630 let go_val = json_to_go(val);
2631 if is_optional {
2635 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2636 if let Some(n) = val.as_u64() {
2637 let next = n + 1;
2638 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2639 } else {
2640 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2641 }
2642 let _ = writeln!(
2643 out_ref,
2644 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2645 );
2646 let _ = writeln!(out_ref, "\t\t}}");
2647 let _ = writeln!(out_ref, "\t}}");
2648 } else if let Some(n) = val.as_u64() {
2649 let next = n + 1;
2650 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2651 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2652 let _ = writeln!(out_ref, "\t}}");
2653 } else {
2654 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2655 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2656 let _ = writeln!(out_ref, "\t}}");
2657 }
2658 }
2659 }
2660 "less_than" => {
2661 if let Some(val) = &assertion.value {
2662 let go_val = json_to_go(val);
2663 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2664 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2665 let _ = writeln!(out_ref, "\t}}");
2666 }
2667 }
2668 "greater_than_or_equal" => {
2669 if let Some(val) = &assertion.value {
2670 let go_val = json_to_go(val);
2671 if let Some(ref guard) = nil_guard_expr {
2672 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2673 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2674 let _ = writeln!(
2675 out_ref,
2676 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2677 );
2678 let _ = writeln!(out_ref, "\t\t}}");
2679 let _ = writeln!(out_ref, "\t}}");
2680 } else if is_optional && !field_expr.starts_with("len(") {
2681 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2683 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2684 let _ = writeln!(
2685 out_ref,
2686 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2687 );
2688 let _ = writeln!(out_ref, "\t\t}}");
2689 let _ = writeln!(out_ref, "\t}}");
2690 } else {
2691 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2692 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2693 let _ = writeln!(out_ref, "\t}}");
2694 }
2695 }
2696 }
2697 "less_than_or_equal" => {
2698 if let Some(val) = &assertion.value {
2699 let go_val = json_to_go(val);
2700 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2701 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2702 let _ = writeln!(out_ref, "\t}}");
2703 }
2704 }
2705 "starts_with" => {
2706 if let Some(expected) = &assertion.value {
2707 let go_val = json_to_go(expected);
2708 let field_for_prefix = if is_optional
2709 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2710 {
2711 format!("string(*{field_expr})")
2712 } else {
2713 format!("string({field_expr})")
2714 };
2715 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2716 let _ = writeln!(
2717 out_ref,
2718 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2719 );
2720 let _ = writeln!(out_ref, "\t}}");
2721 }
2722 }
2723 "count_min" => {
2724 if let Some(val) = &assertion.value {
2725 if let Some(n) = val.as_u64() {
2726 if is_optional {
2727 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2728 let len_expr = if field_is_slice {
2730 format!("len({field_expr})")
2731 } else {
2732 format!("len(*{field_expr})")
2733 };
2734 let _ = writeln!(
2735 out_ref,
2736 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2737 );
2738 let _ = writeln!(out_ref, "\t}}");
2739 } else {
2740 let _ = writeln!(
2741 out_ref,
2742 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2743 );
2744 }
2745 }
2746 }
2747 }
2748 "count_equals" => {
2749 if let Some(val) = &assertion.value {
2750 if let Some(n) = val.as_u64() {
2751 if is_optional {
2752 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2753 let len_expr = if field_is_slice {
2755 format!("len({field_expr})")
2756 } else {
2757 format!("len(*{field_expr})")
2758 };
2759 let _ = writeln!(
2760 out_ref,
2761 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2762 );
2763 let _ = writeln!(out_ref, "\t}}");
2764 } else {
2765 let _ = writeln!(
2766 out_ref,
2767 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2768 );
2769 }
2770 }
2771 }
2772 }
2773 "is_true" => {
2774 if is_optional {
2775 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2776 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2777 let _ = writeln!(out_ref, "\t}}");
2778 } else {
2779 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2780 }
2781 }
2782 "is_false" => {
2783 if is_optional {
2784 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2785 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2786 let _ = writeln!(out_ref, "\t}}");
2787 } else {
2788 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2789 }
2790 }
2791 "method_result" => {
2792 if let Some(method_name) = &assertion.method {
2793 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2794 let check = assertion.check.as_deref().unwrap_or("is_true");
2795 let deref_expr = if info.is_pointer {
2798 format!("*{}", info.call_expr)
2799 } else {
2800 info.call_expr.clone()
2801 };
2802 match check {
2803 "equals" => {
2804 if let Some(val) = &assertion.value {
2805 if val.is_boolean() {
2806 if val.as_bool() == Some(true) {
2807 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2808 } else {
2809 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2810 }
2811 } else {
2812 let go_val = if let Some(cast) = info.value_cast {
2816 if val.is_number() {
2817 format!("{cast}({})", json_to_go(val))
2818 } else {
2819 json_to_go(val)
2820 }
2821 } else {
2822 json_to_go(val)
2823 };
2824 let _ = writeln!(
2825 out_ref,
2826 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2827 );
2828 }
2829 }
2830 }
2831 "is_true" => {
2832 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2833 }
2834 "is_false" => {
2835 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2836 }
2837 "greater_than_or_equal" => {
2838 if let Some(val) = &assertion.value {
2839 let n = val.as_u64().unwrap_or(0);
2840 let cast = info.value_cast.unwrap_or("uint");
2842 let _ = writeln!(
2843 out_ref,
2844 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2845 );
2846 }
2847 }
2848 "count_min" => {
2849 if let Some(val) = &assertion.value {
2850 let n = val.as_u64().unwrap_or(0);
2851 let _ = writeln!(
2852 out_ref,
2853 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2854 );
2855 }
2856 }
2857 "contains" => {
2858 if let Some(val) = &assertion.value {
2859 let go_val = json_to_go(val);
2860 let _ = writeln!(
2861 out_ref,
2862 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2863 );
2864 }
2865 }
2866 "is_error" => {
2867 let _ = writeln!(out_ref, "\t{{");
2868 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2869 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2870 let _ = writeln!(out_ref, "\t}}");
2871 }
2872 other_check => {
2873 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2874 }
2875 }
2876 } else {
2877 panic!("Go e2e generator: method_result assertion missing 'method' field");
2878 }
2879 }
2880 "min_length" => {
2881 if let Some(val) = &assertion.value {
2882 if let Some(n) = val.as_u64() {
2883 if is_optional {
2884 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2885 let _ = writeln!(
2886 out_ref,
2887 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2888 );
2889 let _ = writeln!(out_ref, "\t}}");
2890 } else if field_expr.starts_with("len(") {
2891 let _ = writeln!(
2892 out_ref,
2893 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2894 );
2895 } else {
2896 let _ = writeln!(
2897 out_ref,
2898 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2899 );
2900 }
2901 }
2902 }
2903 }
2904 "max_length" => {
2905 if let Some(val) = &assertion.value {
2906 if let Some(n) = val.as_u64() {
2907 if is_optional {
2908 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2909 let _ = writeln!(
2910 out_ref,
2911 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2912 );
2913 let _ = writeln!(out_ref, "\t}}");
2914 } else if field_expr.starts_with("len(") {
2915 let _ = writeln!(
2916 out_ref,
2917 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2918 );
2919 } else {
2920 let _ = writeln!(
2921 out_ref,
2922 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2923 );
2924 }
2925 }
2926 }
2927 }
2928 "ends_with" => {
2929 if let Some(expected) = &assertion.value {
2930 let go_val = json_to_go(expected);
2931 let field_for_suffix = if is_optional
2932 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2933 {
2934 format!("string(*{field_expr})")
2935 } else {
2936 format!("string({field_expr})")
2937 };
2938 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2939 let _ = writeln!(
2940 out_ref,
2941 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2942 );
2943 let _ = writeln!(out_ref, "\t}}");
2944 }
2945 }
2946 "matches_regex" => {
2947 if let Some(expected) = &assertion.value {
2948 let go_val = json_to_go(expected);
2949 let field_for_regex = if is_optional
2950 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2951 {
2952 format!("*{field_expr}")
2953 } else {
2954 field_expr.clone()
2955 };
2956 let _ = writeln!(
2957 out_ref,
2958 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2959 );
2960 }
2961 }
2962 "not_error" => {
2963 }
2965 "error" => {
2966 }
2968 other => {
2969 panic!("Go e2e generator: unsupported assertion type: {other}");
2970 }
2971 }
2972
2973 if let Some(ref arr) = array_guard {
2976 if !assertion_buf.is_empty() {
2977 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2978 for line in assertion_buf.lines() {
2980 let _ = writeln!(out, "\t{line}");
2981 }
2982 let _ = writeln!(out, "\t}}");
2983 }
2984 } else {
2985 out.push_str(&assertion_buf);
2986 }
2987}
2988
2989struct GoMethodCallInfo {
2991 call_expr: String,
2993 is_pointer: bool,
2995 value_cast: Option<&'static str>,
2998}
2999
3000fn build_go_method_call(
3015 result_var: &str,
3016 method_name: &str,
3017 args: Option<&serde_json::Value>,
3018 import_alias: &str,
3019) -> GoMethodCallInfo {
3020 match method_name {
3021 "root_node_type" => GoMethodCallInfo {
3022 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3023 is_pointer: false,
3024 value_cast: None,
3025 },
3026 "named_children_count" => GoMethodCallInfo {
3027 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3028 is_pointer: false,
3029 value_cast: Some("uint"),
3030 },
3031 "has_error_nodes" => GoMethodCallInfo {
3032 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3033 is_pointer: true,
3034 value_cast: None,
3035 },
3036 "error_count" | "tree_error_count" => GoMethodCallInfo {
3037 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3038 is_pointer: true,
3039 value_cast: Some("uint"),
3040 },
3041 "tree_to_sexp" => GoMethodCallInfo {
3042 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3043 is_pointer: true,
3044 value_cast: None,
3045 },
3046 "contains_node_type" => {
3047 let node_type = args
3048 .and_then(|a| a.get("node_type"))
3049 .and_then(|v| v.as_str())
3050 .unwrap_or("");
3051 GoMethodCallInfo {
3052 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3053 is_pointer: true,
3054 value_cast: None,
3055 }
3056 }
3057 "find_nodes_by_type" => {
3058 let node_type = args
3059 .and_then(|a| a.get("node_type"))
3060 .and_then(|v| v.as_str())
3061 .unwrap_or("");
3062 GoMethodCallInfo {
3063 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3064 is_pointer: true,
3065 value_cast: None,
3066 }
3067 }
3068 "run_query" => {
3069 let query_source = args
3070 .and_then(|a| a.get("query_source"))
3071 .and_then(|v| v.as_str())
3072 .unwrap_or("");
3073 let language = args
3074 .and_then(|a| a.get("language"))
3075 .and_then(|v| v.as_str())
3076 .unwrap_or("");
3077 let query_lit = go_string_literal(query_source);
3078 let lang_lit = go_string_literal(language);
3079 GoMethodCallInfo {
3081 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3082 is_pointer: false,
3083 value_cast: None,
3084 }
3085 }
3086 other => {
3087 let method_pascal = other.to_upper_camel_case();
3088 GoMethodCallInfo {
3089 call_expr: format!("{result_var}.{method_pascal}()"),
3090 is_pointer: false,
3091 value_cast: None,
3092 }
3093 }
3094 }
3095}
3096
3097fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3107 match value {
3108 serde_json::Value::Object(map) => {
3109 let new_map: serde_json::Map<String, serde_json::Value> = map
3110 .into_iter()
3111 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3112 .collect();
3113 serde_json::Value::Object(new_map)
3114 }
3115 serde_json::Value::Array(arr) => {
3116 if is_byte_array(&arr) {
3119 let bytes: Vec<u8> = arr
3120 .iter()
3121 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3122 .collect();
3123 let encoded = base64_encode(&bytes);
3125 serde_json::Value::String(encoded)
3126 } else {
3127 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3128 }
3129 }
3130 serde_json::Value::String(s) => {
3131 serde_json::Value::String(pascal_to_snake_case(&s))
3134 }
3135 other => other,
3136 }
3137}
3138
3139fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3141 if arr.is_empty() {
3142 return false;
3143 }
3144 arr.iter().all(|v| {
3145 if let serde_json::Value::Number(n) = v {
3146 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3147 } else {
3148 false
3149 }
3150 })
3151}
3152
3153fn base64_encode(bytes: &[u8]) -> String {
3156 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3157 let mut result = String::new();
3158 let mut i = 0;
3159
3160 while i + 2 < bytes.len() {
3161 let b1 = bytes[i];
3162 let b2 = bytes[i + 1];
3163 let b3 = bytes[i + 2];
3164
3165 result.push(TABLE[(b1 >> 2) as usize] as char);
3166 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3167 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3168 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3169
3170 i += 3;
3171 }
3172
3173 if i < bytes.len() {
3175 let b1 = bytes[i];
3176 result.push(TABLE[(b1 >> 2) as usize] as char);
3177
3178 if i + 1 < bytes.len() {
3179 let b2 = bytes[i + 1];
3180 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3181 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3182 result.push('=');
3183 } else {
3184 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3185 result.push_str("==");
3186 }
3187 }
3188
3189 result
3190}
3191
3192fn camel_to_snake_case(s: &str) -> String {
3194 let mut result = String::new();
3195 let mut prev_upper = false;
3196 for (i, c) in s.char_indices() {
3197 if c.is_uppercase() {
3198 if i > 0 && !prev_upper {
3199 result.push('_');
3200 }
3201 result.push(c.to_lowercase().next().unwrap_or(c));
3202 prev_upper = true;
3203 } else {
3204 if prev_upper && i > 1 {
3205 }
3209 result.push(c);
3210 prev_upper = false;
3211 }
3212 }
3213 result
3214}
3215
3216fn pascal_to_snake_case(s: &str) -> String {
3221 let first_char = s.chars().next();
3223 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3224 return s.to_string();
3225 }
3226 camel_to_snake_case(s)
3227}
3228
3229fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3233 let elem = element_type.unwrap_or("String").trim();
3234 let go_elem = rust_type_to_go(elem, import_alias);
3235 format!("[]{go_elem}")
3236}
3237
3238fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3241 let trimmed = rust.trim();
3242 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3243 return format!("[]{}", rust_type_to_go(inner, import_alias));
3244 }
3245 match trimmed {
3246 "String" | "&str" | "str" => "string".to_string(),
3247 "bool" => "bool".to_string(),
3248 "f32" => "float32".to_string(),
3249 "f64" => "float64".to_string(),
3250 "i8" => "int8".to_string(),
3251 "i16" => "int16".to_string(),
3252 "i32" => "int32".to_string(),
3253 "i64" | "isize" => "int64".to_string(),
3254 "u8" => "uint8".to_string(),
3255 "u16" => "uint16".to_string(),
3256 "u32" => "uint32".to_string(),
3257 "u64" | "usize" => "uint64".to_string(),
3258 _ => format!("{import_alias}.{trimmed}"),
3259 }
3260}
3261
3262fn json_to_go(value: &serde_json::Value) -> String {
3263 match value {
3264 serde_json::Value::String(s) => go_string_literal(s),
3265 serde_json::Value::Bool(b) => b.to_string(),
3266 serde_json::Value::Number(n) => n.to_string(),
3267 serde_json::Value::Null => "nil".to_string(),
3268 other => go_string_literal(&other.to_string()),
3270 }
3271}
3272
3273fn visitor_struct_name(fixture_id: &str) -> String {
3282 use heck::ToUpperCamelCase;
3283 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3285}
3286
3287fn emit_go_visitor_struct(
3292 out: &mut String,
3293 struct_name: &str,
3294 visitor_spec: &crate::fixture::VisitorSpec,
3295 import_alias: &str,
3296) {
3297 let _ = writeln!(out, "type {struct_name} struct{{");
3298 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3299 let _ = writeln!(out, "}}");
3300 for (method_name, action) in &visitor_spec.callbacks {
3301 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3302 }
3303}
3304
3305fn emit_go_visitor_method(
3307 out: &mut String,
3308 struct_name: &str,
3309 method_name: &str,
3310 action: &CallbackAction,
3311 import_alias: &str,
3312) {
3313 let camel_method = method_to_camel(method_name);
3314 let params = match method_name {
3317 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3318 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3319 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3320 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3321 "visit_code_inline"
3322 | "visit_strong"
3323 | "visit_emphasis"
3324 | "visit_strikethrough"
3325 | "visit_underline"
3326 | "visit_subscript"
3327 | "visit_superscript"
3328 | "visit_mark"
3329 | "visit_button"
3330 | "visit_summary"
3331 | "visit_figcaption"
3332 | "visit_definition_term"
3333 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3334 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3335 "visit_list_item" => {
3336 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3337 }
3338 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3339 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3340 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3341 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3342 "visit_input" => {
3343 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3344 }
3345 "visit_audio" | "visit_video" | "visit_iframe" => {
3346 format!("_ {import_alias}.NodeContext, src *string")
3347 }
3348 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3349 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3350 format!("_ {import_alias}.NodeContext, output string")
3351 }
3352 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3353 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3354 _ => format!("_ {import_alias}.NodeContext"),
3355 };
3356
3357 let _ = writeln!(
3358 out,
3359 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3360 );
3361 match action {
3362 CallbackAction::Skip => {
3363 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3364 }
3365 CallbackAction::Continue => {
3366 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3367 }
3368 CallbackAction::PreserveHtml => {
3369 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3370 }
3371 CallbackAction::Custom { output } => {
3372 let escaped = go_string_literal(output);
3373 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3374 }
3375 CallbackAction::CustomTemplate { template, .. } => {
3376 let ptr_params = go_visitor_ptr_params(method_name);
3383 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3384 let escaped_fmt = go_string_literal(&fmt_str);
3385 if fmt_args.is_empty() {
3386 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3387 } else {
3388 let args_str = fmt_args.join(", ");
3389 let _ = writeln!(
3390 out,
3391 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3392 );
3393 }
3394 }
3395 }
3396 let _ = writeln!(out, "}}");
3397}
3398
3399fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3402 match method_name {
3403 "visit_link" => ["title"].into(),
3404 "visit_image" => ["title"].into(),
3405 "visit_heading" => ["id"].into(),
3406 "visit_code_block" => ["lang"].into(),
3407 "visit_form" => ["action", "method"].into(),
3408 "visit_input" => ["name", "value"].into(),
3409 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3410 _ => std::collections::HashSet::new(),
3411 }
3412}
3413
3414fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3426 let mut fmt_str = String::new();
3427 let mut args: Vec<String> = Vec::new();
3428 let mut chars = template.chars().peekable();
3429 while let Some(c) = chars.next() {
3430 if c == '{' {
3431 let mut name = String::new();
3433 for inner in chars.by_ref() {
3434 if inner == '}' {
3435 break;
3436 }
3437 name.push(inner);
3438 }
3439 fmt_str.push_str("%s");
3440 let go_name = go_param_name(&name);
3442 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3444 format!("*{go_name}")
3445 } else {
3446 go_name
3447 };
3448 args.push(arg_expr);
3449 } else {
3450 fmt_str.push(c);
3451 }
3452 }
3453 (fmt_str, args)
3454}
3455
3456fn method_to_camel(snake: &str) -> String {
3458 use heck::ToUpperCamelCase;
3459 snake.to_upper_camel_case()
3460}
3461
3462#[cfg(test)]
3463mod tests {
3464 use super::*;
3465 use crate::config::{CallConfig, E2eConfig};
3466 use crate::fixture::{Assertion, Fixture};
3467
3468 fn make_fixture(id: &str) -> Fixture {
3469 Fixture {
3470 id: id.to_string(),
3471 category: None,
3472 description: "test fixture".to_string(),
3473 tags: vec![],
3474 skip: None,
3475 env: None,
3476 call: None,
3477 input: serde_json::Value::Null,
3478 mock_response: Some(crate::fixture::MockResponse {
3479 status: 200,
3480 body: Some(serde_json::Value::Null),
3481 stream_chunks: None,
3482 headers: std::collections::HashMap::new(),
3483 }),
3484 source: String::new(),
3485 http: None,
3486 assertions: vec![Assertion {
3487 assertion_type: "not_error".to_string(),
3488 ..Default::default()
3489 }],
3490 visitor: None,
3491 }
3492 }
3493
3494 #[test]
3498 fn test_go_method_name_uses_go_casing() {
3499 let e2e_config = E2eConfig {
3500 call: CallConfig {
3501 function: "clean_extracted_text".to_string(),
3502 module: "github.com/example/mylib".to_string(),
3503 result_var: "result".to_string(),
3504 returns_result: true,
3505 ..CallConfig::default()
3506 },
3507 ..E2eConfig::default()
3508 };
3509
3510 let fixture = make_fixture("basic_text");
3511 let mut out = String::new();
3512 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config);
3513
3514 assert!(
3515 out.contains("kreuzberg.CleanExtractedText("),
3516 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3517 );
3518 assert!(
3519 !out.contains("kreuzberg.clean_extracted_text("),
3520 "must not emit raw snake_case method name, got:\n{out}"
3521 );
3522 }
3523
3524 #[test]
3525 fn test_streaming_fixture_emits_collect_snippet() {
3526 let streaming_fixture_json = r#"{
3528 "id": "basic_stream",
3529 "description": "basic streaming test",
3530 "call": "chat_stream",
3531 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3532 "mock_response": {
3533 "status": 200,
3534 "stream_chunks": [{"delta": "hello"}]
3535 },
3536 "assertions": [
3537 {"type": "count_min", "field": "chunks", "value": 1}
3538 ]
3539 }"#;
3540 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3541 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3542
3543 let e2e_config = E2eConfig {
3544 call: CallConfig {
3545 function: "chat_stream".to_string(),
3546 module: "github.com/example/mylib".to_string(),
3547 result_var: "result".to_string(),
3548 returns_result: true,
3549 r#async: true,
3550 ..CallConfig::default()
3551 },
3552 ..E2eConfig::default()
3553 };
3554
3555 let mut out = String::new();
3556 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3557
3558 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3559 assert!(
3560 out.contains("for chunk := range stream"),
3561 "should emit collect loop, got:\n{out}"
3562 );
3563 }
3564
3565 #[test]
3566 fn test_streaming_with_client_factory_and_json_arg() {
3567 use alef_core::config::e2e::{ArgMapping, CallOverride};
3571 let streaming_fixture_json = r#"{
3572 "id": "basic_stream_client",
3573 "description": "basic streaming test with client",
3574 "call": "chat_stream",
3575 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3576 "mock_response": {
3577 "status": 200,
3578 "stream_chunks": [{"delta": "hello"}]
3579 },
3580 "assertions": [
3581 {"type": "count_min", "field": "chunks", "value": 1}
3582 ]
3583 }"#;
3584 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3585 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3586
3587 let go_override = CallOverride {
3588 client_factory: Some("CreateClient".to_string()),
3589 ..Default::default()
3590 };
3591
3592 let mut call_overrides = std::collections::HashMap::new();
3593 call_overrides.insert("go".to_string(), go_override);
3594
3595 let e2e_config = E2eConfig {
3596 call: CallConfig {
3597 function: "chat_stream".to_string(),
3598 module: "github.com/example/mylib".to_string(),
3599 result_var: "result".to_string(),
3600 returns_result: false, r#async: true,
3602 args: vec![ArgMapping {
3603 name: "request".to_string(),
3604 field: "input".to_string(),
3605 arg_type: "json_object".to_string(),
3606 optional: false,
3607 owned: true,
3608 element_type: None,
3609 go_type: None,
3610 }],
3611 overrides: call_overrides,
3612 ..CallConfig::default()
3613 },
3614 ..E2eConfig::default()
3615 };
3616
3617 let mut out = String::new();
3618 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3619
3620 eprintln!("generated:\n{out}");
3621 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3622 assert!(
3623 out.contains("for chunk := range stream"),
3624 "should emit collect loop, got:\n{out}"
3625 );
3626 }
3627
3628 #[test]
3632 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3633 let mut optional_fields = std::collections::HashSet::new();
3634 optional_fields.insert("segments".to_string());
3635 let mut array_fields = std::collections::HashSet::new();
3636 array_fields.insert("segments".to_string());
3637
3638 let e2e_config = E2eConfig {
3639 call: CallConfig {
3640 function: "transcribe".to_string(),
3641 module: "github.com/example/mylib".to_string(),
3642 result_var: "result".to_string(),
3643 returns_result: true,
3644 ..CallConfig::default()
3645 },
3646 fields_optional: optional_fields,
3647 fields_array: array_fields,
3648 ..E2eConfig::default()
3649 };
3650
3651 let fixture = Fixture {
3652 id: "edge_transcribe_with_timestamps".to_string(),
3653 category: None,
3654 description: "Transcription with timestamp segments".to_string(),
3655 tags: vec![],
3656 skip: None,
3657 env: None,
3658 call: None,
3659 input: serde_json::Value::Null,
3660 mock_response: Some(crate::fixture::MockResponse {
3661 status: 200,
3662 body: Some(serde_json::Value::Null),
3663 stream_chunks: None,
3664 headers: std::collections::HashMap::new(),
3665 }),
3666 source: String::new(),
3667 http: None,
3668 assertions: vec![
3669 Assertion {
3670 assertion_type: "not_error".to_string(),
3671 ..Default::default()
3672 },
3673 Assertion {
3674 assertion_type: "equals".to_string(),
3675 field: Some("segments[0].id".to_string()),
3676 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3677 ..Default::default()
3678 },
3679 ],
3680 visitor: None,
3681 };
3682
3683 let mut out = String::new();
3684 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3685
3686 eprintln!("generated:\n{out}");
3687
3688 assert!(
3690 out.contains("result.Segments != nil"),
3691 "guard must be on Segments (the slice), not an element; got:\n{out}"
3692 );
3693 assert!(
3695 !out.contains("result.Segments[0] != nil"),
3696 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3697 );
3698 }
3699}