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 needs_assert = fixtures.iter().any(|f| {
740 if !emits_executable_test(f) {
741 return false;
742 }
743 if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
747 return true;
748 }
749 let is_streaming_fixture = f.is_streaming_mock();
754 let cc =
755 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
756 let per_call_resolver = FieldResolver::new(
757 e2e_config.effective_fields(cc),
758 e2e_config.effective_fields_optional(cc),
759 e2e_config.effective_result_fields(cc),
760 e2e_config.effective_fields_array(cc),
761 &std::collections::HashSet::new(),
762 );
763 f.assertions.iter().any(|a| {
764 let field_is_streaming_virtual = a
765 .field
766 .as_deref()
767 .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
768 let field_valid = a
769 .field
770 .as_ref()
771 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
772 .unwrap_or(true)
773 || (is_streaming_fixture && field_is_streaming_virtual);
774 let synthetic_field_needs_assert = match a.field.as_deref() {
775 Some(
776 "chunks_have_content"
777 | "chunks_have_embeddings"
778 | "chunks_have_heading_context"
779 | "first_chunk_starts_with_heading",
780 ) => {
781 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
782 }
783 Some("embeddings") => {
784 matches!(
785 a.assertion_type.as_str(),
786 "count_equals" | "count_min" | "not_empty" | "is_empty"
787 )
788 }
789 _ => false,
790 };
791 let type_needs_assert = matches!(
792 a.assertion_type.as_str(),
793 "count_equals"
794 | "count_min"
795 | "count_max"
796 | "is_true"
797 | "is_false"
798 | "method_result"
799 | "min_length"
800 | "max_length"
801 | "matches_regex"
802 | "greater_than"
803 | "greater_than_or_equal"
804 | "less_than"
805 | "less_than_or_equal"
806 );
807 synthetic_field_needs_assert || (type_needs_assert && (field_valid || field_is_streaming_virtual))
808 })
809 });
810
811 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
813 let needs_http = has_http_fixtures;
814 let needs_io = has_http_fixtures;
816
817 let needs_reflect = fixtures.iter().any(|f| {
820 if let Some(http) = &f.http {
821 let body_needs_reflect = http
822 .expected_response
823 .body
824 .as_ref()
825 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
826 let partial_needs_reflect = http.expected_response.body_partial.is_some();
827 body_needs_reflect || partial_needs_reflect
828 } else {
829 false
830 }
831 });
832
833 let mut body = String::new();
838 for fixture in fixtures.iter() {
839 if let Some(visitor_spec) = &fixture.visitor {
840 let struct_name = visitor_struct_name(&fixture.id);
841 emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
842 let _ = writeln!(body);
843 }
844 }
845 for (i, fixture) in fixtures.iter().enumerate() {
846 render_test_function(&mut body, fixture, import_alias, e2e_config, adapters);
847 if i + 1 < fixtures.len() {
848 let _ = writeln!(body);
849 }
850 }
851 let needs_assert = needs_assert || body.contains("assert.");
852
853 let _ = writeln!(out, "// E2e tests for category: {category}");
854 let _ = writeln!(out, "package e2e_test");
855 let _ = writeln!(out);
856 let _ = writeln!(out, "import (");
857 if needs_base64 {
858 let _ = writeln!(out, "\t\"encoding/base64\"");
859 }
860 if needs_json || needs_reflect {
861 let _ = writeln!(out, "\t\"encoding/json\"");
862 }
863 if needs_fmt {
864 let _ = writeln!(out, "\t\"fmt\"");
865 }
866 if needs_io {
867 let _ = writeln!(out, "\t\"io\"");
868 }
869 if needs_http {
870 let _ = writeln!(out, "\t\"net/http\"");
871 }
872 if needs_os {
873 let _ = writeln!(out, "\t\"os\"");
874 }
875 let _ = needs_filepath; if needs_reflect {
877 let _ = writeln!(out, "\t\"reflect\"");
878 }
879 if needs_strings {
880 let _ = writeln!(out, "\t\"strings\"");
881 }
882 let _ = writeln!(out, "\t\"testing\"");
883 if needs_assert {
884 let _ = writeln!(out);
885 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
886 }
887
888 if needs_pkg {
889 let _ = writeln!(out);
890 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
891 }
892 let _ = writeln!(out, ")");
893 let _ = writeln!(out);
894
895 out.push_str(&body);
897
898 while out.ends_with("\n\n") {
900 out.pop();
901 }
902 if !out.ends_with('\n') {
903 out.push('\n');
904 }
905 out
906}
907
908fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
917 if fixture.is_http_test() {
919 return false;
920 }
921 let call_config = e2e_config.resolve_call_for_fixture(
922 fixture.call.as_deref(),
923 &fixture.id,
924 &fixture.resolved_category(),
925 &fixture.tags,
926 &fixture.input,
927 );
928 if call_config.skip_languages.iter().any(|l| l == "go") {
931 return false;
932 }
933 let go_override = call_config
934 .overrides
935 .get("go")
936 .or_else(|| e2e_config.call.overrides.get("go"));
937 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
940 return true;
941 }
942 let fn_name = go_override
946 .and_then(|o| o.function.as_deref())
947 .filter(|s| !s.is_empty())
948 .unwrap_or(call_config.function.as_str());
949 !fn_name.is_empty()
950}
951
952fn render_test_function(
953 out: &mut String,
954 fixture: &Fixture,
955 import_alias: &str,
956 e2e_config: &crate::config::E2eConfig,
957 adapters: &[alef_core::config::AdapterConfig],
958) {
959 let fn_name = fixture.id.to_upper_camel_case();
960 let description = &fixture.description;
961
962 if fixture.http.is_some() {
964 render_http_test_function(out, fixture);
965 return;
966 }
967
968 if !fixture_has_go_callable(fixture, e2e_config) {
973 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
974 let _ = writeln!(out, "\t// {description}");
975 let _ = writeln!(
976 out,
977 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
978 );
979 let _ = writeln!(out, "}}");
980 return;
981 }
982
983 let call_config = e2e_config.resolve_call_for_fixture(
985 fixture.call.as_deref(),
986 &fixture.id,
987 &fixture.resolved_category(),
988 &fixture.tags,
989 &fixture.input,
990 );
991 let call_field_resolver = FieldResolver::new(
993 e2e_config.effective_fields(call_config),
994 e2e_config.effective_fields_optional(call_config),
995 e2e_config.effective_result_fields(call_config),
996 e2e_config.effective_fields_array(call_config),
997 &std::collections::HashSet::new(),
998 );
999 let field_resolver = &call_field_resolver;
1000 let lang = "go";
1001 let overrides = call_config.overrides.get(lang);
1002
1003 let base_function_name = overrides
1007 .and_then(|o| o.function.as_deref())
1008 .unwrap_or(&call_config.function);
1009 let function_name = to_go_name(base_function_name);
1010 let result_var = &call_config.result_var;
1011 let args = &call_config.args;
1012
1013 let returns_result = overrides
1016 .and_then(|o| o.returns_result)
1017 .unwrap_or(call_config.returns_result);
1018
1019 let returns_void = call_config.returns_void;
1022
1023 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
1029 || call_config.result_is_simple
1030 || call_config
1031 .overrides
1032 .get("rust")
1033 .map(|o| o.result_is_simple)
1034 .unwrap_or(false);
1035
1036 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
1042
1043 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
1045 e2e_config
1046 .call
1047 .overrides
1048 .get("go")
1049 .and_then(|o| o.options_type.as_deref())
1050 });
1051
1052 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1054 e2e_config
1055 .call
1056 .overrides
1057 .get("go")
1058 .map(|o| o.options_ptr)
1059 .unwrap_or(false)
1060 });
1061
1062 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1063 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1067
1068 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1071 e2e_config
1072 .call
1073 .overrides
1074 .get(lang)
1075 .and_then(|o| o.client_factory.as_deref())
1076 });
1077
1078 let (mut setup_lines, args_str) = build_args_and_setup(
1079 &fixture.input,
1080 args,
1081 import_alias,
1082 call_options_type,
1083 fixture,
1084 call_options_ptr,
1085 validation_creation_failure,
1086 );
1087
1088 let mut visitor_opts_var: Option<String> = None;
1091 if fixture.visitor.is_some() {
1092 let struct_name = visitor_struct_name(&fixture.id);
1093 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1094 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1096 let opts_var = "opts".to_string();
1097 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1098 setup_lines.push("opts.Visitor = visitor".to_string());
1099 visitor_opts_var = Some(opts_var);
1100 }
1101
1102 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1103 let final_args = {
1104 let mut parts: Vec<String> = Vec::new();
1105 if !args_str.is_empty() {
1106 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1108 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1109 } else {
1110 args_str
1111 };
1112 parts.push(processed_args);
1113 }
1114 parts.extend(go_extra_args);
1115 parts.join(", ")
1116 };
1117
1118 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1119 let _ = writeln!(out, "\t// {description}");
1120
1121 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1125 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1126 if let Some(var) = api_key_var {
1127 if has_mock {
1128 let fixture_id = &fixture.id;
1132 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1133 let _ = writeln!(out, "\tvar baseURL *string");
1134 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1135 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1136 let _ = writeln!(out, "\t}} else {{");
1137 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1138 let _ = writeln!(
1139 out,
1140 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1141 );
1142 let _ = writeln!(out, "\t\tbaseURL = &u");
1143 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1144 let _ = writeln!(out, "\t}}");
1145 } else {
1146 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1147 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1148 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1149 let _ = writeln!(out, "\t}}");
1150 }
1151 }
1152
1153 for line in &setup_lines {
1154 let _ = writeln!(out, "\t{line}");
1155 }
1156
1157 let call_prefix = if let Some(factory) = client_factory {
1161 let factory_name = to_go_name(factory);
1162 let fixture_id = &fixture.id;
1163 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1166 ("apiKey".to_string(), "baseURL".to_string())
1168 } else if api_key_var.is_some() {
1169 ("apiKey".to_string(), "nil".to_string())
1171 } else if fixture.has_host_root_route() {
1172 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1173 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1174 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1175 let _ = writeln!(
1176 out,
1177 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1178 );
1179 let _ = writeln!(out, "\t}}");
1180 ("\"test-key\"".to_string(), "&mockURL".to_string())
1181 } else {
1182 let _ = writeln!(
1183 out,
1184 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1185 );
1186 ("\"test-key\"".to_string(), "&mockURL".to_string())
1187 };
1188 let _ = writeln!(
1189 out,
1190 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1191 );
1192 let _ = writeln!(out, "\tif clientErr != nil {{");
1193 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1194 let _ = writeln!(out, "\t}}");
1195 "client".to_string()
1196 } else {
1197 import_alias.to_string()
1198 };
1199
1200 let binding_returns_error_pre = args
1205 .iter()
1206 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1207 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1208
1209 if expects_error {
1210 if effective_returns_result_pre && !returns_void {
1211 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1212 } else {
1213 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1214 }
1215 let _ = writeln!(out, "\tif err == nil {{");
1216 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1217 let _ = writeln!(out, "\t}}");
1218 let _ = writeln!(out, "}}");
1219 return;
1220 }
1221
1222 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1224
1225 use heck::ToSnakeCase;
1230 let fn_snake = function_name.to_snake_case();
1231 let base_snake = base_function_name.to_snake_case();
1232 let streaming_item_type = if is_streaming {
1233 adapters
1234 .iter()
1235 .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1236 .find(|a| a.name == fn_snake || a.name == base_snake)
1237 .and_then(|a| a.item_type.as_deref())
1238 .and_then(|t| t.rsplit("::").next())
1239 .unwrap_or("Item") } else {
1241 "Item" };
1243
1244 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1249 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1250 return false;
1251 }
1252 if a.assertion_type == "method_result" {
1254 return true;
1255 }
1256 match &a.field {
1257 Some(f) if !f.is_empty() => {
1258 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1259 return true;
1260 }
1261 field_resolver.is_valid_for_result(f)
1262 }
1263 _ => true,
1264 }
1265 });
1266
1267 let binding_returns_error = args
1274 .iter()
1275 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1276 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1278
1279 if !effective_returns_result && result_is_simple {
1285 let result_binding = if has_usable_assertion {
1287 result_var.to_string()
1288 } else {
1289 "_".to_string()
1290 };
1291 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1293 let _ = writeln!(
1294 out,
1295 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1296 );
1297 if has_usable_assertion && result_binding != "_" {
1298 if result_is_array {
1299 let _ = writeln!(out, "\tvalue := {result_var}");
1301 } else {
1302 let only_nil_assertions = fixture
1305 .assertions
1306 .iter()
1307 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1308 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1309 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1310
1311 if !only_nil_assertions {
1312 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1315 if result_is_ptr {
1316 let _ = writeln!(out, "\tif {result_var} == nil {{");
1317 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1318 let _ = writeln!(out, "\t}}");
1319 let _ = writeln!(out, "\tvalue := *{result_var}");
1320 } else {
1321 let _ = writeln!(out, "\tvalue := {result_var}");
1323 }
1324 }
1325 }
1326 }
1327 } else if !effective_returns_result || returns_void {
1328 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1331 let _ = writeln!(out, "\tif err != nil {{");
1332 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1333 let _ = writeln!(out, "\t}}");
1334 let _ = writeln!(out, "}}");
1336 return;
1337 } else {
1338 let result_binding = if is_streaming {
1341 "stream".to_string()
1342 } else if has_usable_assertion {
1343 result_var.to_string()
1344 } else {
1345 "_".to_string()
1346 };
1347 let _ = writeln!(
1348 out,
1349 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1350 );
1351 let _ = writeln!(out, "\tif err != nil {{");
1352 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1353 let _ = writeln!(out, "\t}}");
1354 if is_streaming {
1356 let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1357 let _ = writeln!(out, "\tfor chunk := range stream {{");
1358 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1359 let _ = writeln!(out, "\t}}");
1360 }
1361 if result_is_simple && has_usable_assertion && result_binding != "_" {
1362 if result_is_array {
1363 let _ = writeln!(out, "\tvalue := {}", result_var);
1365 } else {
1366 let only_nil_assertions = fixture
1369 .assertions
1370 .iter()
1371 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1372 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1373 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1374
1375 if !only_nil_assertions {
1376 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1379 if result_is_ptr {
1380 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1381 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1382 let _ = writeln!(out, "\t}}");
1383 let _ = writeln!(out, "\tvalue := *{}", result_var);
1384 } else {
1385 let _ = writeln!(out, "\tvalue := {}", result_var);
1387 }
1388 }
1389 }
1390 }
1391 }
1392
1393 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1397 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1398 let only_nil_assertions = fixture
1399 .assertions
1400 .iter()
1401 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1402 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1403 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1404 !only_nil_assertions
1405 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1406 true
1407 } else {
1408 result_is_simple && has_usable_assertion
1409 };
1410
1411 let effective_result_var = if has_deref_value {
1412 "value".to_string()
1413 } else {
1414 result_var.to_string()
1415 };
1416
1417 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1422 for assertion in &fixture.assertions {
1423 if let Some(f) = &assertion.field {
1424 if !f.is_empty() {
1425 let resolved = field_resolver.resolve(f);
1426 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1427 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1432 let is_array_field = field_resolver.is_array(resolved);
1433 if !is_string_field || is_array_field {
1434 continue;
1437 }
1438 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1439 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1440 if field_resolver.has_map_access(f) {
1441 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1444 } else {
1445 let _ = writeln!(out, "\tvar {local_var} string");
1446 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1447 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1451 let _ = writeln!(out, "\t}}");
1452 }
1453 optional_locals.insert(f.clone(), local_var);
1454 }
1455 }
1456 }
1457 }
1458
1459 for assertion in &fixture.assertions {
1461 if let Some(f) = &assertion.field {
1462 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1463 let parts: Vec<&str> = f.split('.').collect();
1466 let mut guard_expr: Option<String> = None;
1467 for i in 1..parts.len() {
1468 let prefix = parts[..i].join(".");
1469 let resolved_prefix = field_resolver.resolve(&prefix);
1470 if field_resolver.is_optional(resolved_prefix) {
1471 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1477 let suffix = &resolved_prefix[bracket_pos + 1..];
1478 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1479 if is_numeric_index {
1480 &resolved_prefix[..bracket_pos]
1481 } else {
1482 resolved_prefix
1483 }
1484 } else {
1485 resolved_prefix
1486 };
1487 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1488 guard_expr = Some(accessor);
1489 break;
1490 }
1491 }
1492 if let Some(guard) = guard_expr {
1493 if field_resolver.is_valid_for_result(f) {
1496 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1502 if is_struct_value {
1503 render_assertion(
1506 out,
1507 assertion,
1508 &effective_result_var,
1509 import_alias,
1510 field_resolver,
1511 &optional_locals,
1512 result_is_simple,
1513 result_is_array,
1514 is_streaming,
1515 );
1516 continue;
1517 }
1518 let _ = writeln!(out, "\tif {guard} != nil {{");
1519 let mut nil_buf = String::new();
1522 render_assertion(
1523 &mut nil_buf,
1524 assertion,
1525 &effective_result_var,
1526 import_alias,
1527 field_resolver,
1528 &optional_locals,
1529 result_is_simple,
1530 result_is_array,
1531 is_streaming,
1532 );
1533 for line in nil_buf.lines() {
1534 let _ = writeln!(out, "\t{line}");
1535 }
1536 let _ = writeln!(out, "\t}}");
1537 } else {
1538 render_assertion(
1539 out,
1540 assertion,
1541 &effective_result_var,
1542 import_alias,
1543 field_resolver,
1544 &optional_locals,
1545 result_is_simple,
1546 result_is_array,
1547 is_streaming,
1548 );
1549 }
1550 continue;
1551 }
1552 }
1553 }
1554 render_assertion(
1555 out,
1556 assertion,
1557 &effective_result_var,
1558 import_alias,
1559 field_resolver,
1560 &optional_locals,
1561 result_is_simple,
1562 result_is_array,
1563 is_streaming,
1564 );
1565 }
1566
1567 let _ = writeln!(out, "}}");
1568}
1569
1570fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1576 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1577}
1578
1579struct GoTestClientRenderer;
1591
1592impl client::TestClientRenderer for GoTestClientRenderer {
1593 fn language_name(&self) -> &'static str {
1594 "go"
1595 }
1596
1597 fn sanitize_test_name(&self, id: &str) -> String {
1601 id.to_upper_camel_case()
1602 }
1603
1604 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1607 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1608 let _ = writeln!(out, "\t// {description}");
1609 if let Some(reason) = skip_reason {
1610 let escaped = go_string_literal(reason);
1611 let _ = writeln!(out, "\tt.Skip({escaped})");
1612 }
1613 }
1614
1615 fn render_test_close(&self, out: &mut String) {
1616 let _ = writeln!(out, "}}");
1617 }
1618
1619 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1625 let method = ctx.method.to_uppercase();
1626 let path = ctx.path;
1627
1628 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1629 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1630 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1631 let _ = writeln!(out, "\t}}");
1632
1633 let body_expr = if let Some(body) = ctx.body {
1635 let json = serde_json::to_string(body).unwrap_or_default();
1636 let escaped = go_string_literal(&json);
1637 format!("strings.NewReader({})", escaped)
1638 } else {
1639 "strings.NewReader(\"\")".to_string()
1640 };
1641
1642 let _ = writeln!(out, "\tbody := {body_expr}");
1643 let _ = writeln!(
1644 out,
1645 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1646 );
1647 let _ = writeln!(out, "\tif err != nil {{");
1648 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1649 let _ = writeln!(out, "\t}}");
1650
1651 if ctx.body.is_some() {
1653 let content_type = ctx.content_type.unwrap_or("application/json");
1654 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1655 }
1656
1657 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1659 header_names.sort();
1660 for name in header_names {
1661 let value = &ctx.headers[name];
1662 let escaped_name = go_string_literal(name);
1663 let escaped_value = go_string_literal(value);
1664 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1665 }
1666
1667 if !ctx.cookies.is_empty() {
1669 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1670 cookie_names.sort();
1671 for name in cookie_names {
1672 let value = &ctx.cookies[name];
1673 let escaped_name = go_string_literal(name);
1674 let escaped_value = go_string_literal(value);
1675 let _ = writeln!(
1676 out,
1677 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1678 );
1679 }
1680 }
1681
1682 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1684 let _ = writeln!(
1685 out,
1686 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1687 );
1688 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1689 let _ = writeln!(out, "\t\t}},");
1690 let _ = writeln!(out, "\t}}");
1691 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1692 let _ = writeln!(out, "\tif err != nil {{");
1693 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1694 let _ = writeln!(out, "\t}}");
1695 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1696
1697 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1701 let _ = writeln!(out, "\tif err != nil {{");
1702 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1703 let _ = writeln!(out, "\t}}");
1704 let _ = writeln!(out, "\t_ = bodyBytes");
1705 }
1706
1707 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1708 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1709 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1710 let _ = writeln!(out, "\t}}");
1711 }
1712
1713 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1716 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1718 return;
1719 }
1720 if name.eq_ignore_ascii_case("connection") {
1722 return;
1723 }
1724 let escaped_name = go_string_literal(name);
1725 let escaped_value = go_string_literal(expected);
1726 let _ = writeln!(
1727 out,
1728 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1729 );
1730 let _ = writeln!(
1731 out,
1732 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1733 );
1734 let _ = writeln!(out, "\t}}");
1735 }
1736
1737 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1742 match expected {
1743 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1744 let json_str = serde_json::to_string(expected).unwrap_or_default();
1745 let escaped = go_string_literal(&json_str);
1746 let _ = writeln!(out, "\tvar got any");
1747 let _ = writeln!(out, "\tvar want any");
1748 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1749 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1750 let _ = writeln!(out, "\t}}");
1751 let _ = writeln!(
1752 out,
1753 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1754 );
1755 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1756 let _ = writeln!(out, "\t}}");
1757 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1758 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1759 let _ = writeln!(out, "\t}}");
1760 }
1761 serde_json::Value::String(s) => {
1762 let escaped = go_string_literal(s);
1763 let _ = writeln!(out, "\twant := {escaped}");
1764 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1765 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1766 let _ = writeln!(out, "\t}}");
1767 }
1768 other => {
1769 let escaped = go_string_literal(&other.to_string());
1770 let _ = writeln!(out, "\twant := {escaped}");
1771 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1772 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1773 let _ = writeln!(out, "\t}}");
1774 }
1775 }
1776 }
1777
1778 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1781 if let Some(obj) = expected.as_object() {
1782 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1783 let _ = writeln!(
1784 out,
1785 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1786 );
1787 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1788 let _ = writeln!(out, "\t}}");
1789 for (key, val) in obj {
1790 let escaped_key = go_string_literal(key);
1791 let json_val = serde_json::to_string(val).unwrap_or_default();
1792 let escaped_val = go_string_literal(&json_val);
1793 let _ = writeln!(out, "\t{{");
1794 let _ = writeln!(out, "\t\tvar _wantVal any");
1795 let _ = writeln!(
1796 out,
1797 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1798 );
1799 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1800 let _ = writeln!(out, "\t\t}}");
1801 let _ = writeln!(
1802 out,
1803 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1804 );
1805 let _ = writeln!(
1806 out,
1807 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1808 );
1809 let _ = writeln!(out, "\t\t}}");
1810 let _ = writeln!(out, "\t}}");
1811 }
1812 }
1813 }
1814
1815 fn render_assert_validation_errors(
1820 &self,
1821 out: &mut String,
1822 _response_var: &str,
1823 errors: &[ValidationErrorExpectation],
1824 ) {
1825 let _ = writeln!(out, "\tvar _veBody map[string]any");
1826 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1827 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1828 let _ = writeln!(out, "\t}}");
1829 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1830 for ve in errors {
1831 let escaped_msg = go_string_literal(&ve.msg);
1832 let _ = writeln!(out, "\t{{");
1833 let _ = writeln!(out, "\t\t_found := false");
1834 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1835 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1836 let _ = writeln!(
1837 out,
1838 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1839 );
1840 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1841 let _ = writeln!(out, "\t\t\t\t\tbreak");
1842 let _ = writeln!(out, "\t\t\t\t}}");
1843 let _ = writeln!(out, "\t\t\t}}");
1844 let _ = writeln!(out, "\t\t}}");
1845 let _ = writeln!(out, "\t\tif !_found {{");
1846 let _ = writeln!(
1847 out,
1848 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1849 );
1850 let _ = writeln!(out, "\t\t}}");
1851 let _ = writeln!(out, "\t}}");
1852 }
1853 }
1854}
1855
1856fn build_args_and_setup(
1864 input: &serde_json::Value,
1865 args: &[crate::config::ArgMapping],
1866 import_alias: &str,
1867 options_type: Option<&str>,
1868 fixture: &crate::fixture::Fixture,
1869 options_ptr: bool,
1870 expects_error: bool,
1871) -> (Vec<String>, String) {
1872 let fixture_id = &fixture.id;
1873 use heck::ToUpperCamelCase;
1874
1875 if args.is_empty() {
1876 return (Vec::new(), String::new());
1877 }
1878
1879 let mut setup_lines: Vec<String> = Vec::new();
1880 let mut parts: Vec<String> = Vec::new();
1881
1882 for arg in args {
1883 if arg.arg_type == "mock_url" {
1884 if fixture.has_host_root_route() {
1885 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1886 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1887 setup_lines.push(format!(
1888 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1889 arg.name, arg.name
1890 ));
1891 } else {
1892 setup_lines.push(format!(
1893 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1894 arg.name,
1895 ));
1896 }
1897 parts.push(arg.name.clone());
1898 continue;
1899 }
1900
1901 if arg.arg_type == "mock_url_list" {
1902 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1907 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1908 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1909
1910 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1911 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1912 } else {
1913 Vec::new()
1914 };
1915
1916 let paths_literal = paths.join(", ");
1917 let var_name = &arg.name;
1918
1919 setup_lines.push(format!(
1920 "{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}}"
1921 ));
1922 setup_lines.push(format!(
1923 "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}}"
1924 ));
1925 parts.push(var_name.to_string());
1926 continue;
1927 }
1928
1929 if arg.arg_type == "handle" {
1930 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1932 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1933 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1934 let create_err_handler = if expects_error {
1938 "assert.Error(t, createErr)\n\t\treturn".to_string()
1939 } else {
1940 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1941 };
1942 if config_value.is_null()
1943 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1944 {
1945 setup_lines.push(format!(
1946 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1947 name = arg.name,
1948 ));
1949 } else {
1950 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1951 let go_literal = go_string_literal(&json_str);
1952 let name = &arg.name;
1953 setup_lines.push(format!(
1954 "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}}"
1955 ));
1956 setup_lines.push(format!(
1957 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1958 ));
1959 }
1960 parts.push(arg.name.clone());
1961 continue;
1962 }
1963
1964 let val: Option<&serde_json::Value> = if arg.field == "input" {
1965 Some(input)
1966 } else {
1967 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1968 input.get(field)
1969 };
1970
1971 if arg.arg_type == "bytes" {
1978 let var_name = format!("{}Bytes", arg.name);
1979 match val {
1980 None | Some(serde_json::Value::Null) => {
1981 if arg.optional {
1982 parts.push("nil".to_string());
1983 } else {
1984 parts.push("[]byte{}".to_string());
1985 }
1986 }
1987 Some(serde_json::Value::String(s)) => {
1988 let go_path = go_string_literal(s);
1993 setup_lines.push(format!(
1994 "{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}}"
1995 ));
1996 parts.push(var_name);
1997 }
1998 Some(other) => {
1999 parts.push(format!("[]byte({})", json_to_go(other)));
2000 }
2001 }
2002 continue;
2003 }
2004
2005 match val {
2006 None | Some(serde_json::Value::Null) if arg.optional => {
2007 match arg.arg_type.as_str() {
2009 "string" => {
2010 parts.push("nil".to_string());
2012 }
2013 "json_object" => {
2014 if options_ptr {
2015 parts.push("nil".to_string());
2017 } else if let Some(opts_type) = options_type {
2018 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2020 } else {
2021 parts.push("nil".to_string());
2022 }
2023 }
2024 _ => {
2025 parts.push("nil".to_string());
2026 }
2027 }
2028 }
2029 None | Some(serde_json::Value::Null) => {
2030 let default_val = match arg.arg_type.as_str() {
2032 "string" => "\"\"".to_string(),
2033 "int" | "integer" | "i64" => "0".to_string(),
2034 "float" | "number" => "0.0".to_string(),
2035 "bool" | "boolean" => "false".to_string(),
2036 "json_object" => {
2037 if options_ptr {
2038 "nil".to_string()
2040 } else if let Some(opts_type) = options_type {
2041 format!("{import_alias}.{opts_type}{{}}")
2042 } else {
2043 "nil".to_string()
2044 }
2045 }
2046 _ => "nil".to_string(),
2047 };
2048 parts.push(default_val);
2049 }
2050 Some(v) => {
2051 match arg.arg_type.as_str() {
2052 "json_object" => {
2053 let is_array = v.is_array();
2056 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2057 if is_empty_obj {
2058 if options_ptr {
2059 parts.push("nil".to_string());
2061 } else if let Some(opts_type) = options_type {
2062 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2063 } else {
2064 parts.push("nil".to_string());
2065 }
2066 } else if is_array {
2067 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2072 if go_t.starts_with('[') {
2076 go_t.to_string()
2077 } else {
2078 let qualified = if go_t.contains('.') {
2080 go_t.to_string()
2081 } else {
2082 format!("{import_alias}.{go_t}")
2083 };
2084 format!("[]{qualified}")
2085 }
2086 } else {
2087 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2088 };
2089 let converted_v = convert_json_for_go(v.clone());
2091 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2092 let go_literal = go_string_literal(&json_str);
2093 let var_name = &arg.name;
2094 setup_lines.push(format!(
2095 "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}}"
2096 ));
2097 parts.push(var_name.to_string());
2098 } else if let Some(opts_type) = options_type {
2099 let remapped_v = if options_ptr {
2104 convert_json_for_go(v.clone())
2105 } else {
2106 v.clone()
2107 };
2108 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2109 let go_literal = go_string_literal(&json_str);
2110 let var_name = &arg.name;
2111 setup_lines.push(format!(
2112 "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}}"
2113 ));
2114 let arg_expr = if options_ptr {
2116 format!("&{var_name}")
2117 } else {
2118 var_name.to_string()
2119 };
2120 parts.push(arg_expr);
2121 } else {
2122 parts.push(json_to_go(v));
2123 }
2124 }
2125 "string" if arg.optional => {
2126 let var_name = format!("{}Val", arg.name);
2128 let go_val = json_to_go(v);
2129 setup_lines.push(format!("{var_name} := {go_val}"));
2130 parts.push(format!("&{var_name}"));
2131 }
2132 _ => {
2133 parts.push(json_to_go(v));
2134 }
2135 }
2136 }
2137 }
2138 }
2139
2140 (setup_lines, parts.join(", "))
2141}
2142
2143#[allow(clippy::too_many_arguments)]
2144fn render_assertion(
2145 out: &mut String,
2146 assertion: &Assertion,
2147 result_var: &str,
2148 import_alias: &str,
2149 field_resolver: &FieldResolver,
2150 optional_locals: &std::collections::HashMap<String, String>,
2151 result_is_simple: bool,
2152 result_is_array: bool,
2153 is_streaming: bool,
2154) {
2155 if !result_is_simple {
2158 if let Some(f) = &assertion.field {
2159 let embed_deref = format!("(*{result_var})");
2162 match f.as_str() {
2163 "chunks_have_content" => {
2164 let pred = format!(
2165 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2166 );
2167 match assertion.assertion_type.as_str() {
2168 "is_true" => {
2169 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2170 }
2171 "is_false" => {
2172 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2173 }
2174 _ => {
2175 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2176 }
2177 }
2178 return;
2179 }
2180 "chunks_have_embeddings" => {
2181 let pred = format!(
2182 "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 }}()"
2183 );
2184 match assertion.assertion_type.as_str() {
2185 "is_true" => {
2186 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2187 }
2188 "is_false" => {
2189 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2190 }
2191 _ => {
2192 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2193 }
2194 }
2195 return;
2196 }
2197 "chunks_have_heading_context" => {
2198 let pred = format!(
2199 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2200 );
2201 match assertion.assertion_type.as_str() {
2202 "is_true" => {
2203 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2204 }
2205 "is_false" => {
2206 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2207 }
2208 _ => {
2209 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2210 }
2211 }
2212 return;
2213 }
2214 "first_chunk_starts_with_heading" => {
2215 let pred = format!(
2216 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2217 );
2218 match assertion.assertion_type.as_str() {
2219 "is_true" => {
2220 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2221 }
2222 "is_false" => {
2223 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2224 }
2225 _ => {
2226 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2227 }
2228 }
2229 return;
2230 }
2231 "embeddings" => {
2232 match assertion.assertion_type.as_str() {
2233 "count_equals" => {
2234 if let Some(val) = &assertion.value {
2235 if let Some(n) = val.as_u64() {
2236 let _ = writeln!(
2237 out,
2238 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2239 );
2240 }
2241 }
2242 }
2243 "count_min" => {
2244 if let Some(val) = &assertion.value {
2245 if let Some(n) = val.as_u64() {
2246 let _ = writeln!(
2247 out,
2248 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2249 );
2250 }
2251 }
2252 }
2253 "not_empty" => {
2254 let _ = writeln!(
2255 out,
2256 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2257 );
2258 }
2259 "is_empty" => {
2260 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2261 }
2262 _ => {
2263 let _ = writeln!(
2264 out,
2265 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2266 );
2267 }
2268 }
2269 return;
2270 }
2271 "embedding_dimensions" => {
2272 let expr = format!(
2273 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2274 );
2275 match assertion.assertion_type.as_str() {
2276 "equals" => {
2277 if let Some(val) = &assertion.value {
2278 if let Some(n) = val.as_u64() {
2279 let _ = writeln!(
2280 out,
2281 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2282 );
2283 }
2284 }
2285 }
2286 "greater_than" => {
2287 if let Some(val) = &assertion.value {
2288 if let Some(n) = val.as_u64() {
2289 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2290 }
2291 }
2292 }
2293 _ => {
2294 let _ = writeln!(
2295 out,
2296 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2297 );
2298 }
2299 }
2300 return;
2301 }
2302 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2303 let pred = match f.as_str() {
2304 "embeddings_valid" => {
2305 format!(
2306 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2307 )
2308 }
2309 "embeddings_finite" => {
2310 format!(
2311 "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 }}()"
2312 )
2313 }
2314 "embeddings_non_zero" => {
2315 format!(
2316 "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 }}()"
2317 )
2318 }
2319 "embeddings_normalized" => {
2320 format!(
2321 "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 }}()"
2322 )
2323 }
2324 _ => unreachable!(),
2325 };
2326 match assertion.assertion_type.as_str() {
2327 "is_true" => {
2328 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2329 }
2330 "is_false" => {
2331 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2332 }
2333 _ => {
2334 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2335 }
2336 }
2337 return;
2338 }
2339 "keywords" | "keywords_count" => {
2342 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2343 return;
2344 }
2345 _ => {}
2346 }
2347 }
2348 }
2349
2350 if !result_is_simple && is_streaming {
2357 if let Some(f) = &assertion.field {
2358 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2359 if let Some(expr) =
2360 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2361 {
2362 match assertion.assertion_type.as_str() {
2363 "count_min" => {
2364 if let Some(val) = &assertion.value {
2365 if let Some(n) = val.as_u64() {
2366 let _ = writeln!(
2367 out,
2368 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2369 );
2370 }
2371 }
2372 }
2373 "count_equals" => {
2374 if let Some(val) = &assertion.value {
2375 if let Some(n) = val.as_u64() {
2376 let _ = writeln!(
2377 out,
2378 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2379 );
2380 }
2381 }
2382 }
2383 "equals" => {
2384 if let Some(serde_json::Value::String(s)) = &assertion.value {
2385 let escaped = crate::escape::go_string_literal(s);
2386 let is_deep_path = f.contains('.') || f.contains('[');
2391 let safe_expr = if is_deep_path {
2392 format!(
2393 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2394 )
2395 } else {
2396 expr.clone()
2397 };
2398 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2399 } else if let Some(val) = &assertion.value {
2400 if let Some(n) = val.as_u64() {
2401 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2402 }
2403 }
2404 }
2405 "not_empty" => {
2406 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2407 }
2408 "is_empty" => {
2409 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2410 }
2411 "is_true" => {
2412 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2413 }
2414 "is_false" => {
2415 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2416 }
2417 "greater_than" => {
2418 if let Some(val) = &assertion.value {
2419 if let Some(n) = val.as_u64() {
2420 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2421 }
2422 }
2423 }
2424 "greater_than_or_equal" => {
2425 if let Some(val) = &assertion.value {
2426 if let Some(n) = val.as_u64() {
2427 let _ =
2428 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2429 }
2430 }
2431 }
2432 "contains" => {
2433 if let Some(serde_json::Value::String(s)) = &assertion.value {
2434 let escaped = crate::escape::go_string_literal(s);
2435 let _ =
2436 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2437 }
2438 }
2439 _ => {
2440 let _ = writeln!(
2441 out,
2442 "\t// streaming field '{f}': assertion type '{}' not rendered",
2443 assertion.assertion_type
2444 );
2445 }
2446 }
2447 }
2448 return;
2449 }
2450 }
2451 }
2452
2453 if !result_is_simple {
2456 if let Some(f) = &assertion.field {
2457 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2458 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2459 return;
2460 }
2461 }
2462 }
2463
2464 let field_expr = if result_is_simple {
2465 result_var.to_string()
2467 } else {
2468 match &assertion.field {
2469 Some(f) if !f.is_empty() => {
2470 if let Some(local_var) = optional_locals.get(f.as_str()) {
2472 local_var.clone()
2473 } else {
2474 field_resolver.accessor(f, "go", result_var)
2475 }
2476 }
2477 _ => result_var.to_string(),
2478 }
2479 };
2480
2481 let is_optional = assertion
2485 .field
2486 .as_ref()
2487 .map(|f| {
2488 let resolved = field_resolver.resolve(f);
2489 let check_path = resolved
2490 .strip_suffix(".length")
2491 .or_else(|| resolved.strip_suffix(".count"))
2492 .or_else(|| resolved.strip_suffix(".size"))
2493 .unwrap_or(resolved);
2494 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2495 })
2496 .unwrap_or(false);
2497
2498 let field_is_array_for_len = assertion
2502 .field
2503 .as_ref()
2504 .map(|f| {
2505 let resolved = field_resolver.resolve(f);
2506 let check_path = resolved
2507 .strip_suffix(".length")
2508 .or_else(|| resolved.strip_suffix(".count"))
2509 .or_else(|| resolved.strip_suffix(".size"))
2510 .unwrap_or(resolved);
2511 field_resolver.is_array(check_path)
2512 })
2513 .unwrap_or(false);
2514 let field_expr =
2515 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2516 let inner = &field_expr[4..field_expr.len() - 1];
2517 format!("len(*{inner})")
2518 } else {
2519 field_expr
2520 };
2521 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2523 Some(field_expr[5..field_expr.len() - 1].to_string())
2524 } else {
2525 None
2526 };
2527
2528 let field_is_slice = assertion
2532 .field
2533 .as_ref()
2534 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2535 .unwrap_or(false);
2536 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2537 format!("*{field_expr}")
2538 } else {
2539 field_expr.clone()
2540 };
2541
2542 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2547 let mut array_expr = field_expr[..idx].to_string();
2548 if let Some(stripped) = array_expr.strip_prefix("len(") {
2549 array_expr = stripped.to_string();
2550 }
2551 Some(array_expr)
2552 } else {
2553 None
2554 };
2555
2556 let mut assertion_buf = String::new();
2559 let out_ref = &mut assertion_buf;
2560
2561 match assertion.assertion_type.as_str() {
2562 "equals" => {
2563 if let Some(expected) = &assertion.value {
2564 let go_val = json_to_go(expected);
2565 if expected.is_string() {
2567 let resolved_name = assertion
2571 .field
2572 .as_ref()
2573 .map(|f| field_resolver.resolve(f))
2574 .unwrap_or_default();
2575 let is_struct = resolved_name.contains("FormatMetadata");
2576 let trimmed_field = if is_struct {
2577 if is_optional && !field_expr.starts_with("len(") {
2579 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2580 } else {
2581 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2582 }
2583 } else if is_optional && !field_expr.starts_with("len(") {
2584 format!("strings.TrimSpace(string(*{field_expr}))")
2585 } else {
2586 format!("strings.TrimSpace(string({field_expr}))")
2587 };
2588 if is_optional && !field_expr.starts_with("len(") {
2589 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2590 } else {
2591 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2592 }
2593 } else if is_optional && !field_expr.starts_with("len(") {
2594 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2595 } else {
2596 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2597 }
2598 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2599 let _ = writeln!(out_ref, "\t}}");
2600 }
2601 }
2602 "contains" => {
2603 if let Some(expected) = &assertion.value {
2604 let go_val = json_to_go(expected);
2605 let resolved_field = assertion.field.as_deref().unwrap_or("");
2611 let resolved_name = field_resolver.resolve(resolved_field);
2612 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2613 let is_opt =
2614 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2615 let field_for_contains = if is_opt && field_is_array {
2616 format!("jsonString({field_expr})")
2618 } else if is_opt {
2619 format!("fmt.Sprint(*{field_expr})")
2620 } else if field_is_array {
2621 format!("jsonString({field_expr})")
2622 } else {
2623 format!("fmt.Sprint({field_expr})")
2624 };
2625 if is_opt {
2626 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2627 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2628 let _ = writeln!(
2629 out_ref,
2630 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2631 );
2632 let _ = writeln!(out_ref, "\t}}");
2633 let _ = writeln!(out_ref, "\t}}");
2634 } else {
2635 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2636 let _ = writeln!(
2637 out_ref,
2638 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2639 );
2640 let _ = writeln!(out_ref, "\t}}");
2641 }
2642 }
2643 }
2644 "contains_all" => {
2645 if let Some(values) = &assertion.values {
2646 let resolved_field = assertion.field.as_deref().unwrap_or("");
2647 let resolved_name = field_resolver.resolve(resolved_field);
2648 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2649 let is_opt =
2650 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2651 for val in values {
2652 let go_val = json_to_go(val);
2653 let field_for_contains = if is_opt && field_is_array {
2654 format!("jsonString({field_expr})")
2656 } else if is_opt {
2657 format!("fmt.Sprint(*{field_expr})")
2658 } else if field_is_array {
2659 format!("jsonString({field_expr})")
2660 } else {
2661 format!("fmt.Sprint({field_expr})")
2662 };
2663 if is_opt {
2664 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2665 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2666 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2667 let _ = writeln!(out_ref, "\t}}");
2668 let _ = writeln!(out_ref, "\t}}");
2669 } else {
2670 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2671 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2672 let _ = writeln!(out_ref, "\t}}");
2673 }
2674 }
2675 }
2676 }
2677 "not_contains" => {
2678 if let Some(expected) = &assertion.value {
2679 let go_val = json_to_go(expected);
2680 let resolved_field = assertion.field.as_deref().unwrap_or("");
2681 let resolved_name = field_resolver.resolve(resolved_field);
2682 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2683 let is_opt =
2684 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2685 let field_for_contains = if is_opt && field_is_array {
2686 format!("jsonString({field_expr})")
2688 } else if is_opt {
2689 format!("fmt.Sprint(*{field_expr})")
2690 } else if field_is_array {
2691 format!("jsonString({field_expr})")
2692 } else {
2693 format!("fmt.Sprint({field_expr})")
2694 };
2695 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2696 let _ = writeln!(
2697 out_ref,
2698 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2699 );
2700 let _ = writeln!(out_ref, "\t}}");
2701 }
2702 }
2703 "not_empty" => {
2704 let field_is_array = {
2707 let rf = assertion.field.as_deref().unwrap_or("");
2708 let rn = field_resolver.resolve(rf);
2709 field_resolver.is_array(rn)
2710 };
2711 if is_optional && !field_is_array {
2712 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2714 } else if is_optional && field_is_slice {
2715 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2717 } else if is_optional {
2718 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2720 } else if result_is_simple && result_is_array {
2721 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2723 } else {
2724 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2725 }
2726 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2727 let _ = writeln!(out_ref, "\t}}");
2728 }
2729 "is_empty" => {
2730 let field_is_array = {
2731 let rf = assertion.field.as_deref().unwrap_or("");
2732 let rn = field_resolver.resolve(rf);
2733 field_resolver.is_array(rn)
2734 };
2735 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2738 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2740 } else if is_optional && !field_is_array {
2741 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2743 } else if is_optional && field_is_slice {
2744 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2746 } else if is_optional {
2747 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2749 } else {
2750 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2751 }
2752 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2753 let _ = writeln!(out_ref, "\t}}");
2754 }
2755 "contains_any" => {
2756 if let Some(values) = &assertion.values {
2757 let resolved_field = assertion.field.as_deref().unwrap_or("");
2758 let resolved_name = field_resolver.resolve(resolved_field);
2759 let field_is_array = field_resolver.is_array(resolved_name);
2760 let is_opt =
2761 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2762 let field_for_contains = if is_opt && field_is_array {
2763 format!("jsonString({field_expr})")
2765 } else if is_opt {
2766 format!("fmt.Sprint(*{field_expr})")
2767 } else if field_is_array {
2768 format!("jsonString({field_expr})")
2769 } else {
2770 format!("fmt.Sprint({field_expr})")
2771 };
2772 let _ = writeln!(out_ref, "\t{{");
2773 let _ = writeln!(out_ref, "\t\tfound := false");
2774 for val in values {
2775 let go_val = json_to_go(val);
2776 let _ = writeln!(
2777 out_ref,
2778 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2779 );
2780 }
2781 let _ = writeln!(out_ref, "\t\tif !found {{");
2782 let _ = writeln!(
2783 out_ref,
2784 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2785 );
2786 let _ = writeln!(out_ref, "\t\t}}");
2787 let _ = writeln!(out_ref, "\t}}");
2788 }
2789 }
2790 "greater_than" => {
2791 if let Some(val) = &assertion.value {
2792 let go_val = json_to_go(val);
2793 if is_optional {
2797 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2798 if let Some(n) = val.as_u64() {
2799 let next = n + 1;
2800 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2801 } else {
2802 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2803 }
2804 let _ = writeln!(
2805 out_ref,
2806 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2807 );
2808 let _ = writeln!(out_ref, "\t\t}}");
2809 let _ = writeln!(out_ref, "\t}}");
2810 } else if let Some(n) = val.as_u64() {
2811 let next = n + 1;
2812 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2813 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2814 let _ = writeln!(out_ref, "\t}}");
2815 } else {
2816 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2817 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2818 let _ = writeln!(out_ref, "\t}}");
2819 }
2820 }
2821 }
2822 "less_than" => {
2823 if let Some(val) = &assertion.value {
2824 let go_val = json_to_go(val);
2825 if let Some(ref guard) = nil_guard_expr {
2826 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2827 let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2828 let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2829 let _ = writeln!(out_ref, "\t\t}}");
2830 let _ = writeln!(out_ref, "\t}}");
2831 } else if is_optional && !field_expr.starts_with("len(") {
2832 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2834 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2835 let _ = writeln!(
2836 out_ref,
2837 "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2838 );
2839 let _ = writeln!(out_ref, "\t\t}}");
2840 let _ = writeln!(out_ref, "\t}}");
2841 } else {
2842 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2843 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2844 let _ = writeln!(out_ref, "\t}}");
2845 }
2846 }
2847 }
2848 "greater_than_or_equal" => {
2849 if let Some(val) = &assertion.value {
2850 let go_val = json_to_go(val);
2851 if let Some(ref guard) = nil_guard_expr {
2852 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2853 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2854 let _ = writeln!(
2855 out_ref,
2856 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2857 );
2858 let _ = writeln!(out_ref, "\t\t}}");
2859 let _ = writeln!(out_ref, "\t}}");
2860 } else if is_optional && !field_expr.starts_with("len(") {
2861 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2863 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2864 let _ = writeln!(
2865 out_ref,
2866 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2867 );
2868 let _ = writeln!(out_ref, "\t\t}}");
2869 let _ = writeln!(out_ref, "\t}}");
2870 } else {
2871 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2872 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2873 let _ = writeln!(out_ref, "\t}}");
2874 }
2875 }
2876 }
2877 "less_than_or_equal" => {
2878 if let Some(val) = &assertion.value {
2879 let go_val = json_to_go(val);
2880 if is_optional && !field_expr.starts_with("len(") {
2881 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2883 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2884 let _ = writeln!(
2885 out_ref,
2886 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2887 );
2888 let _ = writeln!(out_ref, "\t\t}}");
2889 let _ = writeln!(out_ref, "\t}}");
2890 } else {
2891 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2892 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2893 let _ = writeln!(out_ref, "\t}}");
2894 }
2895 }
2896 }
2897 "starts_with" => {
2898 if let Some(expected) = &assertion.value {
2899 let go_val = json_to_go(expected);
2900 let field_for_prefix = if is_optional
2901 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2902 {
2903 format!("string(*{field_expr})")
2904 } else {
2905 format!("string({field_expr})")
2906 };
2907 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2908 let _ = writeln!(
2909 out_ref,
2910 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2911 );
2912 let _ = writeln!(out_ref, "\t}}");
2913 }
2914 }
2915 "count_min" => {
2916 if let Some(val) = &assertion.value {
2917 if let Some(n) = val.as_u64() {
2918 if is_optional {
2919 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2920 let len_expr = if field_is_slice {
2922 format!("len({field_expr})")
2923 } else {
2924 format!("len(*{field_expr})")
2925 };
2926 let _ = writeln!(
2927 out_ref,
2928 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2929 );
2930 let _ = writeln!(out_ref, "\t}}");
2931 } else {
2932 let _ = writeln!(
2933 out_ref,
2934 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2935 );
2936 }
2937 }
2938 }
2939 }
2940 "count_equals" => {
2941 if let Some(val) = &assertion.value {
2942 if let Some(n) = val.as_u64() {
2943 if is_optional {
2944 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2945 let len_expr = if field_is_slice {
2947 format!("len({field_expr})")
2948 } else {
2949 format!("len(*{field_expr})")
2950 };
2951 let _ = writeln!(
2952 out_ref,
2953 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2954 );
2955 let _ = writeln!(out_ref, "\t}}");
2956 } else {
2957 let _ = writeln!(
2958 out_ref,
2959 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2960 );
2961 }
2962 }
2963 }
2964 }
2965 "is_true" => {
2966 if is_optional {
2967 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2968 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2969 let _ = writeln!(out_ref, "\t}}");
2970 } else {
2971 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2972 }
2973 }
2974 "is_false" => {
2975 if is_optional {
2976 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2977 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2978 let _ = writeln!(out_ref, "\t}}");
2979 } else {
2980 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2981 }
2982 }
2983 "method_result" => {
2984 if let Some(method_name) = &assertion.method {
2985 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2986 let check = assertion.check.as_deref().unwrap_or("is_true");
2987 let deref_expr = if info.is_pointer {
2990 format!("*{}", info.call_expr)
2991 } else {
2992 info.call_expr.clone()
2993 };
2994 match check {
2995 "equals" => {
2996 if let Some(val) = &assertion.value {
2997 if val.is_boolean() {
2998 if val.as_bool() == Some(true) {
2999 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3000 } else {
3001 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3002 }
3003 } else {
3004 let go_val = if let Some(cast) = info.value_cast {
3008 if val.is_number() {
3009 format!("{cast}({})", json_to_go(val))
3010 } else {
3011 json_to_go(val)
3012 }
3013 } else {
3014 json_to_go(val)
3015 };
3016 let _ = writeln!(
3017 out_ref,
3018 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
3019 );
3020 }
3021 }
3022 }
3023 "is_true" => {
3024 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3025 }
3026 "is_false" => {
3027 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3028 }
3029 "greater_than_or_equal" => {
3030 if let Some(val) = &assertion.value {
3031 let n = val.as_u64().unwrap_or(0);
3032 let cast = info.value_cast.unwrap_or("uint");
3034 let _ = writeln!(
3035 out_ref,
3036 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3037 );
3038 }
3039 }
3040 "count_min" => {
3041 if let Some(val) = &assertion.value {
3042 let n = val.as_u64().unwrap_or(0);
3043 let _ = writeln!(
3044 out_ref,
3045 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3046 );
3047 }
3048 }
3049 "contains" => {
3050 if let Some(val) = &assertion.value {
3051 let go_val = json_to_go(val);
3052 let _ = writeln!(
3053 out_ref,
3054 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3055 );
3056 }
3057 }
3058 "is_error" => {
3059 let _ = writeln!(out_ref, "\t{{");
3060 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3061 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3062 let _ = writeln!(out_ref, "\t}}");
3063 }
3064 other_check => {
3065 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3066 }
3067 }
3068 } else {
3069 panic!("Go e2e generator: method_result assertion missing 'method' field");
3070 }
3071 }
3072 "min_length" => {
3073 if let Some(val) = &assertion.value {
3074 if let Some(n) = val.as_u64() {
3075 if is_optional {
3076 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3077 let _ = writeln!(
3078 out_ref,
3079 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3080 );
3081 let _ = writeln!(out_ref, "\t}}");
3082 } else if field_expr.starts_with("len(") {
3083 let _ = writeln!(
3084 out_ref,
3085 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3086 );
3087 } else {
3088 let _ = writeln!(
3089 out_ref,
3090 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3091 );
3092 }
3093 }
3094 }
3095 }
3096 "max_length" => {
3097 if let Some(val) = &assertion.value {
3098 if let Some(n) = val.as_u64() {
3099 if is_optional {
3100 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3101 let _ = writeln!(
3102 out_ref,
3103 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3104 );
3105 let _ = writeln!(out_ref, "\t}}");
3106 } else if field_expr.starts_with("len(") {
3107 let _ = writeln!(
3108 out_ref,
3109 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3110 );
3111 } else {
3112 let _ = writeln!(
3113 out_ref,
3114 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3115 );
3116 }
3117 }
3118 }
3119 }
3120 "ends_with" => {
3121 if let Some(expected) = &assertion.value {
3122 let go_val = json_to_go(expected);
3123 let field_for_suffix = if is_optional
3124 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3125 {
3126 format!("string(*{field_expr})")
3127 } else {
3128 format!("string({field_expr})")
3129 };
3130 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3131 let _ = writeln!(
3132 out_ref,
3133 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3134 );
3135 let _ = writeln!(out_ref, "\t}}");
3136 }
3137 }
3138 "matches_regex" => {
3139 if let Some(expected) = &assertion.value {
3140 let go_val = json_to_go(expected);
3141 let field_for_regex = if is_optional
3142 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3143 {
3144 format!("*{field_expr}")
3145 } else {
3146 field_expr.clone()
3147 };
3148 let _ = writeln!(
3149 out_ref,
3150 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3151 );
3152 }
3153 }
3154 "not_error" => {
3155 }
3157 "error" => {
3158 }
3160 other => {
3161 panic!("Go e2e generator: unsupported assertion type: {other}");
3162 }
3163 }
3164
3165 if let Some(ref arr) = array_guard {
3168 if !assertion_buf.is_empty() {
3169 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3170 for line in assertion_buf.lines() {
3172 let _ = writeln!(out, "\t{line}");
3173 }
3174 let _ = writeln!(out, "\t}}");
3175 }
3176 } else {
3177 out.push_str(&assertion_buf);
3178 }
3179}
3180
3181struct GoMethodCallInfo {
3183 call_expr: String,
3185 is_pointer: bool,
3187 value_cast: Option<&'static str>,
3190}
3191
3192fn build_go_method_call(
3207 result_var: &str,
3208 method_name: &str,
3209 args: Option<&serde_json::Value>,
3210 import_alias: &str,
3211) -> GoMethodCallInfo {
3212 match method_name {
3213 "root_node_type" => GoMethodCallInfo {
3214 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3215 is_pointer: false,
3216 value_cast: None,
3217 },
3218 "named_children_count" => GoMethodCallInfo {
3219 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3220 is_pointer: false,
3221 value_cast: Some("uint"),
3222 },
3223 "has_error_nodes" => GoMethodCallInfo {
3224 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3225 is_pointer: true,
3226 value_cast: None,
3227 },
3228 "error_count" | "tree_error_count" => GoMethodCallInfo {
3229 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3230 is_pointer: true,
3231 value_cast: Some("uint"),
3232 },
3233 "tree_to_sexp" => GoMethodCallInfo {
3234 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3235 is_pointer: true,
3236 value_cast: None,
3237 },
3238 "contains_node_type" => {
3239 let node_type = args
3240 .and_then(|a| a.get("node_type"))
3241 .and_then(|v| v.as_str())
3242 .unwrap_or("");
3243 GoMethodCallInfo {
3244 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3245 is_pointer: true,
3246 value_cast: None,
3247 }
3248 }
3249 "find_nodes_by_type" => {
3250 let node_type = args
3251 .and_then(|a| a.get("node_type"))
3252 .and_then(|v| v.as_str())
3253 .unwrap_or("");
3254 GoMethodCallInfo {
3255 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3256 is_pointer: true,
3257 value_cast: None,
3258 }
3259 }
3260 "run_query" => {
3261 let query_source = args
3262 .and_then(|a| a.get("query_source"))
3263 .and_then(|v| v.as_str())
3264 .unwrap_or("");
3265 let language = args
3266 .and_then(|a| a.get("language"))
3267 .and_then(|v| v.as_str())
3268 .unwrap_or("");
3269 let query_lit = go_string_literal(query_source);
3270 let lang_lit = go_string_literal(language);
3271 GoMethodCallInfo {
3273 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3274 is_pointer: false,
3275 value_cast: None,
3276 }
3277 }
3278 other => {
3279 let method_pascal = other.to_upper_camel_case();
3280 GoMethodCallInfo {
3281 call_expr: format!("{result_var}.{method_pascal}()"),
3282 is_pointer: false,
3283 value_cast: None,
3284 }
3285 }
3286 }
3287}
3288
3289fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3299 match value {
3300 serde_json::Value::Object(map) => {
3301 let new_map: serde_json::Map<String, serde_json::Value> = map
3302 .into_iter()
3303 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3304 .collect();
3305 serde_json::Value::Object(new_map)
3306 }
3307 serde_json::Value::Array(arr) => {
3308 if is_byte_array(&arr) {
3311 let bytes: Vec<u8> = arr
3312 .iter()
3313 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3314 .collect();
3315 let encoded = base64_encode(&bytes);
3317 serde_json::Value::String(encoded)
3318 } else {
3319 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3320 }
3321 }
3322 serde_json::Value::String(s) => {
3323 serde_json::Value::String(pascal_to_snake_case(&s))
3326 }
3327 other => other,
3328 }
3329}
3330
3331fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3333 if arr.is_empty() {
3334 return false;
3335 }
3336 arr.iter().all(|v| {
3337 if let serde_json::Value::Number(n) = v {
3338 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3339 } else {
3340 false
3341 }
3342 })
3343}
3344
3345fn base64_encode(bytes: &[u8]) -> String {
3348 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3349 let mut result = String::new();
3350 let mut i = 0;
3351
3352 while i + 2 < bytes.len() {
3353 let b1 = bytes[i];
3354 let b2 = bytes[i + 1];
3355 let b3 = bytes[i + 2];
3356
3357 result.push(TABLE[(b1 >> 2) as usize] as char);
3358 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3359 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3360 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3361
3362 i += 3;
3363 }
3364
3365 if i < bytes.len() {
3367 let b1 = bytes[i];
3368 result.push(TABLE[(b1 >> 2) as usize] as char);
3369
3370 if i + 1 < bytes.len() {
3371 let b2 = bytes[i + 1];
3372 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3373 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3374 result.push('=');
3375 } else {
3376 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3377 result.push_str("==");
3378 }
3379 }
3380
3381 result
3382}
3383
3384fn camel_to_snake_case(s: &str) -> String {
3386 let mut result = String::new();
3387 let mut prev_upper = false;
3388 for (i, c) in s.char_indices() {
3389 if c.is_uppercase() {
3390 if i > 0 && !prev_upper {
3391 result.push('_');
3392 }
3393 result.push(c.to_lowercase().next().unwrap_or(c));
3394 prev_upper = true;
3395 } else {
3396 if prev_upper && i > 1 {
3397 }
3401 result.push(c);
3402 prev_upper = false;
3403 }
3404 }
3405 result
3406}
3407
3408fn pascal_to_snake_case(s: &str) -> String {
3413 let first_char = s.chars().next();
3415 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3416 return s.to_string();
3417 }
3418 camel_to_snake_case(s)
3419}
3420
3421fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3425 let elem = element_type.unwrap_or("String").trim();
3426 let go_elem = rust_type_to_go(elem, import_alias);
3427 format!("[]{go_elem}")
3428}
3429
3430fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3433 let trimmed = rust.trim();
3434 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3435 return format!("[]{}", rust_type_to_go(inner, import_alias));
3436 }
3437 match trimmed {
3438 "String" | "&str" | "str" => "string".to_string(),
3439 "bool" => "bool".to_string(),
3440 "f32" => "float32".to_string(),
3441 "f64" => "float64".to_string(),
3442 "i8" => "int8".to_string(),
3443 "i16" => "int16".to_string(),
3444 "i32" => "int32".to_string(),
3445 "i64" | "isize" => "int64".to_string(),
3446 "u8" => "uint8".to_string(),
3447 "u16" => "uint16".to_string(),
3448 "u32" => "uint32".to_string(),
3449 "u64" | "usize" => "uint64".to_string(),
3450 _ => format!("{import_alias}.{trimmed}"),
3451 }
3452}
3453
3454fn json_to_go(value: &serde_json::Value) -> String {
3455 match value {
3456 serde_json::Value::String(s) => go_string_literal(s),
3457 serde_json::Value::Bool(b) => b.to_string(),
3458 serde_json::Value::Number(n) => n.to_string(),
3459 serde_json::Value::Null => "nil".to_string(),
3460 other => go_string_literal(&other.to_string()),
3462 }
3463}
3464
3465fn visitor_struct_name(fixture_id: &str) -> String {
3474 use heck::ToUpperCamelCase;
3475 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3477}
3478
3479fn emit_go_visitor_struct(
3484 out: &mut String,
3485 struct_name: &str,
3486 visitor_spec: &crate::fixture::VisitorSpec,
3487 import_alias: &str,
3488) {
3489 let _ = writeln!(out, "type {struct_name} struct{{");
3490 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3491 let _ = writeln!(out, "}}");
3492 for (method_name, action) in &visitor_spec.callbacks {
3493 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3494 }
3495}
3496
3497fn emit_go_visitor_method(
3499 out: &mut String,
3500 struct_name: &str,
3501 method_name: &str,
3502 action: &CallbackAction,
3503 import_alias: &str,
3504) {
3505 let camel_method = method_to_camel(method_name);
3506 let params = match method_name {
3509 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3510 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3511 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3512 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3513 "visit_code_inline"
3514 | "visit_strong"
3515 | "visit_emphasis"
3516 | "visit_strikethrough"
3517 | "visit_underline"
3518 | "visit_subscript"
3519 | "visit_superscript"
3520 | "visit_mark"
3521 | "visit_button"
3522 | "visit_summary"
3523 | "visit_figcaption"
3524 | "visit_definition_term"
3525 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3526 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3527 "visit_list_item" => {
3528 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3529 }
3530 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3531 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3532 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3533 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3534 "visit_input" => {
3535 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3536 }
3537 "visit_audio" | "visit_video" | "visit_iframe" => {
3538 format!("_ {import_alias}.NodeContext, src *string")
3539 }
3540 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3541 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3542 format!("_ {import_alias}.NodeContext, output string")
3543 }
3544 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3545 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3546 _ => format!("_ {import_alias}.NodeContext"),
3547 };
3548
3549 let _ = writeln!(
3550 out,
3551 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3552 );
3553 match action {
3554 CallbackAction::Skip => {
3555 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3556 }
3557 CallbackAction::Continue => {
3558 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3559 }
3560 CallbackAction::PreserveHtml => {
3561 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3562 }
3563 CallbackAction::Custom { output } => {
3564 let escaped = go_string_literal(output);
3565 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3566 }
3567 CallbackAction::CustomTemplate { template, .. } => {
3568 let ptr_params = go_visitor_ptr_params(method_name);
3575 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3576 let escaped_fmt = go_string_literal(&fmt_str);
3577 if fmt_args.is_empty() {
3578 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3579 } else {
3580 let args_str = fmt_args.join(", ");
3581 let _ = writeln!(
3582 out,
3583 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3584 );
3585 }
3586 }
3587 }
3588 let _ = writeln!(out, "}}");
3589}
3590
3591fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3594 match method_name {
3595 "visit_link" => ["title"].into(),
3596 "visit_image" => ["title"].into(),
3597 "visit_heading" => ["id"].into(),
3598 "visit_code_block" => ["lang"].into(),
3599 "visit_form" => ["action", "method"].into(),
3600 "visit_input" => ["name", "value"].into(),
3601 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3602 _ => std::collections::HashSet::new(),
3603 }
3604}
3605
3606fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3618 let mut fmt_str = String::new();
3619 let mut args: Vec<String> = Vec::new();
3620 let mut chars = template.chars().peekable();
3621 while let Some(c) = chars.next() {
3622 if c == '{' {
3623 let mut name = String::new();
3625 for inner in chars.by_ref() {
3626 if inner == '}' {
3627 break;
3628 }
3629 name.push(inner);
3630 }
3631 fmt_str.push_str("%s");
3632 let go_name = go_param_name(&name);
3634 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3636 format!("*{go_name}")
3637 } else {
3638 go_name
3639 };
3640 args.push(arg_expr);
3641 } else {
3642 fmt_str.push(c);
3643 }
3644 }
3645 (fmt_str, args)
3646}
3647
3648fn method_to_camel(snake: &str) -> String {
3650 use heck::ToUpperCamelCase;
3651 snake.to_upper_camel_case()
3652}
3653
3654#[cfg(test)]
3655mod tests {
3656 use super::*;
3657 use crate::config::{CallConfig, E2eConfig};
3658 use crate::fixture::{Assertion, Fixture};
3659
3660 fn make_fixture(id: &str) -> Fixture {
3661 Fixture {
3662 id: id.to_string(),
3663 category: None,
3664 description: "test fixture".to_string(),
3665 tags: vec![],
3666 skip: None,
3667 env: None,
3668 call: None,
3669 input: serde_json::Value::Null,
3670 mock_response: Some(crate::fixture::MockResponse {
3671 status: 200,
3672 body: Some(serde_json::Value::Null),
3673 stream_chunks: None,
3674 headers: std::collections::HashMap::new(),
3675 }),
3676 source: String::new(),
3677 http: None,
3678 assertions: vec![Assertion {
3679 assertion_type: "not_error".to_string(),
3680 ..Default::default()
3681 }],
3682 visitor: None,
3683 }
3684 }
3685
3686 #[test]
3690 fn test_go_method_name_uses_go_casing() {
3691 let e2e_config = E2eConfig {
3692 call: CallConfig {
3693 function: "clean_extracted_text".to_string(),
3694 module: "github.com/example/mylib".to_string(),
3695 result_var: "result".to_string(),
3696 returns_result: true,
3697 ..CallConfig::default()
3698 },
3699 ..E2eConfig::default()
3700 };
3701
3702 let fixture = make_fixture("basic_text");
3703 let mut out = String::new();
3704 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3705
3706 assert!(
3707 out.contains("kreuzberg.CleanExtractedText("),
3708 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3709 );
3710 assert!(
3711 !out.contains("kreuzberg.clean_extracted_text("),
3712 "must not emit raw snake_case method name, got:\n{out}"
3713 );
3714 }
3715
3716 #[test]
3717 fn test_streaming_fixture_emits_collect_snippet() {
3718 let streaming_fixture_json = r#"{
3720 "id": "basic_stream",
3721 "description": "basic streaming test",
3722 "call": "chat_stream",
3723 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3724 "mock_response": {
3725 "status": 200,
3726 "stream_chunks": [{"delta": "hello"}]
3727 },
3728 "assertions": [
3729 {"type": "count_min", "field": "chunks", "value": 1}
3730 ]
3731 }"#;
3732 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3733 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3734
3735 let e2e_config = E2eConfig {
3736 call: CallConfig {
3737 function: "chat_stream".to_string(),
3738 module: "github.com/example/mylib".to_string(),
3739 result_var: "result".to_string(),
3740 returns_result: true,
3741 r#async: true,
3742 ..CallConfig::default()
3743 },
3744 ..E2eConfig::default()
3745 };
3746
3747 let mut out = String::new();
3748 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3749
3750 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3751 assert!(
3752 out.contains("for chunk := range stream"),
3753 "should emit collect loop, got:\n{out}"
3754 );
3755 }
3756
3757 #[test]
3758 fn test_streaming_with_client_factory_and_json_arg() {
3759 use alef_core::config::e2e::{ArgMapping, CallOverride};
3763 let streaming_fixture_json = r#"{
3764 "id": "basic_stream_client",
3765 "description": "basic streaming test with client",
3766 "call": "chat_stream",
3767 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3768 "mock_response": {
3769 "status": 200,
3770 "stream_chunks": [{"delta": "hello"}]
3771 },
3772 "assertions": [
3773 {"type": "count_min", "field": "chunks", "value": 1}
3774 ]
3775 }"#;
3776 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3777 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3778
3779 let go_override = CallOverride {
3780 client_factory: Some("CreateClient".to_string()),
3781 ..Default::default()
3782 };
3783
3784 let mut call_overrides = std::collections::HashMap::new();
3785 call_overrides.insert("go".to_string(), go_override);
3786
3787 let e2e_config = E2eConfig {
3788 call: CallConfig {
3789 function: "chat_stream".to_string(),
3790 module: "github.com/example/mylib".to_string(),
3791 result_var: "result".to_string(),
3792 returns_result: false, r#async: true,
3794 args: vec![ArgMapping {
3795 name: "request".to_string(),
3796 field: "input".to_string(),
3797 arg_type: "json_object".to_string(),
3798 optional: false,
3799 owned: true,
3800 element_type: None,
3801 go_type: None,
3802 }],
3803 overrides: call_overrides,
3804 ..CallConfig::default()
3805 },
3806 ..E2eConfig::default()
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 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3814 assert!(
3815 out.contains("for chunk := range stream"),
3816 "should emit collect loop, got:\n{out}"
3817 );
3818 }
3819
3820 #[test]
3824 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3825 let mut optional_fields = std::collections::HashSet::new();
3826 optional_fields.insert("segments".to_string());
3827 let mut array_fields = std::collections::HashSet::new();
3828 array_fields.insert("segments".to_string());
3829
3830 let e2e_config = E2eConfig {
3831 call: CallConfig {
3832 function: "transcribe".to_string(),
3833 module: "github.com/example/mylib".to_string(),
3834 result_var: "result".to_string(),
3835 returns_result: true,
3836 ..CallConfig::default()
3837 },
3838 fields_optional: optional_fields,
3839 fields_array: array_fields,
3840 ..E2eConfig::default()
3841 };
3842
3843 let fixture = Fixture {
3844 id: "edge_transcribe_with_timestamps".to_string(),
3845 category: None,
3846 description: "Transcription with timestamp segments".to_string(),
3847 tags: vec![],
3848 skip: None,
3849 env: None,
3850 call: None,
3851 input: serde_json::Value::Null,
3852 mock_response: Some(crate::fixture::MockResponse {
3853 status: 200,
3854 body: Some(serde_json::Value::Null),
3855 stream_chunks: None,
3856 headers: std::collections::HashMap::new(),
3857 }),
3858 source: String::new(),
3859 http: None,
3860 assertions: vec![
3861 Assertion {
3862 assertion_type: "not_error".to_string(),
3863 ..Default::default()
3864 },
3865 Assertion {
3866 assertion_type: "equals".to_string(),
3867 field: Some("segments[0].id".to_string()),
3868 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3869 ..Default::default()
3870 },
3871 ],
3872 visitor: None,
3873 };
3874
3875 let mut out = String::new();
3876 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3877
3878 eprintln!("generated:\n{out}");
3879
3880 assert!(
3885 out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3886 "guard must be on Segments (the slice), not an element; got:\n{out}"
3887 );
3888 assert!(
3890 !out.contains("result.Segments[0] != nil"),
3891 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3892 );
3893 }
3894}