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 call_result_is_simple = |cc: &alef_core::config::e2e::CallConfig| -> bool {
612 cc.overrides.get("go").is_some_and(|o| o.result_is_simple)
613 || cc.result_is_simple
614 || cc.overrides.get("rust").map(|o| o.result_is_simple).unwrap_or(false)
615 };
616
617 let needs_fmt = fixtures.iter().any(|f| {
623 if f.visitor.as_ref().is_some_and(|v| {
625 v.callbacks.values().any(|action| {
626 if let CallbackAction::CustomTemplate { template, .. } = action {
627 template.contains('{')
628 } else {
629 false
630 }
631 })
632 }) {
633 return true;
634 }
635
636 if !emits_executable_test(f) {
637 return false;
638 }
639
640 if f.assertions.iter().any(|a| {
642 matches!(
643 a.assertion_type.as_str(),
644 "contains" | "contains_all" | "contains_any" | "not_contains"
645 ) && {
646 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
647 !e2e_config
649 .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
650 .result_is_array
651 } else {
652 let field = a.field.as_deref().unwrap_or("");
655 let cc = e2e_config.resolve_call_for_fixture(
656 f.call.as_deref(),
657 &f.id,
658 &f.resolved_category(),
659 &f.tags,
660 &f.input,
661 );
662 let per_call_resolver = FieldResolver::new(
663 e2e_config.effective_fields(cc),
664 e2e_config.effective_fields_optional(cc),
665 e2e_config.effective_result_fields(cc),
666 e2e_config.effective_fields_array(cc),
667 &std::collections::HashSet::new(),
668 );
669 let resolved_name = per_call_resolver.resolve(field);
670 !per_call_resolver.is_array(resolved_name)
673 && (call_result_is_simple(cc) || per_call_resolver.is_valid_for_result(field))
674 }
675 }
676 }) {
677 return true;
678 }
679
680 f.assertions.iter().any(|a| {
682 if let Some(field) = &a.field {
683 if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
684 let cc = e2e_config.resolve_call_for_fixture(
685 f.call.as_deref(),
686 &f.id,
687 &f.resolved_category(),
688 &f.tags,
689 &f.input,
690 );
691 let per_call_resolver = FieldResolver::new(
692 e2e_config.effective_fields(cc),
693 e2e_config.effective_fields_optional(cc),
694 e2e_config.effective_result_fields(cc),
695 e2e_config.effective_fields_array(cc),
696 &std::collections::HashSet::new(),
697 );
698 let resolved = per_call_resolver.resolve(field);
699 per_call_resolver.is_optional(resolved)
701 && !per_call_resolver.is_array(resolved)
702 && !per_call_resolver.has_map_access(field)
703 && per_call_resolver.is_valid_for_result(field)
704 } else {
705 false
706 }
707 } else {
708 false
709 }
710 })
711 });
712
713 let needs_strings = fixtures.iter().any(|f| {
717 if !emits_executable_test(f) {
718 return false;
719 }
720 let cc =
722 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
723 if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
724 return true;
725 }
726 let per_call_resolver = FieldResolver::new(
727 e2e_config.effective_fields(cc),
728 e2e_config.effective_fields_optional(cc),
729 e2e_config.effective_result_fields(cc),
730 e2e_config.effective_fields_array(cc),
731 &std::collections::HashSet::new(),
732 );
733 f.assertions.iter().any(|a| {
734 let type_needs_strings = if a.assertion_type == "equals" {
735 a.value.as_ref().is_some_and(|v| v.is_string())
737 } else {
738 matches!(
739 a.assertion_type.as_str(),
740 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
741 )
742 };
743 let simple_result = call_result_is_simple(cc);
746 let field_valid = a
747 .field
748 .as_ref()
749 .map(|f| f.is_empty() || simple_result || per_call_resolver.is_valid_for_result(f))
750 .unwrap_or(true);
751 type_needs_strings && field_valid
752 })
753 });
754
755 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
764 let needs_http = has_http_fixtures;
765 let needs_io = has_http_fixtures;
767
768 let needs_reflect = fixtures.iter().any(|f| {
771 if let Some(http) = &f.http {
772 let body_needs_reflect = http
773 .expected_response
774 .body
775 .as_ref()
776 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
777 let partial_needs_reflect = http.expected_response.body_partial.is_some();
778 body_needs_reflect || partial_needs_reflect
779 } else {
780 false
781 }
782 });
783
784 let mut body = String::new();
789 for fixture in fixtures.iter() {
790 if let Some(visitor_spec) = &fixture.visitor {
791 let struct_name = visitor_struct_name(&fixture.id);
792 emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
793 let _ = writeln!(body);
794 }
795 }
796 for (i, fixture) in fixtures.iter().enumerate() {
797 render_test_function(&mut body, fixture, import_alias, e2e_config, adapters);
798 if i + 1 < fixtures.len() {
799 let _ = writeln!(body);
800 }
801 }
802 let needs_assert = body.contains("assert.");
803
804 let _ = writeln!(out, "// E2e tests for category: {category}");
805 let _ = writeln!(out, "package e2e_test");
806 let _ = writeln!(out);
807 let _ = writeln!(out, "import (");
808 if needs_base64 {
809 let _ = writeln!(out, "\t\"encoding/base64\"");
810 }
811 if needs_json || needs_reflect {
812 let _ = writeln!(out, "\t\"encoding/json\"");
813 }
814 if needs_fmt {
815 let _ = writeln!(out, "\t\"fmt\"");
816 }
817 if needs_io {
818 let _ = writeln!(out, "\t\"io\"");
819 }
820 if needs_http {
821 let _ = writeln!(out, "\t\"net/http\"");
822 }
823 if needs_os {
824 let _ = writeln!(out, "\t\"os\"");
825 }
826 let _ = needs_filepath; if needs_reflect {
828 let _ = writeln!(out, "\t\"reflect\"");
829 }
830 if needs_strings {
831 let _ = writeln!(out, "\t\"strings\"");
832 }
833 let _ = writeln!(out, "\t\"testing\"");
834 if needs_assert {
835 let _ = writeln!(out);
836 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
837 }
838
839 if needs_pkg {
840 let _ = writeln!(out);
841 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
842 }
843 let _ = writeln!(out, ")");
844 let _ = writeln!(out);
845
846 out.push_str(&body);
848
849 while out.ends_with("\n\n") {
851 out.pop();
852 }
853 if !out.ends_with('\n') {
854 out.push('\n');
855 }
856 out
857}
858
859fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
868 if fixture.is_http_test() {
870 return false;
871 }
872 let call_config = e2e_config.resolve_call_for_fixture(
873 fixture.call.as_deref(),
874 &fixture.id,
875 &fixture.resolved_category(),
876 &fixture.tags,
877 &fixture.input,
878 );
879 if call_config.skip_languages.iter().any(|l| l == "go") {
882 return false;
883 }
884 let go_override = call_config
885 .overrides
886 .get("go")
887 .or_else(|| e2e_config.call.overrides.get("go"));
888 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
891 return true;
892 }
893 let fn_name = go_override
897 .and_then(|o| o.function.as_deref())
898 .filter(|s| !s.is_empty())
899 .unwrap_or(call_config.function.as_str());
900 !fn_name.is_empty()
901}
902
903fn render_test_function(
904 out: &mut String,
905 fixture: &Fixture,
906 import_alias: &str,
907 e2e_config: &crate::config::E2eConfig,
908 adapters: &[alef_core::config::AdapterConfig],
909) {
910 let fn_name = fixture.id.to_upper_camel_case();
911 let description = &fixture.description;
912
913 if fixture.http.is_some() {
915 render_http_test_function(out, fixture);
916 return;
917 }
918
919 if !fixture_has_go_callable(fixture, e2e_config) {
924 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
925 let _ = writeln!(out, "\t// {description}");
926 let _ = writeln!(
927 out,
928 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
929 );
930 let _ = writeln!(out, "}}");
931 return;
932 }
933
934 let call_config = e2e_config.resolve_call_for_fixture(
936 fixture.call.as_deref(),
937 &fixture.id,
938 &fixture.resolved_category(),
939 &fixture.tags,
940 &fixture.input,
941 );
942 let call_field_resolver = FieldResolver::new(
944 e2e_config.effective_fields(call_config),
945 e2e_config.effective_fields_optional(call_config),
946 e2e_config.effective_result_fields(call_config),
947 e2e_config.effective_fields_array(call_config),
948 &std::collections::HashSet::new(),
949 );
950 let field_resolver = &call_field_resolver;
951 let lang = "go";
952 let overrides = call_config.overrides.get(lang);
953
954 let base_function_name = overrides
958 .and_then(|o| o.function.as_deref())
959 .unwrap_or(&call_config.function);
960 let function_name = to_go_name(base_function_name);
961 let result_var = &call_config.result_var;
962 let args = &call_config.args;
963
964 let returns_result = overrides
967 .and_then(|o| o.returns_result)
968 .unwrap_or(call_config.returns_result);
969
970 let returns_void = call_config.returns_void;
973
974 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
980 || call_config.result_is_simple
981 || call_config
982 .overrides
983 .get("rust")
984 .map(|o| o.result_is_simple)
985 .unwrap_or(false);
986
987 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
993
994 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
996 e2e_config
997 .call
998 .overrides
999 .get("go")
1000 .and_then(|o| o.options_type.as_deref())
1001 });
1002
1003 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1005 e2e_config
1006 .call
1007 .overrides
1008 .get("go")
1009 .map(|o| o.options_ptr)
1010 .unwrap_or(false)
1011 });
1012
1013 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1014 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1018
1019 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1022 e2e_config
1023 .call
1024 .overrides
1025 .get(lang)
1026 .and_then(|o| o.client_factory.as_deref())
1027 });
1028
1029 let (mut setup_lines, args_str) = build_args_and_setup(
1030 &fixture.input,
1031 args,
1032 import_alias,
1033 call_options_type,
1034 fixture,
1035 call_options_ptr,
1036 validation_creation_failure,
1037 );
1038
1039 let mut visitor_opts_var: Option<String> = None;
1042 if fixture.visitor.is_some() {
1043 let struct_name = visitor_struct_name(&fixture.id);
1044 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1045 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1047 let opts_var = "opts".to_string();
1048 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1049 setup_lines.push("opts.Visitor = visitor".to_string());
1050 visitor_opts_var = Some(opts_var);
1051 }
1052
1053 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1054 let final_args = {
1055 let mut parts: Vec<String> = Vec::new();
1056 if !args_str.is_empty() {
1057 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1059 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1060 } else {
1061 args_str
1062 };
1063 parts.push(processed_args);
1064 }
1065 parts.extend(go_extra_args);
1066 parts.join(", ")
1067 };
1068
1069 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1070 let _ = writeln!(out, "\t// {description}");
1071
1072 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1076 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1077 if let Some(var) = api_key_var {
1078 if has_mock {
1079 let fixture_id = &fixture.id;
1083 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1084 let _ = writeln!(out, "\tvar baseURL *string");
1085 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1086 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1087 let _ = writeln!(out, "\t}} else {{");
1088 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1089 let _ = writeln!(
1090 out,
1091 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1092 );
1093 let _ = writeln!(out, "\t\tbaseURL = &u");
1094 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1095 let _ = writeln!(out, "\t}}");
1096 } else {
1097 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1098 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1099 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1100 let _ = writeln!(out, "\t}}");
1101 }
1102 }
1103
1104 for line in &setup_lines {
1105 let _ = writeln!(out, "\t{line}");
1106 }
1107
1108 let call_prefix = if let Some(factory) = client_factory {
1112 let factory_name = to_go_name(factory);
1113 let fixture_id = &fixture.id;
1114 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1117 ("apiKey".to_string(), "baseURL".to_string())
1119 } else if api_key_var.is_some() {
1120 ("apiKey".to_string(), "nil".to_string())
1122 } else if fixture.has_host_root_route() {
1123 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1124 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1125 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1126 let _ = writeln!(
1127 out,
1128 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1129 );
1130 let _ = writeln!(out, "\t}}");
1131 ("\"test-key\"".to_string(), "&mockURL".to_string())
1132 } else {
1133 let _ = writeln!(
1134 out,
1135 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1136 );
1137 ("\"test-key\"".to_string(), "&mockURL".to_string())
1138 };
1139 let _ = writeln!(
1140 out,
1141 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1142 );
1143 let _ = writeln!(out, "\tif clientErr != nil {{");
1144 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1145 let _ = writeln!(out, "\t}}");
1146 "client".to_string()
1147 } else {
1148 import_alias.to_string()
1149 };
1150
1151 let binding_returns_error_pre = args
1156 .iter()
1157 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1158 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1159
1160 if expects_error {
1161 if effective_returns_result_pre && !returns_void {
1162 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1163 } else {
1164 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1165 }
1166 let _ = writeln!(out, "\tif err == nil {{");
1167 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1168 let _ = writeln!(out, "\t}}");
1169 let _ = writeln!(out, "}}");
1170 return;
1171 }
1172
1173 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1175
1176 use heck::ToSnakeCase;
1181 let fn_snake = function_name.to_snake_case();
1182 let base_snake = base_function_name.to_snake_case();
1183 let streaming_item_type = if is_streaming {
1184 adapters
1185 .iter()
1186 .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1187 .find(|a| a.name == fn_snake || a.name == base_snake)
1188 .and_then(|a| a.item_type.as_deref())
1189 .and_then(|t| t.rsplit("::").next())
1190 .unwrap_or("Item") } else {
1192 "Item" };
1194
1195 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1200 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1201 return false;
1202 }
1203 if a.assertion_type == "method_result" {
1205 return true;
1206 }
1207 match &a.field {
1208 Some(f) if !f.is_empty() => {
1209 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1210 return true;
1211 }
1212 if result_is_simple {
1218 return true;
1219 }
1220 field_resolver.is_valid_for_result(f)
1221 }
1222 _ => true,
1223 }
1224 });
1225
1226 let binding_returns_error = args
1233 .iter()
1234 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1235 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1237
1238 if !effective_returns_result && result_is_simple {
1244 let result_binding = if has_usable_assertion {
1246 result_var.to_string()
1247 } else {
1248 "_".to_string()
1249 };
1250 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1252 let _ = writeln!(
1253 out,
1254 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1255 );
1256 if has_usable_assertion && result_binding != "_" {
1257 if result_is_array {
1258 let _ = writeln!(out, "\tvalue := {result_var}");
1260 } else {
1261 let only_nil_assertions = fixture
1264 .assertions
1265 .iter()
1266 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1267 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1268 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1269
1270 if !only_nil_assertions {
1271 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1274 if result_is_ptr {
1275 let _ = writeln!(out, "\tif {result_var} == nil {{");
1276 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1277 let _ = writeln!(out, "\t}}");
1278 let _ = writeln!(out, "\tvalue := *{result_var}");
1279 } else {
1280 let _ = writeln!(out, "\tvalue := {result_var}");
1282 }
1283 }
1284 }
1285 }
1286 } else if !effective_returns_result || returns_void {
1287 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1290 let _ = writeln!(out, "\tif err != nil {{");
1291 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1292 let _ = writeln!(out, "\t}}");
1293 let _ = writeln!(out, "}}");
1295 return;
1296 } else {
1297 let result_binding = if is_streaming {
1300 "stream".to_string()
1301 } else if has_usable_assertion {
1302 result_var.to_string()
1303 } else {
1304 "_".to_string()
1305 };
1306 let _ = writeln!(
1307 out,
1308 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1309 );
1310 let _ = writeln!(out, "\tif err != nil {{");
1311 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1312 let _ = writeln!(out, "\t}}");
1313 if is_streaming {
1315 let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1316 let _ = writeln!(out, "\tfor chunk := range stream {{");
1317 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1318 let _ = writeln!(out, "\t}}");
1319 }
1320 if result_is_simple && has_usable_assertion && result_binding != "_" {
1321 if result_is_array {
1322 let _ = writeln!(out, "\tvalue := {}", result_var);
1324 } else {
1325 let only_nil_assertions = fixture
1328 .assertions
1329 .iter()
1330 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1331 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1332 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1333
1334 if !only_nil_assertions {
1335 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1338 if result_is_ptr {
1339 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1340 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1341 let _ = writeln!(out, "\t}}");
1342 let _ = writeln!(out, "\tvalue := *{}", result_var);
1343 } else {
1344 let _ = writeln!(out, "\tvalue := {}", result_var);
1346 }
1347 }
1348 }
1349 }
1350 }
1351
1352 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1356 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1357 let only_nil_assertions = fixture
1358 .assertions
1359 .iter()
1360 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1361 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1362 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1363 !only_nil_assertions
1364 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1365 true
1366 } else {
1367 result_is_simple && has_usable_assertion
1368 };
1369
1370 let effective_result_var = if has_deref_value {
1371 "value".to_string()
1372 } else {
1373 result_var.to_string()
1374 };
1375
1376 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1381 for assertion in &fixture.assertions {
1382 if let Some(f) = &assertion.field {
1383 if !f.is_empty() {
1384 if !result_is_simple && !field_resolver.is_valid_for_result(f) {
1387 continue;
1388 }
1389 let resolved = field_resolver.resolve(f);
1390 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1391 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1396 let is_array_field = field_resolver.is_array(resolved);
1397 if !is_string_field || is_array_field {
1398 continue;
1401 }
1402 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1403 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1404 if field_resolver.has_map_access(f) {
1405 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1408 } else {
1409 let _ = writeln!(out, "\tvar {local_var} string");
1410 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1411 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1415 let _ = writeln!(out, "\t}}");
1416 }
1417 optional_locals.insert(f.clone(), local_var);
1418 }
1419 }
1420 }
1421 }
1422
1423 for assertion in &fixture.assertions {
1425 if let Some(f) = &assertion.field {
1426 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1427 let parts: Vec<&str> = f.split('.').collect();
1430 let mut guard_expr: Option<String> = None;
1431 for i in 1..parts.len() {
1432 let prefix = parts[..i].join(".");
1433 let resolved_prefix = field_resolver.resolve(&prefix);
1434 if field_resolver.is_optional(resolved_prefix) {
1435 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1441 let suffix = &resolved_prefix[bracket_pos + 1..];
1442 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1443 if is_numeric_index {
1444 &resolved_prefix[..bracket_pos]
1445 } else {
1446 resolved_prefix
1447 }
1448 } else {
1449 resolved_prefix
1450 };
1451 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1452 guard_expr = Some(accessor);
1453 break;
1454 }
1455 }
1456 if let Some(guard) = guard_expr {
1457 if field_resolver.is_valid_for_result(f) {
1460 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1466 if is_struct_value {
1467 render_assertion(
1470 out,
1471 assertion,
1472 &effective_result_var,
1473 import_alias,
1474 field_resolver,
1475 &optional_locals,
1476 result_is_simple,
1477 result_is_array,
1478 is_streaming,
1479 );
1480 continue;
1481 }
1482 let _ = writeln!(out, "\tif {guard} != nil {{");
1483 let mut nil_buf = String::new();
1486 render_assertion(
1487 &mut nil_buf,
1488 assertion,
1489 &effective_result_var,
1490 import_alias,
1491 field_resolver,
1492 &optional_locals,
1493 result_is_simple,
1494 result_is_array,
1495 is_streaming,
1496 );
1497 for line in nil_buf.lines() {
1498 let _ = writeln!(out, "\t{line}");
1499 }
1500 let _ = writeln!(out, "\t}}");
1501 } else {
1502 render_assertion(
1503 out,
1504 assertion,
1505 &effective_result_var,
1506 import_alias,
1507 field_resolver,
1508 &optional_locals,
1509 result_is_simple,
1510 result_is_array,
1511 is_streaming,
1512 );
1513 }
1514 continue;
1515 }
1516 }
1517 }
1518 render_assertion(
1519 out,
1520 assertion,
1521 &effective_result_var,
1522 import_alias,
1523 field_resolver,
1524 &optional_locals,
1525 result_is_simple,
1526 result_is_array,
1527 is_streaming,
1528 );
1529 }
1530
1531 let _ = writeln!(out, "}}");
1532}
1533
1534fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1540 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1541}
1542
1543struct GoTestClientRenderer;
1555
1556impl client::TestClientRenderer for GoTestClientRenderer {
1557 fn language_name(&self) -> &'static str {
1558 "go"
1559 }
1560
1561 fn sanitize_test_name(&self, id: &str) -> String {
1565 id.to_upper_camel_case()
1566 }
1567
1568 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1571 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1572 let _ = writeln!(out, "\t// {description}");
1573 if let Some(reason) = skip_reason {
1574 let escaped = go_string_literal(reason);
1575 let _ = writeln!(out, "\tt.Skip({escaped})");
1576 }
1577 }
1578
1579 fn render_test_close(&self, out: &mut String) {
1580 let _ = writeln!(out, "}}");
1581 }
1582
1583 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1589 let method = ctx.method.to_uppercase();
1590 let path = ctx.path;
1591
1592 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1593 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1594 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1595 let _ = writeln!(out, "\t}}");
1596
1597 let body_expr = if let Some(body) = ctx.body {
1599 let json = serde_json::to_string(body).unwrap_or_default();
1600 let escaped = go_string_literal(&json);
1601 format!("strings.NewReader({})", escaped)
1602 } else {
1603 "strings.NewReader(\"\")".to_string()
1604 };
1605
1606 let _ = writeln!(out, "\tbody := {body_expr}");
1607 let _ = writeln!(
1608 out,
1609 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1610 );
1611 let _ = writeln!(out, "\tif err != nil {{");
1612 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1613 let _ = writeln!(out, "\t}}");
1614
1615 if ctx.body.is_some() {
1617 let content_type = ctx.content_type.unwrap_or("application/json");
1618 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1619 }
1620
1621 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1623 header_names.sort();
1624 for name in header_names {
1625 let value = &ctx.headers[name];
1626 let escaped_name = go_string_literal(name);
1627 let escaped_value = go_string_literal(value);
1628 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1629 }
1630
1631 if !ctx.cookies.is_empty() {
1633 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1634 cookie_names.sort();
1635 for name in cookie_names {
1636 let value = &ctx.cookies[name];
1637 let escaped_name = go_string_literal(name);
1638 let escaped_value = go_string_literal(value);
1639 let _ = writeln!(
1640 out,
1641 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1642 );
1643 }
1644 }
1645
1646 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1648 let _ = writeln!(
1649 out,
1650 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1651 );
1652 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1653 let _ = writeln!(out, "\t\t}},");
1654 let _ = writeln!(out, "\t}}");
1655 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1656 let _ = writeln!(out, "\tif err != nil {{");
1657 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1658 let _ = writeln!(out, "\t}}");
1659 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1660
1661 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1665 let _ = writeln!(out, "\tif err != nil {{");
1666 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1667 let _ = writeln!(out, "\t}}");
1668 let _ = writeln!(out, "\t_ = bodyBytes");
1669 }
1670
1671 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1672 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1673 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1674 let _ = writeln!(out, "\t}}");
1675 }
1676
1677 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1680 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1682 return;
1683 }
1684 if name.eq_ignore_ascii_case("connection") {
1686 return;
1687 }
1688 let escaped_name = go_string_literal(name);
1689 let escaped_value = go_string_literal(expected);
1690 let _ = writeln!(
1691 out,
1692 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1693 );
1694 let _ = writeln!(
1695 out,
1696 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1697 );
1698 let _ = writeln!(out, "\t}}");
1699 }
1700
1701 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1706 match expected {
1707 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1708 let json_str = serde_json::to_string(expected).unwrap_or_default();
1709 let escaped = go_string_literal(&json_str);
1710 let _ = writeln!(out, "\tvar got any");
1711 let _ = writeln!(out, "\tvar want any");
1712 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1713 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1714 let _ = writeln!(out, "\t}}");
1715 let _ = writeln!(
1716 out,
1717 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1718 );
1719 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1720 let _ = writeln!(out, "\t}}");
1721 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1722 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1723 let _ = writeln!(out, "\t}}");
1724 }
1725 serde_json::Value::String(s) => {
1726 let escaped = go_string_literal(s);
1727 let _ = writeln!(out, "\twant := {escaped}");
1728 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1729 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1730 let _ = writeln!(out, "\t}}");
1731 }
1732 other => {
1733 let escaped = go_string_literal(&other.to_string());
1734 let _ = writeln!(out, "\twant := {escaped}");
1735 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1736 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1737 let _ = writeln!(out, "\t}}");
1738 }
1739 }
1740 }
1741
1742 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1745 if let Some(obj) = expected.as_object() {
1746 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1747 let _ = writeln!(
1748 out,
1749 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1750 );
1751 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1752 let _ = writeln!(out, "\t}}");
1753 for (key, val) in obj {
1754 let escaped_key = go_string_literal(key);
1755 let json_val = serde_json::to_string(val).unwrap_or_default();
1756 let escaped_val = go_string_literal(&json_val);
1757 let _ = writeln!(out, "\t{{");
1758 let _ = writeln!(out, "\t\tvar _wantVal any");
1759 let _ = writeln!(
1760 out,
1761 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1762 );
1763 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1764 let _ = writeln!(out, "\t\t}}");
1765 let _ = writeln!(
1766 out,
1767 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1768 );
1769 let _ = writeln!(
1770 out,
1771 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1772 );
1773 let _ = writeln!(out, "\t\t}}");
1774 let _ = writeln!(out, "\t}}");
1775 }
1776 }
1777 }
1778
1779 fn render_assert_validation_errors(
1784 &self,
1785 out: &mut String,
1786 _response_var: &str,
1787 errors: &[ValidationErrorExpectation],
1788 ) {
1789 let _ = writeln!(out, "\tvar _veBody map[string]any");
1790 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1791 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1792 let _ = writeln!(out, "\t}}");
1793 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1794 for ve in errors {
1795 let escaped_msg = go_string_literal(&ve.msg);
1796 let _ = writeln!(out, "\t{{");
1797 let _ = writeln!(out, "\t\t_found := false");
1798 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1799 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1800 let _ = writeln!(
1801 out,
1802 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1803 );
1804 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1805 let _ = writeln!(out, "\t\t\t\t\tbreak");
1806 let _ = writeln!(out, "\t\t\t\t}}");
1807 let _ = writeln!(out, "\t\t\t}}");
1808 let _ = writeln!(out, "\t\t}}");
1809 let _ = writeln!(out, "\t\tif !_found {{");
1810 let _ = writeln!(
1811 out,
1812 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1813 );
1814 let _ = writeln!(out, "\t\t}}");
1815 let _ = writeln!(out, "\t}}");
1816 }
1817 }
1818}
1819
1820fn build_args_and_setup(
1828 input: &serde_json::Value,
1829 args: &[crate::config::ArgMapping],
1830 import_alias: &str,
1831 options_type: Option<&str>,
1832 fixture: &crate::fixture::Fixture,
1833 options_ptr: bool,
1834 expects_error: bool,
1835) -> (Vec<String>, String) {
1836 let fixture_id = &fixture.id;
1837 use heck::ToUpperCamelCase;
1838
1839 if args.is_empty() {
1840 return (Vec::new(), String::new());
1841 }
1842
1843 let mut setup_lines: Vec<String> = Vec::new();
1844 let mut parts: Vec<String> = Vec::new();
1845
1846 for arg in args {
1847 if arg.arg_type == "mock_url" {
1848 if fixture.has_host_root_route() {
1849 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1850 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1851 setup_lines.push(format!(
1852 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1853 arg.name, arg.name
1854 ));
1855 } else {
1856 setup_lines.push(format!(
1857 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1858 arg.name,
1859 ));
1860 }
1861 parts.push(arg.name.clone());
1862 continue;
1863 }
1864
1865 if arg.arg_type == "mock_url_list" {
1866 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1871 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1872 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1873
1874 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1875 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1876 } else {
1877 Vec::new()
1878 };
1879
1880 let paths_literal = paths.join(", ");
1881 let var_name = &arg.name;
1882
1883 setup_lines.push(format!(
1884 "{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}}"
1885 ));
1886 setup_lines.push(format!(
1887 "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}}"
1888 ));
1889 parts.push(var_name.to_string());
1890 continue;
1891 }
1892
1893 if arg.arg_type == "handle" {
1894 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1896 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1897 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1898 let create_err_handler = if expects_error {
1902 "assert.Error(t, createErr)\n\t\treturn".to_string()
1903 } else {
1904 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1905 };
1906 if config_value.is_null()
1907 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1908 {
1909 setup_lines.push(format!(
1910 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1911 name = arg.name,
1912 ));
1913 } else {
1914 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1915 let go_literal = go_string_literal(&json_str);
1916 let name = &arg.name;
1917 setup_lines.push(format!(
1918 "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}}"
1919 ));
1920 setup_lines.push(format!(
1921 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1922 ));
1923 }
1924 parts.push(arg.name.clone());
1925 continue;
1926 }
1927
1928 let val: Option<&serde_json::Value> = if arg.field == "input" {
1929 Some(input)
1930 } else {
1931 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1932 input.get(field)
1933 };
1934
1935 if arg.arg_type == "bytes" {
1942 let var_name = format!("{}Bytes", arg.name);
1943 match val {
1944 None | Some(serde_json::Value::Null) => {
1945 if arg.optional {
1946 parts.push("nil".to_string());
1947 } else {
1948 parts.push("[]byte{}".to_string());
1949 }
1950 }
1951 Some(serde_json::Value::String(s)) => {
1952 let go_path = go_string_literal(s);
1957 setup_lines.push(format!(
1958 "{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}}"
1959 ));
1960 parts.push(var_name);
1961 }
1962 Some(other) => {
1963 parts.push(format!("[]byte({})", json_to_go(other)));
1964 }
1965 }
1966 continue;
1967 }
1968
1969 match val {
1970 None | Some(serde_json::Value::Null) if arg.optional => {
1971 match arg.arg_type.as_str() {
1973 "string" => {
1974 parts.push("nil".to_string());
1976 }
1977 "json_object" => {
1978 if options_ptr {
1979 parts.push("nil".to_string());
1981 } else if let Some(opts_type) = options_type {
1982 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1984 } else {
1985 parts.push("nil".to_string());
1986 }
1987 }
1988 _ => {
1989 parts.push("nil".to_string());
1990 }
1991 }
1992 }
1993 None | Some(serde_json::Value::Null) => {
1994 let default_val = match arg.arg_type.as_str() {
1996 "string" => "\"\"".to_string(),
1997 "int" | "integer" | "i64" => "0".to_string(),
1998 "float" | "number" => "0.0".to_string(),
1999 "bool" | "boolean" => "false".to_string(),
2000 "json_object" => {
2001 if options_ptr {
2002 "nil".to_string()
2004 } else if let Some(opts_type) = options_type {
2005 format!("{import_alias}.{opts_type}{{}}")
2006 } else {
2007 "nil".to_string()
2008 }
2009 }
2010 _ => "nil".to_string(),
2011 };
2012 parts.push(default_val);
2013 }
2014 Some(v) => {
2015 match arg.arg_type.as_str() {
2016 "json_object" => {
2017 let is_array = v.is_array();
2020 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2021 if is_empty_obj {
2022 if options_ptr {
2023 parts.push("nil".to_string());
2025 } else if let Some(opts_type) = options_type {
2026 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2027 } else {
2028 parts.push("nil".to_string());
2029 }
2030 } else if is_array {
2031 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2036 if go_t.starts_with('[') {
2040 go_t.to_string()
2041 } else {
2042 let qualified = if go_t.contains('.') {
2044 go_t.to_string()
2045 } else {
2046 format!("{import_alias}.{go_t}")
2047 };
2048 format!("[]{qualified}")
2049 }
2050 } else {
2051 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2052 };
2053 let converted_v = convert_json_for_go(v.clone());
2055 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2056 let go_literal = go_string_literal(&json_str);
2057 let var_name = &arg.name;
2058 setup_lines.push(format!(
2059 "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}}"
2060 ));
2061 parts.push(var_name.to_string());
2062 } else if let Some(opts_type) = options_type {
2063 let remapped_v = if options_ptr {
2068 convert_json_for_go(v.clone())
2069 } else {
2070 v.clone()
2071 };
2072 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2073 let go_literal = go_string_literal(&json_str);
2074 let var_name = &arg.name;
2075 setup_lines.push(format!(
2076 "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}}"
2077 ));
2078 let arg_expr = if options_ptr {
2080 format!("&{var_name}")
2081 } else {
2082 var_name.to_string()
2083 };
2084 parts.push(arg_expr);
2085 } else {
2086 parts.push(json_to_go(v));
2087 }
2088 }
2089 "string" if arg.optional => {
2090 let var_name = format!("{}Val", arg.name);
2092 let go_val = json_to_go(v);
2093 setup_lines.push(format!("{var_name} := {go_val}"));
2094 parts.push(format!("&{var_name}"));
2095 }
2096 _ => {
2097 parts.push(json_to_go(v));
2098 }
2099 }
2100 }
2101 }
2102 }
2103
2104 (setup_lines, parts.join(", "))
2105}
2106
2107#[allow(clippy::too_many_arguments)]
2108fn render_assertion(
2109 out: &mut String,
2110 assertion: &Assertion,
2111 result_var: &str,
2112 import_alias: &str,
2113 field_resolver: &FieldResolver,
2114 optional_locals: &std::collections::HashMap<String, String>,
2115 result_is_simple: bool,
2116 result_is_array: bool,
2117 is_streaming: bool,
2118) {
2119 if !result_is_simple {
2122 if let Some(f) = &assertion.field {
2123 let embed_deref = format!("(*{result_var})");
2126 match f.as_str() {
2127 "chunks_have_content" => {
2128 let pred = format!(
2129 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2130 );
2131 match assertion.assertion_type.as_str() {
2132 "is_true" => {
2133 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2134 }
2135 "is_false" => {
2136 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2137 }
2138 _ => {
2139 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2140 }
2141 }
2142 return;
2143 }
2144 "chunks_have_embeddings" => {
2145 let pred = format!(
2146 "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 }}()"
2147 );
2148 match assertion.assertion_type.as_str() {
2149 "is_true" => {
2150 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2151 }
2152 "is_false" => {
2153 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2154 }
2155 _ => {
2156 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2157 }
2158 }
2159 return;
2160 }
2161 "chunks_have_heading_context" => {
2162 let pred = format!(
2163 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2164 );
2165 match assertion.assertion_type.as_str() {
2166 "is_true" => {
2167 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2168 }
2169 "is_false" => {
2170 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2171 }
2172 _ => {
2173 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2174 }
2175 }
2176 return;
2177 }
2178 "first_chunk_starts_with_heading" => {
2179 let pred = format!(
2180 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2181 );
2182 match assertion.assertion_type.as_str() {
2183 "is_true" => {
2184 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2185 }
2186 "is_false" => {
2187 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2188 }
2189 _ => {
2190 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2191 }
2192 }
2193 return;
2194 }
2195 "embeddings" => {
2196 match assertion.assertion_type.as_str() {
2197 "count_equals" => {
2198 if let Some(val) = &assertion.value {
2199 if let Some(n) = val.as_u64() {
2200 let _ = writeln!(
2201 out,
2202 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2203 );
2204 }
2205 }
2206 }
2207 "count_min" => {
2208 if let Some(val) = &assertion.value {
2209 if let Some(n) = val.as_u64() {
2210 let _ = writeln!(
2211 out,
2212 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2213 );
2214 }
2215 }
2216 }
2217 "not_empty" => {
2218 let _ = writeln!(
2219 out,
2220 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2221 );
2222 }
2223 "is_empty" => {
2224 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2225 }
2226 _ => {
2227 let _ = writeln!(
2228 out,
2229 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2230 );
2231 }
2232 }
2233 return;
2234 }
2235 "embedding_dimensions" => {
2236 let expr = format!(
2237 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2238 );
2239 match assertion.assertion_type.as_str() {
2240 "equals" => {
2241 if let Some(val) = &assertion.value {
2242 if let Some(n) = val.as_u64() {
2243 let _ = writeln!(
2244 out,
2245 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2246 );
2247 }
2248 }
2249 }
2250 "greater_than" => {
2251 if let Some(val) = &assertion.value {
2252 if let Some(n) = val.as_u64() {
2253 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2254 }
2255 }
2256 }
2257 _ => {
2258 let _ = writeln!(
2259 out,
2260 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2261 );
2262 }
2263 }
2264 return;
2265 }
2266 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2267 let pred = match f.as_str() {
2268 "embeddings_valid" => {
2269 format!(
2270 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2271 )
2272 }
2273 "embeddings_finite" => {
2274 format!(
2275 "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 }}()"
2276 )
2277 }
2278 "embeddings_non_zero" => {
2279 format!(
2280 "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 }}()"
2281 )
2282 }
2283 "embeddings_normalized" => {
2284 format!(
2285 "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 }}()"
2286 )
2287 }
2288 _ => unreachable!(),
2289 };
2290 match assertion.assertion_type.as_str() {
2291 "is_true" => {
2292 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2293 }
2294 "is_false" => {
2295 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2296 }
2297 _ => {
2298 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2299 }
2300 }
2301 return;
2302 }
2303 "keywords" | "keywords_count" => {
2306 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2307 return;
2308 }
2309 _ => {}
2310 }
2311 }
2312 }
2313
2314 if !result_is_simple && is_streaming {
2321 if let Some(f) = &assertion.field {
2322 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2323 if let Some(expr) =
2324 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2325 {
2326 match assertion.assertion_type.as_str() {
2327 "count_min" => {
2328 if let Some(val) = &assertion.value {
2329 if let Some(n) = val.as_u64() {
2330 let _ = writeln!(
2331 out,
2332 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2333 );
2334 }
2335 }
2336 }
2337 "count_equals" => {
2338 if let Some(val) = &assertion.value {
2339 if let Some(n) = val.as_u64() {
2340 let _ = writeln!(
2341 out,
2342 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2343 );
2344 }
2345 }
2346 }
2347 "equals" => {
2348 if let Some(serde_json::Value::String(s)) = &assertion.value {
2349 let escaped = crate::escape::go_string_literal(s);
2350 let is_deep_path = f.contains('.') || f.contains('[');
2355 let safe_expr = if is_deep_path {
2356 format!(
2357 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2358 )
2359 } else {
2360 expr.clone()
2361 };
2362 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2363 } else if let Some(val) = &assertion.value {
2364 if let Some(n) = val.as_u64() {
2365 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2366 }
2367 }
2368 }
2369 "not_empty" => {
2370 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2371 }
2372 "is_empty" => {
2373 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2374 }
2375 "is_true" => {
2376 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2377 }
2378 "is_false" => {
2379 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2380 }
2381 "greater_than" => {
2382 if let Some(val) = &assertion.value {
2383 if let Some(n) = val.as_u64() {
2384 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2385 }
2386 }
2387 }
2388 "greater_than_or_equal" => {
2389 if let Some(val) = &assertion.value {
2390 if let Some(n) = val.as_u64() {
2391 let _ =
2392 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2393 }
2394 }
2395 }
2396 "contains" => {
2397 if let Some(serde_json::Value::String(s)) = &assertion.value {
2398 let escaped = crate::escape::go_string_literal(s);
2399 let _ =
2400 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2401 }
2402 }
2403 _ => {
2404 let _ = writeln!(
2405 out,
2406 "\t// streaming field '{f}': assertion type '{}' not rendered",
2407 assertion.assertion_type
2408 );
2409 }
2410 }
2411 }
2412 return;
2413 }
2414 }
2415 }
2416
2417 if !result_is_simple {
2420 if let Some(f) = &assertion.field {
2421 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2422 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2423 return;
2424 }
2425 }
2426 }
2427
2428 let field_expr = if result_is_simple {
2429 result_var.to_string()
2431 } else {
2432 match &assertion.field {
2433 Some(f) if !f.is_empty() => {
2434 if let Some(local_var) = optional_locals.get(f.as_str()) {
2436 local_var.clone()
2437 } else {
2438 field_resolver.accessor(f, "go", result_var)
2439 }
2440 }
2441 _ => result_var.to_string(),
2442 }
2443 };
2444
2445 let is_optional = assertion
2449 .field
2450 .as_ref()
2451 .map(|f| {
2452 let resolved = field_resolver.resolve(f);
2453 let check_path = resolved
2454 .strip_suffix(".length")
2455 .or_else(|| resolved.strip_suffix(".count"))
2456 .or_else(|| resolved.strip_suffix(".size"))
2457 .unwrap_or(resolved);
2458 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2459 })
2460 .unwrap_or(false);
2461
2462 let field_is_array_for_len = assertion
2466 .field
2467 .as_ref()
2468 .map(|f| {
2469 let resolved = field_resolver.resolve(f);
2470 let check_path = resolved
2471 .strip_suffix(".length")
2472 .or_else(|| resolved.strip_suffix(".count"))
2473 .or_else(|| resolved.strip_suffix(".size"))
2474 .unwrap_or(resolved);
2475 field_resolver.is_array(check_path)
2476 })
2477 .unwrap_or(false);
2478 let field_expr =
2479 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2480 let inner = &field_expr[4..field_expr.len() - 1];
2481 format!("len(*{inner})")
2482 } else {
2483 field_expr
2484 };
2485 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2487 Some(field_expr[5..field_expr.len() - 1].to_string())
2488 } else {
2489 None
2490 };
2491
2492 let field_is_slice = assertion
2496 .field
2497 .as_ref()
2498 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2499 .unwrap_or(false);
2500 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2501 format!("*{field_expr}")
2502 } else {
2503 field_expr.clone()
2504 };
2505
2506 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2511 let mut array_expr = field_expr[..idx].to_string();
2512 if let Some(stripped) = array_expr.strip_prefix("len(") {
2513 array_expr = stripped.to_string();
2514 }
2515 Some(array_expr)
2516 } else {
2517 None
2518 };
2519
2520 let mut assertion_buf = String::new();
2523 let out_ref = &mut assertion_buf;
2524
2525 match assertion.assertion_type.as_str() {
2526 "equals" => {
2527 if let Some(expected) = &assertion.value {
2528 let go_val = json_to_go(expected);
2529 if expected.is_string() {
2531 let resolved_name = assertion
2535 .field
2536 .as_ref()
2537 .map(|f| field_resolver.resolve(f))
2538 .unwrap_or_default();
2539 let is_struct = resolved_name.contains("FormatMetadata");
2540 let trimmed_field = if is_struct {
2541 if is_optional && !field_expr.starts_with("len(") {
2543 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2544 } else {
2545 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2546 }
2547 } else if is_optional && !field_expr.starts_with("len(") {
2548 format!("strings.TrimSpace(string(*{field_expr}))")
2549 } else {
2550 format!("strings.TrimSpace(string({field_expr}))")
2551 };
2552 if is_optional && !field_expr.starts_with("len(") {
2553 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2554 } else {
2555 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2556 }
2557 } else if is_optional && !field_expr.starts_with("len(") {
2558 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2559 } else {
2560 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2561 }
2562 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2563 let _ = writeln!(out_ref, "\t}}");
2564 }
2565 }
2566 "contains" => {
2567 if let Some(expected) = &assertion.value {
2568 let go_val = json_to_go(expected);
2569 let resolved_field = assertion.field.as_deref().unwrap_or("");
2575 let resolved_name = field_resolver.resolve(resolved_field);
2576 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2577 let is_opt =
2578 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2579 let field_for_contains = if is_opt && field_is_array {
2580 format!("jsonString({field_expr})")
2582 } else if is_opt {
2583 format!("fmt.Sprint(*{field_expr})")
2584 } else if field_is_array {
2585 format!("jsonString({field_expr})")
2586 } else {
2587 format!("fmt.Sprint({field_expr})")
2588 };
2589 if is_opt {
2590 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2591 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2592 let _ = writeln!(
2593 out_ref,
2594 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2595 );
2596 let _ = writeln!(out_ref, "\t}}");
2597 let _ = writeln!(out_ref, "\t}}");
2598 } else {
2599 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2600 let _ = writeln!(
2601 out_ref,
2602 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2603 );
2604 let _ = writeln!(out_ref, "\t}}");
2605 }
2606 }
2607 }
2608 "contains_all" => {
2609 if let Some(values) = &assertion.values {
2610 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 for val in values {
2616 let go_val = json_to_go(val);
2617 let field_for_contains = if is_opt && field_is_array {
2618 format!("jsonString({field_expr})")
2620 } else if is_opt {
2621 format!("fmt.Sprint(*{field_expr})")
2622 } else if field_is_array {
2623 format!("jsonString({field_expr})")
2624 } else {
2625 format!("fmt.Sprint({field_expr})")
2626 };
2627 if is_opt {
2628 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2629 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2630 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2631 let _ = writeln!(out_ref, "\t}}");
2632 let _ = writeln!(out_ref, "\t}}");
2633 } else {
2634 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2635 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2636 let _ = writeln!(out_ref, "\t}}");
2637 }
2638 }
2639 }
2640 }
2641 "not_contains" => {
2642 if let Some(expected) = &assertion.value {
2643 let go_val = json_to_go(expected);
2644 let resolved_field = assertion.field.as_deref().unwrap_or("");
2645 let resolved_name = field_resolver.resolve(resolved_field);
2646 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2647 let is_opt =
2648 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2649 let field_for_contains = if is_opt && field_is_array {
2650 format!("jsonString({field_expr})")
2652 } else if is_opt {
2653 format!("fmt.Sprint(*{field_expr})")
2654 } else if field_is_array {
2655 format!("jsonString({field_expr})")
2656 } else {
2657 format!("fmt.Sprint({field_expr})")
2658 };
2659 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2660 let _ = writeln!(
2661 out_ref,
2662 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2663 );
2664 let _ = writeln!(out_ref, "\t}}");
2665 }
2666 }
2667 "not_empty" => {
2668 let field_is_array = {
2671 let rf = assertion.field.as_deref().unwrap_or("");
2672 let rn = field_resolver.resolve(rf);
2673 field_resolver.is_array(rn)
2674 };
2675 if is_optional && !field_is_array {
2676 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2678 } else if is_optional && field_is_slice {
2679 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2681 } else if is_optional {
2682 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2684 } else if result_is_simple && result_is_array {
2685 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2687 } else {
2688 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2689 }
2690 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2691 let _ = writeln!(out_ref, "\t}}");
2692 }
2693 "is_empty" => {
2694 let field_is_array = {
2695 let rf = assertion.field.as_deref().unwrap_or("");
2696 let rn = field_resolver.resolve(rf);
2697 field_resolver.is_array(rn)
2698 };
2699 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2702 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2704 } else if is_optional && !field_is_array {
2705 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2707 } else if is_optional && field_is_slice {
2708 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2710 } else if is_optional {
2711 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2713 } else {
2714 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2715 }
2716 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2717 let _ = writeln!(out_ref, "\t}}");
2718 }
2719 "contains_any" => {
2720 if let Some(values) = &assertion.values {
2721 let resolved_field = assertion.field.as_deref().unwrap_or("");
2722 let resolved_name = field_resolver.resolve(resolved_field);
2723 let field_is_array = field_resolver.is_array(resolved_name);
2724 let is_opt =
2725 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2726 let field_for_contains = if is_opt && field_is_array {
2727 format!("jsonString({field_expr})")
2729 } else if is_opt {
2730 format!("fmt.Sprint(*{field_expr})")
2731 } else if field_is_array {
2732 format!("jsonString({field_expr})")
2733 } else {
2734 format!("fmt.Sprint({field_expr})")
2735 };
2736 let _ = writeln!(out_ref, "\t{{");
2737 let _ = writeln!(out_ref, "\t\tfound := false");
2738 for val in values {
2739 let go_val = json_to_go(val);
2740 let _ = writeln!(
2741 out_ref,
2742 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2743 );
2744 }
2745 let _ = writeln!(out_ref, "\t\tif !found {{");
2746 let _ = writeln!(
2747 out_ref,
2748 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2749 );
2750 let _ = writeln!(out_ref, "\t\t}}");
2751 let _ = writeln!(out_ref, "\t}}");
2752 }
2753 }
2754 "greater_than" => {
2755 if let Some(val) = &assertion.value {
2756 let go_val = json_to_go(val);
2757 if is_optional {
2761 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2762 if let Some(n) = val.as_u64() {
2763 let next = n + 1;
2764 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2765 } else {
2766 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2767 }
2768 let _ = writeln!(
2769 out_ref,
2770 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2771 );
2772 let _ = writeln!(out_ref, "\t\t}}");
2773 let _ = writeln!(out_ref, "\t}}");
2774 } else if let Some(n) = val.as_u64() {
2775 let next = n + 1;
2776 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2777 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2778 let _ = writeln!(out_ref, "\t}}");
2779 } else {
2780 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2781 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2782 let _ = writeln!(out_ref, "\t}}");
2783 }
2784 }
2785 }
2786 "less_than" => {
2787 if let Some(val) = &assertion.value {
2788 let go_val = json_to_go(val);
2789 if let Some(ref guard) = nil_guard_expr {
2790 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2791 let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2792 let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2793 let _ = writeln!(out_ref, "\t\t}}");
2794 let _ = writeln!(out_ref, "\t}}");
2795 } else if is_optional && !field_expr.starts_with("len(") {
2796 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2798 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2799 let _ = writeln!(
2800 out_ref,
2801 "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2802 );
2803 let _ = writeln!(out_ref, "\t\t}}");
2804 let _ = writeln!(out_ref, "\t}}");
2805 } else {
2806 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2807 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2808 let _ = writeln!(out_ref, "\t}}");
2809 }
2810 }
2811 }
2812 "greater_than_or_equal" => {
2813 if let Some(val) = &assertion.value {
2814 let go_val = json_to_go(val);
2815 if let Some(ref guard) = nil_guard_expr {
2816 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2817 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2818 let _ = writeln!(
2819 out_ref,
2820 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2821 );
2822 let _ = writeln!(out_ref, "\t\t}}");
2823 let _ = writeln!(out_ref, "\t}}");
2824 } else if is_optional && !field_expr.starts_with("len(") {
2825 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2827 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2828 let _ = writeln!(
2829 out_ref,
2830 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2831 );
2832 let _ = writeln!(out_ref, "\t\t}}");
2833 let _ = writeln!(out_ref, "\t}}");
2834 } else {
2835 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2836 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2837 let _ = writeln!(out_ref, "\t}}");
2838 }
2839 }
2840 }
2841 "less_than_or_equal" => {
2842 if let Some(val) = &assertion.value {
2843 let go_val = json_to_go(val);
2844 if is_optional && !field_expr.starts_with("len(") {
2845 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2847 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2848 let _ = writeln!(
2849 out_ref,
2850 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2851 );
2852 let _ = writeln!(out_ref, "\t\t}}");
2853 let _ = writeln!(out_ref, "\t}}");
2854 } else {
2855 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2856 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2857 let _ = writeln!(out_ref, "\t}}");
2858 }
2859 }
2860 }
2861 "starts_with" => {
2862 if let Some(expected) = &assertion.value {
2863 let go_val = json_to_go(expected);
2864 let field_for_prefix = if is_optional
2865 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2866 {
2867 format!("string(*{field_expr})")
2868 } else {
2869 format!("string({field_expr})")
2870 };
2871 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2872 let _ = writeln!(
2873 out_ref,
2874 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2875 );
2876 let _ = writeln!(out_ref, "\t}}");
2877 }
2878 }
2879 "count_min" => {
2880 if let Some(val) = &assertion.value {
2881 if let Some(n) = val.as_u64() {
2882 if is_optional {
2883 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2884 let len_expr = if field_is_slice {
2886 format!("len({field_expr})")
2887 } else {
2888 format!("len(*{field_expr})")
2889 };
2890 let _ = writeln!(
2891 out_ref,
2892 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2893 );
2894 let _ = writeln!(out_ref, "\t}}");
2895 } else {
2896 let _ = writeln!(
2897 out_ref,
2898 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2899 );
2900 }
2901 }
2902 }
2903 }
2904 "count_equals" => {
2905 if let Some(val) = &assertion.value {
2906 if let Some(n) = val.as_u64() {
2907 if is_optional {
2908 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2909 let len_expr = if field_is_slice {
2911 format!("len({field_expr})")
2912 } else {
2913 format!("len(*{field_expr})")
2914 };
2915 let _ = writeln!(
2916 out_ref,
2917 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2918 );
2919 let _ = writeln!(out_ref, "\t}}");
2920 } else {
2921 let _ = writeln!(
2922 out_ref,
2923 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2924 );
2925 }
2926 }
2927 }
2928 }
2929 "is_true" => {
2930 if is_optional {
2931 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2932 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2933 let _ = writeln!(out_ref, "\t}}");
2934 } else {
2935 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2936 }
2937 }
2938 "is_false" => {
2939 if is_optional {
2940 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2941 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2942 let _ = writeln!(out_ref, "\t}}");
2943 } else {
2944 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2945 }
2946 }
2947 "method_result" => {
2948 if let Some(method_name) = &assertion.method {
2949 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2950 let check = assertion.check.as_deref().unwrap_or("is_true");
2951 let deref_expr = if info.is_pointer {
2954 format!("*{}", info.call_expr)
2955 } else {
2956 info.call_expr.clone()
2957 };
2958 match check {
2959 "equals" => {
2960 if let Some(val) = &assertion.value {
2961 if val.is_boolean() {
2962 if val.as_bool() == Some(true) {
2963 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2964 } else {
2965 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2966 }
2967 } else {
2968 let go_val = if let Some(cast) = info.value_cast {
2972 if val.is_number() {
2973 format!("{cast}({})", json_to_go(val))
2974 } else {
2975 json_to_go(val)
2976 }
2977 } else {
2978 json_to_go(val)
2979 };
2980 let _ = writeln!(
2981 out_ref,
2982 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2983 );
2984 }
2985 }
2986 }
2987 "is_true" => {
2988 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2989 }
2990 "is_false" => {
2991 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2992 }
2993 "greater_than_or_equal" => {
2994 if let Some(val) = &assertion.value {
2995 let n = val.as_u64().unwrap_or(0);
2996 let cast = info.value_cast.unwrap_or("uint");
2998 let _ = writeln!(
2999 out_ref,
3000 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3001 );
3002 }
3003 }
3004 "count_min" => {
3005 if let Some(val) = &assertion.value {
3006 let n = val.as_u64().unwrap_or(0);
3007 let _ = writeln!(
3008 out_ref,
3009 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3010 );
3011 }
3012 }
3013 "contains" => {
3014 if let Some(val) = &assertion.value {
3015 let go_val = json_to_go(val);
3016 let _ = writeln!(
3017 out_ref,
3018 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3019 );
3020 }
3021 }
3022 "is_error" => {
3023 let _ = writeln!(out_ref, "\t{{");
3024 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3025 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3026 let _ = writeln!(out_ref, "\t}}");
3027 }
3028 other_check => {
3029 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3030 }
3031 }
3032 } else {
3033 panic!("Go e2e generator: method_result assertion missing 'method' field");
3034 }
3035 }
3036 "min_length" => {
3037 if let Some(val) = &assertion.value {
3038 if let Some(n) = val.as_u64() {
3039 if is_optional {
3040 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3041 let _ = writeln!(
3042 out_ref,
3043 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3044 );
3045 let _ = writeln!(out_ref, "\t}}");
3046 } else if field_expr.starts_with("len(") {
3047 let _ = writeln!(
3048 out_ref,
3049 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3050 );
3051 } else {
3052 let _ = writeln!(
3053 out_ref,
3054 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3055 );
3056 }
3057 }
3058 }
3059 }
3060 "max_length" => {
3061 if let Some(val) = &assertion.value {
3062 if let Some(n) = val.as_u64() {
3063 if is_optional {
3064 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3065 let _ = writeln!(
3066 out_ref,
3067 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3068 );
3069 let _ = writeln!(out_ref, "\t}}");
3070 } else if field_expr.starts_with("len(") {
3071 let _ = writeln!(
3072 out_ref,
3073 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3074 );
3075 } else {
3076 let _ = writeln!(
3077 out_ref,
3078 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3079 );
3080 }
3081 }
3082 }
3083 }
3084 "ends_with" => {
3085 if let Some(expected) = &assertion.value {
3086 let go_val = json_to_go(expected);
3087 let field_for_suffix = if is_optional
3088 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3089 {
3090 format!("string(*{field_expr})")
3091 } else {
3092 format!("string({field_expr})")
3093 };
3094 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3095 let _ = writeln!(
3096 out_ref,
3097 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3098 );
3099 let _ = writeln!(out_ref, "\t}}");
3100 }
3101 }
3102 "matches_regex" => {
3103 if let Some(expected) = &assertion.value {
3104 let go_val = json_to_go(expected);
3105 let field_for_regex = if is_optional
3106 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3107 {
3108 format!("*{field_expr}")
3109 } else {
3110 field_expr.clone()
3111 };
3112 let _ = writeln!(
3113 out_ref,
3114 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3115 );
3116 }
3117 }
3118 "not_error" => {
3119 }
3121 "error" => {
3122 }
3124 other => {
3125 panic!("Go e2e generator: unsupported assertion type: {other}");
3126 }
3127 }
3128
3129 if let Some(ref arr) = array_guard {
3132 if !assertion_buf.is_empty() {
3133 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3134 for line in assertion_buf.lines() {
3136 let _ = writeln!(out, "\t{line}");
3137 }
3138 let _ = writeln!(out, "\t}}");
3139 }
3140 } else {
3141 out.push_str(&assertion_buf);
3142 }
3143}
3144
3145struct GoMethodCallInfo {
3147 call_expr: String,
3149 is_pointer: bool,
3151 value_cast: Option<&'static str>,
3154}
3155
3156fn build_go_method_call(
3171 result_var: &str,
3172 method_name: &str,
3173 args: Option<&serde_json::Value>,
3174 import_alias: &str,
3175) -> GoMethodCallInfo {
3176 match method_name {
3177 "root_node_type" => GoMethodCallInfo {
3178 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3179 is_pointer: false,
3180 value_cast: None,
3181 },
3182 "named_children_count" => GoMethodCallInfo {
3183 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3184 is_pointer: false,
3185 value_cast: Some("uint"),
3186 },
3187 "has_error_nodes" => GoMethodCallInfo {
3188 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3189 is_pointer: true,
3190 value_cast: None,
3191 },
3192 "error_count" | "tree_error_count" => GoMethodCallInfo {
3193 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3194 is_pointer: true,
3195 value_cast: Some("uint"),
3196 },
3197 "tree_to_sexp" => GoMethodCallInfo {
3198 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3199 is_pointer: true,
3200 value_cast: None,
3201 },
3202 "contains_node_type" => {
3203 let node_type = args
3204 .and_then(|a| a.get("node_type"))
3205 .and_then(|v| v.as_str())
3206 .unwrap_or("");
3207 GoMethodCallInfo {
3208 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3209 is_pointer: true,
3210 value_cast: None,
3211 }
3212 }
3213 "find_nodes_by_type" => {
3214 let node_type = args
3215 .and_then(|a| a.get("node_type"))
3216 .and_then(|v| v.as_str())
3217 .unwrap_or("");
3218 GoMethodCallInfo {
3219 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3220 is_pointer: true,
3221 value_cast: None,
3222 }
3223 }
3224 "run_query" => {
3225 let query_source = args
3226 .and_then(|a| a.get("query_source"))
3227 .and_then(|v| v.as_str())
3228 .unwrap_or("");
3229 let language = args
3230 .and_then(|a| a.get("language"))
3231 .and_then(|v| v.as_str())
3232 .unwrap_or("");
3233 let query_lit = go_string_literal(query_source);
3234 let lang_lit = go_string_literal(language);
3235 GoMethodCallInfo {
3237 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3238 is_pointer: false,
3239 value_cast: None,
3240 }
3241 }
3242 other => {
3243 let method_pascal = other.to_upper_camel_case();
3244 GoMethodCallInfo {
3245 call_expr: format!("{result_var}.{method_pascal}()"),
3246 is_pointer: false,
3247 value_cast: None,
3248 }
3249 }
3250 }
3251}
3252
3253fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3263 match value {
3264 serde_json::Value::Object(map) => {
3265 let new_map: serde_json::Map<String, serde_json::Value> = map
3266 .into_iter()
3267 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3268 .collect();
3269 serde_json::Value::Object(new_map)
3270 }
3271 serde_json::Value::Array(arr) => {
3272 if is_byte_array(&arr) {
3275 let bytes: Vec<u8> = arr
3276 .iter()
3277 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3278 .collect();
3279 let encoded = base64_encode(&bytes);
3281 serde_json::Value::String(encoded)
3282 } else {
3283 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3284 }
3285 }
3286 serde_json::Value::String(s) => {
3287 serde_json::Value::String(pascal_to_snake_case(&s))
3290 }
3291 other => other,
3292 }
3293}
3294
3295fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3297 if arr.is_empty() {
3298 return false;
3299 }
3300 arr.iter().all(|v| {
3301 if let serde_json::Value::Number(n) = v {
3302 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3303 } else {
3304 false
3305 }
3306 })
3307}
3308
3309fn base64_encode(bytes: &[u8]) -> String {
3312 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3313 let mut result = String::new();
3314 let mut i = 0;
3315
3316 while i + 2 < bytes.len() {
3317 let b1 = bytes[i];
3318 let b2 = bytes[i + 1];
3319 let b3 = bytes[i + 2];
3320
3321 result.push(TABLE[(b1 >> 2) as usize] as char);
3322 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3323 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3324 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3325
3326 i += 3;
3327 }
3328
3329 if i < bytes.len() {
3331 let b1 = bytes[i];
3332 result.push(TABLE[(b1 >> 2) as usize] as char);
3333
3334 if i + 1 < bytes.len() {
3335 let b2 = bytes[i + 1];
3336 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3337 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3338 result.push('=');
3339 } else {
3340 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3341 result.push_str("==");
3342 }
3343 }
3344
3345 result
3346}
3347
3348fn camel_to_snake_case(s: &str) -> String {
3350 let mut result = String::new();
3351 let mut prev_upper = false;
3352 for (i, c) in s.char_indices() {
3353 if c.is_uppercase() {
3354 if i > 0 && !prev_upper {
3355 result.push('_');
3356 }
3357 result.push(c.to_lowercase().next().unwrap_or(c));
3358 prev_upper = true;
3359 } else {
3360 if prev_upper && i > 1 {
3361 }
3365 result.push(c);
3366 prev_upper = false;
3367 }
3368 }
3369 result
3370}
3371
3372fn pascal_to_snake_case(s: &str) -> String {
3377 let first_char = s.chars().next();
3379 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3380 return s.to_string();
3381 }
3382 camel_to_snake_case(s)
3383}
3384
3385fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3389 let elem = element_type.unwrap_or("String").trim();
3390 let go_elem = rust_type_to_go(elem, import_alias);
3391 format!("[]{go_elem}")
3392}
3393
3394fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3397 let trimmed = rust.trim();
3398 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3399 return format!("[]{}", rust_type_to_go(inner, import_alias));
3400 }
3401 match trimmed {
3402 "String" | "&str" | "str" => "string".to_string(),
3403 "bool" => "bool".to_string(),
3404 "f32" => "float32".to_string(),
3405 "f64" => "float64".to_string(),
3406 "i8" => "int8".to_string(),
3407 "i16" => "int16".to_string(),
3408 "i32" => "int32".to_string(),
3409 "i64" | "isize" => "int64".to_string(),
3410 "u8" => "uint8".to_string(),
3411 "u16" => "uint16".to_string(),
3412 "u32" => "uint32".to_string(),
3413 "u64" | "usize" => "uint64".to_string(),
3414 _ => format!("{import_alias}.{trimmed}"),
3415 }
3416}
3417
3418fn json_to_go(value: &serde_json::Value) -> String {
3419 match value {
3420 serde_json::Value::String(s) => go_string_literal(s),
3421 serde_json::Value::Bool(b) => b.to_string(),
3422 serde_json::Value::Number(n) => n.to_string(),
3423 serde_json::Value::Null => "nil".to_string(),
3424 other => go_string_literal(&other.to_string()),
3426 }
3427}
3428
3429fn visitor_struct_name(fixture_id: &str) -> String {
3438 use heck::ToUpperCamelCase;
3439 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3441}
3442
3443fn emit_go_visitor_struct(
3448 out: &mut String,
3449 struct_name: &str,
3450 visitor_spec: &crate::fixture::VisitorSpec,
3451 import_alias: &str,
3452) {
3453 let _ = writeln!(out, "type {struct_name} struct{{");
3454 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3455 let _ = writeln!(out, "}}");
3456 for (method_name, action) in &visitor_spec.callbacks {
3457 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3458 }
3459}
3460
3461fn emit_go_visitor_method(
3463 out: &mut String,
3464 struct_name: &str,
3465 method_name: &str,
3466 action: &CallbackAction,
3467 import_alias: &str,
3468) {
3469 let camel_method = method_to_camel(method_name);
3470 let params = match method_name {
3473 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3474 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3475 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3476 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3477 "visit_code_inline"
3478 | "visit_strong"
3479 | "visit_emphasis"
3480 | "visit_strikethrough"
3481 | "visit_underline"
3482 | "visit_subscript"
3483 | "visit_superscript"
3484 | "visit_mark"
3485 | "visit_button"
3486 | "visit_summary"
3487 | "visit_figcaption"
3488 | "visit_definition_term"
3489 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3490 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3491 "visit_list_item" => {
3492 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3493 }
3494 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3495 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3496 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3497 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3498 "visit_input" => {
3499 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3500 }
3501 "visit_audio" | "visit_video" | "visit_iframe" => {
3502 format!("_ {import_alias}.NodeContext, src *string")
3503 }
3504 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3505 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3506 format!("_ {import_alias}.NodeContext, output string")
3507 }
3508 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3509 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3510 _ => format!("_ {import_alias}.NodeContext"),
3511 };
3512
3513 let _ = writeln!(
3514 out,
3515 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3516 );
3517 match action {
3518 CallbackAction::Skip => {
3519 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3520 }
3521 CallbackAction::Continue => {
3522 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3523 }
3524 CallbackAction::PreserveHtml => {
3525 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3526 }
3527 CallbackAction::Custom { output } => {
3528 let escaped = go_string_literal(output);
3529 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3530 }
3531 CallbackAction::CustomTemplate { template, .. } => {
3532 let ptr_params = go_visitor_ptr_params(method_name);
3539 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3540 let escaped_fmt = go_string_literal(&fmt_str);
3541 if fmt_args.is_empty() {
3542 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3543 } else {
3544 let args_str = fmt_args.join(", ");
3545 let _ = writeln!(
3546 out,
3547 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3548 );
3549 }
3550 }
3551 }
3552 let _ = writeln!(out, "}}");
3553}
3554
3555fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3558 match method_name {
3559 "visit_link" => ["title"].into(),
3560 "visit_image" => ["title"].into(),
3561 "visit_heading" => ["id"].into(),
3562 "visit_code_block" => ["lang"].into(),
3563 "visit_form" => ["action", "method"].into(),
3564 "visit_input" => ["name", "value"].into(),
3565 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3566 _ => std::collections::HashSet::new(),
3567 }
3568}
3569
3570fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3582 let mut fmt_str = String::new();
3583 let mut args: Vec<String> = Vec::new();
3584 let mut chars = template.chars().peekable();
3585 while let Some(c) = chars.next() {
3586 if c == '{' {
3587 let mut name = String::new();
3589 for inner in chars.by_ref() {
3590 if inner == '}' {
3591 break;
3592 }
3593 name.push(inner);
3594 }
3595 fmt_str.push_str("%s");
3596 let go_name = go_param_name(&name);
3598 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3600 format!("*{go_name}")
3601 } else {
3602 go_name
3603 };
3604 args.push(arg_expr);
3605 } else {
3606 fmt_str.push(c);
3607 }
3608 }
3609 (fmt_str, args)
3610}
3611
3612fn method_to_camel(snake: &str) -> String {
3614 use heck::ToUpperCamelCase;
3615 snake.to_upper_camel_case()
3616}
3617
3618#[cfg(test)]
3619mod tests {
3620 use super::*;
3621 use crate::config::{CallConfig, E2eConfig};
3622 use crate::fixture::{Assertion, Fixture};
3623
3624 fn make_fixture(id: &str) -> Fixture {
3625 Fixture {
3626 id: id.to_string(),
3627 category: None,
3628 description: "test fixture".to_string(),
3629 tags: vec![],
3630 skip: None,
3631 env: None,
3632 call: None,
3633 input: serde_json::Value::Null,
3634 mock_response: Some(crate::fixture::MockResponse {
3635 status: 200,
3636 body: Some(serde_json::Value::Null),
3637 stream_chunks: None,
3638 headers: std::collections::HashMap::new(),
3639 }),
3640 source: String::new(),
3641 http: None,
3642 assertions: vec![Assertion {
3643 assertion_type: "not_error".to_string(),
3644 ..Default::default()
3645 }],
3646 visitor: None,
3647 }
3648 }
3649
3650 #[test]
3654 fn test_go_method_name_uses_go_casing() {
3655 let e2e_config = E2eConfig {
3656 call: CallConfig {
3657 function: "clean_extracted_text".to_string(),
3658 module: "github.com/example/mylib".to_string(),
3659 result_var: "result".to_string(),
3660 returns_result: true,
3661 ..CallConfig::default()
3662 },
3663 ..E2eConfig::default()
3664 };
3665
3666 let fixture = make_fixture("basic_text");
3667 let mut out = String::new();
3668 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3669
3670 assert!(
3671 out.contains("kreuzberg.CleanExtractedText("),
3672 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3673 );
3674 assert!(
3675 !out.contains("kreuzberg.clean_extracted_text("),
3676 "must not emit raw snake_case method name, got:\n{out}"
3677 );
3678 }
3679
3680 #[test]
3681 fn test_streaming_fixture_emits_collect_snippet() {
3682 let streaming_fixture_json = r#"{
3684 "id": "basic_stream",
3685 "description": "basic streaming test",
3686 "call": "chat_stream",
3687 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3688 "mock_response": {
3689 "status": 200,
3690 "stream_chunks": [{"delta": "hello"}]
3691 },
3692 "assertions": [
3693 {"type": "count_min", "field": "chunks", "value": 1}
3694 ]
3695 }"#;
3696 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3697 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3698
3699 let e2e_config = E2eConfig {
3700 call: CallConfig {
3701 function: "chat_stream".to_string(),
3702 module: "github.com/example/mylib".to_string(),
3703 result_var: "result".to_string(),
3704 returns_result: true,
3705 r#async: true,
3706 ..CallConfig::default()
3707 },
3708 ..E2eConfig::default()
3709 };
3710
3711 let mut out = String::new();
3712 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3713
3714 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3715 assert!(
3716 out.contains("for chunk := range stream"),
3717 "should emit collect loop, got:\n{out}"
3718 );
3719 }
3720
3721 #[test]
3722 fn test_streaming_with_client_factory_and_json_arg() {
3723 use alef_core::config::e2e::{ArgMapping, CallOverride};
3727 let streaming_fixture_json = r#"{
3728 "id": "basic_stream_client",
3729 "description": "basic streaming test with client",
3730 "call": "chat_stream",
3731 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3732 "mock_response": {
3733 "status": 200,
3734 "stream_chunks": [{"delta": "hello"}]
3735 },
3736 "assertions": [
3737 {"type": "count_min", "field": "chunks", "value": 1}
3738 ]
3739 }"#;
3740 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3741 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3742
3743 let go_override = CallOverride {
3744 client_factory: Some("CreateClient".to_string()),
3745 ..Default::default()
3746 };
3747
3748 let mut call_overrides = std::collections::HashMap::new();
3749 call_overrides.insert("go".to_string(), go_override);
3750
3751 let e2e_config = E2eConfig {
3752 call: CallConfig {
3753 function: "chat_stream".to_string(),
3754 module: "github.com/example/mylib".to_string(),
3755 result_var: "result".to_string(),
3756 returns_result: false, r#async: true,
3758 args: vec![ArgMapping {
3759 name: "request".to_string(),
3760 field: "input".to_string(),
3761 arg_type: "json_object".to_string(),
3762 optional: false,
3763 owned: true,
3764 element_type: None,
3765 go_type: None,
3766 }],
3767 overrides: call_overrides,
3768 ..CallConfig::default()
3769 },
3770 ..E2eConfig::default()
3771 };
3772
3773 let mut out = String::new();
3774 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3775
3776 eprintln!("generated:\n{out}");
3777 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3778 assert!(
3779 out.contains("for chunk := range stream"),
3780 "should emit collect loop, got:\n{out}"
3781 );
3782 }
3783
3784 #[test]
3788 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3789 let mut optional_fields = std::collections::HashSet::new();
3790 optional_fields.insert("segments".to_string());
3791 let mut array_fields = std::collections::HashSet::new();
3792 array_fields.insert("segments".to_string());
3793
3794 let e2e_config = E2eConfig {
3795 call: CallConfig {
3796 function: "transcribe".to_string(),
3797 module: "github.com/example/mylib".to_string(),
3798 result_var: "result".to_string(),
3799 returns_result: true,
3800 ..CallConfig::default()
3801 },
3802 fields_optional: optional_fields,
3803 fields_array: array_fields,
3804 ..E2eConfig::default()
3805 };
3806
3807 let fixture = Fixture {
3808 id: "edge_transcribe_with_timestamps".to_string(),
3809 category: None,
3810 description: "Transcription with timestamp segments".to_string(),
3811 tags: vec![],
3812 skip: None,
3813 env: None,
3814 call: None,
3815 input: serde_json::Value::Null,
3816 mock_response: Some(crate::fixture::MockResponse {
3817 status: 200,
3818 body: Some(serde_json::Value::Null),
3819 stream_chunks: None,
3820 headers: std::collections::HashMap::new(),
3821 }),
3822 source: String::new(),
3823 http: None,
3824 assertions: vec![
3825 Assertion {
3826 assertion_type: "not_error".to_string(),
3827 ..Default::default()
3828 },
3829 Assertion {
3830 assertion_type: "equals".to_string(),
3831 field: Some("segments[0].id".to_string()),
3832 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3833 ..Default::default()
3834 },
3835 ],
3836 visitor: None,
3837 };
3838
3839 let mut out = String::new();
3840 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3841
3842 eprintln!("generated:\n{out}");
3843
3844 assert!(
3849 out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3850 "guard must be on Segments (the slice), not an element; got:\n{out}"
3851 );
3852 assert!(
3854 !out.contains("result.Segments[0] != nil"),
3855 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3856 );
3857 }
3858
3859 #[test]
3865 fn test_result_is_simple_contains_binds_result_and_emits_imports() {
3866 use alef_core::config::e2e::ArgMapping;
3867
3868 let e2e_config = E2eConfig {
3869 call: CallConfig {
3870 function: "detect_mime_type_from_bytes".to_string(),
3871 module: "github.com/example/mylib".to_string(),
3872 result_var: "result".to_string(),
3873 returns_result: true,
3874 result_is_simple: true,
3875 args: vec![ArgMapping {
3876 name: "content".to_string(),
3877 field: "input.data".to_string(),
3878 arg_type: "bytes".to_string(),
3879 optional: false,
3880 owned: false,
3881 element_type: None,
3882 go_type: None,
3883 }],
3884 ..CallConfig::default()
3885 },
3886 ..E2eConfig::default()
3887 };
3888
3889 let fixture = Fixture {
3890 id: "mime_detect_bytes".to_string(),
3891 category: None,
3892 description: "Detect MIME type from file bytes".to_string(),
3893 tags: vec![],
3894 skip: None,
3895 env: None,
3896 call: None,
3897 input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
3898 mock_response: None,
3899 source: String::new(),
3900 http: None,
3901 assertions: vec![Assertion {
3902 assertion_type: "contains".to_string(),
3903 field: Some("result".to_string()),
3904 value: Some(serde_json::Value::String("pdf".to_string())),
3905 ..Default::default()
3906 }],
3907 visitor: None,
3908 };
3909
3910 let out = render_test_file(
3911 "mime_utilities",
3912 &[&fixture],
3913 "github.com/example/mylib",
3914 "kreuzberg",
3915 &e2e_config,
3916 &[],
3917 );
3918
3919 assert!(
3920 out.contains("result, err := kreuzberg.DetectMimeTypeFromBytes("),
3921 "expected the call to bind to `result`, not `_`; got:\n{out}"
3922 );
3923 assert!(
3924 out.contains("strings.Contains(") && out.contains("fmt.Sprint("),
3925 "expected `strings.Contains(fmt.Sprint(...))` rendering; got:\n{out}"
3926 );
3927 assert!(
3928 out.contains("\t\"fmt\""),
3929 "expected the `fmt` import to be emitted; got:\n{out}"
3930 );
3931 assert!(
3932 out.contains("\t\"strings\""),
3933 "expected the `strings` import to be emitted; got:\n{out}"
3934 );
3935 }
3936}