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(
197 &group.category,
198 &active,
199 &module_path,
200 &import_alias,
201 e2e_config,
202 &config.adapters,
203 );
204 files.push(GeneratedFile {
205 path: output_base.join(filename),
206 content,
207 generated_header: true,
208 });
209 }
210
211 Ok(files)
212 }
213
214 fn language_name(&self) -> &'static str {
215 "go"
216 }
217}
218
219fn fix_go_major_version(module_path: &str, version: &str) -> String {
226 let major = module_path
228 .rsplit('/')
229 .next()
230 .and_then(|seg| seg.strip_prefix('v'))
231 .and_then(|n| n.parse::<u64>().ok())
232 .filter(|&n| n >= 2);
233
234 let Some(n) = major else {
235 return version.to_string();
236 };
237
238 let expected_prefix = format!("v{n}.");
240 if version.starts_with(&expected_prefix) {
241 return version.to_string();
242 }
243
244 format!("v{n}.0.0")
245}
246
247fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
248 let mut out = String::new();
249 let _ = writeln!(out, "module e2e_go");
250 let _ = writeln!(out);
251 let _ = writeln!(out, "go 1.26");
252 let _ = writeln!(out);
253 let _ = writeln!(out, "require (");
254 let _ = writeln!(out, "\t{go_module_path} {version}");
255 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
256 let _ = writeln!(out, ")");
257
258 if let Some(path) = replace_path {
259 let _ = writeln!(out);
260 let _ = writeln!(out, "replace {go_module_path} => {path}");
261 }
262
263 out
264}
265
266fn render_main_test_go(test_documents_dir: &str) -> String {
272 let mut out = String::new();
274 let _ = writeln!(out, "package e2e_test");
275 let _ = writeln!(out);
276 let _ = writeln!(out, "import (");
277 let _ = writeln!(out, "\t\"bufio\"");
278 let _ = writeln!(out, "\t\"encoding/json\"");
279 let _ = writeln!(out, "\t\"io\"");
280 let _ = writeln!(out, "\t\"os\"");
281 let _ = writeln!(out, "\t\"os/exec\"");
282 let _ = writeln!(out, "\t\"path/filepath\"");
283 let _ = writeln!(out, "\t\"runtime\"");
284 let _ = writeln!(out, "\t\"strings\"");
285 let _ = writeln!(out, "\t\"testing\"");
286 let _ = writeln!(out, ")");
287 let _ = writeln!(out);
288 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
289 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
290 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
291 let _ = writeln!(out);
292 let _ = writeln!(
293 out,
294 "\t// Change to the configured test-documents directory (if it exists) so that fixture"
295 );
296 let _ = writeln!(
297 out,
298 "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
299 );
300 let _ = writeln!(
301 out,
302 "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
303 );
304 let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
305 let _ = writeln!(
306 out,
307 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
308 );
309 let _ = writeln!(
310 out,
311 "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
312 );
313 let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
314 let _ = writeln!(out, "\t\t\tpanic(err)");
315 let _ = writeln!(out, "\t\t}}");
316 let _ = writeln!(out, "\t}}");
317 let _ = writeln!(out);
318 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
319 let _ = writeln!(
320 out,
321 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
322 );
323 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
324 let _ = writeln!(
325 out,
326 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
327 );
328 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
329 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
330 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
331 let _ = writeln!(out, "\t\tif err != nil {{");
332 let _ = writeln!(out, "\t\t\tpanic(err)");
333 let _ = writeln!(out, "\t\t}}");
334 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
335 let _ = writeln!(
336 out,
337 "\t\t// server does not see EOF and exit immediately. The mock-server"
338 );
339 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
340 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
341 let _ = writeln!(out, "\t\tif err != nil {{");
342 let _ = writeln!(out, "\t\t\tpanic(err)");
343 let _ = writeln!(out, "\t\t}}");
344 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
345 let _ = writeln!(out, "\t\t\tpanic(err)");
346 let _ = writeln!(out, "\t\t}}");
347 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
348 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
349 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
350 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
351 let _ = writeln!(
352 out,
353 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
354 );
355 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
356 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
357 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
358 let _ = writeln!(
359 out,
360 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
361 );
362 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
363 let _ = writeln!(
364 out,
365 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
366 );
367 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
368 let _ = writeln!(
369 out,
370 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
371 );
372 let _ = writeln!(out, "\t\t\t\t\t}}");
373 let _ = writeln!(out, "\t\t\t\t}}");
374 let _ = writeln!(out, "\t\t\t\tbreak");
375 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
376 let _ = writeln!(out, "\t\t\t\tbreak");
377 let _ = writeln!(out, "\t\t\t}}");
378 let _ = writeln!(out, "\t\t}}");
379 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
380 let _ = writeln!(out, "\t\tcode := m.Run()");
381 let _ = writeln!(out, "\t\t_ = stdin.Close()");
382 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
383 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
384 let _ = writeln!(out, "\t\tos.Exit(code)");
385 let _ = writeln!(out, "\t}} else {{");
386 let _ = writeln!(out, "\t\tcode := m.Run()");
387 let _ = writeln!(out, "\t\tos.Exit(code)");
388 let _ = writeln!(out, "\t}}");
389 let _ = writeln!(out, "}}");
390 out
391}
392
393fn render_helpers_test_go() -> String {
396 let mut out = String::new();
397 let _ = writeln!(out, "package e2e_test");
398 let _ = writeln!(out);
399 let _ = writeln!(out, "import \"encoding/json\"");
400 let _ = writeln!(out);
401 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
402 let _ = writeln!(
403 out,
404 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
405 );
406 let _ = writeln!(out, "func jsonString(value any) string {{");
407 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
408 let _ = writeln!(out, "\tif err != nil {{");
409 let _ = writeln!(out, "\t\treturn \"\"");
410 let _ = writeln!(out, "\t}}");
411 let _ = writeln!(out, "\treturn string(encoded)");
412 let _ = writeln!(out, "}}");
413 out
414}
415
416fn render_test_file(
417 category: &str,
418 fixtures: &[&Fixture],
419 go_module_path: &str,
420 import_alias: &str,
421 e2e_config: &crate::config::E2eConfig,
422 adapters: &[alef_core::config::AdapterConfig],
423) -> String {
424 let mut out = String::new();
425 let emits_executable_test =
426 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
427
428 out.push_str(&hash::header(CommentStyle::DoubleSlash));
430 let _ = writeln!(out);
431
432 let needs_pkg = fixtures
441 .iter()
442 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
443
444 let needs_os = fixtures.iter().any(|f| {
447 if f.is_http_test() {
448 return true;
449 }
450 if !emits_executable_test(f) {
451 return false;
452 }
453 let call_config =
454 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
455 let go_override = call_config
456 .overrides
457 .get("go")
458 .or_else(|| e2e_config.call.overrides.get("go"));
459 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
460 return true;
461 }
462 let call_args = &call_config.args;
463 if call_args
466 .iter()
467 .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
468 {
469 return true;
470 }
471 call_args.iter().any(|a| {
472 if a.arg_type != "bytes" {
473 return false;
474 }
475 let mut current = &f.input;
478 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
479 for segment in path.split('.') {
480 match current.get(segment) {
481 Some(next) => current = next,
482 None => return false,
483 }
484 }
485 current.is_string()
486 })
487 });
488
489 let needs_filepath = false;
492
493 let _needs_json_stringify = fixtures.iter().any(|f| {
494 emits_executable_test(f)
495 && f.assertions.iter().any(|a| {
496 matches!(
497 a.assertion_type.as_str(),
498 "contains" | "contains_all" | "contains_any" | "not_contains"
499 ) && {
500 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
503 e2e_config
505 .resolve_call_for_fixture(
506 f.call.as_deref(),
507 &f.id,
508 &f.resolved_category(),
509 &f.tags,
510 &f.input,
511 )
512 .result_is_array
513 } else {
514 let cc = e2e_config.resolve_call_for_fixture(
516 f.call.as_deref(),
517 &f.id,
518 &f.resolved_category(),
519 &f.tags,
520 &f.input,
521 );
522 let per_call_resolver = FieldResolver::new(
523 e2e_config.effective_fields(cc),
524 e2e_config.effective_fields_optional(cc),
525 e2e_config.effective_result_fields(cc),
526 e2e_config.effective_fields_array(cc),
527 &std::collections::HashSet::new(),
528 );
529 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
530 per_call_resolver.is_array(resolved_name)
531 }
532 }
533 })
534 });
535
536 let needs_json = fixtures.iter().any(|f| {
540 if let Some(http) = &f.http {
543 let body_needs_json = http
544 .expected_response
545 .body
546 .as_ref()
547 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
548 let partial_needs_json = http.expected_response.body_partial.is_some();
549 let ve_needs_json = http
550 .expected_response
551 .validation_errors
552 .as_ref()
553 .is_some_and(|v| !v.is_empty());
554 if body_needs_json || partial_needs_json || ve_needs_json {
555 return true;
556 }
557 }
558 if !emits_executable_test(f) {
559 return false;
560 }
561
562 let call =
563 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
564 let call_args = &call.args;
565 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
567 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
568 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
569 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
570 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
571 })
572 };
573 let go_override = call.overrides.get("go");
575 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
576 e2e_config
577 .call
578 .overrides
579 .get("go")
580 .and_then(|o| o.options_type.as_deref())
581 });
582 let has_json_obj = call_args.iter().any(|a| {
583 if a.arg_type != "json_object" {
584 return false;
585 }
586 let v = if a.field == "input" {
587 &f.input
588 } else {
589 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
590 f.input.get(field).unwrap_or(&serde_json::Value::Null)
591 };
592 if v.is_array() {
593 return true;
594 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
596 });
597 has_handle || has_json_obj
598 });
599
600 let needs_base64 = false;
605
606 let needs_fmt = fixtures.iter().any(|f| {
612 if f.visitor.as_ref().is_some_and(|v| {
614 v.callbacks.values().any(|action| {
615 if let CallbackAction::CustomTemplate { template, .. } = action {
616 template.contains('{')
617 } else {
618 false
619 }
620 })
621 }) {
622 return true;
623 }
624
625 if !emits_executable_test(f) {
626 return false;
627 }
628
629 if f.assertions.iter().any(|a| {
631 matches!(
632 a.assertion_type.as_str(),
633 "contains" | "contains_all" | "contains_any" | "not_contains"
634 ) && {
635 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
636 !e2e_config
638 .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
639 .result_is_array
640 } else {
641 let field = a.field.as_deref().unwrap_or("");
644 let cc = e2e_config.resolve_call_for_fixture(
645 f.call.as_deref(),
646 &f.id,
647 &f.resolved_category(),
648 &f.tags,
649 &f.input,
650 );
651 let per_call_resolver = FieldResolver::new(
652 e2e_config.effective_fields(cc),
653 e2e_config.effective_fields_optional(cc),
654 e2e_config.effective_result_fields(cc),
655 e2e_config.effective_fields_array(cc),
656 &std::collections::HashSet::new(),
657 );
658 let resolved_name = per_call_resolver.resolve(field);
659 !per_call_resolver.is_array(resolved_name) && per_call_resolver.is_valid_for_result(field)
660 }
661 }
662 }) {
663 return true;
664 }
665
666 f.assertions.iter().any(|a| {
668 if let Some(field) = &a.field {
669 if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
670 let cc = e2e_config.resolve_call_for_fixture(
671 f.call.as_deref(),
672 &f.id,
673 &f.resolved_category(),
674 &f.tags,
675 &f.input,
676 );
677 let per_call_resolver = FieldResolver::new(
678 e2e_config.effective_fields(cc),
679 e2e_config.effective_fields_optional(cc),
680 e2e_config.effective_result_fields(cc),
681 e2e_config.effective_fields_array(cc),
682 &std::collections::HashSet::new(),
683 );
684 let resolved = per_call_resolver.resolve(field);
685 per_call_resolver.is_optional(resolved)
687 && !per_call_resolver.is_array(resolved)
688 && !per_call_resolver.has_map_access(field)
689 && per_call_resolver.is_valid_for_result(field)
690 } else {
691 false
692 }
693 } else {
694 false
695 }
696 })
697 });
698
699 let needs_strings = fixtures.iter().any(|f| {
703 if !emits_executable_test(f) {
704 return false;
705 }
706 let cc =
708 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
709 if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
710 return true;
711 }
712 let per_call_resolver = FieldResolver::new(
713 e2e_config.effective_fields(cc),
714 e2e_config.effective_fields_optional(cc),
715 e2e_config.effective_result_fields(cc),
716 e2e_config.effective_fields_array(cc),
717 &std::collections::HashSet::new(),
718 );
719 f.assertions.iter().any(|a| {
720 let type_needs_strings = if a.assertion_type == "equals" {
721 a.value.as_ref().is_some_and(|v| v.is_string())
723 } else {
724 matches!(
725 a.assertion_type.as_str(),
726 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
727 )
728 };
729 let field_valid = a
730 .field
731 .as_ref()
732 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
733 .unwrap_or(true);
734 type_needs_strings && field_valid
735 })
736 });
737
738 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
747 let needs_http = has_http_fixtures;
748 let needs_io = has_http_fixtures;
750
751 let needs_reflect = fixtures.iter().any(|f| {
754 if let Some(http) = &f.http {
755 let body_needs_reflect = http
756 .expected_response
757 .body
758 .as_ref()
759 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
760 let partial_needs_reflect = http.expected_response.body_partial.is_some();
761 body_needs_reflect || partial_needs_reflect
762 } else {
763 false
764 }
765 });
766
767 let mut body = String::new();
772 for fixture in fixtures.iter() {
773 if let Some(visitor_spec) = &fixture.visitor {
774 let struct_name = visitor_struct_name(&fixture.id);
775 emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
776 let _ = writeln!(body);
777 }
778 }
779 for (i, fixture) in fixtures.iter().enumerate() {
780 render_test_function(&mut body, fixture, import_alias, e2e_config, adapters);
781 if i + 1 < fixtures.len() {
782 let _ = writeln!(body);
783 }
784 }
785 let needs_assert = body.contains("assert.");
786
787 let _ = writeln!(out, "// E2e tests for category: {category}");
788 let _ = writeln!(out, "package e2e_test");
789 let _ = writeln!(out);
790 let _ = writeln!(out, "import (");
791 if needs_base64 {
792 let _ = writeln!(out, "\t\"encoding/base64\"");
793 }
794 if needs_json || needs_reflect {
795 let _ = writeln!(out, "\t\"encoding/json\"");
796 }
797 if needs_fmt {
798 let _ = writeln!(out, "\t\"fmt\"");
799 }
800 if needs_io {
801 let _ = writeln!(out, "\t\"io\"");
802 }
803 if needs_http {
804 let _ = writeln!(out, "\t\"net/http\"");
805 }
806 if needs_os {
807 let _ = writeln!(out, "\t\"os\"");
808 }
809 let _ = needs_filepath; if needs_reflect {
811 let _ = writeln!(out, "\t\"reflect\"");
812 }
813 if needs_strings {
814 let _ = writeln!(out, "\t\"strings\"");
815 }
816 let _ = writeln!(out, "\t\"testing\"");
817 if needs_assert {
818 let _ = writeln!(out);
819 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
820 }
821
822 if needs_pkg {
823 let _ = writeln!(out);
824 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
825 }
826 let _ = writeln!(out, ")");
827 let _ = writeln!(out);
828
829 out.push_str(&body);
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 adapters: &[alef_core::config::AdapterConfig],
892) {
893 let fn_name = fixture.id.to_upper_camel_case();
894 let description = &fixture.description;
895
896 if fixture.http.is_some() {
898 render_http_test_function(out, fixture);
899 return;
900 }
901
902 if !fixture_has_go_callable(fixture, e2e_config) {
907 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
908 let _ = writeln!(out, "\t// {description}");
909 let _ = writeln!(
910 out,
911 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
912 );
913 let _ = writeln!(out, "}}");
914 return;
915 }
916
917 let call_config = e2e_config.resolve_call_for_fixture(
919 fixture.call.as_deref(),
920 &fixture.id,
921 &fixture.resolved_category(),
922 &fixture.tags,
923 &fixture.input,
924 );
925 let call_field_resolver = FieldResolver::new(
927 e2e_config.effective_fields(call_config),
928 e2e_config.effective_fields_optional(call_config),
929 e2e_config.effective_result_fields(call_config),
930 e2e_config.effective_fields_array(call_config),
931 &std::collections::HashSet::new(),
932 );
933 let field_resolver = &call_field_resolver;
934 let lang = "go";
935 let overrides = call_config.overrides.get(lang);
936
937 let base_function_name = overrides
941 .and_then(|o| o.function.as_deref())
942 .unwrap_or(&call_config.function);
943 let function_name = to_go_name(base_function_name);
944 let result_var = &call_config.result_var;
945 let args = &call_config.args;
946
947 let returns_result = overrides
950 .and_then(|o| o.returns_result)
951 .unwrap_or(call_config.returns_result);
952
953 let returns_void = call_config.returns_void;
956
957 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
963 || call_config.result_is_simple
964 || call_config
965 .overrides
966 .get("rust")
967 .map(|o| o.result_is_simple)
968 .unwrap_or(false);
969
970 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
976
977 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
979 e2e_config
980 .call
981 .overrides
982 .get("go")
983 .and_then(|o| o.options_type.as_deref())
984 });
985
986 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
988 e2e_config
989 .call
990 .overrides
991 .get("go")
992 .map(|o| o.options_ptr)
993 .unwrap_or(false)
994 });
995
996 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
997 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1001
1002 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1005 e2e_config
1006 .call
1007 .overrides
1008 .get(lang)
1009 .and_then(|o| o.client_factory.as_deref())
1010 });
1011
1012 let (mut setup_lines, args_str) = build_args_and_setup(
1013 &fixture.input,
1014 args,
1015 import_alias,
1016 call_options_type,
1017 fixture,
1018 call_options_ptr,
1019 validation_creation_failure,
1020 );
1021
1022 let mut visitor_opts_var: Option<String> = None;
1025 if fixture.visitor.is_some() {
1026 let struct_name = visitor_struct_name(&fixture.id);
1027 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1028 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1030 let opts_var = "opts".to_string();
1031 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1032 setup_lines.push("opts.Visitor = visitor".to_string());
1033 visitor_opts_var = Some(opts_var);
1034 }
1035
1036 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1037 let final_args = {
1038 let mut parts: Vec<String> = Vec::new();
1039 if !args_str.is_empty() {
1040 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1042 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1043 } else {
1044 args_str
1045 };
1046 parts.push(processed_args);
1047 }
1048 parts.extend(go_extra_args);
1049 parts.join(", ")
1050 };
1051
1052 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1053 let _ = writeln!(out, "\t// {description}");
1054
1055 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1059 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1060 if let Some(var) = api_key_var {
1061 if has_mock {
1062 let fixture_id = &fixture.id;
1066 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1067 let _ = writeln!(out, "\tvar baseURL *string");
1068 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1069 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1070 let _ = writeln!(out, "\t}} else {{");
1071 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1072 let _ = writeln!(
1073 out,
1074 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1075 );
1076 let _ = writeln!(out, "\t\tbaseURL = &u");
1077 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1078 let _ = writeln!(out, "\t}}");
1079 } else {
1080 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1081 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1082 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1083 let _ = writeln!(out, "\t}}");
1084 }
1085 }
1086
1087 for line in &setup_lines {
1088 let _ = writeln!(out, "\t{line}");
1089 }
1090
1091 let call_prefix = if let Some(factory) = client_factory {
1095 let factory_name = to_go_name(factory);
1096 let fixture_id = &fixture.id;
1097 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1100 ("apiKey".to_string(), "baseURL".to_string())
1102 } else if api_key_var.is_some() {
1103 ("apiKey".to_string(), "nil".to_string())
1105 } else if fixture.has_host_root_route() {
1106 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1107 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1108 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1109 let _ = writeln!(
1110 out,
1111 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1112 );
1113 let _ = writeln!(out, "\t}}");
1114 ("\"test-key\"".to_string(), "&mockURL".to_string())
1115 } else {
1116 let _ = writeln!(
1117 out,
1118 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1119 );
1120 ("\"test-key\"".to_string(), "&mockURL".to_string())
1121 };
1122 let _ = writeln!(
1123 out,
1124 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1125 );
1126 let _ = writeln!(out, "\tif clientErr != nil {{");
1127 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1128 let _ = writeln!(out, "\t}}");
1129 "client".to_string()
1130 } else {
1131 import_alias.to_string()
1132 };
1133
1134 let binding_returns_error_pre = args
1139 .iter()
1140 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1141 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1142
1143 if expects_error {
1144 if effective_returns_result_pre && !returns_void {
1145 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1146 } else {
1147 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1148 }
1149 let _ = writeln!(out, "\tif err == nil {{");
1150 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1151 let _ = writeln!(out, "\t}}");
1152 let _ = writeln!(out, "}}");
1153 return;
1154 }
1155
1156 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1158
1159 use heck::ToSnakeCase;
1164 let fn_snake = function_name.to_snake_case();
1165 let base_snake = base_function_name.to_snake_case();
1166 let streaming_item_type = if is_streaming {
1167 adapters
1168 .iter()
1169 .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1170 .find(|a| a.name == fn_snake || a.name == base_snake)
1171 .and_then(|a| a.item_type.as_deref())
1172 .and_then(|t| t.rsplit("::").next())
1173 .unwrap_or("Item") } else {
1175 "Item" };
1177
1178 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1183 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1184 return false;
1185 }
1186 if a.assertion_type == "method_result" {
1188 return true;
1189 }
1190 match &a.field {
1191 Some(f) if !f.is_empty() => {
1192 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1193 return true;
1194 }
1195 field_resolver.is_valid_for_result(f)
1196 }
1197 _ => true,
1198 }
1199 });
1200
1201 let binding_returns_error = args
1208 .iter()
1209 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1210 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1212
1213 if !effective_returns_result && result_is_simple {
1219 let result_binding = if has_usable_assertion {
1221 result_var.to_string()
1222 } else {
1223 "_".to_string()
1224 };
1225 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1227 let _ = writeln!(
1228 out,
1229 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1230 );
1231 if has_usable_assertion && result_binding != "_" {
1232 if result_is_array {
1233 let _ = writeln!(out, "\tvalue := {result_var}");
1235 } else {
1236 let only_nil_assertions = fixture
1239 .assertions
1240 .iter()
1241 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1242 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1243 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1244
1245 if !only_nil_assertions {
1246 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1249 if result_is_ptr {
1250 let _ = writeln!(out, "\tif {result_var} == nil {{");
1251 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1252 let _ = writeln!(out, "\t}}");
1253 let _ = writeln!(out, "\tvalue := *{result_var}");
1254 } else {
1255 let _ = writeln!(out, "\tvalue := {result_var}");
1257 }
1258 }
1259 }
1260 }
1261 } else if !effective_returns_result || returns_void {
1262 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1265 let _ = writeln!(out, "\tif err != nil {{");
1266 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1267 let _ = writeln!(out, "\t}}");
1268 let _ = writeln!(out, "}}");
1270 return;
1271 } else {
1272 let result_binding = if is_streaming {
1275 "stream".to_string()
1276 } else if has_usable_assertion {
1277 result_var.to_string()
1278 } else {
1279 "_".to_string()
1280 };
1281 let _ = writeln!(
1282 out,
1283 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1284 );
1285 let _ = writeln!(out, "\tif err != nil {{");
1286 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1287 let _ = writeln!(out, "\t}}");
1288 if is_streaming {
1290 let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1291 let _ = writeln!(out, "\tfor chunk := range stream {{");
1292 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1293 let _ = writeln!(out, "\t}}");
1294 }
1295 if result_is_simple && has_usable_assertion && result_binding != "_" {
1296 if result_is_array {
1297 let _ = writeln!(out, "\tvalue := {}", result_var);
1299 } else {
1300 let only_nil_assertions = fixture
1303 .assertions
1304 .iter()
1305 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1306 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1307 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1308
1309 if !only_nil_assertions {
1310 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1313 if result_is_ptr {
1314 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1315 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1316 let _ = writeln!(out, "\t}}");
1317 let _ = writeln!(out, "\tvalue := *{}", result_var);
1318 } else {
1319 let _ = writeln!(out, "\tvalue := {}", result_var);
1321 }
1322 }
1323 }
1324 }
1325 }
1326
1327 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1331 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1332 let only_nil_assertions = fixture
1333 .assertions
1334 .iter()
1335 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1336 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1337 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1338 !only_nil_assertions
1339 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1340 true
1341 } else {
1342 result_is_simple && has_usable_assertion
1343 };
1344
1345 let effective_result_var = if has_deref_value {
1346 "value".to_string()
1347 } else {
1348 result_var.to_string()
1349 };
1350
1351 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1356 for assertion in &fixture.assertions {
1357 if let Some(f) = &assertion.field {
1358 if !f.is_empty() {
1359 let resolved = field_resolver.resolve(f);
1360 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1361 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1366 let is_array_field = field_resolver.is_array(resolved);
1367 if !is_string_field || is_array_field {
1368 continue;
1371 }
1372 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1373 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1374 if field_resolver.has_map_access(f) {
1375 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1378 } else {
1379 let _ = writeln!(out, "\tvar {local_var} string");
1380 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1381 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1385 let _ = writeln!(out, "\t}}");
1386 }
1387 optional_locals.insert(f.clone(), local_var);
1388 }
1389 }
1390 }
1391 }
1392
1393 for assertion in &fixture.assertions {
1395 if let Some(f) = &assertion.field {
1396 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1397 let parts: Vec<&str> = f.split('.').collect();
1400 let mut guard_expr: Option<String> = None;
1401 for i in 1..parts.len() {
1402 let prefix = parts[..i].join(".");
1403 let resolved_prefix = field_resolver.resolve(&prefix);
1404 if field_resolver.is_optional(resolved_prefix) {
1405 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1411 let suffix = &resolved_prefix[bracket_pos + 1..];
1412 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1413 if is_numeric_index {
1414 &resolved_prefix[..bracket_pos]
1415 } else {
1416 resolved_prefix
1417 }
1418 } else {
1419 resolved_prefix
1420 };
1421 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1422 guard_expr = Some(accessor);
1423 break;
1424 }
1425 }
1426 if let Some(guard) = guard_expr {
1427 if field_resolver.is_valid_for_result(f) {
1430 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1436 if is_struct_value {
1437 render_assertion(
1440 out,
1441 assertion,
1442 &effective_result_var,
1443 import_alias,
1444 field_resolver,
1445 &optional_locals,
1446 result_is_simple,
1447 result_is_array,
1448 is_streaming,
1449 );
1450 continue;
1451 }
1452 let _ = writeln!(out, "\tif {guard} != nil {{");
1453 let mut nil_buf = String::new();
1456 render_assertion(
1457 &mut nil_buf,
1458 assertion,
1459 &effective_result_var,
1460 import_alias,
1461 field_resolver,
1462 &optional_locals,
1463 result_is_simple,
1464 result_is_array,
1465 is_streaming,
1466 );
1467 for line in nil_buf.lines() {
1468 let _ = writeln!(out, "\t{line}");
1469 }
1470 let _ = writeln!(out, "\t}}");
1471 } else {
1472 render_assertion(
1473 out,
1474 assertion,
1475 &effective_result_var,
1476 import_alias,
1477 field_resolver,
1478 &optional_locals,
1479 result_is_simple,
1480 result_is_array,
1481 is_streaming,
1482 );
1483 }
1484 continue;
1485 }
1486 }
1487 }
1488 render_assertion(
1489 out,
1490 assertion,
1491 &effective_result_var,
1492 import_alias,
1493 field_resolver,
1494 &optional_locals,
1495 result_is_simple,
1496 result_is_array,
1497 is_streaming,
1498 );
1499 }
1500
1501 let _ = writeln!(out, "}}");
1502}
1503
1504fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1510 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1511}
1512
1513struct GoTestClientRenderer;
1525
1526impl client::TestClientRenderer for GoTestClientRenderer {
1527 fn language_name(&self) -> &'static str {
1528 "go"
1529 }
1530
1531 fn sanitize_test_name(&self, id: &str) -> String {
1535 id.to_upper_camel_case()
1536 }
1537
1538 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1541 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1542 let _ = writeln!(out, "\t// {description}");
1543 if let Some(reason) = skip_reason {
1544 let escaped = go_string_literal(reason);
1545 let _ = writeln!(out, "\tt.Skip({escaped})");
1546 }
1547 }
1548
1549 fn render_test_close(&self, out: &mut String) {
1550 let _ = writeln!(out, "}}");
1551 }
1552
1553 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1559 let method = ctx.method.to_uppercase();
1560 let path = ctx.path;
1561
1562 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1563 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1564 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1565 let _ = writeln!(out, "\t}}");
1566
1567 let body_expr = if let Some(body) = ctx.body {
1569 let json = serde_json::to_string(body).unwrap_or_default();
1570 let escaped = go_string_literal(&json);
1571 format!("strings.NewReader({})", escaped)
1572 } else {
1573 "strings.NewReader(\"\")".to_string()
1574 };
1575
1576 let _ = writeln!(out, "\tbody := {body_expr}");
1577 let _ = writeln!(
1578 out,
1579 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1580 );
1581 let _ = writeln!(out, "\tif err != nil {{");
1582 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1583 let _ = writeln!(out, "\t}}");
1584
1585 if ctx.body.is_some() {
1587 let content_type = ctx.content_type.unwrap_or("application/json");
1588 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1589 }
1590
1591 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1593 header_names.sort();
1594 for name in header_names {
1595 let value = &ctx.headers[name];
1596 let escaped_name = go_string_literal(name);
1597 let escaped_value = go_string_literal(value);
1598 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1599 }
1600
1601 if !ctx.cookies.is_empty() {
1603 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1604 cookie_names.sort();
1605 for name in cookie_names {
1606 let value = &ctx.cookies[name];
1607 let escaped_name = go_string_literal(name);
1608 let escaped_value = go_string_literal(value);
1609 let _ = writeln!(
1610 out,
1611 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1612 );
1613 }
1614 }
1615
1616 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1618 let _ = writeln!(
1619 out,
1620 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1621 );
1622 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1623 let _ = writeln!(out, "\t\t}},");
1624 let _ = writeln!(out, "\t}}");
1625 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1626 let _ = writeln!(out, "\tif err != nil {{");
1627 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1628 let _ = writeln!(out, "\t}}");
1629 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1630
1631 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1635 let _ = writeln!(out, "\tif err != nil {{");
1636 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1637 let _ = writeln!(out, "\t}}");
1638 let _ = writeln!(out, "\t_ = bodyBytes");
1639 }
1640
1641 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1642 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1643 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1644 let _ = writeln!(out, "\t}}");
1645 }
1646
1647 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1650 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1652 return;
1653 }
1654 if name.eq_ignore_ascii_case("connection") {
1656 return;
1657 }
1658 let escaped_name = go_string_literal(name);
1659 let escaped_value = go_string_literal(expected);
1660 let _ = writeln!(
1661 out,
1662 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1663 );
1664 let _ = writeln!(
1665 out,
1666 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1667 );
1668 let _ = writeln!(out, "\t}}");
1669 }
1670
1671 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1676 match expected {
1677 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1678 let json_str = serde_json::to_string(expected).unwrap_or_default();
1679 let escaped = go_string_literal(&json_str);
1680 let _ = writeln!(out, "\tvar got any");
1681 let _ = writeln!(out, "\tvar want any");
1682 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1683 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1684 let _ = writeln!(out, "\t}}");
1685 let _ = writeln!(
1686 out,
1687 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1688 );
1689 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1690 let _ = writeln!(out, "\t}}");
1691 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1692 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1693 let _ = writeln!(out, "\t}}");
1694 }
1695 serde_json::Value::String(s) => {
1696 let escaped = go_string_literal(s);
1697 let _ = writeln!(out, "\twant := {escaped}");
1698 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1699 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1700 let _ = writeln!(out, "\t}}");
1701 }
1702 other => {
1703 let escaped = go_string_literal(&other.to_string());
1704 let _ = writeln!(out, "\twant := {escaped}");
1705 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1706 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1707 let _ = writeln!(out, "\t}}");
1708 }
1709 }
1710 }
1711
1712 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1715 if let Some(obj) = expected.as_object() {
1716 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1717 let _ = writeln!(
1718 out,
1719 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1720 );
1721 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1722 let _ = writeln!(out, "\t}}");
1723 for (key, val) in obj {
1724 let escaped_key = go_string_literal(key);
1725 let json_val = serde_json::to_string(val).unwrap_or_default();
1726 let escaped_val = go_string_literal(&json_val);
1727 let _ = writeln!(out, "\t{{");
1728 let _ = writeln!(out, "\t\tvar _wantVal any");
1729 let _ = writeln!(
1730 out,
1731 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1732 );
1733 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1734 let _ = writeln!(out, "\t\t}}");
1735 let _ = writeln!(
1736 out,
1737 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1738 );
1739 let _ = writeln!(
1740 out,
1741 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1742 );
1743 let _ = writeln!(out, "\t\t}}");
1744 let _ = writeln!(out, "\t}}");
1745 }
1746 }
1747 }
1748
1749 fn render_assert_validation_errors(
1754 &self,
1755 out: &mut String,
1756 _response_var: &str,
1757 errors: &[ValidationErrorExpectation],
1758 ) {
1759 let _ = writeln!(out, "\tvar _veBody map[string]any");
1760 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1761 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1762 let _ = writeln!(out, "\t}}");
1763 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1764 for ve in errors {
1765 let escaped_msg = go_string_literal(&ve.msg);
1766 let _ = writeln!(out, "\t{{");
1767 let _ = writeln!(out, "\t\t_found := false");
1768 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1769 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1770 let _ = writeln!(
1771 out,
1772 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1773 );
1774 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1775 let _ = writeln!(out, "\t\t\t\t\tbreak");
1776 let _ = writeln!(out, "\t\t\t\t}}");
1777 let _ = writeln!(out, "\t\t\t}}");
1778 let _ = writeln!(out, "\t\t}}");
1779 let _ = writeln!(out, "\t\tif !_found {{");
1780 let _ = writeln!(
1781 out,
1782 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1783 );
1784 let _ = writeln!(out, "\t\t}}");
1785 let _ = writeln!(out, "\t}}");
1786 }
1787 }
1788}
1789
1790fn build_args_and_setup(
1798 input: &serde_json::Value,
1799 args: &[crate::config::ArgMapping],
1800 import_alias: &str,
1801 options_type: Option<&str>,
1802 fixture: &crate::fixture::Fixture,
1803 options_ptr: bool,
1804 expects_error: bool,
1805) -> (Vec<String>, String) {
1806 let fixture_id = &fixture.id;
1807 use heck::ToUpperCamelCase;
1808
1809 if args.is_empty() {
1810 return (Vec::new(), String::new());
1811 }
1812
1813 let mut setup_lines: Vec<String> = Vec::new();
1814 let mut parts: Vec<String> = Vec::new();
1815
1816 for arg in args {
1817 if arg.arg_type == "mock_url" {
1818 if fixture.has_host_root_route() {
1819 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1820 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1821 setup_lines.push(format!(
1822 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1823 arg.name, arg.name
1824 ));
1825 } else {
1826 setup_lines.push(format!(
1827 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1828 arg.name,
1829 ));
1830 }
1831 parts.push(arg.name.clone());
1832 continue;
1833 }
1834
1835 if arg.arg_type == "mock_url_list" {
1836 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1841 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1842 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1843
1844 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1845 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1846 } else {
1847 Vec::new()
1848 };
1849
1850 let paths_literal = paths.join(", ");
1851 let var_name = &arg.name;
1852
1853 setup_lines.push(format!(
1854 "{var_name}Base := os.Getenv(\"{env_key}\")\n\tif {var_name}Base == \"\" {{\n\t\t{var_name}Base = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
1855 ));
1856 setup_lines.push(format!(
1857 "var {var_name} []string\n\tfor _, p := range []string{{{paths_literal}}} {{\n\t\tif strings.HasPrefix(p, \"http\") {{\n\t\t\t{var_name} = append({var_name}, p)\n\t\t}} else {{\n\t\t\t{var_name} = append({var_name}, {var_name}Base + p)\n\t\t}}\n\t}}"
1858 ));
1859 parts.push(var_name.to_string());
1860 continue;
1861 }
1862
1863 if arg.arg_type == "handle" {
1864 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1866 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1867 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1868 let create_err_handler = if expects_error {
1872 "assert.Error(t, createErr)\n\t\treturn".to_string()
1873 } else {
1874 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1875 };
1876 if config_value.is_null()
1877 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1878 {
1879 setup_lines.push(format!(
1880 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1881 name = arg.name,
1882 ));
1883 } else {
1884 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1885 let go_literal = go_string_literal(&json_str);
1886 let name = &arg.name;
1887 setup_lines.push(format!(
1888 "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}}"
1889 ));
1890 setup_lines.push(format!(
1891 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1892 ));
1893 }
1894 parts.push(arg.name.clone());
1895 continue;
1896 }
1897
1898 let val: Option<&serde_json::Value> = if arg.field == "input" {
1899 Some(input)
1900 } else {
1901 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1902 input.get(field)
1903 };
1904
1905 if arg.arg_type == "bytes" {
1912 let var_name = format!("{}Bytes", arg.name);
1913 match val {
1914 None | Some(serde_json::Value::Null) => {
1915 if arg.optional {
1916 parts.push("nil".to_string());
1917 } else {
1918 parts.push("[]byte{}".to_string());
1919 }
1920 }
1921 Some(serde_json::Value::String(s)) => {
1922 let go_path = go_string_literal(s);
1927 setup_lines.push(format!(
1928 "{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}}"
1929 ));
1930 parts.push(var_name);
1931 }
1932 Some(other) => {
1933 parts.push(format!("[]byte({})", json_to_go(other)));
1934 }
1935 }
1936 continue;
1937 }
1938
1939 match val {
1940 None | Some(serde_json::Value::Null) if arg.optional => {
1941 match arg.arg_type.as_str() {
1943 "string" => {
1944 parts.push("nil".to_string());
1946 }
1947 "json_object" => {
1948 if options_ptr {
1949 parts.push("nil".to_string());
1951 } else if let Some(opts_type) = options_type {
1952 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1954 } else {
1955 parts.push("nil".to_string());
1956 }
1957 }
1958 _ => {
1959 parts.push("nil".to_string());
1960 }
1961 }
1962 }
1963 None | Some(serde_json::Value::Null) => {
1964 let default_val = match arg.arg_type.as_str() {
1966 "string" => "\"\"".to_string(),
1967 "int" | "integer" | "i64" => "0".to_string(),
1968 "float" | "number" => "0.0".to_string(),
1969 "bool" | "boolean" => "false".to_string(),
1970 "json_object" => {
1971 if options_ptr {
1972 "nil".to_string()
1974 } else if let Some(opts_type) = options_type {
1975 format!("{import_alias}.{opts_type}{{}}")
1976 } else {
1977 "nil".to_string()
1978 }
1979 }
1980 _ => "nil".to_string(),
1981 };
1982 parts.push(default_val);
1983 }
1984 Some(v) => {
1985 match arg.arg_type.as_str() {
1986 "json_object" => {
1987 let is_array = v.is_array();
1990 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1991 if is_empty_obj {
1992 if options_ptr {
1993 parts.push("nil".to_string());
1995 } else if let Some(opts_type) = options_type {
1996 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1997 } else {
1998 parts.push("nil".to_string());
1999 }
2000 } else if is_array {
2001 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2006 if go_t.starts_with('[') {
2010 go_t.to_string()
2011 } else {
2012 let qualified = if go_t.contains('.') {
2014 go_t.to_string()
2015 } else {
2016 format!("{import_alias}.{go_t}")
2017 };
2018 format!("[]{qualified}")
2019 }
2020 } else {
2021 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2022 };
2023 let converted_v = convert_json_for_go(v.clone());
2025 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2026 let go_literal = go_string_literal(&json_str);
2027 let var_name = &arg.name;
2028 setup_lines.push(format!(
2029 "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}}"
2030 ));
2031 parts.push(var_name.to_string());
2032 } else if let Some(opts_type) = options_type {
2033 let remapped_v = if options_ptr {
2038 convert_json_for_go(v.clone())
2039 } else {
2040 v.clone()
2041 };
2042 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2043 let go_literal = go_string_literal(&json_str);
2044 let var_name = &arg.name;
2045 setup_lines.push(format!(
2046 "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}}"
2047 ));
2048 let arg_expr = if options_ptr {
2050 format!("&{var_name}")
2051 } else {
2052 var_name.to_string()
2053 };
2054 parts.push(arg_expr);
2055 } else {
2056 parts.push(json_to_go(v));
2057 }
2058 }
2059 "string" if arg.optional => {
2060 let var_name = format!("{}Val", arg.name);
2062 let go_val = json_to_go(v);
2063 setup_lines.push(format!("{var_name} := {go_val}"));
2064 parts.push(format!("&{var_name}"));
2065 }
2066 _ => {
2067 parts.push(json_to_go(v));
2068 }
2069 }
2070 }
2071 }
2072 }
2073
2074 (setup_lines, parts.join(", "))
2075}
2076
2077#[allow(clippy::too_many_arguments)]
2078fn render_assertion(
2079 out: &mut String,
2080 assertion: &Assertion,
2081 result_var: &str,
2082 import_alias: &str,
2083 field_resolver: &FieldResolver,
2084 optional_locals: &std::collections::HashMap<String, String>,
2085 result_is_simple: bool,
2086 result_is_array: bool,
2087 is_streaming: bool,
2088) {
2089 if !result_is_simple {
2092 if let Some(f) = &assertion.field {
2093 let embed_deref = format!("(*{result_var})");
2096 match f.as_str() {
2097 "chunks_have_content" => {
2098 let pred = format!(
2099 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2100 );
2101 match assertion.assertion_type.as_str() {
2102 "is_true" => {
2103 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2104 }
2105 "is_false" => {
2106 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2107 }
2108 _ => {
2109 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2110 }
2111 }
2112 return;
2113 }
2114 "chunks_have_embeddings" => {
2115 let pred = format!(
2116 "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 }}()"
2117 );
2118 match assertion.assertion_type.as_str() {
2119 "is_true" => {
2120 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2121 }
2122 "is_false" => {
2123 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2124 }
2125 _ => {
2126 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2127 }
2128 }
2129 return;
2130 }
2131 "chunks_have_heading_context" => {
2132 let pred = format!(
2133 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2134 );
2135 match assertion.assertion_type.as_str() {
2136 "is_true" => {
2137 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2138 }
2139 "is_false" => {
2140 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2141 }
2142 _ => {
2143 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2144 }
2145 }
2146 return;
2147 }
2148 "first_chunk_starts_with_heading" => {
2149 let pred = format!(
2150 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2151 );
2152 match assertion.assertion_type.as_str() {
2153 "is_true" => {
2154 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2155 }
2156 "is_false" => {
2157 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2158 }
2159 _ => {
2160 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2161 }
2162 }
2163 return;
2164 }
2165 "embeddings" => {
2166 match assertion.assertion_type.as_str() {
2167 "count_equals" => {
2168 if let Some(val) = &assertion.value {
2169 if let Some(n) = val.as_u64() {
2170 let _ = writeln!(
2171 out,
2172 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2173 );
2174 }
2175 }
2176 }
2177 "count_min" => {
2178 if let Some(val) = &assertion.value {
2179 if let Some(n) = val.as_u64() {
2180 let _ = writeln!(
2181 out,
2182 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2183 );
2184 }
2185 }
2186 }
2187 "not_empty" => {
2188 let _ = writeln!(
2189 out,
2190 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2191 );
2192 }
2193 "is_empty" => {
2194 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2195 }
2196 _ => {
2197 let _ = writeln!(
2198 out,
2199 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2200 );
2201 }
2202 }
2203 return;
2204 }
2205 "embedding_dimensions" => {
2206 let expr = format!(
2207 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2208 );
2209 match assertion.assertion_type.as_str() {
2210 "equals" => {
2211 if let Some(val) = &assertion.value {
2212 if let Some(n) = val.as_u64() {
2213 let _ = writeln!(
2214 out,
2215 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2216 );
2217 }
2218 }
2219 }
2220 "greater_than" => {
2221 if let Some(val) = &assertion.value {
2222 if let Some(n) = val.as_u64() {
2223 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2224 }
2225 }
2226 }
2227 _ => {
2228 let _ = writeln!(
2229 out,
2230 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2231 );
2232 }
2233 }
2234 return;
2235 }
2236 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2237 let pred = match f.as_str() {
2238 "embeddings_valid" => {
2239 format!(
2240 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2241 )
2242 }
2243 "embeddings_finite" => {
2244 format!(
2245 "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 }}()"
2246 )
2247 }
2248 "embeddings_non_zero" => {
2249 format!(
2250 "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 }}()"
2251 )
2252 }
2253 "embeddings_normalized" => {
2254 format!(
2255 "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 }}()"
2256 )
2257 }
2258 _ => unreachable!(),
2259 };
2260 match assertion.assertion_type.as_str() {
2261 "is_true" => {
2262 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2263 }
2264 "is_false" => {
2265 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2266 }
2267 _ => {
2268 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2269 }
2270 }
2271 return;
2272 }
2273 "keywords" | "keywords_count" => {
2276 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2277 return;
2278 }
2279 _ => {}
2280 }
2281 }
2282 }
2283
2284 if !result_is_simple && is_streaming {
2291 if let Some(f) = &assertion.field {
2292 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2293 if let Some(expr) =
2294 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2295 {
2296 match assertion.assertion_type.as_str() {
2297 "count_min" => {
2298 if let Some(val) = &assertion.value {
2299 if let Some(n) = val.as_u64() {
2300 let _ = writeln!(
2301 out,
2302 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2303 );
2304 }
2305 }
2306 }
2307 "count_equals" => {
2308 if let Some(val) = &assertion.value {
2309 if let Some(n) = val.as_u64() {
2310 let _ = writeln!(
2311 out,
2312 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2313 );
2314 }
2315 }
2316 }
2317 "equals" => {
2318 if let Some(serde_json::Value::String(s)) = &assertion.value {
2319 let escaped = crate::escape::go_string_literal(s);
2320 let is_deep_path = f.contains('.') || f.contains('[');
2325 let safe_expr = if is_deep_path {
2326 format!(
2327 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2328 )
2329 } else {
2330 expr.clone()
2331 };
2332 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2333 } else if let Some(val) = &assertion.value {
2334 if let Some(n) = val.as_u64() {
2335 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2336 }
2337 }
2338 }
2339 "not_empty" => {
2340 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2341 }
2342 "is_empty" => {
2343 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2344 }
2345 "is_true" => {
2346 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2347 }
2348 "is_false" => {
2349 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2350 }
2351 "greater_than" => {
2352 if let Some(val) = &assertion.value {
2353 if let Some(n) = val.as_u64() {
2354 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2355 }
2356 }
2357 }
2358 "greater_than_or_equal" => {
2359 if let Some(val) = &assertion.value {
2360 if let Some(n) = val.as_u64() {
2361 let _ =
2362 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2363 }
2364 }
2365 }
2366 "contains" => {
2367 if let Some(serde_json::Value::String(s)) = &assertion.value {
2368 let escaped = crate::escape::go_string_literal(s);
2369 let _ =
2370 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2371 }
2372 }
2373 _ => {
2374 let _ = writeln!(
2375 out,
2376 "\t// streaming field '{f}': assertion type '{}' not rendered",
2377 assertion.assertion_type
2378 );
2379 }
2380 }
2381 }
2382 return;
2383 }
2384 }
2385 }
2386
2387 if !result_is_simple {
2390 if let Some(f) = &assertion.field {
2391 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2392 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2393 return;
2394 }
2395 }
2396 }
2397
2398 let field_expr = if result_is_simple {
2399 result_var.to_string()
2401 } else {
2402 match &assertion.field {
2403 Some(f) if !f.is_empty() => {
2404 if let Some(local_var) = optional_locals.get(f.as_str()) {
2406 local_var.clone()
2407 } else {
2408 field_resolver.accessor(f, "go", result_var)
2409 }
2410 }
2411 _ => result_var.to_string(),
2412 }
2413 };
2414
2415 let is_optional = assertion
2419 .field
2420 .as_ref()
2421 .map(|f| {
2422 let resolved = field_resolver.resolve(f);
2423 let check_path = resolved
2424 .strip_suffix(".length")
2425 .or_else(|| resolved.strip_suffix(".count"))
2426 .or_else(|| resolved.strip_suffix(".size"))
2427 .unwrap_or(resolved);
2428 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2429 })
2430 .unwrap_or(false);
2431
2432 let field_is_array_for_len = assertion
2436 .field
2437 .as_ref()
2438 .map(|f| {
2439 let resolved = field_resolver.resolve(f);
2440 let check_path = resolved
2441 .strip_suffix(".length")
2442 .or_else(|| resolved.strip_suffix(".count"))
2443 .or_else(|| resolved.strip_suffix(".size"))
2444 .unwrap_or(resolved);
2445 field_resolver.is_array(check_path)
2446 })
2447 .unwrap_or(false);
2448 let field_expr =
2449 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2450 let inner = &field_expr[4..field_expr.len() - 1];
2451 format!("len(*{inner})")
2452 } else {
2453 field_expr
2454 };
2455 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2457 Some(field_expr[5..field_expr.len() - 1].to_string())
2458 } else {
2459 None
2460 };
2461
2462 let field_is_slice = assertion
2466 .field
2467 .as_ref()
2468 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2469 .unwrap_or(false);
2470 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2471 format!("*{field_expr}")
2472 } else {
2473 field_expr.clone()
2474 };
2475
2476 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2481 let mut array_expr = field_expr[..idx].to_string();
2482 if let Some(stripped) = array_expr.strip_prefix("len(") {
2483 array_expr = stripped.to_string();
2484 }
2485 Some(array_expr)
2486 } else {
2487 None
2488 };
2489
2490 let mut assertion_buf = String::new();
2493 let out_ref = &mut assertion_buf;
2494
2495 match assertion.assertion_type.as_str() {
2496 "equals" => {
2497 if let Some(expected) = &assertion.value {
2498 let go_val = json_to_go(expected);
2499 if expected.is_string() {
2501 let resolved_name = assertion
2505 .field
2506 .as_ref()
2507 .map(|f| field_resolver.resolve(f))
2508 .unwrap_or_default();
2509 let is_struct = resolved_name.contains("FormatMetadata");
2510 let trimmed_field = if is_struct {
2511 if is_optional && !field_expr.starts_with("len(") {
2513 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2514 } else {
2515 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2516 }
2517 } else if is_optional && !field_expr.starts_with("len(") {
2518 format!("strings.TrimSpace(string(*{field_expr}))")
2519 } else {
2520 format!("strings.TrimSpace(string({field_expr}))")
2521 };
2522 if is_optional && !field_expr.starts_with("len(") {
2523 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2524 } else {
2525 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2526 }
2527 } else if is_optional && !field_expr.starts_with("len(") {
2528 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2529 } else {
2530 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2531 }
2532 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2533 let _ = writeln!(out_ref, "\t}}");
2534 }
2535 }
2536 "contains" => {
2537 if let Some(expected) = &assertion.value {
2538 let go_val = json_to_go(expected);
2539 let resolved_field = assertion.field.as_deref().unwrap_or("");
2545 let resolved_name = field_resolver.resolve(resolved_field);
2546 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2547 let is_opt =
2548 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2549 let field_for_contains = if is_opt && field_is_array {
2550 format!("jsonString({field_expr})")
2552 } else if is_opt {
2553 format!("fmt.Sprint(*{field_expr})")
2554 } else if field_is_array {
2555 format!("jsonString({field_expr})")
2556 } else {
2557 format!("fmt.Sprint({field_expr})")
2558 };
2559 if is_opt {
2560 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2561 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2562 let _ = writeln!(
2563 out_ref,
2564 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2565 );
2566 let _ = writeln!(out_ref, "\t}}");
2567 let _ = writeln!(out_ref, "\t}}");
2568 } else {
2569 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2570 let _ = writeln!(
2571 out_ref,
2572 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2573 );
2574 let _ = writeln!(out_ref, "\t}}");
2575 }
2576 }
2577 }
2578 "contains_all" => {
2579 if let Some(values) = &assertion.values {
2580 let resolved_field = assertion.field.as_deref().unwrap_or("");
2581 let resolved_name = field_resolver.resolve(resolved_field);
2582 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2583 let is_opt =
2584 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2585 for val in values {
2586 let go_val = json_to_go(val);
2587 let field_for_contains = if is_opt && field_is_array {
2588 format!("jsonString({field_expr})")
2590 } else if is_opt {
2591 format!("fmt.Sprint(*{field_expr})")
2592 } else if field_is_array {
2593 format!("jsonString({field_expr})")
2594 } else {
2595 format!("fmt.Sprint({field_expr})")
2596 };
2597 if is_opt {
2598 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2599 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2600 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2601 let _ = writeln!(out_ref, "\t}}");
2602 let _ = writeln!(out_ref, "\t}}");
2603 } else {
2604 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2605 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2606 let _ = writeln!(out_ref, "\t}}");
2607 }
2608 }
2609 }
2610 }
2611 "not_contains" => {
2612 if let Some(expected) = &assertion.value {
2613 let go_val = json_to_go(expected);
2614 let resolved_field = assertion.field.as_deref().unwrap_or("");
2615 let resolved_name = field_resolver.resolve(resolved_field);
2616 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2617 let is_opt =
2618 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2619 let field_for_contains = if is_opt && field_is_array {
2620 format!("jsonString({field_expr})")
2622 } else if is_opt {
2623 format!("fmt.Sprint(*{field_expr})")
2624 } else if field_is_array {
2625 format!("jsonString({field_expr})")
2626 } else {
2627 format!("fmt.Sprint({field_expr})")
2628 };
2629 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2630 let _ = writeln!(
2631 out_ref,
2632 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2633 );
2634 let _ = writeln!(out_ref, "\t}}");
2635 }
2636 }
2637 "not_empty" => {
2638 let field_is_array = {
2641 let rf = assertion.field.as_deref().unwrap_or("");
2642 let rn = field_resolver.resolve(rf);
2643 field_resolver.is_array(rn)
2644 };
2645 if is_optional && !field_is_array {
2646 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2648 } else if is_optional && field_is_slice {
2649 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2651 } else if is_optional {
2652 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2654 } else if result_is_simple && result_is_array {
2655 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2657 } else {
2658 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2659 }
2660 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2661 let _ = writeln!(out_ref, "\t}}");
2662 }
2663 "is_empty" => {
2664 let field_is_array = {
2665 let rf = assertion.field.as_deref().unwrap_or("");
2666 let rn = field_resolver.resolve(rf);
2667 field_resolver.is_array(rn)
2668 };
2669 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2672 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2674 } else if is_optional && !field_is_array {
2675 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2677 } else if is_optional && field_is_slice {
2678 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2680 } else if is_optional {
2681 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2683 } else {
2684 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2685 }
2686 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2687 let _ = writeln!(out_ref, "\t}}");
2688 }
2689 "contains_any" => {
2690 if let Some(values) = &assertion.values {
2691 let resolved_field = assertion.field.as_deref().unwrap_or("");
2692 let resolved_name = field_resolver.resolve(resolved_field);
2693 let field_is_array = field_resolver.is_array(resolved_name);
2694 let is_opt =
2695 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2696 let field_for_contains = if is_opt && field_is_array {
2697 format!("jsonString({field_expr})")
2699 } else if is_opt {
2700 format!("fmt.Sprint(*{field_expr})")
2701 } else if field_is_array {
2702 format!("jsonString({field_expr})")
2703 } else {
2704 format!("fmt.Sprint({field_expr})")
2705 };
2706 let _ = writeln!(out_ref, "\t{{");
2707 let _ = writeln!(out_ref, "\t\tfound := false");
2708 for val in values {
2709 let go_val = json_to_go(val);
2710 let _ = writeln!(
2711 out_ref,
2712 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2713 );
2714 }
2715 let _ = writeln!(out_ref, "\t\tif !found {{");
2716 let _ = writeln!(
2717 out_ref,
2718 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2719 );
2720 let _ = writeln!(out_ref, "\t\t}}");
2721 let _ = writeln!(out_ref, "\t}}");
2722 }
2723 }
2724 "greater_than" => {
2725 if let Some(val) = &assertion.value {
2726 let go_val = json_to_go(val);
2727 if is_optional {
2731 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2732 if let Some(n) = val.as_u64() {
2733 let next = n + 1;
2734 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2735 } else {
2736 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2737 }
2738 let _ = writeln!(
2739 out_ref,
2740 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2741 );
2742 let _ = writeln!(out_ref, "\t\t}}");
2743 let _ = writeln!(out_ref, "\t}}");
2744 } else if let Some(n) = val.as_u64() {
2745 let next = n + 1;
2746 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2747 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2748 let _ = writeln!(out_ref, "\t}}");
2749 } else {
2750 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2751 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2752 let _ = writeln!(out_ref, "\t}}");
2753 }
2754 }
2755 }
2756 "less_than" => {
2757 if let Some(val) = &assertion.value {
2758 let go_val = json_to_go(val);
2759 if let Some(ref guard) = nil_guard_expr {
2760 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2761 let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2762 let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2763 let _ = writeln!(out_ref, "\t\t}}");
2764 let _ = writeln!(out_ref, "\t}}");
2765 } else if is_optional && !field_expr.starts_with("len(") {
2766 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2768 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2769 let _ = writeln!(
2770 out_ref,
2771 "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2772 );
2773 let _ = writeln!(out_ref, "\t\t}}");
2774 let _ = writeln!(out_ref, "\t}}");
2775 } else {
2776 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2777 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2778 let _ = writeln!(out_ref, "\t}}");
2779 }
2780 }
2781 }
2782 "greater_than_or_equal" => {
2783 if let Some(val) = &assertion.value {
2784 let go_val = json_to_go(val);
2785 if let Some(ref guard) = nil_guard_expr {
2786 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2787 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2788 let _ = writeln!(
2789 out_ref,
2790 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2791 );
2792 let _ = writeln!(out_ref, "\t\t}}");
2793 let _ = writeln!(out_ref, "\t}}");
2794 } else if is_optional && !field_expr.starts_with("len(") {
2795 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2797 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2798 let _ = writeln!(
2799 out_ref,
2800 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2801 );
2802 let _ = writeln!(out_ref, "\t\t}}");
2803 let _ = writeln!(out_ref, "\t}}");
2804 } else {
2805 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2806 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2807 let _ = writeln!(out_ref, "\t}}");
2808 }
2809 }
2810 }
2811 "less_than_or_equal" => {
2812 if let Some(val) = &assertion.value {
2813 let go_val = json_to_go(val);
2814 if is_optional && !field_expr.starts_with("len(") {
2815 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2817 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2818 let _ = writeln!(
2819 out_ref,
2820 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2821 );
2822 let _ = writeln!(out_ref, "\t\t}}");
2823 let _ = writeln!(out_ref, "\t}}");
2824 } else {
2825 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2826 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2827 let _ = writeln!(out_ref, "\t}}");
2828 }
2829 }
2830 }
2831 "starts_with" => {
2832 if let Some(expected) = &assertion.value {
2833 let go_val = json_to_go(expected);
2834 let field_for_prefix = if is_optional
2835 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2836 {
2837 format!("string(*{field_expr})")
2838 } else {
2839 format!("string({field_expr})")
2840 };
2841 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2842 let _ = writeln!(
2843 out_ref,
2844 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2845 );
2846 let _ = writeln!(out_ref, "\t}}");
2847 }
2848 }
2849 "count_min" => {
2850 if let Some(val) = &assertion.value {
2851 if let Some(n) = val.as_u64() {
2852 if is_optional {
2853 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2854 let len_expr = if field_is_slice {
2856 format!("len({field_expr})")
2857 } else {
2858 format!("len(*{field_expr})")
2859 };
2860 let _ = writeln!(
2861 out_ref,
2862 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2863 );
2864 let _ = writeln!(out_ref, "\t}}");
2865 } else {
2866 let _ = writeln!(
2867 out_ref,
2868 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2869 );
2870 }
2871 }
2872 }
2873 }
2874 "count_equals" => {
2875 if let Some(val) = &assertion.value {
2876 if let Some(n) = val.as_u64() {
2877 if is_optional {
2878 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2879 let len_expr = if field_is_slice {
2881 format!("len({field_expr})")
2882 } else {
2883 format!("len(*{field_expr})")
2884 };
2885 let _ = writeln!(
2886 out_ref,
2887 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2888 );
2889 let _ = writeln!(out_ref, "\t}}");
2890 } else {
2891 let _ = writeln!(
2892 out_ref,
2893 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2894 );
2895 }
2896 }
2897 }
2898 }
2899 "is_true" => {
2900 if is_optional {
2901 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2902 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2903 let _ = writeln!(out_ref, "\t}}");
2904 } else {
2905 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2906 }
2907 }
2908 "is_false" => {
2909 if is_optional {
2910 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2911 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2912 let _ = writeln!(out_ref, "\t}}");
2913 } else {
2914 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2915 }
2916 }
2917 "method_result" => {
2918 if let Some(method_name) = &assertion.method {
2919 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2920 let check = assertion.check.as_deref().unwrap_or("is_true");
2921 let deref_expr = if info.is_pointer {
2924 format!("*{}", info.call_expr)
2925 } else {
2926 info.call_expr.clone()
2927 };
2928 match check {
2929 "equals" => {
2930 if let Some(val) = &assertion.value {
2931 if val.is_boolean() {
2932 if val.as_bool() == Some(true) {
2933 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2934 } else {
2935 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2936 }
2937 } else {
2938 let go_val = if let Some(cast) = info.value_cast {
2942 if val.is_number() {
2943 format!("{cast}({})", json_to_go(val))
2944 } else {
2945 json_to_go(val)
2946 }
2947 } else {
2948 json_to_go(val)
2949 };
2950 let _ = writeln!(
2951 out_ref,
2952 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2953 );
2954 }
2955 }
2956 }
2957 "is_true" => {
2958 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2959 }
2960 "is_false" => {
2961 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2962 }
2963 "greater_than_or_equal" => {
2964 if let Some(val) = &assertion.value {
2965 let n = val.as_u64().unwrap_or(0);
2966 let cast = info.value_cast.unwrap_or("uint");
2968 let _ = writeln!(
2969 out_ref,
2970 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2971 );
2972 }
2973 }
2974 "count_min" => {
2975 if let Some(val) = &assertion.value {
2976 let n = val.as_u64().unwrap_or(0);
2977 let _ = writeln!(
2978 out_ref,
2979 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2980 );
2981 }
2982 }
2983 "contains" => {
2984 if let Some(val) = &assertion.value {
2985 let go_val = json_to_go(val);
2986 let _ = writeln!(
2987 out_ref,
2988 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2989 );
2990 }
2991 }
2992 "is_error" => {
2993 let _ = writeln!(out_ref, "\t{{");
2994 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2995 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2996 let _ = writeln!(out_ref, "\t}}");
2997 }
2998 other_check => {
2999 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3000 }
3001 }
3002 } else {
3003 panic!("Go e2e generator: method_result assertion missing 'method' field");
3004 }
3005 }
3006 "min_length" => {
3007 if let Some(val) = &assertion.value {
3008 if let Some(n) = val.as_u64() {
3009 if is_optional {
3010 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3011 let _ = writeln!(
3012 out_ref,
3013 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3014 );
3015 let _ = writeln!(out_ref, "\t}}");
3016 } else if field_expr.starts_with("len(") {
3017 let _ = writeln!(
3018 out_ref,
3019 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3020 );
3021 } else {
3022 let _ = writeln!(
3023 out_ref,
3024 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3025 );
3026 }
3027 }
3028 }
3029 }
3030 "max_length" => {
3031 if let Some(val) = &assertion.value {
3032 if let Some(n) = val.as_u64() {
3033 if is_optional {
3034 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3035 let _ = writeln!(
3036 out_ref,
3037 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3038 );
3039 let _ = writeln!(out_ref, "\t}}");
3040 } else if field_expr.starts_with("len(") {
3041 let _ = writeln!(
3042 out_ref,
3043 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3044 );
3045 } else {
3046 let _ = writeln!(
3047 out_ref,
3048 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3049 );
3050 }
3051 }
3052 }
3053 }
3054 "ends_with" => {
3055 if let Some(expected) = &assertion.value {
3056 let go_val = json_to_go(expected);
3057 let field_for_suffix = if is_optional
3058 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3059 {
3060 format!("string(*{field_expr})")
3061 } else {
3062 format!("string({field_expr})")
3063 };
3064 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3065 let _ = writeln!(
3066 out_ref,
3067 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3068 );
3069 let _ = writeln!(out_ref, "\t}}");
3070 }
3071 }
3072 "matches_regex" => {
3073 if let Some(expected) = &assertion.value {
3074 let go_val = json_to_go(expected);
3075 let field_for_regex = if is_optional
3076 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3077 {
3078 format!("*{field_expr}")
3079 } else {
3080 field_expr.clone()
3081 };
3082 let _ = writeln!(
3083 out_ref,
3084 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3085 );
3086 }
3087 }
3088 "not_error" => {
3089 }
3091 "error" => {
3092 }
3094 other => {
3095 panic!("Go e2e generator: unsupported assertion type: {other}");
3096 }
3097 }
3098
3099 if let Some(ref arr) = array_guard {
3102 if !assertion_buf.is_empty() {
3103 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3104 for line in assertion_buf.lines() {
3106 let _ = writeln!(out, "\t{line}");
3107 }
3108 let _ = writeln!(out, "\t}}");
3109 }
3110 } else {
3111 out.push_str(&assertion_buf);
3112 }
3113}
3114
3115struct GoMethodCallInfo {
3117 call_expr: String,
3119 is_pointer: bool,
3121 value_cast: Option<&'static str>,
3124}
3125
3126fn build_go_method_call(
3141 result_var: &str,
3142 method_name: &str,
3143 args: Option<&serde_json::Value>,
3144 import_alias: &str,
3145) -> GoMethodCallInfo {
3146 match method_name {
3147 "root_node_type" => GoMethodCallInfo {
3148 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3149 is_pointer: false,
3150 value_cast: None,
3151 },
3152 "named_children_count" => GoMethodCallInfo {
3153 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3154 is_pointer: false,
3155 value_cast: Some("uint"),
3156 },
3157 "has_error_nodes" => GoMethodCallInfo {
3158 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3159 is_pointer: true,
3160 value_cast: None,
3161 },
3162 "error_count" | "tree_error_count" => GoMethodCallInfo {
3163 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3164 is_pointer: true,
3165 value_cast: Some("uint"),
3166 },
3167 "tree_to_sexp" => GoMethodCallInfo {
3168 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3169 is_pointer: true,
3170 value_cast: None,
3171 },
3172 "contains_node_type" => {
3173 let node_type = args
3174 .and_then(|a| a.get("node_type"))
3175 .and_then(|v| v.as_str())
3176 .unwrap_or("");
3177 GoMethodCallInfo {
3178 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3179 is_pointer: true,
3180 value_cast: None,
3181 }
3182 }
3183 "find_nodes_by_type" => {
3184 let node_type = args
3185 .and_then(|a| a.get("node_type"))
3186 .and_then(|v| v.as_str())
3187 .unwrap_or("");
3188 GoMethodCallInfo {
3189 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3190 is_pointer: true,
3191 value_cast: None,
3192 }
3193 }
3194 "run_query" => {
3195 let query_source = args
3196 .and_then(|a| a.get("query_source"))
3197 .and_then(|v| v.as_str())
3198 .unwrap_or("");
3199 let language = args
3200 .and_then(|a| a.get("language"))
3201 .and_then(|v| v.as_str())
3202 .unwrap_or("");
3203 let query_lit = go_string_literal(query_source);
3204 let lang_lit = go_string_literal(language);
3205 GoMethodCallInfo {
3207 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3208 is_pointer: false,
3209 value_cast: None,
3210 }
3211 }
3212 other => {
3213 let method_pascal = other.to_upper_camel_case();
3214 GoMethodCallInfo {
3215 call_expr: format!("{result_var}.{method_pascal}()"),
3216 is_pointer: false,
3217 value_cast: None,
3218 }
3219 }
3220 }
3221}
3222
3223fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3233 match value {
3234 serde_json::Value::Object(map) => {
3235 let new_map: serde_json::Map<String, serde_json::Value> = map
3236 .into_iter()
3237 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3238 .collect();
3239 serde_json::Value::Object(new_map)
3240 }
3241 serde_json::Value::Array(arr) => {
3242 if is_byte_array(&arr) {
3245 let bytes: Vec<u8> = arr
3246 .iter()
3247 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3248 .collect();
3249 let encoded = base64_encode(&bytes);
3251 serde_json::Value::String(encoded)
3252 } else {
3253 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3254 }
3255 }
3256 serde_json::Value::String(s) => {
3257 serde_json::Value::String(pascal_to_snake_case(&s))
3260 }
3261 other => other,
3262 }
3263}
3264
3265fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3267 if arr.is_empty() {
3268 return false;
3269 }
3270 arr.iter().all(|v| {
3271 if let serde_json::Value::Number(n) = v {
3272 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3273 } else {
3274 false
3275 }
3276 })
3277}
3278
3279fn base64_encode(bytes: &[u8]) -> String {
3282 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3283 let mut result = String::new();
3284 let mut i = 0;
3285
3286 while i + 2 < bytes.len() {
3287 let b1 = bytes[i];
3288 let b2 = bytes[i + 1];
3289 let b3 = bytes[i + 2];
3290
3291 result.push(TABLE[(b1 >> 2) as usize] as char);
3292 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3293 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3294 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3295
3296 i += 3;
3297 }
3298
3299 if i < bytes.len() {
3301 let b1 = bytes[i];
3302 result.push(TABLE[(b1 >> 2) as usize] as char);
3303
3304 if i + 1 < bytes.len() {
3305 let b2 = bytes[i + 1];
3306 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3307 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3308 result.push('=');
3309 } else {
3310 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3311 result.push_str("==");
3312 }
3313 }
3314
3315 result
3316}
3317
3318fn camel_to_snake_case(s: &str) -> String {
3320 let mut result = String::new();
3321 let mut prev_upper = false;
3322 for (i, c) in s.char_indices() {
3323 if c.is_uppercase() {
3324 if i > 0 && !prev_upper {
3325 result.push('_');
3326 }
3327 result.push(c.to_lowercase().next().unwrap_or(c));
3328 prev_upper = true;
3329 } else {
3330 if prev_upper && i > 1 {
3331 }
3335 result.push(c);
3336 prev_upper = false;
3337 }
3338 }
3339 result
3340}
3341
3342fn pascal_to_snake_case(s: &str) -> String {
3347 let first_char = s.chars().next();
3349 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3350 return s.to_string();
3351 }
3352 camel_to_snake_case(s)
3353}
3354
3355fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3359 let elem = element_type.unwrap_or("String").trim();
3360 let go_elem = rust_type_to_go(elem, import_alias);
3361 format!("[]{go_elem}")
3362}
3363
3364fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3367 let trimmed = rust.trim();
3368 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3369 return format!("[]{}", rust_type_to_go(inner, import_alias));
3370 }
3371 match trimmed {
3372 "String" | "&str" | "str" => "string".to_string(),
3373 "bool" => "bool".to_string(),
3374 "f32" => "float32".to_string(),
3375 "f64" => "float64".to_string(),
3376 "i8" => "int8".to_string(),
3377 "i16" => "int16".to_string(),
3378 "i32" => "int32".to_string(),
3379 "i64" | "isize" => "int64".to_string(),
3380 "u8" => "uint8".to_string(),
3381 "u16" => "uint16".to_string(),
3382 "u32" => "uint32".to_string(),
3383 "u64" | "usize" => "uint64".to_string(),
3384 _ => format!("{import_alias}.{trimmed}"),
3385 }
3386}
3387
3388fn json_to_go(value: &serde_json::Value) -> String {
3389 match value {
3390 serde_json::Value::String(s) => go_string_literal(s),
3391 serde_json::Value::Bool(b) => b.to_string(),
3392 serde_json::Value::Number(n) => n.to_string(),
3393 serde_json::Value::Null => "nil".to_string(),
3394 other => go_string_literal(&other.to_string()),
3396 }
3397}
3398
3399fn visitor_struct_name(fixture_id: &str) -> String {
3408 use heck::ToUpperCamelCase;
3409 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3411}
3412
3413fn emit_go_visitor_struct(
3418 out: &mut String,
3419 struct_name: &str,
3420 visitor_spec: &crate::fixture::VisitorSpec,
3421 import_alias: &str,
3422) {
3423 let _ = writeln!(out, "type {struct_name} struct{{");
3424 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3425 let _ = writeln!(out, "}}");
3426 for (method_name, action) in &visitor_spec.callbacks {
3427 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3428 }
3429}
3430
3431fn emit_go_visitor_method(
3433 out: &mut String,
3434 struct_name: &str,
3435 method_name: &str,
3436 action: &CallbackAction,
3437 import_alias: &str,
3438) {
3439 let camel_method = method_to_camel(method_name);
3440 let params = match method_name {
3443 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3444 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3445 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3446 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3447 "visit_code_inline"
3448 | "visit_strong"
3449 | "visit_emphasis"
3450 | "visit_strikethrough"
3451 | "visit_underline"
3452 | "visit_subscript"
3453 | "visit_superscript"
3454 | "visit_mark"
3455 | "visit_button"
3456 | "visit_summary"
3457 | "visit_figcaption"
3458 | "visit_definition_term"
3459 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3460 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3461 "visit_list_item" => {
3462 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3463 }
3464 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3465 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3466 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3467 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3468 "visit_input" => {
3469 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3470 }
3471 "visit_audio" | "visit_video" | "visit_iframe" => {
3472 format!("_ {import_alias}.NodeContext, src *string")
3473 }
3474 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3475 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3476 format!("_ {import_alias}.NodeContext, output string")
3477 }
3478 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3479 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3480 _ => format!("_ {import_alias}.NodeContext"),
3481 };
3482
3483 let _ = writeln!(
3484 out,
3485 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3486 );
3487 match action {
3488 CallbackAction::Skip => {
3489 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3490 }
3491 CallbackAction::Continue => {
3492 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3493 }
3494 CallbackAction::PreserveHtml => {
3495 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3496 }
3497 CallbackAction::Custom { output } => {
3498 let escaped = go_string_literal(output);
3499 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3500 }
3501 CallbackAction::CustomTemplate { template, .. } => {
3502 let ptr_params = go_visitor_ptr_params(method_name);
3509 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3510 let escaped_fmt = go_string_literal(&fmt_str);
3511 if fmt_args.is_empty() {
3512 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3513 } else {
3514 let args_str = fmt_args.join(", ");
3515 let _ = writeln!(
3516 out,
3517 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3518 );
3519 }
3520 }
3521 }
3522 let _ = writeln!(out, "}}");
3523}
3524
3525fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3528 match method_name {
3529 "visit_link" => ["title"].into(),
3530 "visit_image" => ["title"].into(),
3531 "visit_heading" => ["id"].into(),
3532 "visit_code_block" => ["lang"].into(),
3533 "visit_form" => ["action", "method"].into(),
3534 "visit_input" => ["name", "value"].into(),
3535 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3536 _ => std::collections::HashSet::new(),
3537 }
3538}
3539
3540fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3552 let mut fmt_str = String::new();
3553 let mut args: Vec<String> = Vec::new();
3554 let mut chars = template.chars().peekable();
3555 while let Some(c) = chars.next() {
3556 if c == '{' {
3557 let mut name = String::new();
3559 for inner in chars.by_ref() {
3560 if inner == '}' {
3561 break;
3562 }
3563 name.push(inner);
3564 }
3565 fmt_str.push_str("%s");
3566 let go_name = go_param_name(&name);
3568 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3570 format!("*{go_name}")
3571 } else {
3572 go_name
3573 };
3574 args.push(arg_expr);
3575 } else {
3576 fmt_str.push(c);
3577 }
3578 }
3579 (fmt_str, args)
3580}
3581
3582fn method_to_camel(snake: &str) -> String {
3584 use heck::ToUpperCamelCase;
3585 snake.to_upper_camel_case()
3586}
3587
3588#[cfg(test)]
3589mod tests {
3590 use super::*;
3591 use crate::config::{CallConfig, E2eConfig};
3592 use crate::fixture::{Assertion, Fixture};
3593
3594 fn make_fixture(id: &str) -> Fixture {
3595 Fixture {
3596 id: id.to_string(),
3597 category: None,
3598 description: "test fixture".to_string(),
3599 tags: vec![],
3600 skip: None,
3601 env: None,
3602 call: None,
3603 input: serde_json::Value::Null,
3604 mock_response: Some(crate::fixture::MockResponse {
3605 status: 200,
3606 body: Some(serde_json::Value::Null),
3607 stream_chunks: None,
3608 headers: std::collections::HashMap::new(),
3609 }),
3610 source: String::new(),
3611 http: None,
3612 assertions: vec![Assertion {
3613 assertion_type: "not_error".to_string(),
3614 ..Default::default()
3615 }],
3616 visitor: None,
3617 }
3618 }
3619
3620 #[test]
3624 fn test_go_method_name_uses_go_casing() {
3625 let e2e_config = E2eConfig {
3626 call: CallConfig {
3627 function: "clean_extracted_text".to_string(),
3628 module: "github.com/example/mylib".to_string(),
3629 result_var: "result".to_string(),
3630 returns_result: true,
3631 ..CallConfig::default()
3632 },
3633 ..E2eConfig::default()
3634 };
3635
3636 let fixture = make_fixture("basic_text");
3637 let mut out = String::new();
3638 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3639
3640 assert!(
3641 out.contains("kreuzberg.CleanExtractedText("),
3642 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3643 );
3644 assert!(
3645 !out.contains("kreuzberg.clean_extracted_text("),
3646 "must not emit raw snake_case method name, got:\n{out}"
3647 );
3648 }
3649
3650 #[test]
3651 fn test_streaming_fixture_emits_collect_snippet() {
3652 let streaming_fixture_json = r#"{
3654 "id": "basic_stream",
3655 "description": "basic streaming test",
3656 "call": "chat_stream",
3657 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3658 "mock_response": {
3659 "status": 200,
3660 "stream_chunks": [{"delta": "hello"}]
3661 },
3662 "assertions": [
3663 {"type": "count_min", "field": "chunks", "value": 1}
3664 ]
3665 }"#;
3666 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3667 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3668
3669 let e2e_config = E2eConfig {
3670 call: CallConfig {
3671 function: "chat_stream".to_string(),
3672 module: "github.com/example/mylib".to_string(),
3673 result_var: "result".to_string(),
3674 returns_result: true,
3675 r#async: true,
3676 ..CallConfig::default()
3677 },
3678 ..E2eConfig::default()
3679 };
3680
3681 let mut out = String::new();
3682 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3683
3684 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3685 assert!(
3686 out.contains("for chunk := range stream"),
3687 "should emit collect loop, got:\n{out}"
3688 );
3689 }
3690
3691 #[test]
3692 fn test_streaming_with_client_factory_and_json_arg() {
3693 use alef_core::config::e2e::{ArgMapping, CallOverride};
3697 let streaming_fixture_json = r#"{
3698 "id": "basic_stream_client",
3699 "description": "basic streaming test with client",
3700 "call": "chat_stream",
3701 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3702 "mock_response": {
3703 "status": 200,
3704 "stream_chunks": [{"delta": "hello"}]
3705 },
3706 "assertions": [
3707 {"type": "count_min", "field": "chunks", "value": 1}
3708 ]
3709 }"#;
3710 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3711 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3712
3713 let go_override = CallOverride {
3714 client_factory: Some("CreateClient".to_string()),
3715 ..Default::default()
3716 };
3717
3718 let mut call_overrides = std::collections::HashMap::new();
3719 call_overrides.insert("go".to_string(), go_override);
3720
3721 let e2e_config = E2eConfig {
3722 call: CallConfig {
3723 function: "chat_stream".to_string(),
3724 module: "github.com/example/mylib".to_string(),
3725 result_var: "result".to_string(),
3726 returns_result: false, r#async: true,
3728 args: vec![ArgMapping {
3729 name: "request".to_string(),
3730 field: "input".to_string(),
3731 arg_type: "json_object".to_string(),
3732 optional: false,
3733 owned: true,
3734 element_type: None,
3735 go_type: None,
3736 }],
3737 overrides: call_overrides,
3738 ..CallConfig::default()
3739 },
3740 ..E2eConfig::default()
3741 };
3742
3743 let mut out = String::new();
3744 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3745
3746 eprintln!("generated:\n{out}");
3747 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3748 assert!(
3749 out.contains("for chunk := range stream"),
3750 "should emit collect loop, got:\n{out}"
3751 );
3752 }
3753
3754 #[test]
3758 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3759 let mut optional_fields = std::collections::HashSet::new();
3760 optional_fields.insert("segments".to_string());
3761 let mut array_fields = std::collections::HashSet::new();
3762 array_fields.insert("segments".to_string());
3763
3764 let e2e_config = E2eConfig {
3765 call: CallConfig {
3766 function: "transcribe".to_string(),
3767 module: "github.com/example/mylib".to_string(),
3768 result_var: "result".to_string(),
3769 returns_result: true,
3770 ..CallConfig::default()
3771 },
3772 fields_optional: optional_fields,
3773 fields_array: array_fields,
3774 ..E2eConfig::default()
3775 };
3776
3777 let fixture = Fixture {
3778 id: "edge_transcribe_with_timestamps".to_string(),
3779 category: None,
3780 description: "Transcription with timestamp segments".to_string(),
3781 tags: vec![],
3782 skip: None,
3783 env: None,
3784 call: None,
3785 input: serde_json::Value::Null,
3786 mock_response: Some(crate::fixture::MockResponse {
3787 status: 200,
3788 body: Some(serde_json::Value::Null),
3789 stream_chunks: None,
3790 headers: std::collections::HashMap::new(),
3791 }),
3792 source: String::new(),
3793 http: None,
3794 assertions: vec![
3795 Assertion {
3796 assertion_type: "not_error".to_string(),
3797 ..Default::default()
3798 },
3799 Assertion {
3800 assertion_type: "equals".to_string(),
3801 field: Some("segments[0].id".to_string()),
3802 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3803 ..Default::default()
3804 },
3805 ],
3806 visitor: None,
3807 };
3808
3809 let mut out = String::new();
3810 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3811
3812 eprintln!("generated:\n{out}");
3813
3814 assert!(
3819 out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3820 "guard must be on Segments (the slice), not an element; got:\n{out}"
3821 );
3822 assert!(
3824 !out.contains("result.Segments[0] != nil"),
3825 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3826 );
3827 }
3828}