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 let resolved = field_resolver.resolve(f);
1385 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1386 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1391 let is_array_field = field_resolver.is_array(resolved);
1392 if !is_string_field || is_array_field {
1393 continue;
1396 }
1397 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1398 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1399 if field_resolver.has_map_access(f) {
1400 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1403 } else {
1404 let _ = writeln!(out, "\tvar {local_var} string");
1405 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1406 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1410 let _ = writeln!(out, "\t}}");
1411 }
1412 optional_locals.insert(f.clone(), local_var);
1413 }
1414 }
1415 }
1416 }
1417
1418 for assertion in &fixture.assertions {
1420 if let Some(f) = &assertion.field {
1421 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1422 let parts: Vec<&str> = f.split('.').collect();
1425 let mut guard_expr: Option<String> = None;
1426 for i in 1..parts.len() {
1427 let prefix = parts[..i].join(".");
1428 let resolved_prefix = field_resolver.resolve(&prefix);
1429 if field_resolver.is_optional(resolved_prefix) {
1430 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1436 let suffix = &resolved_prefix[bracket_pos + 1..];
1437 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1438 if is_numeric_index {
1439 &resolved_prefix[..bracket_pos]
1440 } else {
1441 resolved_prefix
1442 }
1443 } else {
1444 resolved_prefix
1445 };
1446 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1447 guard_expr = Some(accessor);
1448 break;
1449 }
1450 }
1451 if let Some(guard) = guard_expr {
1452 if field_resolver.is_valid_for_result(f) {
1455 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1461 if is_struct_value {
1462 render_assertion(
1465 out,
1466 assertion,
1467 &effective_result_var,
1468 import_alias,
1469 field_resolver,
1470 &optional_locals,
1471 result_is_simple,
1472 result_is_array,
1473 is_streaming,
1474 );
1475 continue;
1476 }
1477 let _ = writeln!(out, "\tif {guard} != nil {{");
1478 let mut nil_buf = String::new();
1481 render_assertion(
1482 &mut nil_buf,
1483 assertion,
1484 &effective_result_var,
1485 import_alias,
1486 field_resolver,
1487 &optional_locals,
1488 result_is_simple,
1489 result_is_array,
1490 is_streaming,
1491 );
1492 for line in nil_buf.lines() {
1493 let _ = writeln!(out, "\t{line}");
1494 }
1495 let _ = writeln!(out, "\t}}");
1496 } else {
1497 render_assertion(
1498 out,
1499 assertion,
1500 &effective_result_var,
1501 import_alias,
1502 field_resolver,
1503 &optional_locals,
1504 result_is_simple,
1505 result_is_array,
1506 is_streaming,
1507 );
1508 }
1509 continue;
1510 }
1511 }
1512 }
1513 render_assertion(
1514 out,
1515 assertion,
1516 &effective_result_var,
1517 import_alias,
1518 field_resolver,
1519 &optional_locals,
1520 result_is_simple,
1521 result_is_array,
1522 is_streaming,
1523 );
1524 }
1525
1526 let _ = writeln!(out, "}}");
1527}
1528
1529fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1535 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1536}
1537
1538struct GoTestClientRenderer;
1550
1551impl client::TestClientRenderer for GoTestClientRenderer {
1552 fn language_name(&self) -> &'static str {
1553 "go"
1554 }
1555
1556 fn sanitize_test_name(&self, id: &str) -> String {
1560 id.to_upper_camel_case()
1561 }
1562
1563 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1566 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1567 let _ = writeln!(out, "\t// {description}");
1568 if let Some(reason) = skip_reason {
1569 let escaped = go_string_literal(reason);
1570 let _ = writeln!(out, "\tt.Skip({escaped})");
1571 }
1572 }
1573
1574 fn render_test_close(&self, out: &mut String) {
1575 let _ = writeln!(out, "}}");
1576 }
1577
1578 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1584 let method = ctx.method.to_uppercase();
1585 let path = ctx.path;
1586
1587 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1588 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1589 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1590 let _ = writeln!(out, "\t}}");
1591
1592 let body_expr = if let Some(body) = ctx.body {
1594 let json = serde_json::to_string(body).unwrap_or_default();
1595 let escaped = go_string_literal(&json);
1596 format!("strings.NewReader({})", escaped)
1597 } else {
1598 "strings.NewReader(\"\")".to_string()
1599 };
1600
1601 let _ = writeln!(out, "\tbody := {body_expr}");
1602 let _ = writeln!(
1603 out,
1604 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1605 );
1606 let _ = writeln!(out, "\tif err != nil {{");
1607 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1608 let _ = writeln!(out, "\t}}");
1609
1610 if ctx.body.is_some() {
1612 let content_type = ctx.content_type.unwrap_or("application/json");
1613 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1614 }
1615
1616 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1618 header_names.sort();
1619 for name in header_names {
1620 let value = &ctx.headers[name];
1621 let escaped_name = go_string_literal(name);
1622 let escaped_value = go_string_literal(value);
1623 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1624 }
1625
1626 if !ctx.cookies.is_empty() {
1628 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1629 cookie_names.sort();
1630 for name in cookie_names {
1631 let value = &ctx.cookies[name];
1632 let escaped_name = go_string_literal(name);
1633 let escaped_value = go_string_literal(value);
1634 let _ = writeln!(
1635 out,
1636 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1637 );
1638 }
1639 }
1640
1641 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1643 let _ = writeln!(
1644 out,
1645 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1646 );
1647 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1648 let _ = writeln!(out, "\t\t}},");
1649 let _ = writeln!(out, "\t}}");
1650 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1651 let _ = writeln!(out, "\tif err != nil {{");
1652 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1653 let _ = writeln!(out, "\t}}");
1654 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1655
1656 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1660 let _ = writeln!(out, "\tif err != nil {{");
1661 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1662 let _ = writeln!(out, "\t}}");
1663 let _ = writeln!(out, "\t_ = bodyBytes");
1664 }
1665
1666 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1667 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1668 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1669 let _ = writeln!(out, "\t}}");
1670 }
1671
1672 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1675 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1677 return;
1678 }
1679 if name.eq_ignore_ascii_case("connection") {
1681 return;
1682 }
1683 let escaped_name = go_string_literal(name);
1684 let escaped_value = go_string_literal(expected);
1685 let _ = writeln!(
1686 out,
1687 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1688 );
1689 let _ = writeln!(
1690 out,
1691 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1692 );
1693 let _ = writeln!(out, "\t}}");
1694 }
1695
1696 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1701 match expected {
1702 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1703 let json_str = serde_json::to_string(expected).unwrap_or_default();
1704 let escaped = go_string_literal(&json_str);
1705 let _ = writeln!(out, "\tvar got any");
1706 let _ = writeln!(out, "\tvar want any");
1707 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1708 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1709 let _ = writeln!(out, "\t}}");
1710 let _ = writeln!(
1711 out,
1712 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1713 );
1714 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1715 let _ = writeln!(out, "\t}}");
1716 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1717 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1718 let _ = writeln!(out, "\t}}");
1719 }
1720 serde_json::Value::String(s) => {
1721 let escaped = go_string_literal(s);
1722 let _ = writeln!(out, "\twant := {escaped}");
1723 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1724 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1725 let _ = writeln!(out, "\t}}");
1726 }
1727 other => {
1728 let escaped = go_string_literal(&other.to_string());
1729 let _ = writeln!(out, "\twant := {escaped}");
1730 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1731 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1732 let _ = writeln!(out, "\t}}");
1733 }
1734 }
1735 }
1736
1737 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1740 if let Some(obj) = expected.as_object() {
1741 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1742 let _ = writeln!(
1743 out,
1744 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1745 );
1746 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1747 let _ = writeln!(out, "\t}}");
1748 for (key, val) in obj {
1749 let escaped_key = go_string_literal(key);
1750 let json_val = serde_json::to_string(val).unwrap_or_default();
1751 let escaped_val = go_string_literal(&json_val);
1752 let _ = writeln!(out, "\t{{");
1753 let _ = writeln!(out, "\t\tvar _wantVal any");
1754 let _ = writeln!(
1755 out,
1756 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1757 );
1758 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1759 let _ = writeln!(out, "\t\t}}");
1760 let _ = writeln!(
1761 out,
1762 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1763 );
1764 let _ = writeln!(
1765 out,
1766 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1767 );
1768 let _ = writeln!(out, "\t\t}}");
1769 let _ = writeln!(out, "\t}}");
1770 }
1771 }
1772 }
1773
1774 fn render_assert_validation_errors(
1779 &self,
1780 out: &mut String,
1781 _response_var: &str,
1782 errors: &[ValidationErrorExpectation],
1783 ) {
1784 let _ = writeln!(out, "\tvar _veBody map[string]any");
1785 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1786 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1787 let _ = writeln!(out, "\t}}");
1788 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1789 for ve in errors {
1790 let escaped_msg = go_string_literal(&ve.msg);
1791 let _ = writeln!(out, "\t{{");
1792 let _ = writeln!(out, "\t\t_found := false");
1793 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1794 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1795 let _ = writeln!(
1796 out,
1797 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1798 );
1799 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1800 let _ = writeln!(out, "\t\t\t\t\tbreak");
1801 let _ = writeln!(out, "\t\t\t\t}}");
1802 let _ = writeln!(out, "\t\t\t}}");
1803 let _ = writeln!(out, "\t\t}}");
1804 let _ = writeln!(out, "\t\tif !_found {{");
1805 let _ = writeln!(
1806 out,
1807 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1808 );
1809 let _ = writeln!(out, "\t\t}}");
1810 let _ = writeln!(out, "\t}}");
1811 }
1812 }
1813}
1814
1815fn build_args_and_setup(
1823 input: &serde_json::Value,
1824 args: &[crate::config::ArgMapping],
1825 import_alias: &str,
1826 options_type: Option<&str>,
1827 fixture: &crate::fixture::Fixture,
1828 options_ptr: bool,
1829 expects_error: bool,
1830) -> (Vec<String>, String) {
1831 let fixture_id = &fixture.id;
1832 use heck::ToUpperCamelCase;
1833
1834 if args.is_empty() {
1835 return (Vec::new(), String::new());
1836 }
1837
1838 let mut setup_lines: Vec<String> = Vec::new();
1839 let mut parts: Vec<String> = Vec::new();
1840
1841 for arg in args {
1842 if arg.arg_type == "mock_url" {
1843 if fixture.has_host_root_route() {
1844 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1845 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1846 setup_lines.push(format!(
1847 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1848 arg.name, arg.name
1849 ));
1850 } else {
1851 setup_lines.push(format!(
1852 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1853 arg.name,
1854 ));
1855 }
1856 parts.push(arg.name.clone());
1857 continue;
1858 }
1859
1860 if arg.arg_type == "mock_url_list" {
1861 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1866 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1867 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1868
1869 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1870 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1871 } else {
1872 Vec::new()
1873 };
1874
1875 let paths_literal = paths.join(", ");
1876 let var_name = &arg.name;
1877
1878 setup_lines.push(format!(
1879 "{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}}"
1880 ));
1881 setup_lines.push(format!(
1882 "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}}"
1883 ));
1884 parts.push(var_name.to_string());
1885 continue;
1886 }
1887
1888 if arg.arg_type == "handle" {
1889 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1891 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1892 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1893 let create_err_handler = if expects_error {
1897 "assert.Error(t, createErr)\n\t\treturn".to_string()
1898 } else {
1899 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1900 };
1901 if config_value.is_null()
1902 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1903 {
1904 setup_lines.push(format!(
1905 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1906 name = arg.name,
1907 ));
1908 } else {
1909 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1910 let go_literal = go_string_literal(&json_str);
1911 let name = &arg.name;
1912 setup_lines.push(format!(
1913 "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}}"
1914 ));
1915 setup_lines.push(format!(
1916 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1917 ));
1918 }
1919 parts.push(arg.name.clone());
1920 continue;
1921 }
1922
1923 let val: Option<&serde_json::Value> = if arg.field == "input" {
1924 Some(input)
1925 } else {
1926 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1927 input.get(field)
1928 };
1929
1930 if arg.arg_type == "bytes" {
1937 let var_name = format!("{}Bytes", arg.name);
1938 match val {
1939 None | Some(serde_json::Value::Null) => {
1940 if arg.optional {
1941 parts.push("nil".to_string());
1942 } else {
1943 parts.push("[]byte{}".to_string());
1944 }
1945 }
1946 Some(serde_json::Value::String(s)) => {
1947 let go_path = go_string_literal(s);
1952 setup_lines.push(format!(
1953 "{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}}"
1954 ));
1955 parts.push(var_name);
1956 }
1957 Some(other) => {
1958 parts.push(format!("[]byte({})", json_to_go(other)));
1959 }
1960 }
1961 continue;
1962 }
1963
1964 match val {
1965 None | Some(serde_json::Value::Null) if arg.optional => {
1966 match arg.arg_type.as_str() {
1968 "string" => {
1969 parts.push("nil".to_string());
1971 }
1972 "json_object" => {
1973 if options_ptr {
1974 parts.push("nil".to_string());
1976 } else if let Some(opts_type) = options_type {
1977 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1979 } else {
1980 parts.push("nil".to_string());
1981 }
1982 }
1983 _ => {
1984 parts.push("nil".to_string());
1985 }
1986 }
1987 }
1988 None | Some(serde_json::Value::Null) => {
1989 let default_val = match arg.arg_type.as_str() {
1991 "string" => "\"\"".to_string(),
1992 "int" | "integer" | "i64" => "0".to_string(),
1993 "float" | "number" => "0.0".to_string(),
1994 "bool" | "boolean" => "false".to_string(),
1995 "json_object" => {
1996 if options_ptr {
1997 "nil".to_string()
1999 } else if let Some(opts_type) = options_type {
2000 format!("{import_alias}.{opts_type}{{}}")
2001 } else {
2002 "nil".to_string()
2003 }
2004 }
2005 _ => "nil".to_string(),
2006 };
2007 parts.push(default_val);
2008 }
2009 Some(v) => {
2010 match arg.arg_type.as_str() {
2011 "json_object" => {
2012 let is_array = v.is_array();
2015 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2016 if is_empty_obj {
2017 if options_ptr {
2018 parts.push("nil".to_string());
2020 } else if let Some(opts_type) = options_type {
2021 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2022 } else {
2023 parts.push("nil".to_string());
2024 }
2025 } else if is_array {
2026 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2031 if go_t.starts_with('[') {
2035 go_t.to_string()
2036 } else {
2037 let qualified = if go_t.contains('.') {
2039 go_t.to_string()
2040 } else {
2041 format!("{import_alias}.{go_t}")
2042 };
2043 format!("[]{qualified}")
2044 }
2045 } else {
2046 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2047 };
2048 let converted_v = convert_json_for_go(v.clone());
2050 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2051 let go_literal = go_string_literal(&json_str);
2052 let var_name = &arg.name;
2053 setup_lines.push(format!(
2054 "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}}"
2055 ));
2056 parts.push(var_name.to_string());
2057 } else if let Some(opts_type) = options_type {
2058 let remapped_v = if options_ptr {
2063 convert_json_for_go(v.clone())
2064 } else {
2065 v.clone()
2066 };
2067 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2068 let go_literal = go_string_literal(&json_str);
2069 let var_name = &arg.name;
2070 setup_lines.push(format!(
2071 "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}}"
2072 ));
2073 let arg_expr = if options_ptr {
2075 format!("&{var_name}")
2076 } else {
2077 var_name.to_string()
2078 };
2079 parts.push(arg_expr);
2080 } else {
2081 parts.push(json_to_go(v));
2082 }
2083 }
2084 "string" if arg.optional => {
2085 let var_name = format!("{}Val", arg.name);
2087 let go_val = json_to_go(v);
2088 setup_lines.push(format!("{var_name} := {go_val}"));
2089 parts.push(format!("&{var_name}"));
2090 }
2091 _ => {
2092 parts.push(json_to_go(v));
2093 }
2094 }
2095 }
2096 }
2097 }
2098
2099 (setup_lines, parts.join(", "))
2100}
2101
2102#[allow(clippy::too_many_arguments)]
2103fn render_assertion(
2104 out: &mut String,
2105 assertion: &Assertion,
2106 result_var: &str,
2107 import_alias: &str,
2108 field_resolver: &FieldResolver,
2109 optional_locals: &std::collections::HashMap<String, String>,
2110 result_is_simple: bool,
2111 result_is_array: bool,
2112 is_streaming: bool,
2113) {
2114 if !result_is_simple {
2117 if let Some(f) = &assertion.field {
2118 let embed_deref = format!("(*{result_var})");
2121 match f.as_str() {
2122 "chunks_have_content" => {
2123 let pred = format!(
2124 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2125 );
2126 match assertion.assertion_type.as_str() {
2127 "is_true" => {
2128 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2129 }
2130 "is_false" => {
2131 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2132 }
2133 _ => {
2134 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2135 }
2136 }
2137 return;
2138 }
2139 "chunks_have_embeddings" => {
2140 let pred = format!(
2141 "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 }}()"
2142 );
2143 match assertion.assertion_type.as_str() {
2144 "is_true" => {
2145 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2146 }
2147 "is_false" => {
2148 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2149 }
2150 _ => {
2151 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2152 }
2153 }
2154 return;
2155 }
2156 "chunks_have_heading_context" => {
2157 let pred = format!(
2158 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2159 );
2160 match assertion.assertion_type.as_str() {
2161 "is_true" => {
2162 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2163 }
2164 "is_false" => {
2165 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2166 }
2167 _ => {
2168 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2169 }
2170 }
2171 return;
2172 }
2173 "first_chunk_starts_with_heading" => {
2174 let pred = format!(
2175 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2176 );
2177 match assertion.assertion_type.as_str() {
2178 "is_true" => {
2179 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2180 }
2181 "is_false" => {
2182 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2183 }
2184 _ => {
2185 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2186 }
2187 }
2188 return;
2189 }
2190 "embeddings" => {
2191 match assertion.assertion_type.as_str() {
2192 "count_equals" => {
2193 if let Some(val) = &assertion.value {
2194 if let Some(n) = val.as_u64() {
2195 let _ = writeln!(
2196 out,
2197 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2198 );
2199 }
2200 }
2201 }
2202 "count_min" => {
2203 if let Some(val) = &assertion.value {
2204 if let Some(n) = val.as_u64() {
2205 let _ = writeln!(
2206 out,
2207 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2208 );
2209 }
2210 }
2211 }
2212 "not_empty" => {
2213 let _ = writeln!(
2214 out,
2215 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2216 );
2217 }
2218 "is_empty" => {
2219 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2220 }
2221 _ => {
2222 let _ = writeln!(
2223 out,
2224 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2225 );
2226 }
2227 }
2228 return;
2229 }
2230 "embedding_dimensions" => {
2231 let expr = format!(
2232 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2233 );
2234 match assertion.assertion_type.as_str() {
2235 "equals" => {
2236 if let Some(val) = &assertion.value {
2237 if let Some(n) = val.as_u64() {
2238 let _ = writeln!(
2239 out,
2240 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2241 );
2242 }
2243 }
2244 }
2245 "greater_than" => {
2246 if let Some(val) = &assertion.value {
2247 if let Some(n) = val.as_u64() {
2248 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2249 }
2250 }
2251 }
2252 _ => {
2253 let _ = writeln!(
2254 out,
2255 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2256 );
2257 }
2258 }
2259 return;
2260 }
2261 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2262 let pred = match f.as_str() {
2263 "embeddings_valid" => {
2264 format!(
2265 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2266 )
2267 }
2268 "embeddings_finite" => {
2269 format!(
2270 "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 }}()"
2271 )
2272 }
2273 "embeddings_non_zero" => {
2274 format!(
2275 "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 }}()"
2276 )
2277 }
2278 "embeddings_normalized" => {
2279 format!(
2280 "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 }}()"
2281 )
2282 }
2283 _ => unreachable!(),
2284 };
2285 match assertion.assertion_type.as_str() {
2286 "is_true" => {
2287 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2288 }
2289 "is_false" => {
2290 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2291 }
2292 _ => {
2293 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2294 }
2295 }
2296 return;
2297 }
2298 "keywords" | "keywords_count" => {
2301 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2302 return;
2303 }
2304 _ => {}
2305 }
2306 }
2307 }
2308
2309 if !result_is_simple && is_streaming {
2316 if let Some(f) = &assertion.field {
2317 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2318 if let Some(expr) =
2319 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2320 {
2321 match assertion.assertion_type.as_str() {
2322 "count_min" => {
2323 if let Some(val) = &assertion.value {
2324 if let Some(n) = val.as_u64() {
2325 let _ = writeln!(
2326 out,
2327 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2328 );
2329 }
2330 }
2331 }
2332 "count_equals" => {
2333 if let Some(val) = &assertion.value {
2334 if let Some(n) = val.as_u64() {
2335 let _ = writeln!(
2336 out,
2337 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2338 );
2339 }
2340 }
2341 }
2342 "equals" => {
2343 if let Some(serde_json::Value::String(s)) = &assertion.value {
2344 let escaped = crate::escape::go_string_literal(s);
2345 let is_deep_path = f.contains('.') || f.contains('[');
2350 let safe_expr = if is_deep_path {
2351 format!(
2352 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2353 )
2354 } else {
2355 expr.clone()
2356 };
2357 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2358 } else if let Some(val) = &assertion.value {
2359 if let Some(n) = val.as_u64() {
2360 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2361 }
2362 }
2363 }
2364 "not_empty" => {
2365 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2366 }
2367 "is_empty" => {
2368 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2369 }
2370 "is_true" => {
2371 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2372 }
2373 "is_false" => {
2374 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2375 }
2376 "greater_than" => {
2377 if let Some(val) = &assertion.value {
2378 if let Some(n) = val.as_u64() {
2379 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2380 }
2381 }
2382 }
2383 "greater_than_or_equal" => {
2384 if let Some(val) = &assertion.value {
2385 if let Some(n) = val.as_u64() {
2386 let _ =
2387 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2388 }
2389 }
2390 }
2391 "contains" => {
2392 if let Some(serde_json::Value::String(s)) = &assertion.value {
2393 let escaped = crate::escape::go_string_literal(s);
2394 let _ =
2395 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2396 }
2397 }
2398 _ => {
2399 let _ = writeln!(
2400 out,
2401 "\t// streaming field '{f}': assertion type '{}' not rendered",
2402 assertion.assertion_type
2403 );
2404 }
2405 }
2406 }
2407 return;
2408 }
2409 }
2410 }
2411
2412 if !result_is_simple {
2415 if let Some(f) = &assertion.field {
2416 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2417 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2418 return;
2419 }
2420 }
2421 }
2422
2423 let field_expr = if result_is_simple {
2424 result_var.to_string()
2426 } else {
2427 match &assertion.field {
2428 Some(f) if !f.is_empty() => {
2429 if let Some(local_var) = optional_locals.get(f.as_str()) {
2431 local_var.clone()
2432 } else {
2433 field_resolver.accessor(f, "go", result_var)
2434 }
2435 }
2436 _ => result_var.to_string(),
2437 }
2438 };
2439
2440 let is_optional = assertion
2444 .field
2445 .as_ref()
2446 .map(|f| {
2447 let resolved = field_resolver.resolve(f);
2448 let check_path = resolved
2449 .strip_suffix(".length")
2450 .or_else(|| resolved.strip_suffix(".count"))
2451 .or_else(|| resolved.strip_suffix(".size"))
2452 .unwrap_or(resolved);
2453 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2454 })
2455 .unwrap_or(false);
2456
2457 let field_is_array_for_len = assertion
2461 .field
2462 .as_ref()
2463 .map(|f| {
2464 let resolved = field_resolver.resolve(f);
2465 let check_path = resolved
2466 .strip_suffix(".length")
2467 .or_else(|| resolved.strip_suffix(".count"))
2468 .or_else(|| resolved.strip_suffix(".size"))
2469 .unwrap_or(resolved);
2470 field_resolver.is_array(check_path)
2471 })
2472 .unwrap_or(false);
2473 let field_expr =
2474 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2475 let inner = &field_expr[4..field_expr.len() - 1];
2476 format!("len(*{inner})")
2477 } else {
2478 field_expr
2479 };
2480 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2482 Some(field_expr[5..field_expr.len() - 1].to_string())
2483 } else {
2484 None
2485 };
2486
2487 let field_is_slice = assertion
2491 .field
2492 .as_ref()
2493 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2494 .unwrap_or(false);
2495 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2496 format!("*{field_expr}")
2497 } else {
2498 field_expr.clone()
2499 };
2500
2501 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2506 let mut array_expr = field_expr[..idx].to_string();
2507 if let Some(stripped) = array_expr.strip_prefix("len(") {
2508 array_expr = stripped.to_string();
2509 }
2510 Some(array_expr)
2511 } else {
2512 None
2513 };
2514
2515 let mut assertion_buf = String::new();
2518 let out_ref = &mut assertion_buf;
2519
2520 match assertion.assertion_type.as_str() {
2521 "equals" => {
2522 if let Some(expected) = &assertion.value {
2523 let go_val = json_to_go(expected);
2524 if expected.is_string() {
2526 let resolved_name = assertion
2530 .field
2531 .as_ref()
2532 .map(|f| field_resolver.resolve(f))
2533 .unwrap_or_default();
2534 let is_struct = resolved_name.contains("FormatMetadata");
2535 let trimmed_field = if is_struct {
2536 if is_optional && !field_expr.starts_with("len(") {
2538 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2539 } else {
2540 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2541 }
2542 } else if is_optional && !field_expr.starts_with("len(") {
2543 format!("strings.TrimSpace(string(*{field_expr}))")
2544 } else {
2545 format!("strings.TrimSpace(string({field_expr}))")
2546 };
2547 if is_optional && !field_expr.starts_with("len(") {
2548 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2549 } else {
2550 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2551 }
2552 } else if is_optional && !field_expr.starts_with("len(") {
2553 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2554 } else {
2555 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2556 }
2557 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2558 let _ = writeln!(out_ref, "\t}}");
2559 }
2560 }
2561 "contains" => {
2562 if let Some(expected) = &assertion.value {
2563 let go_val = json_to_go(expected);
2564 let resolved_field = assertion.field.as_deref().unwrap_or("");
2570 let resolved_name = field_resolver.resolve(resolved_field);
2571 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2572 let is_opt =
2573 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2574 let field_for_contains = if is_opt && field_is_array {
2575 format!("jsonString({field_expr})")
2577 } else if is_opt {
2578 format!("fmt.Sprint(*{field_expr})")
2579 } else if field_is_array {
2580 format!("jsonString({field_expr})")
2581 } else {
2582 format!("fmt.Sprint({field_expr})")
2583 };
2584 if is_opt {
2585 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2586 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2587 let _ = writeln!(
2588 out_ref,
2589 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2590 );
2591 let _ = writeln!(out_ref, "\t}}");
2592 let _ = writeln!(out_ref, "\t}}");
2593 } else {
2594 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2595 let _ = writeln!(
2596 out_ref,
2597 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2598 );
2599 let _ = writeln!(out_ref, "\t}}");
2600 }
2601 }
2602 }
2603 "contains_all" => {
2604 if let Some(values) = &assertion.values {
2605 let resolved_field = assertion.field.as_deref().unwrap_or("");
2606 let resolved_name = field_resolver.resolve(resolved_field);
2607 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2608 let is_opt =
2609 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2610 for val in values {
2611 let go_val = json_to_go(val);
2612 let field_for_contains = if is_opt && field_is_array {
2613 format!("jsonString({field_expr})")
2615 } else if is_opt {
2616 format!("fmt.Sprint(*{field_expr})")
2617 } else if field_is_array {
2618 format!("jsonString({field_expr})")
2619 } else {
2620 format!("fmt.Sprint({field_expr})")
2621 };
2622 if is_opt {
2623 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2624 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2625 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2626 let _ = writeln!(out_ref, "\t}}");
2627 let _ = writeln!(out_ref, "\t}}");
2628 } else {
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 }
2633 }
2634 }
2635 }
2636 "not_contains" => {
2637 if let Some(expected) = &assertion.value {
2638 let go_val = json_to_go(expected);
2639 let resolved_field = assertion.field.as_deref().unwrap_or("");
2640 let resolved_name = field_resolver.resolve(resolved_field);
2641 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2642 let is_opt =
2643 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2644 let field_for_contains = if is_opt && field_is_array {
2645 format!("jsonString({field_expr})")
2647 } else if is_opt {
2648 format!("fmt.Sprint(*{field_expr})")
2649 } else if field_is_array {
2650 format!("jsonString({field_expr})")
2651 } else {
2652 format!("fmt.Sprint({field_expr})")
2653 };
2654 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2655 let _ = writeln!(
2656 out_ref,
2657 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2658 );
2659 let _ = writeln!(out_ref, "\t}}");
2660 }
2661 }
2662 "not_empty" => {
2663 let field_is_array = {
2666 let rf = assertion.field.as_deref().unwrap_or("");
2667 let rn = field_resolver.resolve(rf);
2668 field_resolver.is_array(rn)
2669 };
2670 if is_optional && !field_is_array {
2671 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2673 } else if is_optional && field_is_slice {
2674 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2676 } else if is_optional {
2677 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2679 } else if result_is_simple && result_is_array {
2680 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2682 } else {
2683 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2684 }
2685 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2686 let _ = writeln!(out_ref, "\t}}");
2687 }
2688 "is_empty" => {
2689 let field_is_array = {
2690 let rf = assertion.field.as_deref().unwrap_or("");
2691 let rn = field_resolver.resolve(rf);
2692 field_resolver.is_array(rn)
2693 };
2694 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2697 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2699 } else if is_optional && !field_is_array {
2700 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2702 } else if is_optional && field_is_slice {
2703 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2705 } else if is_optional {
2706 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2708 } else {
2709 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2710 }
2711 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2712 let _ = writeln!(out_ref, "\t}}");
2713 }
2714 "contains_any" => {
2715 if let Some(values) = &assertion.values {
2716 let resolved_field = assertion.field.as_deref().unwrap_or("");
2717 let resolved_name = field_resolver.resolve(resolved_field);
2718 let field_is_array = field_resolver.is_array(resolved_name);
2719 let is_opt =
2720 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2721 let field_for_contains = if is_opt && field_is_array {
2722 format!("jsonString({field_expr})")
2724 } else if is_opt {
2725 format!("fmt.Sprint(*{field_expr})")
2726 } else if field_is_array {
2727 format!("jsonString({field_expr})")
2728 } else {
2729 format!("fmt.Sprint({field_expr})")
2730 };
2731 let _ = writeln!(out_ref, "\t{{");
2732 let _ = writeln!(out_ref, "\t\tfound := false");
2733 for val in values {
2734 let go_val = json_to_go(val);
2735 let _ = writeln!(
2736 out_ref,
2737 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2738 );
2739 }
2740 let _ = writeln!(out_ref, "\t\tif !found {{");
2741 let _ = writeln!(
2742 out_ref,
2743 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2744 );
2745 let _ = writeln!(out_ref, "\t\t}}");
2746 let _ = writeln!(out_ref, "\t}}");
2747 }
2748 }
2749 "greater_than" => {
2750 if let Some(val) = &assertion.value {
2751 let go_val = json_to_go(val);
2752 if is_optional {
2756 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2757 if let Some(n) = val.as_u64() {
2758 let next = n + 1;
2759 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2760 } else {
2761 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2762 }
2763 let _ = writeln!(
2764 out_ref,
2765 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2766 );
2767 let _ = writeln!(out_ref, "\t\t}}");
2768 let _ = writeln!(out_ref, "\t}}");
2769 } else if let Some(n) = val.as_u64() {
2770 let next = n + 1;
2771 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2772 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2773 let _ = writeln!(out_ref, "\t}}");
2774 } else {
2775 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2776 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2777 let _ = writeln!(out_ref, "\t}}");
2778 }
2779 }
2780 }
2781 "less_than" => {
2782 if let Some(val) = &assertion.value {
2783 let go_val = json_to_go(val);
2784 if let Some(ref guard) = nil_guard_expr {
2785 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2786 let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2787 let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2788 let _ = writeln!(out_ref, "\t\t}}");
2789 let _ = writeln!(out_ref, "\t}}");
2790 } else if is_optional && !field_expr.starts_with("len(") {
2791 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2793 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2794 let _ = writeln!(
2795 out_ref,
2796 "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2797 );
2798 let _ = writeln!(out_ref, "\t\t}}");
2799 let _ = writeln!(out_ref, "\t}}");
2800 } else {
2801 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2802 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2803 let _ = writeln!(out_ref, "\t}}");
2804 }
2805 }
2806 }
2807 "greater_than_or_equal" => {
2808 if let Some(val) = &assertion.value {
2809 let go_val = json_to_go(val);
2810 if let Some(ref guard) = nil_guard_expr {
2811 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2812 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2813 let _ = writeln!(
2814 out_ref,
2815 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2816 );
2817 let _ = writeln!(out_ref, "\t\t}}");
2818 let _ = writeln!(out_ref, "\t}}");
2819 } else if is_optional && !field_expr.starts_with("len(") {
2820 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2822 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2823 let _ = writeln!(
2824 out_ref,
2825 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2826 );
2827 let _ = writeln!(out_ref, "\t\t}}");
2828 let _ = writeln!(out_ref, "\t}}");
2829 } else {
2830 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2831 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2832 let _ = writeln!(out_ref, "\t}}");
2833 }
2834 }
2835 }
2836 "less_than_or_equal" => {
2837 if let Some(val) = &assertion.value {
2838 let go_val = json_to_go(val);
2839 if is_optional && !field_expr.starts_with("len(") {
2840 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2842 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2843 let _ = writeln!(
2844 out_ref,
2845 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2846 );
2847 let _ = writeln!(out_ref, "\t\t}}");
2848 let _ = writeln!(out_ref, "\t}}");
2849 } else {
2850 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2851 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2852 let _ = writeln!(out_ref, "\t}}");
2853 }
2854 }
2855 }
2856 "starts_with" => {
2857 if let Some(expected) = &assertion.value {
2858 let go_val = json_to_go(expected);
2859 let field_for_prefix = if is_optional
2860 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2861 {
2862 format!("string(*{field_expr})")
2863 } else {
2864 format!("string({field_expr})")
2865 };
2866 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2867 let _ = writeln!(
2868 out_ref,
2869 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2870 );
2871 let _ = writeln!(out_ref, "\t}}");
2872 }
2873 }
2874 "count_min" => {
2875 if let Some(val) = &assertion.value {
2876 if let Some(n) = val.as_u64() {
2877 if is_optional {
2878 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2879 let len_expr = if field_is_slice {
2881 format!("len({field_expr})")
2882 } else {
2883 format!("len(*{field_expr})")
2884 };
2885 let _ = writeln!(
2886 out_ref,
2887 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2888 );
2889 let _ = writeln!(out_ref, "\t}}");
2890 } else {
2891 let _ = writeln!(
2892 out_ref,
2893 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2894 );
2895 }
2896 }
2897 }
2898 }
2899 "count_equals" => {
2900 if let Some(val) = &assertion.value {
2901 if let Some(n) = val.as_u64() {
2902 if is_optional {
2903 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2904 let len_expr = if field_is_slice {
2906 format!("len({field_expr})")
2907 } else {
2908 format!("len(*{field_expr})")
2909 };
2910 let _ = writeln!(
2911 out_ref,
2912 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2913 );
2914 let _ = writeln!(out_ref, "\t}}");
2915 } else {
2916 let _ = writeln!(
2917 out_ref,
2918 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2919 );
2920 }
2921 }
2922 }
2923 }
2924 "is_true" => {
2925 if is_optional {
2926 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2927 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2928 let _ = writeln!(out_ref, "\t}}");
2929 } else {
2930 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2931 }
2932 }
2933 "is_false" => {
2934 if is_optional {
2935 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2936 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2937 let _ = writeln!(out_ref, "\t}}");
2938 } else {
2939 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2940 }
2941 }
2942 "method_result" => {
2943 if let Some(method_name) = &assertion.method {
2944 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2945 let check = assertion.check.as_deref().unwrap_or("is_true");
2946 let deref_expr = if info.is_pointer {
2949 format!("*{}", info.call_expr)
2950 } else {
2951 info.call_expr.clone()
2952 };
2953 match check {
2954 "equals" => {
2955 if let Some(val) = &assertion.value {
2956 if val.is_boolean() {
2957 if val.as_bool() == Some(true) {
2958 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2959 } else {
2960 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2961 }
2962 } else {
2963 let go_val = if let Some(cast) = info.value_cast {
2967 if val.is_number() {
2968 format!("{cast}({})", json_to_go(val))
2969 } else {
2970 json_to_go(val)
2971 }
2972 } else {
2973 json_to_go(val)
2974 };
2975 let _ = writeln!(
2976 out_ref,
2977 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2978 );
2979 }
2980 }
2981 }
2982 "is_true" => {
2983 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2984 }
2985 "is_false" => {
2986 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2987 }
2988 "greater_than_or_equal" => {
2989 if let Some(val) = &assertion.value {
2990 let n = val.as_u64().unwrap_or(0);
2991 let cast = info.value_cast.unwrap_or("uint");
2993 let _ = writeln!(
2994 out_ref,
2995 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2996 );
2997 }
2998 }
2999 "count_min" => {
3000 if let Some(val) = &assertion.value {
3001 let n = val.as_u64().unwrap_or(0);
3002 let _ = writeln!(
3003 out_ref,
3004 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3005 );
3006 }
3007 }
3008 "contains" => {
3009 if let Some(val) = &assertion.value {
3010 let go_val = json_to_go(val);
3011 let _ = writeln!(
3012 out_ref,
3013 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3014 );
3015 }
3016 }
3017 "is_error" => {
3018 let _ = writeln!(out_ref, "\t{{");
3019 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3020 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3021 let _ = writeln!(out_ref, "\t}}");
3022 }
3023 other_check => {
3024 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3025 }
3026 }
3027 } else {
3028 panic!("Go e2e generator: method_result assertion missing 'method' field");
3029 }
3030 }
3031 "min_length" => {
3032 if let Some(val) = &assertion.value {
3033 if let Some(n) = val.as_u64() {
3034 if is_optional {
3035 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3036 let _ = writeln!(
3037 out_ref,
3038 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3039 );
3040 let _ = writeln!(out_ref, "\t}}");
3041 } else if field_expr.starts_with("len(") {
3042 let _ = writeln!(
3043 out_ref,
3044 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3045 );
3046 } else {
3047 let _ = writeln!(
3048 out_ref,
3049 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3050 );
3051 }
3052 }
3053 }
3054 }
3055 "max_length" => {
3056 if let Some(val) = &assertion.value {
3057 if let Some(n) = val.as_u64() {
3058 if is_optional {
3059 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3060 let _ = writeln!(
3061 out_ref,
3062 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3063 );
3064 let _ = writeln!(out_ref, "\t}}");
3065 } else if field_expr.starts_with("len(") {
3066 let _ = writeln!(
3067 out_ref,
3068 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3069 );
3070 } else {
3071 let _ = writeln!(
3072 out_ref,
3073 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3074 );
3075 }
3076 }
3077 }
3078 }
3079 "ends_with" => {
3080 if let Some(expected) = &assertion.value {
3081 let go_val = json_to_go(expected);
3082 let field_for_suffix = if is_optional
3083 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3084 {
3085 format!("string(*{field_expr})")
3086 } else {
3087 format!("string({field_expr})")
3088 };
3089 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3090 let _ = writeln!(
3091 out_ref,
3092 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3093 );
3094 let _ = writeln!(out_ref, "\t}}");
3095 }
3096 }
3097 "matches_regex" => {
3098 if let Some(expected) = &assertion.value {
3099 let go_val = json_to_go(expected);
3100 let field_for_regex = if is_optional
3101 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3102 {
3103 format!("*{field_expr}")
3104 } else {
3105 field_expr.clone()
3106 };
3107 let _ = writeln!(
3108 out_ref,
3109 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3110 );
3111 }
3112 }
3113 "not_error" => {
3114 }
3116 "error" => {
3117 }
3119 other => {
3120 panic!("Go e2e generator: unsupported assertion type: {other}");
3121 }
3122 }
3123
3124 if let Some(ref arr) = array_guard {
3127 if !assertion_buf.is_empty() {
3128 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3129 for line in assertion_buf.lines() {
3131 let _ = writeln!(out, "\t{line}");
3132 }
3133 let _ = writeln!(out, "\t}}");
3134 }
3135 } else {
3136 out.push_str(&assertion_buf);
3137 }
3138}
3139
3140struct GoMethodCallInfo {
3142 call_expr: String,
3144 is_pointer: bool,
3146 value_cast: Option<&'static str>,
3149}
3150
3151fn build_go_method_call(
3166 result_var: &str,
3167 method_name: &str,
3168 args: Option<&serde_json::Value>,
3169 import_alias: &str,
3170) -> GoMethodCallInfo {
3171 match method_name {
3172 "root_node_type" => GoMethodCallInfo {
3173 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3174 is_pointer: false,
3175 value_cast: None,
3176 },
3177 "named_children_count" => GoMethodCallInfo {
3178 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3179 is_pointer: false,
3180 value_cast: Some("uint"),
3181 },
3182 "has_error_nodes" => GoMethodCallInfo {
3183 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3184 is_pointer: true,
3185 value_cast: None,
3186 },
3187 "error_count" | "tree_error_count" => GoMethodCallInfo {
3188 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3189 is_pointer: true,
3190 value_cast: Some("uint"),
3191 },
3192 "tree_to_sexp" => GoMethodCallInfo {
3193 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3194 is_pointer: true,
3195 value_cast: None,
3196 },
3197 "contains_node_type" => {
3198 let node_type = args
3199 .and_then(|a| a.get("node_type"))
3200 .and_then(|v| v.as_str())
3201 .unwrap_or("");
3202 GoMethodCallInfo {
3203 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3204 is_pointer: true,
3205 value_cast: None,
3206 }
3207 }
3208 "find_nodes_by_type" => {
3209 let node_type = args
3210 .and_then(|a| a.get("node_type"))
3211 .and_then(|v| v.as_str())
3212 .unwrap_or("");
3213 GoMethodCallInfo {
3214 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3215 is_pointer: true,
3216 value_cast: None,
3217 }
3218 }
3219 "run_query" => {
3220 let query_source = args
3221 .and_then(|a| a.get("query_source"))
3222 .and_then(|v| v.as_str())
3223 .unwrap_or("");
3224 let language = args
3225 .and_then(|a| a.get("language"))
3226 .and_then(|v| v.as_str())
3227 .unwrap_or("");
3228 let query_lit = go_string_literal(query_source);
3229 let lang_lit = go_string_literal(language);
3230 GoMethodCallInfo {
3232 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3233 is_pointer: false,
3234 value_cast: None,
3235 }
3236 }
3237 other => {
3238 let method_pascal = other.to_upper_camel_case();
3239 GoMethodCallInfo {
3240 call_expr: format!("{result_var}.{method_pascal}()"),
3241 is_pointer: false,
3242 value_cast: None,
3243 }
3244 }
3245 }
3246}
3247
3248fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3258 match value {
3259 serde_json::Value::Object(map) => {
3260 let new_map: serde_json::Map<String, serde_json::Value> = map
3261 .into_iter()
3262 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3263 .collect();
3264 serde_json::Value::Object(new_map)
3265 }
3266 serde_json::Value::Array(arr) => {
3267 if is_byte_array(&arr) {
3270 let bytes: Vec<u8> = arr
3271 .iter()
3272 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3273 .collect();
3274 let encoded = base64_encode(&bytes);
3276 serde_json::Value::String(encoded)
3277 } else {
3278 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3279 }
3280 }
3281 serde_json::Value::String(s) => {
3282 serde_json::Value::String(pascal_to_snake_case(&s))
3285 }
3286 other => other,
3287 }
3288}
3289
3290fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3292 if arr.is_empty() {
3293 return false;
3294 }
3295 arr.iter().all(|v| {
3296 if let serde_json::Value::Number(n) = v {
3297 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3298 } else {
3299 false
3300 }
3301 })
3302}
3303
3304fn base64_encode(bytes: &[u8]) -> String {
3307 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3308 let mut result = String::new();
3309 let mut i = 0;
3310
3311 while i + 2 < bytes.len() {
3312 let b1 = bytes[i];
3313 let b2 = bytes[i + 1];
3314 let b3 = bytes[i + 2];
3315
3316 result.push(TABLE[(b1 >> 2) as usize] as char);
3317 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3318 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3319 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3320
3321 i += 3;
3322 }
3323
3324 if i < bytes.len() {
3326 let b1 = bytes[i];
3327 result.push(TABLE[(b1 >> 2) as usize] as char);
3328
3329 if i + 1 < bytes.len() {
3330 let b2 = bytes[i + 1];
3331 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3332 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3333 result.push('=');
3334 } else {
3335 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3336 result.push_str("==");
3337 }
3338 }
3339
3340 result
3341}
3342
3343fn camel_to_snake_case(s: &str) -> String {
3345 let mut result = String::new();
3346 let mut prev_upper = false;
3347 for (i, c) in s.char_indices() {
3348 if c.is_uppercase() {
3349 if i > 0 && !prev_upper {
3350 result.push('_');
3351 }
3352 result.push(c.to_lowercase().next().unwrap_or(c));
3353 prev_upper = true;
3354 } else {
3355 if prev_upper && i > 1 {
3356 }
3360 result.push(c);
3361 prev_upper = false;
3362 }
3363 }
3364 result
3365}
3366
3367fn pascal_to_snake_case(s: &str) -> String {
3372 let first_char = s.chars().next();
3374 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3375 return s.to_string();
3376 }
3377 camel_to_snake_case(s)
3378}
3379
3380fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3384 let elem = element_type.unwrap_or("String").trim();
3385 let go_elem = rust_type_to_go(elem, import_alias);
3386 format!("[]{go_elem}")
3387}
3388
3389fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3392 let trimmed = rust.trim();
3393 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3394 return format!("[]{}", rust_type_to_go(inner, import_alias));
3395 }
3396 match trimmed {
3397 "String" | "&str" | "str" => "string".to_string(),
3398 "bool" => "bool".to_string(),
3399 "f32" => "float32".to_string(),
3400 "f64" => "float64".to_string(),
3401 "i8" => "int8".to_string(),
3402 "i16" => "int16".to_string(),
3403 "i32" => "int32".to_string(),
3404 "i64" | "isize" => "int64".to_string(),
3405 "u8" => "uint8".to_string(),
3406 "u16" => "uint16".to_string(),
3407 "u32" => "uint32".to_string(),
3408 "u64" | "usize" => "uint64".to_string(),
3409 _ => format!("{import_alias}.{trimmed}"),
3410 }
3411}
3412
3413fn json_to_go(value: &serde_json::Value) -> String {
3414 match value {
3415 serde_json::Value::String(s) => go_string_literal(s),
3416 serde_json::Value::Bool(b) => b.to_string(),
3417 serde_json::Value::Number(n) => n.to_string(),
3418 serde_json::Value::Null => "nil".to_string(),
3419 other => go_string_literal(&other.to_string()),
3421 }
3422}
3423
3424fn visitor_struct_name(fixture_id: &str) -> String {
3433 use heck::ToUpperCamelCase;
3434 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3436}
3437
3438fn emit_go_visitor_struct(
3443 out: &mut String,
3444 struct_name: &str,
3445 visitor_spec: &crate::fixture::VisitorSpec,
3446 import_alias: &str,
3447) {
3448 let _ = writeln!(out, "type {struct_name} struct{{");
3449 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3450 let _ = writeln!(out, "}}");
3451 for (method_name, action) in &visitor_spec.callbacks {
3452 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3453 }
3454}
3455
3456fn emit_go_visitor_method(
3458 out: &mut String,
3459 struct_name: &str,
3460 method_name: &str,
3461 action: &CallbackAction,
3462 import_alias: &str,
3463) {
3464 let camel_method = method_to_camel(method_name);
3465 let params = match method_name {
3468 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3469 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3470 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3471 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3472 "visit_code_inline"
3473 | "visit_strong"
3474 | "visit_emphasis"
3475 | "visit_strikethrough"
3476 | "visit_underline"
3477 | "visit_subscript"
3478 | "visit_superscript"
3479 | "visit_mark"
3480 | "visit_button"
3481 | "visit_summary"
3482 | "visit_figcaption"
3483 | "visit_definition_term"
3484 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3485 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3486 "visit_list_item" => {
3487 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3488 }
3489 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3490 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3491 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3492 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3493 "visit_input" => {
3494 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3495 }
3496 "visit_audio" | "visit_video" | "visit_iframe" => {
3497 format!("_ {import_alias}.NodeContext, src *string")
3498 }
3499 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3500 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3501 format!("_ {import_alias}.NodeContext, output string")
3502 }
3503 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3504 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3505 _ => format!("_ {import_alias}.NodeContext"),
3506 };
3507
3508 let _ = writeln!(
3509 out,
3510 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3511 );
3512 match action {
3513 CallbackAction::Skip => {
3514 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3515 }
3516 CallbackAction::Continue => {
3517 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3518 }
3519 CallbackAction::PreserveHtml => {
3520 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3521 }
3522 CallbackAction::Custom { output } => {
3523 let escaped = go_string_literal(output);
3524 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3525 }
3526 CallbackAction::CustomTemplate { template, .. } => {
3527 let ptr_params = go_visitor_ptr_params(method_name);
3534 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3535 let escaped_fmt = go_string_literal(&fmt_str);
3536 if fmt_args.is_empty() {
3537 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3538 } else {
3539 let args_str = fmt_args.join(", ");
3540 let _ = writeln!(
3541 out,
3542 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3543 );
3544 }
3545 }
3546 }
3547 let _ = writeln!(out, "}}");
3548}
3549
3550fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3553 match method_name {
3554 "visit_link" => ["title"].into(),
3555 "visit_image" => ["title"].into(),
3556 "visit_heading" => ["id"].into(),
3557 "visit_code_block" => ["lang"].into(),
3558 "visit_form" => ["action", "method"].into(),
3559 "visit_input" => ["name", "value"].into(),
3560 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3561 _ => std::collections::HashSet::new(),
3562 }
3563}
3564
3565fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3577 let mut fmt_str = String::new();
3578 let mut args: Vec<String> = Vec::new();
3579 let mut chars = template.chars().peekable();
3580 while let Some(c) = chars.next() {
3581 if c == '{' {
3582 let mut name = String::new();
3584 for inner in chars.by_ref() {
3585 if inner == '}' {
3586 break;
3587 }
3588 name.push(inner);
3589 }
3590 fmt_str.push_str("%s");
3591 let go_name = go_param_name(&name);
3593 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3595 format!("*{go_name}")
3596 } else {
3597 go_name
3598 };
3599 args.push(arg_expr);
3600 } else {
3601 fmt_str.push(c);
3602 }
3603 }
3604 (fmt_str, args)
3605}
3606
3607fn method_to_camel(snake: &str) -> String {
3609 use heck::ToUpperCamelCase;
3610 snake.to_upper_camel_case()
3611}
3612
3613#[cfg(test)]
3614mod tests {
3615 use super::*;
3616 use crate::config::{CallConfig, E2eConfig};
3617 use crate::fixture::{Assertion, Fixture};
3618
3619 fn make_fixture(id: &str) -> Fixture {
3620 Fixture {
3621 id: id.to_string(),
3622 category: None,
3623 description: "test fixture".to_string(),
3624 tags: vec![],
3625 skip: None,
3626 env: None,
3627 call: None,
3628 input: serde_json::Value::Null,
3629 mock_response: Some(crate::fixture::MockResponse {
3630 status: 200,
3631 body: Some(serde_json::Value::Null),
3632 stream_chunks: None,
3633 headers: std::collections::HashMap::new(),
3634 }),
3635 source: String::new(),
3636 http: None,
3637 assertions: vec![Assertion {
3638 assertion_type: "not_error".to_string(),
3639 ..Default::default()
3640 }],
3641 visitor: None,
3642 }
3643 }
3644
3645 #[test]
3649 fn test_go_method_name_uses_go_casing() {
3650 let e2e_config = E2eConfig {
3651 call: CallConfig {
3652 function: "clean_extracted_text".to_string(),
3653 module: "github.com/example/mylib".to_string(),
3654 result_var: "result".to_string(),
3655 returns_result: true,
3656 ..CallConfig::default()
3657 },
3658 ..E2eConfig::default()
3659 };
3660
3661 let fixture = make_fixture("basic_text");
3662 let mut out = String::new();
3663 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config, &[]);
3664
3665 assert!(
3666 out.contains("kreuzberg.CleanExtractedText("),
3667 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3668 );
3669 assert!(
3670 !out.contains("kreuzberg.clean_extracted_text("),
3671 "must not emit raw snake_case method name, got:\n{out}"
3672 );
3673 }
3674
3675 #[test]
3676 fn test_streaming_fixture_emits_collect_snippet() {
3677 let streaming_fixture_json = r#"{
3679 "id": "basic_stream",
3680 "description": "basic streaming test",
3681 "call": "chat_stream",
3682 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3683 "mock_response": {
3684 "status": 200,
3685 "stream_chunks": [{"delta": "hello"}]
3686 },
3687 "assertions": [
3688 {"type": "count_min", "field": "chunks", "value": 1}
3689 ]
3690 }"#;
3691 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3692 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3693
3694 let e2e_config = E2eConfig {
3695 call: CallConfig {
3696 function: "chat_stream".to_string(),
3697 module: "github.com/example/mylib".to_string(),
3698 result_var: "result".to_string(),
3699 returns_result: true,
3700 r#async: true,
3701 ..CallConfig::default()
3702 },
3703 ..E2eConfig::default()
3704 };
3705
3706 let mut out = String::new();
3707 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3708
3709 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3710 assert!(
3711 out.contains("for chunk := range stream"),
3712 "should emit collect loop, got:\n{out}"
3713 );
3714 }
3715
3716 #[test]
3717 fn test_streaming_with_client_factory_and_json_arg() {
3718 use alef_core::config::e2e::{ArgMapping, CallOverride};
3722 let streaming_fixture_json = r#"{
3723 "id": "basic_stream_client",
3724 "description": "basic streaming test with client",
3725 "call": "chat_stream",
3726 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3727 "mock_response": {
3728 "status": 200,
3729 "stream_chunks": [{"delta": "hello"}]
3730 },
3731 "assertions": [
3732 {"type": "count_min", "field": "chunks", "value": 1}
3733 ]
3734 }"#;
3735 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3736 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3737
3738 let go_override = CallOverride {
3739 client_factory: Some("CreateClient".to_string()),
3740 ..Default::default()
3741 };
3742
3743 let mut call_overrides = std::collections::HashMap::new();
3744 call_overrides.insert("go".to_string(), go_override);
3745
3746 let e2e_config = E2eConfig {
3747 call: CallConfig {
3748 function: "chat_stream".to_string(),
3749 module: "github.com/example/mylib".to_string(),
3750 result_var: "result".to_string(),
3751 returns_result: false, r#async: true,
3753 args: vec![ArgMapping {
3754 name: "request".to_string(),
3755 field: "input".to_string(),
3756 arg_type: "json_object".to_string(),
3757 optional: false,
3758 owned: true,
3759 element_type: None,
3760 go_type: None,
3761 }],
3762 overrides: call_overrides,
3763 ..CallConfig::default()
3764 },
3765 ..E2eConfig::default()
3766 };
3767
3768 let mut out = String::new();
3769 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3770
3771 eprintln!("generated:\n{out}");
3772 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3773 assert!(
3774 out.contains("for chunk := range stream"),
3775 "should emit collect loop, got:\n{out}"
3776 );
3777 }
3778
3779 #[test]
3783 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3784 let mut optional_fields = std::collections::HashSet::new();
3785 optional_fields.insert("segments".to_string());
3786 let mut array_fields = std::collections::HashSet::new();
3787 array_fields.insert("segments".to_string());
3788
3789 let e2e_config = E2eConfig {
3790 call: CallConfig {
3791 function: "transcribe".to_string(),
3792 module: "github.com/example/mylib".to_string(),
3793 result_var: "result".to_string(),
3794 returns_result: true,
3795 ..CallConfig::default()
3796 },
3797 fields_optional: optional_fields,
3798 fields_array: array_fields,
3799 ..E2eConfig::default()
3800 };
3801
3802 let fixture = Fixture {
3803 id: "edge_transcribe_with_timestamps".to_string(),
3804 category: None,
3805 description: "Transcription with timestamp segments".to_string(),
3806 tags: vec![],
3807 skip: None,
3808 env: None,
3809 call: None,
3810 input: serde_json::Value::Null,
3811 mock_response: Some(crate::fixture::MockResponse {
3812 status: 200,
3813 body: Some(serde_json::Value::Null),
3814 stream_chunks: None,
3815 headers: std::collections::HashMap::new(),
3816 }),
3817 source: String::new(),
3818 http: None,
3819 assertions: vec![
3820 Assertion {
3821 assertion_type: "not_error".to_string(),
3822 ..Default::default()
3823 },
3824 Assertion {
3825 assertion_type: "equals".to_string(),
3826 field: Some("segments[0].id".to_string()),
3827 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3828 ..Default::default()
3829 },
3830 ],
3831 visitor: None,
3832 };
3833
3834 let mut out = String::new();
3835 render_test_function(&mut out, &fixture, "pkg", &e2e_config, &[]);
3836
3837 eprintln!("generated:\n{out}");
3838
3839 assert!(
3844 out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3845 "guard must be on Segments (the slice), not an element; got:\n{out}"
3846 );
3847 assert!(
3849 !out.contains("result.Segments[0] != nil"),
3850 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3851 );
3852 }
3853
3854 #[test]
3860 fn test_result_is_simple_contains_binds_result_and_emits_imports() {
3861 use alef_core::config::e2e::ArgMapping;
3862
3863 let e2e_config = E2eConfig {
3864 call: CallConfig {
3865 function: "detect_mime_type_from_bytes".to_string(),
3866 module: "github.com/example/mylib".to_string(),
3867 result_var: "result".to_string(),
3868 returns_result: true,
3869 result_is_simple: true,
3870 args: vec![ArgMapping {
3871 name: "content".to_string(),
3872 field: "input.data".to_string(),
3873 arg_type: "bytes".to_string(),
3874 optional: false,
3875 owned: false,
3876 element_type: None,
3877 go_type: None,
3878 }],
3879 ..CallConfig::default()
3880 },
3881 ..E2eConfig::default()
3882 };
3883
3884 let fixture = Fixture {
3885 id: "mime_detect_bytes".to_string(),
3886 category: None,
3887 description: "Detect MIME type from file bytes".to_string(),
3888 tags: vec![],
3889 skip: None,
3890 env: None,
3891 call: None,
3892 input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
3893 mock_response: None,
3894 source: String::new(),
3895 http: None,
3896 assertions: vec![Assertion {
3897 assertion_type: "contains".to_string(),
3898 field: Some("result".to_string()),
3899 value: Some(serde_json::Value::String("pdf".to_string())),
3900 ..Default::default()
3901 }],
3902 visitor: None,
3903 };
3904
3905 let out = render_test_file(
3906 "mime_utilities",
3907 &[&fixture],
3908 "github.com/example/mylib",
3909 "kreuzberg",
3910 &e2e_config,
3911 &[],
3912 );
3913
3914 assert!(
3915 out.contains("result, err := kreuzberg.DetectMimeTypeFromBytes("),
3916 "expected the call to bind to `result`, not `_`; got:\n{out}"
3917 );
3918 assert!(
3919 out.contains("strings.Contains(") && out.contains("fmt.Sprint("),
3920 "expected `strings.Contains(fmt.Sprint(...))` rendering; got:\n{out}"
3921 );
3922 assert!(
3923 out.contains("\t\"fmt\""),
3924 "expected the `fmt` import to be emitted; got:\n{out}"
3925 );
3926 assert!(
3927 out.contains("\t\"strings\""),
3928 "expected the `strings` import to be emitted; got:\n{out}"
3929 );
3930 }
3931}