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 data_enum_names: std::collections::HashSet<&str> = enums
40 .iter()
41 .filter(|e| {
42 e.variants
43 .iter()
44 .any(|v| !v.fields.is_empty() && v.fields.iter().any(|f| !f.name.is_empty()))
45 })
46 .map(|e| e.name.as_str())
47 .collect();
48
49 let call = &e2e_config.call;
51 let overrides = call.overrides.get(lang);
52 let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
53 let module_path = overrides
54 .and_then(|o| o.module.as_ref())
55 .cloned()
56 .or_else(|| configured_go_module_path.clone())
57 .unwrap_or_else(|| call.module.clone());
58 let import_alias = overrides
59 .and_then(|o| o.alias.as_ref())
60 .cloned()
61 .unwrap_or_else(|| "pkg".to_string());
62
63 let go_pkg = e2e_config.resolve_package("go");
65 let go_module_path = go_pkg
66 .as_ref()
67 .and_then(|p| p.module.as_ref())
68 .cloned()
69 .or_else(|| configured_go_module_path.clone())
70 .unwrap_or_else(|| module_path.clone());
71 let replace_path = go_pkg
72 .as_ref()
73 .and_then(|p| p.path.as_ref())
74 .cloned()
75 .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
76 let go_version = go_pkg
77 .as_ref()
78 .and_then(|p| p.version.as_ref())
79 .cloned()
80 .unwrap_or_else(|| {
81 config
82 .resolved_version()
83 .map(|v| format!("v{v}"))
84 .unwrap_or_else(|| "v0.0.0".to_string())
85 });
86 let effective_replace = match e2e_config.dep_mode {
89 crate::config::DependencyMode::Registry => None,
90 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
91 };
92 let effective_go_version = if effective_replace.is_some() {
98 fix_go_major_version(&go_module_path, &go_version)
99 } else {
100 go_version.clone()
101 };
102 files.push(GeneratedFile {
103 path: output_base.join("go.mod"),
104 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
105 generated_header: false,
106 });
107
108 let emits_executable_test =
110 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
111 let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
112 emits_executable_test(f)
113 && f.assertions.iter().any(|a| {
114 matches!(
115 a.assertion_type.as_str(),
116 "contains" | "contains_all" | "contains_any" | "not_contains"
117 ) && {
118 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
119 e2e_config
120 .resolve_call_for_fixture(
121 f.call.as_deref(),
122 &f.id,
123 &f.resolved_category(),
124 &f.tags,
125 &f.input,
126 )
127 .result_is_array
128 } else {
129 let cc = e2e_config.resolve_call_for_fixture(
130 f.call.as_deref(),
131 &f.id,
132 &f.resolved_category(),
133 &f.tags,
134 &f.input,
135 );
136 let per_call_resolver = FieldResolver::new(
137 e2e_config.effective_fields(cc),
138 e2e_config.effective_fields_optional(cc),
139 e2e_config.effective_result_fields(cc),
140 e2e_config.effective_fields_array(cc),
141 &std::collections::HashSet::new(),
142 );
143 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
144 per_call_resolver.is_array(resolved_name)
145 }
146 }
147 })
148 });
149
150 if needs_json_stringify {
152 files.push(GeneratedFile {
153 path: output_base.join("helpers_test.go"),
154 content: render_helpers_test_go(),
155 generated_header: true,
156 });
157 }
158
159 let has_file_fixtures = groups
167 .iter()
168 .flat_map(|g| g.fixtures.iter())
169 .any(|f| f.http.is_none() && !f.needs_mock_server());
170
171 let needs_main_test = has_file_fixtures
172 || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
173 if f.needs_mock_server() {
174 return true;
175 }
176 let cc = e2e_config.resolve_call_for_fixture(
177 f.call.as_deref(),
178 &f.id,
179 &f.resolved_category(),
180 &f.tags,
181 &f.input,
182 );
183 let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
184 go_override.and_then(|o| o.client_factory.as_deref()).is_some()
185 });
186
187 if needs_main_test {
188 files.push(GeneratedFile {
189 path: output_base.join("main_test.go"),
190 content: render_main_test_go(&e2e_config.test_documents_dir),
191 generated_header: true,
192 });
193 }
194
195 for group in groups {
197 let active: Vec<&Fixture> = group
198 .fixtures
199 .iter()
200 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
201 .collect();
202
203 if active.is_empty() {
204 continue;
205 }
206
207 let filename = format!("{}_test.go", sanitize_filename(&group.category));
208 let content = render_test_file(
209 &group.category,
210 &active,
211 &module_path,
212 &import_alias,
213 e2e_config,
214 &config.adapters,
215 &data_enum_names,
216 );
217 files.push(GeneratedFile {
218 path: output_base.join(filename),
219 content,
220 generated_header: true,
221 });
222 }
223
224 Ok(files)
225 }
226
227 fn language_name(&self) -> &'static str {
228 "go"
229 }
230}
231
232fn fix_go_major_version(module_path: &str, version: &str) -> String {
239 let major = module_path
241 .rsplit('/')
242 .next()
243 .and_then(|seg| seg.strip_prefix('v'))
244 .and_then(|n| n.parse::<u64>().ok())
245 .filter(|&n| n >= 2);
246
247 let Some(n) = major else {
248 return version.to_string();
249 };
250
251 let expected_prefix = format!("v{n}.");
253 if version.starts_with(&expected_prefix) {
254 return version.to_string();
255 }
256
257 format!("v{n}.0.0")
258}
259
260fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
261 let mut out = String::new();
262 let _ = writeln!(out, "module e2e_go");
263 let _ = writeln!(out);
264 let _ = writeln!(out, "go 1.26");
265 let _ = writeln!(out);
266 let _ = writeln!(out, "require (");
267 let _ = writeln!(out, "\t{go_module_path} {version}");
268 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
269 let _ = writeln!(out, ")");
270
271 if let Some(path) = replace_path {
272 let _ = writeln!(out);
273 let _ = writeln!(out, "replace {go_module_path} => {path}");
274 }
275
276 out
277}
278
279fn render_main_test_go(test_documents_dir: &str) -> String {
285 let mut out = String::new();
287 let _ = writeln!(out, "package e2e_test");
288 let _ = writeln!(out);
289 let _ = writeln!(out, "import (");
290 let _ = writeln!(out, "\t\"bufio\"");
291 let _ = writeln!(out, "\t\"encoding/json\"");
292 let _ = writeln!(out, "\t\"io\"");
293 let _ = writeln!(out, "\t\"os\"");
294 let _ = writeln!(out, "\t\"os/exec\"");
295 let _ = writeln!(out, "\t\"path/filepath\"");
296 let _ = writeln!(out, "\t\"runtime\"");
297 let _ = writeln!(out, "\t\"strings\"");
298 let _ = writeln!(out, "\t\"testing\"");
299 let _ = writeln!(out, ")");
300 let _ = writeln!(out);
301 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
302 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
303 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
304 let _ = writeln!(out);
305 let _ = writeln!(
306 out,
307 "\t// Change to the configured test-documents directory (if it exists) so that fixture"
308 );
309 let _ = writeln!(
310 out,
311 "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
312 );
313 let _ = writeln!(
314 out,
315 "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
316 );
317 let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
318 let _ = writeln!(
319 out,
320 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
321 );
322 let _ = writeln!(
323 out,
324 "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
325 );
326 let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
327 let _ = writeln!(out, "\t\t\tpanic(err)");
328 let _ = writeln!(out, "\t\t}}");
329 let _ = writeln!(out, "\t}}");
330 let _ = writeln!(out);
331 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
332 let _ = writeln!(
333 out,
334 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
335 );
336 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
337 let _ = writeln!(
338 out,
339 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
340 );
341 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
342 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
343 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
344 let _ = writeln!(out, "\t\tif err != nil {{");
345 let _ = writeln!(out, "\t\t\tpanic(err)");
346 let _ = writeln!(out, "\t\t}}");
347 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
348 let _ = writeln!(
349 out,
350 "\t\t// server does not see EOF and exit immediately. The mock-server"
351 );
352 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
353 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
354 let _ = writeln!(out, "\t\tif err != nil {{");
355 let _ = writeln!(out, "\t\t\tpanic(err)");
356 let _ = writeln!(out, "\t\t}}");
357 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
358 let _ = writeln!(out, "\t\t\tpanic(err)");
359 let _ = writeln!(out, "\t\t}}");
360 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
361 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
362 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
363 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
364 let _ = writeln!(
365 out,
366 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
367 );
368 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
369 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
370 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
371 let _ = writeln!(
372 out,
373 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
374 );
375 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
376 let _ = writeln!(
377 out,
378 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
379 );
380 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
381 let _ = writeln!(
382 out,
383 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
384 );
385 let _ = writeln!(out, "\t\t\t\t\t}}");
386 let _ = writeln!(out, "\t\t\t\t}}");
387 let _ = writeln!(out, "\t\t\t\tbreak");
388 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
389 let _ = writeln!(out, "\t\t\t\tbreak");
390 let _ = writeln!(out, "\t\t\t}}");
391 let _ = writeln!(out, "\t\t}}");
392 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
393 let _ = writeln!(out, "\t\tcode := m.Run()");
394 let _ = writeln!(out, "\t\t_ = stdin.Close()");
395 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
396 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
397 let _ = writeln!(out, "\t\tos.Exit(code)");
398 let _ = writeln!(out, "\t}} else {{");
399 let _ = writeln!(out, "\t\tcode := m.Run()");
400 let _ = writeln!(out, "\t\tos.Exit(code)");
401 let _ = writeln!(out, "\t}}");
402 let _ = writeln!(out, "}}");
403 out
404}
405
406fn render_helpers_test_go() -> String {
409 let mut out = String::new();
410 let _ = writeln!(out, "package e2e_test");
411 let _ = writeln!(out);
412 let _ = writeln!(out, "import \"encoding/json\"");
413 let _ = writeln!(out);
414 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
415 let _ = writeln!(
416 out,
417 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
418 );
419 let _ = writeln!(out, "func jsonString(value any) string {{");
420 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
421 let _ = writeln!(out, "\tif err != nil {{");
422 let _ = writeln!(out, "\t\treturn \"\"");
423 let _ = writeln!(out, "\t}}");
424 let _ = writeln!(out, "\treturn string(encoded)");
425 let _ = writeln!(out, "}}");
426 out
427}
428
429fn render_test_file(
430 category: &str,
431 fixtures: &[&Fixture],
432 go_module_path: &str,
433 import_alias: &str,
434 e2e_config: &crate::config::E2eConfig,
435 adapters: &[alef_core::config::AdapterConfig],
436 data_enum_names: &std::collections::HashSet<&str>,
437) -> String {
438 let mut out = String::new();
439 let emits_executable_test =
440 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
441
442 out.push_str(&hash::header(CommentStyle::DoubleSlash));
444 let _ = writeln!(out);
445
446 let needs_pkg = fixtures
455 .iter()
456 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
457
458 let needs_os = fixtures.iter().any(|f| {
461 if f.is_http_test() {
462 return true;
463 }
464 if !emits_executable_test(f) {
465 return false;
466 }
467 let call_config =
468 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
469 let go_override = call_config
470 .overrides
471 .get("go")
472 .or_else(|| e2e_config.call.overrides.get("go"));
473 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
474 return true;
475 }
476 let call_args = &call_config.args;
477 if call_args
480 .iter()
481 .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
482 {
483 return true;
484 }
485 call_args.iter().any(|a| {
486 if a.arg_type != "bytes" {
487 return false;
488 }
489 let mut current = &f.input;
492 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
493 for segment in path.split('.') {
494 match current.get(segment) {
495 Some(next) => current = next,
496 None => return false,
497 }
498 }
499 current.is_string()
500 })
501 });
502
503 let needs_filepath = false;
506
507 let _needs_json_stringify = fixtures.iter().any(|f| {
508 emits_executable_test(f)
509 && f.assertions.iter().any(|a| {
510 matches!(
511 a.assertion_type.as_str(),
512 "contains" | "contains_all" | "contains_any" | "not_contains"
513 ) && {
514 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
517 e2e_config
519 .resolve_call_for_fixture(
520 f.call.as_deref(),
521 &f.id,
522 &f.resolved_category(),
523 &f.tags,
524 &f.input,
525 )
526 .result_is_array
527 } else {
528 let cc = e2e_config.resolve_call_for_fixture(
530 f.call.as_deref(),
531 &f.id,
532 &f.resolved_category(),
533 &f.tags,
534 &f.input,
535 );
536 let per_call_resolver = FieldResolver::new(
537 e2e_config.effective_fields(cc),
538 e2e_config.effective_fields_optional(cc),
539 e2e_config.effective_result_fields(cc),
540 e2e_config.effective_fields_array(cc),
541 &std::collections::HashSet::new(),
542 );
543 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
544 per_call_resolver.is_array(resolved_name)
545 }
546 }
547 })
548 });
549
550 let needs_json = fixtures.iter().any(|f| {
554 if let Some(http) = &f.http {
557 let body_needs_json = http
558 .expected_response
559 .body
560 .as_ref()
561 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
562 let partial_needs_json = http.expected_response.body_partial.is_some();
563 let ve_needs_json = http
564 .expected_response
565 .validation_errors
566 .as_ref()
567 .is_some_and(|v| !v.is_empty());
568 if body_needs_json || partial_needs_json || ve_needs_json {
569 return true;
570 }
571 }
572 if !emits_executable_test(f) {
573 return false;
574 }
575
576 let call =
577 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
578 let call_args = &call.args;
579 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
581 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
582 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
583 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
584 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
585 })
586 };
587 let go_override = call.overrides.get("go");
589 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
590 e2e_config
591 .call
592 .overrides
593 .get("go")
594 .and_then(|o| o.options_type.as_deref())
595 });
596 let has_json_obj = call_args.iter().any(|a| {
597 if a.arg_type != "json_object" {
598 return false;
599 }
600 let v = if a.field == "input" {
601 &f.input
602 } else {
603 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
604 f.input.get(field).unwrap_or(&serde_json::Value::Null)
605 };
606 if v.is_array() {
607 return true;
608 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
610 });
611 has_handle || has_json_obj
612 });
613
614 let needs_base64 = false;
619
620 let call_result_is_simple = |cc: &alef_core::config::e2e::CallConfig| -> bool {
626 cc.overrides.get("go").is_some_and(|o| o.result_is_simple)
627 || cc.result_is_simple
628 || cc.overrides.get("rust").map(|o| o.result_is_simple).unwrap_or(false)
629 };
630
631 let needs_fmt = fixtures.iter().any(|f| {
637 if f.visitor.as_ref().is_some_and(|v| {
639 v.callbacks.values().any(|action| {
640 if let CallbackAction::CustomTemplate { template, .. } = action {
641 template.contains('{')
642 } else {
643 false
644 }
645 })
646 }) {
647 return true;
648 }
649
650 if !emits_executable_test(f) {
651 return false;
652 }
653
654 if f.assertions.iter().any(|a| {
656 matches!(
657 a.assertion_type.as_str(),
658 "contains" | "contains_all" | "contains_any" | "not_contains"
659 ) && {
660 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
661 !e2e_config
663 .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
664 .result_is_array
665 } else {
666 let field = a.field.as_deref().unwrap_or("");
669 let cc = e2e_config.resolve_call_for_fixture(
670 f.call.as_deref(),
671 &f.id,
672 &f.resolved_category(),
673 &f.tags,
674 &f.input,
675 );
676 let per_call_resolver = FieldResolver::new(
677 e2e_config.effective_fields(cc),
678 e2e_config.effective_fields_optional(cc),
679 e2e_config.effective_result_fields(cc),
680 e2e_config.effective_fields_array(cc),
681 &std::collections::HashSet::new(),
682 );
683 let resolved_name = per_call_resolver.resolve(field);
684 !per_call_resolver.is_array(resolved_name)
687 && (call_result_is_simple(cc) || per_call_resolver.is_valid_for_result(field))
688 }
689 }
690 }) {
691 return true;
692 }
693
694 f.assertions.iter().any(|a| {
696 if let Some(field) = &a.field {
697 if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
698 let cc = e2e_config.resolve_call_for_fixture(
699 f.call.as_deref(),
700 &f.id,
701 &f.resolved_category(),
702 &f.tags,
703 &f.input,
704 );
705 let per_call_resolver = FieldResolver::new(
706 e2e_config.effective_fields(cc),
707 e2e_config.effective_fields_optional(cc),
708 e2e_config.effective_result_fields(cc),
709 e2e_config.effective_fields_array(cc),
710 &std::collections::HashSet::new(),
711 );
712 let resolved = per_call_resolver.resolve(field);
713 per_call_resolver.is_optional(resolved)
715 && !per_call_resolver.is_array(resolved)
716 && !per_call_resolver.has_map_access(field)
717 && per_call_resolver.is_valid_for_result(field)
718 } else {
719 false
720 }
721 } else {
722 false
723 }
724 })
725 });
726
727 let needs_strings = fixtures.iter().any(|f| {
731 if !emits_executable_test(f) {
732 return false;
733 }
734 let cc =
736 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
737 if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
738 return true;
739 }
740 let per_call_resolver = FieldResolver::new(
741 e2e_config.effective_fields(cc),
742 e2e_config.effective_fields_optional(cc),
743 e2e_config.effective_result_fields(cc),
744 e2e_config.effective_fields_array(cc),
745 &std::collections::HashSet::new(),
746 );
747 f.assertions.iter().any(|a| {
748 let type_needs_strings = if a.assertion_type == "equals" {
749 a.value.as_ref().is_some_and(|v| v.is_string())
751 } else {
752 matches!(
753 a.assertion_type.as_str(),
754 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
755 )
756 };
757 let simple_result = call_result_is_simple(cc);
760 let field_valid = a
761 .field
762 .as_ref()
763 .map(|f| f.is_empty() || simple_result || per_call_resolver.is_valid_for_result(f))
764 .unwrap_or(true);
765 type_needs_strings && field_valid
766 })
767 });
768
769 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
778 let needs_http = has_http_fixtures;
779 let needs_io = has_http_fixtures;
781
782 let needs_reflect = fixtures.iter().any(|f| {
785 if let Some(http) = &f.http {
786 let body_needs_reflect = http
787 .expected_response
788 .body
789 .as_ref()
790 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
791 let partial_needs_reflect = http.expected_response.body_partial.is_some();
792 body_needs_reflect || partial_needs_reflect
793 } else {
794 false
795 }
796 });
797
798 let mut body = String::new();
803 for fixture in fixtures.iter() {
804 if let Some(visitor_spec) = &fixture.visitor {
805 let struct_name = visitor_struct_name(&fixture.id);
806 emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
807 let _ = writeln!(body);
808 }
809 }
810 for (i, fixture) in fixtures.iter().enumerate() {
811 render_test_function(&mut body, fixture, import_alias, e2e_config, adapters, data_enum_names);
812 if i + 1 < fixtures.len() {
813 let _ = writeln!(body);
814 }
815 }
816 let needs_assert = body.contains("assert.");
817
818 let _ = writeln!(out, "// E2e tests for category: {category}");
819 let _ = writeln!(out, "package e2e_test");
820 let _ = writeln!(out);
821 let _ = writeln!(out, "import (");
822 if needs_base64 {
823 let _ = writeln!(out, "\t\"encoding/base64\"");
824 }
825 if needs_json || needs_reflect {
826 let _ = writeln!(out, "\t\"encoding/json\"");
827 }
828 if needs_fmt {
829 let _ = writeln!(out, "\t\"fmt\"");
830 }
831 if needs_io {
832 let _ = writeln!(out, "\t\"io\"");
833 }
834 if needs_http {
835 let _ = writeln!(out, "\t\"net/http\"");
836 }
837 if needs_os {
838 let _ = writeln!(out, "\t\"os\"");
839 }
840 let _ = needs_filepath; if needs_reflect {
842 let _ = writeln!(out, "\t\"reflect\"");
843 }
844 if needs_strings {
845 let _ = writeln!(out, "\t\"strings\"");
846 }
847 let _ = writeln!(out, "\t\"testing\"");
848 if needs_assert {
849 let _ = writeln!(out);
850 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
851 }
852
853 if needs_pkg {
854 let _ = writeln!(out);
855 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
856 }
857 let _ = writeln!(out, ")");
858 let _ = writeln!(out);
859
860 out.push_str(&body);
862
863 while out.ends_with("\n\n") {
865 out.pop();
866 }
867 if !out.ends_with('\n') {
868 out.push('\n');
869 }
870 out
871}
872
873fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
882 if fixture.is_http_test() {
884 return false;
885 }
886 let call_config = e2e_config.resolve_call_for_fixture(
887 fixture.call.as_deref(),
888 &fixture.id,
889 &fixture.resolved_category(),
890 &fixture.tags,
891 &fixture.input,
892 );
893 if call_config.skip_languages.iter().any(|l| l == "go") {
896 return false;
897 }
898 let go_override = call_config
899 .overrides
900 .get("go")
901 .or_else(|| e2e_config.call.overrides.get("go"));
902 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
905 return true;
906 }
907 let fn_name = go_override
911 .and_then(|o| o.function.as_deref())
912 .filter(|s| !s.is_empty())
913 .unwrap_or(call_config.function.as_str());
914 !fn_name.is_empty()
915}
916
917fn render_test_function(
918 out: &mut String,
919 fixture: &Fixture,
920 import_alias: &str,
921 e2e_config: &crate::config::E2eConfig,
922 adapters: &[alef_core::config::AdapterConfig],
923 data_enum_names: &std::collections::HashSet<&str>,
924) {
925 let fn_name = fixture.id.to_upper_camel_case();
926 let description = &fixture.description;
927
928 if fixture.http.is_some() {
930 render_http_test_function(out, fixture);
931 return;
932 }
933
934 if !fixture_has_go_callable(fixture, e2e_config) {
939 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
940 let _ = writeln!(out, "\t// {description}");
941 let _ = writeln!(
942 out,
943 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
944 );
945 let _ = writeln!(out, "}}");
946 return;
947 }
948
949 let call_config = e2e_config.resolve_call_for_fixture(
951 fixture.call.as_deref(),
952 &fixture.id,
953 &fixture.resolved_category(),
954 &fixture.tags,
955 &fixture.input,
956 );
957 let call_field_resolver = FieldResolver::new(
959 e2e_config.effective_fields(call_config),
960 e2e_config.effective_fields_optional(call_config),
961 e2e_config.effective_result_fields(call_config),
962 e2e_config.effective_fields_array(call_config),
963 &std::collections::HashSet::new(),
964 );
965 let field_resolver = &call_field_resolver;
966 let lang = "go";
967 let overrides = call_config.overrides.get(lang);
968
969 let base_function_name = overrides
973 .and_then(|o| o.function.as_deref())
974 .unwrap_or(&call_config.function);
975 let function_name = to_go_name(base_function_name);
976 let result_var = &call_config.result_var;
977 let args = &call_config.args;
978
979 let returns_result = overrides
982 .and_then(|o| o.returns_result)
983 .unwrap_or(call_config.returns_result);
984
985 let returns_void = call_config.returns_void;
988
989 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
995 || call_config.result_is_simple
996 || call_config
997 .overrides
998 .get("rust")
999 .map(|o| o.result_is_simple)
1000 .unwrap_or(false);
1001
1002 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
1008
1009 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
1011 e2e_config
1012 .call
1013 .overrides
1014 .get("go")
1015 .and_then(|o| o.options_type.as_deref())
1016 });
1017
1018 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1020 e2e_config
1021 .call
1022 .overrides
1023 .get("go")
1024 .map(|o| o.options_ptr)
1025 .unwrap_or(false)
1026 });
1027
1028 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1029 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1033
1034 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1037 e2e_config
1038 .call
1039 .overrides
1040 .get(lang)
1041 .and_then(|o| o.client_factory.as_deref())
1042 });
1043
1044 let (mut setup_lines, args_str) = build_args_and_setup(
1045 &fixture.input,
1046 args,
1047 import_alias,
1048 call_options_type,
1049 fixture,
1050 call_options_ptr,
1051 validation_creation_failure,
1052 data_enum_names,
1053 );
1054
1055 let mut visitor_opts_var: Option<String> = None;
1058 if fixture.visitor.is_some() {
1059 let struct_name = visitor_struct_name(&fixture.id);
1060 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1061 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1063 let opts_var = "opts".to_string();
1064 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1065 setup_lines.push("opts.Visitor = visitor".to_string());
1066 visitor_opts_var = Some(opts_var);
1067 }
1068
1069 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1070 let final_args = {
1071 let mut parts: Vec<String> = Vec::new();
1072 if !args_str.is_empty() {
1073 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1075 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1076 } else {
1077 args_str
1078 };
1079 parts.push(processed_args);
1080 }
1081 parts.extend(go_extra_args);
1082 parts.join(", ")
1083 };
1084
1085 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1086 let _ = writeln!(out, "\t// {description}");
1087
1088 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1092 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1093 if let Some(var) = api_key_var {
1094 if has_mock {
1095 let fixture_id = &fixture.id;
1099 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1100 let _ = writeln!(out, "\tvar baseURL *string");
1101 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1102 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1103 let _ = writeln!(out, "\t}} else {{");
1104 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1105 let _ = writeln!(
1106 out,
1107 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1108 );
1109 let _ = writeln!(out, "\t\tbaseURL = &u");
1110 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1111 let _ = writeln!(out, "\t}}");
1112 } else {
1113 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1114 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1115 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1116 let _ = writeln!(out, "\t}}");
1117 }
1118 }
1119
1120 for line in &setup_lines {
1121 let _ = writeln!(out, "\t{line}");
1122 }
1123
1124 let call_prefix = if let Some(factory) = client_factory {
1128 let factory_name = to_go_name(factory);
1129 let fixture_id = &fixture.id;
1130 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1133 ("apiKey".to_string(), "baseURL".to_string())
1135 } else if api_key_var.is_some() {
1136 ("apiKey".to_string(), "nil".to_string())
1138 } else if fixture.has_host_root_route() {
1139 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1140 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1141 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1142 let _ = writeln!(
1143 out,
1144 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1145 );
1146 let _ = writeln!(out, "\t}}");
1147 ("\"test-key\"".to_string(), "&mockURL".to_string())
1148 } else {
1149 let _ = writeln!(
1150 out,
1151 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1152 );
1153 ("\"test-key\"".to_string(), "&mockURL".to_string())
1154 };
1155 let _ = writeln!(
1156 out,
1157 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1158 );
1159 let _ = writeln!(out, "\tif clientErr != nil {{");
1160 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1161 let _ = writeln!(out, "\t}}");
1162 "client".to_string()
1163 } else {
1164 import_alias.to_string()
1165 };
1166
1167 let binding_returns_error_pre = args
1172 .iter()
1173 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1174 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1175
1176 if expects_error {
1177 if effective_returns_result_pre && !returns_void {
1178 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1179 } else {
1180 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1181 }
1182 let _ = writeln!(out, "\tif err == nil {{");
1183 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1184 let _ = writeln!(out, "\t}}");
1185 let _ = writeln!(out, "}}");
1186 return;
1187 }
1188
1189 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1191
1192 use heck::ToSnakeCase;
1197 let fn_snake = function_name.to_snake_case();
1198 let base_snake = base_function_name.to_snake_case();
1199 let streaming_item_type = if is_streaming {
1200 adapters
1201 .iter()
1202 .filter(|a| matches!(a.pattern, alef_core::config::extras::AdapterPattern::Streaming))
1203 .find(|a| a.name == fn_snake || a.name == base_snake)
1204 .and_then(|a| a.item_type.as_deref())
1205 .and_then(|t| t.rsplit("::").next())
1206 .unwrap_or("Item") } else {
1208 "Item" };
1210
1211 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1216 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1217 return false;
1218 }
1219 if a.assertion_type == "method_result" {
1221 return true;
1222 }
1223 match &a.field {
1224 Some(f) if !f.is_empty() => {
1225 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1226 return true;
1227 }
1228 if result_is_simple {
1234 return true;
1235 }
1236 field_resolver.is_valid_for_result(f)
1237 }
1238 _ => true,
1239 }
1240 });
1241
1242 let binding_returns_error = args
1249 .iter()
1250 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1251 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1253
1254 if !effective_returns_result && result_is_simple {
1260 let result_binding = if has_usable_assertion {
1262 result_var.to_string()
1263 } else {
1264 "_".to_string()
1265 };
1266 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1268 let _ = writeln!(
1269 out,
1270 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1271 );
1272 if has_usable_assertion && result_binding != "_" {
1273 if result_is_array {
1274 let _ = writeln!(out, "\tvalue := {result_var}");
1276 } else {
1277 let only_nil_assertions = fixture
1280 .assertions
1281 .iter()
1282 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1283 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1284 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1285
1286 if !only_nil_assertions {
1287 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1290 if result_is_ptr {
1291 let _ = writeln!(out, "\tif {result_var} == nil {{");
1292 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1293 let _ = writeln!(out, "\t}}");
1294 let _ = writeln!(out, "\tvalue := *{result_var}");
1295 } else {
1296 let _ = writeln!(out, "\tvalue := {result_var}");
1298 }
1299 }
1300 }
1301 }
1302 } else if !effective_returns_result || returns_void {
1303 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1306 let _ = writeln!(out, "\tif err != nil {{");
1307 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1308 let _ = writeln!(out, "\t}}");
1309 let _ = writeln!(out, "}}");
1311 return;
1312 } else {
1313 let result_binding = if is_streaming {
1316 "stream".to_string()
1317 } else if has_usable_assertion {
1318 result_var.to_string()
1319 } else {
1320 "_".to_string()
1321 };
1322 let _ = writeln!(
1323 out,
1324 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1325 );
1326 let _ = writeln!(out, "\tif err != nil {{");
1327 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1328 let _ = writeln!(out, "\t}}");
1329 if is_streaming {
1331 let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
1332 let _ = writeln!(out, "\tfor chunk := range stream {{");
1333 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1334 let _ = writeln!(out, "\t}}");
1335 }
1336 if result_is_simple && has_usable_assertion && result_binding != "_" {
1337 if result_is_array {
1338 let _ = writeln!(out, "\tvalue := {}", result_var);
1340 } else {
1341 let only_nil_assertions = fixture
1344 .assertions
1345 .iter()
1346 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1347 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1348 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1349
1350 if !only_nil_assertions {
1351 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1354 if result_is_ptr {
1355 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1356 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1357 let _ = writeln!(out, "\t}}");
1358 let _ = writeln!(out, "\tvalue := *{}", result_var);
1359 } else {
1360 let _ = writeln!(out, "\tvalue := {}", result_var);
1362 }
1363 }
1364 }
1365 }
1366 }
1367
1368 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1372 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1373 let only_nil_assertions = fixture
1374 .assertions
1375 .iter()
1376 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1377 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1378 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1379 !only_nil_assertions
1380 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1381 true
1382 } else {
1383 result_is_simple && has_usable_assertion
1384 };
1385
1386 let effective_result_var = if has_deref_value {
1387 "value".to_string()
1388 } else {
1389 result_var.to_string()
1390 };
1391
1392 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1397 for assertion in &fixture.assertions {
1398 if let Some(f) = &assertion.field {
1399 if !f.is_empty() {
1400 if !result_is_simple && !field_resolver.is_valid_for_result(f) {
1403 continue;
1404 }
1405 let resolved = field_resolver.resolve(f);
1406 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1407 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1412 let is_array_field = field_resolver.is_array(resolved);
1413 if !is_string_field || is_array_field {
1414 continue;
1417 }
1418 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1419 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1420 if field_resolver.has_map_access(f) {
1421 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1424 } else {
1425 let _ = writeln!(out, "\tvar {local_var} string");
1426 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1427 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1431 let _ = writeln!(out, "\t}}");
1432 }
1433 optional_locals.insert(f.clone(), local_var);
1434 }
1435 }
1436 }
1437 }
1438
1439 for assertion in &fixture.assertions {
1441 if let Some(f) = &assertion.field {
1442 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1443 let parts: Vec<&str> = f.split('.').collect();
1446 let mut guard_expr: Option<String> = None;
1447 for i in 1..parts.len() {
1448 let prefix = parts[..i].join(".");
1449 let resolved_prefix = field_resolver.resolve(&prefix);
1450 if field_resolver.is_optional(resolved_prefix) {
1451 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1457 let suffix = &resolved_prefix[bracket_pos + 1..];
1458 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1459 if is_numeric_index {
1460 &resolved_prefix[..bracket_pos]
1461 } else {
1462 resolved_prefix
1463 }
1464 } else {
1465 resolved_prefix
1466 };
1467 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1468 guard_expr = Some(accessor);
1469 break;
1470 }
1471 }
1472 if let Some(guard) = guard_expr {
1473 if field_resolver.is_valid_for_result(f) {
1476 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1482 if is_struct_value {
1483 render_assertion(
1486 out,
1487 assertion,
1488 &effective_result_var,
1489 import_alias,
1490 field_resolver,
1491 &optional_locals,
1492 result_is_simple,
1493 result_is_array,
1494 is_streaming,
1495 );
1496 continue;
1497 }
1498 let _ = writeln!(out, "\tif {guard} != nil {{");
1499 let mut nil_buf = String::new();
1502 render_assertion(
1503 &mut nil_buf,
1504 assertion,
1505 &effective_result_var,
1506 import_alias,
1507 field_resolver,
1508 &optional_locals,
1509 result_is_simple,
1510 result_is_array,
1511 is_streaming,
1512 );
1513 for line in nil_buf.lines() {
1514 let _ = writeln!(out, "\t{line}");
1515 }
1516 let _ = writeln!(out, "\t}}");
1517 } else {
1518 render_assertion(
1519 out,
1520 assertion,
1521 &effective_result_var,
1522 import_alias,
1523 field_resolver,
1524 &optional_locals,
1525 result_is_simple,
1526 result_is_array,
1527 is_streaming,
1528 );
1529 }
1530 continue;
1531 }
1532 }
1533 }
1534 render_assertion(
1535 out,
1536 assertion,
1537 &effective_result_var,
1538 import_alias,
1539 field_resolver,
1540 &optional_locals,
1541 result_is_simple,
1542 result_is_array,
1543 is_streaming,
1544 );
1545 }
1546
1547 let _ = writeln!(out, "}}");
1548}
1549
1550fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1556 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1557}
1558
1559struct GoTestClientRenderer;
1571
1572impl client::TestClientRenderer for GoTestClientRenderer {
1573 fn language_name(&self) -> &'static str {
1574 "go"
1575 }
1576
1577 fn sanitize_test_name(&self, id: &str) -> String {
1581 id.to_upper_camel_case()
1582 }
1583
1584 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1587 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1588 let _ = writeln!(out, "\t// {description}");
1589 if let Some(reason) = skip_reason {
1590 let escaped = go_string_literal(reason);
1591 let _ = writeln!(out, "\tt.Skip({escaped})");
1592 }
1593 }
1594
1595 fn render_test_close(&self, out: &mut String) {
1596 let _ = writeln!(out, "}}");
1597 }
1598
1599 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1605 let method = ctx.method.to_uppercase();
1606 let path = ctx.path;
1607
1608 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1609 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1610 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1611 let _ = writeln!(out, "\t}}");
1612
1613 let body_expr = if let Some(body) = ctx.body {
1615 let json = serde_json::to_string(body).unwrap_or_default();
1616 let escaped = go_string_literal(&json);
1617 format!("strings.NewReader({})", escaped)
1618 } else {
1619 "strings.NewReader(\"\")".to_string()
1620 };
1621
1622 let _ = writeln!(out, "\tbody := {body_expr}");
1623 let _ = writeln!(
1624 out,
1625 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1626 );
1627 let _ = writeln!(out, "\tif err != nil {{");
1628 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1629 let _ = writeln!(out, "\t}}");
1630
1631 if ctx.body.is_some() {
1633 let content_type = ctx.content_type.unwrap_or("application/json");
1634 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1635 }
1636
1637 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1639 header_names.sort();
1640 for name in header_names {
1641 let value = &ctx.headers[name];
1642 let escaped_name = go_string_literal(name);
1643 let escaped_value = go_string_literal(value);
1644 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1645 }
1646
1647 if !ctx.cookies.is_empty() {
1649 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1650 cookie_names.sort();
1651 for name in cookie_names {
1652 let value = &ctx.cookies[name];
1653 let escaped_name = go_string_literal(name);
1654 let escaped_value = go_string_literal(value);
1655 let _ = writeln!(
1656 out,
1657 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1658 );
1659 }
1660 }
1661
1662 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1664 let _ = writeln!(
1665 out,
1666 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1667 );
1668 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1669 let _ = writeln!(out, "\t\t}},");
1670 let _ = writeln!(out, "\t}}");
1671 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1672 let _ = writeln!(out, "\tif err != nil {{");
1673 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1674 let _ = writeln!(out, "\t}}");
1675 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1676
1677 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1681 let _ = writeln!(out, "\tif err != nil {{");
1682 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1683 let _ = writeln!(out, "\t}}");
1684 let _ = writeln!(out, "\t_ = bodyBytes");
1685 }
1686
1687 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1688 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1689 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1690 let _ = writeln!(out, "\t}}");
1691 }
1692
1693 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1696 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1698 return;
1699 }
1700 if name.eq_ignore_ascii_case("connection") {
1702 return;
1703 }
1704 let escaped_name = go_string_literal(name);
1705 let escaped_value = go_string_literal(expected);
1706 let _ = writeln!(
1707 out,
1708 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1709 );
1710 let _ = writeln!(
1711 out,
1712 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1713 );
1714 let _ = writeln!(out, "\t}}");
1715 }
1716
1717 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1722 match expected {
1723 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1724 let json_str = serde_json::to_string(expected).unwrap_or_default();
1725 let escaped = go_string_literal(&json_str);
1726 let _ = writeln!(out, "\tvar got any");
1727 let _ = writeln!(out, "\tvar want any");
1728 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1729 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1730 let _ = writeln!(out, "\t}}");
1731 let _ = writeln!(
1732 out,
1733 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1734 );
1735 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1736 let _ = writeln!(out, "\t}}");
1737 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1738 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1739 let _ = writeln!(out, "\t}}");
1740 }
1741 serde_json::Value::String(s) => {
1742 let escaped = go_string_literal(s);
1743 let _ = writeln!(out, "\twant := {escaped}");
1744 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1745 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1746 let _ = writeln!(out, "\t}}");
1747 }
1748 other => {
1749 let escaped = go_string_literal(&other.to_string());
1750 let _ = writeln!(out, "\twant := {escaped}");
1751 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1752 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1753 let _ = writeln!(out, "\t}}");
1754 }
1755 }
1756 }
1757
1758 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1761 if let Some(obj) = expected.as_object() {
1762 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1763 let _ = writeln!(
1764 out,
1765 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1766 );
1767 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1768 let _ = writeln!(out, "\t}}");
1769 for (key, val) in obj {
1770 let escaped_key = go_string_literal(key);
1771 let json_val = serde_json::to_string(val).unwrap_or_default();
1772 let escaped_val = go_string_literal(&json_val);
1773 let _ = writeln!(out, "\t{{");
1774 let _ = writeln!(out, "\t\tvar _wantVal any");
1775 let _ = writeln!(
1776 out,
1777 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1778 );
1779 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1780 let _ = writeln!(out, "\t\t}}");
1781 let _ = writeln!(
1782 out,
1783 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1784 );
1785 let _ = writeln!(
1786 out,
1787 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1788 );
1789 let _ = writeln!(out, "\t\t}}");
1790 let _ = writeln!(out, "\t}}");
1791 }
1792 }
1793 }
1794
1795 fn render_assert_validation_errors(
1800 &self,
1801 out: &mut String,
1802 _response_var: &str,
1803 errors: &[ValidationErrorExpectation],
1804 ) {
1805 let _ = writeln!(out, "\tvar _veBody map[string]any");
1806 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1807 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1808 let _ = writeln!(out, "\t}}");
1809 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1810 for ve in errors {
1811 let escaped_msg = go_string_literal(&ve.msg);
1812 let _ = writeln!(out, "\t{{");
1813 let _ = writeln!(out, "\t\t_found := false");
1814 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1815 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1816 let _ = writeln!(
1817 out,
1818 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1819 );
1820 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1821 let _ = writeln!(out, "\t\t\t\t\tbreak");
1822 let _ = writeln!(out, "\t\t\t\t}}");
1823 let _ = writeln!(out, "\t\t\t}}");
1824 let _ = writeln!(out, "\t\t}}");
1825 let _ = writeln!(out, "\t\tif !_found {{");
1826 let _ = writeln!(
1827 out,
1828 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1829 );
1830 let _ = writeln!(out, "\t\t}}");
1831 let _ = writeln!(out, "\t}}");
1832 }
1833 }
1834}
1835
1836#[allow(clippy::too_many_arguments)]
1844fn build_args_and_setup(
1845 input: &serde_json::Value,
1846 args: &[crate::config::ArgMapping],
1847 import_alias: &str,
1848 options_type: Option<&str>,
1849 fixture: &crate::fixture::Fixture,
1850 options_ptr: bool,
1851 expects_error: bool,
1852 data_enum_names: &std::collections::HashSet<&str>,
1853) -> (Vec<String>, String) {
1854 let fixture_id = &fixture.id;
1855 use heck::ToUpperCamelCase;
1856
1857 if args.is_empty() {
1858 return (Vec::new(), String::new());
1859 }
1860
1861 let mut setup_lines: Vec<String> = Vec::new();
1862 let mut parts: Vec<String> = Vec::new();
1863
1864 for arg in args {
1865 if arg.arg_type == "mock_url" {
1866 if fixture.has_host_root_route() {
1867 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1868 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1869 setup_lines.push(format!(
1870 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1871 arg.name, arg.name
1872 ));
1873 } else {
1874 setup_lines.push(format!(
1875 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1876 arg.name,
1877 ));
1878 }
1879 parts.push(arg.name.clone());
1880 continue;
1881 }
1882
1883 if arg.arg_type == "mock_url_list" {
1884 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1889 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1890 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1891
1892 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1893 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1894 } else {
1895 Vec::new()
1896 };
1897
1898 let paths_literal = paths.join(", ");
1899 let var_name = &arg.name;
1900
1901 setup_lines.push(format!(
1902 "{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}}"
1903 ));
1904 setup_lines.push(format!(
1905 "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}}"
1906 ));
1907 parts.push(var_name.to_string());
1908 continue;
1909 }
1910
1911 if arg.arg_type == "handle" {
1912 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1914 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1915 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1916 let create_err_handler = if expects_error {
1920 "assert.Error(t, createErr)\n\t\treturn".to_string()
1921 } else {
1922 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1923 };
1924 if config_value.is_null()
1925 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1926 {
1927 setup_lines.push(format!(
1928 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1929 name = arg.name,
1930 ));
1931 } else {
1932 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1933 let go_literal = go_string_literal(&json_str);
1934 let name = &arg.name;
1935 setup_lines.push(format!(
1936 "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}}"
1937 ));
1938 setup_lines.push(format!(
1939 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1940 ));
1941 }
1942 parts.push(arg.name.clone());
1943 continue;
1944 }
1945
1946 let val: Option<&serde_json::Value> = if arg.field == "input" {
1947 Some(input)
1948 } else {
1949 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1950 input.get(field)
1951 };
1952
1953 if arg.arg_type == "bytes" {
1960 let var_name = format!("{}Bytes", arg.name);
1961 match val {
1962 None | Some(serde_json::Value::Null) => {
1963 if arg.optional {
1964 parts.push("nil".to_string());
1965 } else {
1966 parts.push("[]byte{}".to_string());
1967 }
1968 }
1969 Some(serde_json::Value::String(s)) => {
1970 let go_path = go_string_literal(s);
1975 setup_lines.push(format!(
1976 "{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}}"
1977 ));
1978 parts.push(var_name);
1979 }
1980 Some(other) => {
1981 parts.push(format!("[]byte({})", json_to_go(other)));
1982 }
1983 }
1984 continue;
1985 }
1986
1987 match val {
1988 None | Some(serde_json::Value::Null) if arg.optional => {
1989 match arg.arg_type.as_str() {
1991 "string" => {
1992 parts.push("nil".to_string());
1994 }
1995 "json_object" => {
1996 if options_ptr {
1997 parts.push("nil".to_string());
1999 } else if let Some(opts_type) = options_type {
2000 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2002 } else {
2003 parts.push("nil".to_string());
2004 }
2005 }
2006 _ => {
2007 parts.push("nil".to_string());
2008 }
2009 }
2010 }
2011 None | Some(serde_json::Value::Null) => {
2012 let default_val = match arg.arg_type.as_str() {
2014 "string" => "\"\"".to_string(),
2015 "int" | "integer" | "i64" => "0".to_string(),
2016 "float" | "number" => "0.0".to_string(),
2017 "bool" | "boolean" => "false".to_string(),
2018 "json_object" => {
2019 if options_ptr {
2020 "nil".to_string()
2022 } else if let Some(opts_type) = options_type {
2023 format!("{import_alias}.{opts_type}{{}}")
2024 } else {
2025 "nil".to_string()
2026 }
2027 }
2028 _ => "nil".to_string(),
2029 };
2030 parts.push(default_val);
2031 }
2032 Some(v) => {
2033 match arg.arg_type.as_str() {
2034 "json_object" => {
2035 let is_array = v.is_array();
2038 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2039 if is_empty_obj {
2040 if options_ptr {
2041 parts.push("nil".to_string());
2043 } else if let Some(opts_type) = options_type {
2044 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2045 } else {
2046 parts.push("nil".to_string());
2047 }
2048 } else if is_array {
2049 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2054 if go_t.starts_with('[') {
2058 go_t.to_string()
2059 } else {
2060 let qualified = if go_t.contains('.') {
2062 go_t.to_string()
2063 } else {
2064 format!("{import_alias}.{go_t}")
2065 };
2066 format!("[]{qualified}")
2067 }
2068 } else {
2069 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2070 };
2071
2072 let element_type_name = if let Some(go_t) = arg.go_type.as_deref() {
2074 if go_t.starts_with('[') {
2076 None } else if let Some(idx) = go_t.rfind('.') {
2078 Some(&go_t[idx + 1..]) } else {
2080 Some(go_t)
2081 }
2082 } else {
2083 arg.element_type.as_deref()
2084 };
2085
2086 let is_sum_type = element_type_name.is_some_and(|et| data_enum_names.contains(et));
2087
2088 let converted_v = convert_json_for_go(v.clone());
2090 let var_name = &arg.name;
2091
2092 if is_sum_type {
2093 let element_type = element_type_name.unwrap();
2095 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2096 let go_literal = go_string_literal(&json_str);
2097 setup_lines.push(format!(
2098 "var {var_name}Raw []json.RawMessage\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}Raw); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
2099 ));
2100 setup_lines.push(format!(
2101 "var {var_name} {go_slice_type}\n\tfor _, raw := range {var_name}Raw {{\n\t\telem, err := {import_alias}.Unmarshal{element_type}(raw)\n\t\tif err != nil {{\n\t\t\tt.Fatalf(\"unmarshal {element_type} failed: %v\", err)\n\t\t}}\n\t\t{var_name} = append({var_name}, elem)\n\t}}"
2102 ));
2103 } else {
2104 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2106 let go_literal = go_string_literal(&json_str);
2107 setup_lines.push(format!(
2108 "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}}"
2109 ));
2110 }
2111 parts.push(var_name.to_string());
2112 } else if let Some(opts_type) = options_type {
2113 let remapped_v = if options_ptr {
2118 convert_json_for_go(v.clone())
2119 } else {
2120 v.clone()
2121 };
2122 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2123 let go_literal = go_string_literal(&json_str);
2124 let var_name = &arg.name;
2125 setup_lines.push(format!(
2126 "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}}"
2127 ));
2128 let arg_expr = if options_ptr {
2130 format!("&{var_name}")
2131 } else {
2132 var_name.to_string()
2133 };
2134 parts.push(arg_expr);
2135 } else {
2136 parts.push(json_to_go(v));
2137 }
2138 }
2139 "string" if arg.optional => {
2140 let var_name = format!("{}Val", arg.name);
2142 let go_val = json_to_go(v);
2143 setup_lines.push(format!("{var_name} := {go_val}"));
2144 parts.push(format!("&{var_name}"));
2145 }
2146 _ => {
2147 parts.push(json_to_go(v));
2148 }
2149 }
2150 }
2151 }
2152 }
2153
2154 (setup_lines, parts.join(", "))
2155}
2156
2157#[allow(clippy::too_many_arguments)]
2158fn render_assertion(
2159 out: &mut String,
2160 assertion: &Assertion,
2161 result_var: &str,
2162 import_alias: &str,
2163 field_resolver: &FieldResolver,
2164 optional_locals: &std::collections::HashMap<String, String>,
2165 result_is_simple: bool,
2166 result_is_array: bool,
2167 is_streaming: bool,
2168) {
2169 if !result_is_simple {
2172 if let Some(f) = &assertion.field {
2173 let embed_deref = format!("(*{result_var})");
2176 match f.as_str() {
2177 "chunks_have_content" => {
2178 let pred = format!(
2179 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2180 );
2181 match assertion.assertion_type.as_str() {
2182 "is_true" => {
2183 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2184 }
2185 "is_false" => {
2186 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2187 }
2188 _ => {
2189 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2190 }
2191 }
2192 return;
2193 }
2194 "chunks_have_embeddings" => {
2195 let pred = format!(
2196 "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 }}()"
2197 );
2198 match assertion.assertion_type.as_str() {
2199 "is_true" => {
2200 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2201 }
2202 "is_false" => {
2203 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2204 }
2205 _ => {
2206 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2207 }
2208 }
2209 return;
2210 }
2211 "chunks_have_heading_context" => {
2212 let pred = format!(
2213 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2214 );
2215 match assertion.assertion_type.as_str() {
2216 "is_true" => {
2217 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2218 }
2219 "is_false" => {
2220 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2221 }
2222 _ => {
2223 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2224 }
2225 }
2226 return;
2227 }
2228 "first_chunk_starts_with_heading" => {
2229 let pred = format!(
2230 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2231 );
2232 match assertion.assertion_type.as_str() {
2233 "is_true" => {
2234 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2235 }
2236 "is_false" => {
2237 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2238 }
2239 _ => {
2240 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2241 }
2242 }
2243 return;
2244 }
2245 "embeddings" => {
2246 match assertion.assertion_type.as_str() {
2247 "count_equals" => {
2248 if let Some(val) = &assertion.value {
2249 if let Some(n) = val.as_u64() {
2250 let _ = writeln!(
2251 out,
2252 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2253 );
2254 }
2255 }
2256 }
2257 "count_min" => {
2258 if let Some(val) = &assertion.value {
2259 if let Some(n) = val.as_u64() {
2260 let _ = writeln!(
2261 out,
2262 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2263 );
2264 }
2265 }
2266 }
2267 "not_empty" => {
2268 let _ = writeln!(
2269 out,
2270 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2271 );
2272 }
2273 "is_empty" => {
2274 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2275 }
2276 _ => {
2277 let _ = writeln!(
2278 out,
2279 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2280 );
2281 }
2282 }
2283 return;
2284 }
2285 "embedding_dimensions" => {
2286 let expr = format!(
2287 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2288 );
2289 match assertion.assertion_type.as_str() {
2290 "equals" => {
2291 if let Some(val) = &assertion.value {
2292 if let Some(n) = val.as_u64() {
2293 let _ = writeln!(
2294 out,
2295 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2296 );
2297 }
2298 }
2299 }
2300 "greater_than" => {
2301 if let Some(val) = &assertion.value {
2302 if let Some(n) = val.as_u64() {
2303 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2304 }
2305 }
2306 }
2307 _ => {
2308 let _ = writeln!(
2309 out,
2310 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2311 );
2312 }
2313 }
2314 return;
2315 }
2316 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2317 let pred = match f.as_str() {
2318 "embeddings_valid" => {
2319 format!(
2320 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2321 )
2322 }
2323 "embeddings_finite" => {
2324 format!(
2325 "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 }}()"
2326 )
2327 }
2328 "embeddings_non_zero" => {
2329 format!(
2330 "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 }}()"
2331 )
2332 }
2333 "embeddings_normalized" => {
2334 format!(
2335 "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 }}()"
2336 )
2337 }
2338 _ => unreachable!(),
2339 };
2340 match assertion.assertion_type.as_str() {
2341 "is_true" => {
2342 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2343 }
2344 "is_false" => {
2345 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2346 }
2347 _ => {
2348 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2349 }
2350 }
2351 return;
2352 }
2353 "keywords" | "keywords_count" => {
2356 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2357 return;
2358 }
2359 _ => {}
2360 }
2361 }
2362 }
2363
2364 if !result_is_simple && is_streaming {
2371 if let Some(f) = &assertion.field {
2372 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2373 if let Some(expr) =
2374 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2375 {
2376 match assertion.assertion_type.as_str() {
2377 "count_min" => {
2378 if let Some(val) = &assertion.value {
2379 if let Some(n) = val.as_u64() {
2380 let _ = writeln!(
2381 out,
2382 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2383 );
2384 }
2385 }
2386 }
2387 "count_equals" => {
2388 if let Some(val) = &assertion.value {
2389 if let Some(n) = val.as_u64() {
2390 let _ = writeln!(
2391 out,
2392 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2393 );
2394 }
2395 }
2396 }
2397 "equals" => {
2398 if let Some(serde_json::Value::String(s)) = &assertion.value {
2399 let escaped = crate::escape::go_string_literal(s);
2400 let is_deep_path = f.contains('.') || f.contains('[');
2405 let safe_expr = if is_deep_path {
2406 format!(
2407 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2408 )
2409 } else {
2410 expr.clone()
2411 };
2412 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2413 } else if let Some(val) = &assertion.value {
2414 if let Some(n) = val.as_u64() {
2415 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2416 }
2417 }
2418 }
2419 "not_empty" => {
2420 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2421 }
2422 "is_empty" => {
2423 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2424 }
2425 "is_true" => {
2426 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2427 }
2428 "is_false" => {
2429 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2430 }
2431 "greater_than" => {
2432 if let Some(val) = &assertion.value {
2433 if let Some(n) = val.as_u64() {
2434 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2435 }
2436 }
2437 }
2438 "greater_than_or_equal" => {
2439 if let Some(val) = &assertion.value {
2440 if let Some(n) = val.as_u64() {
2441 let _ =
2442 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2443 }
2444 }
2445 }
2446 "contains" => {
2447 if let Some(serde_json::Value::String(s)) = &assertion.value {
2448 let escaped = crate::escape::go_string_literal(s);
2449 let _ =
2450 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2451 }
2452 }
2453 _ => {
2454 let _ = writeln!(
2455 out,
2456 "\t// streaming field '{f}': assertion type '{}' not rendered",
2457 assertion.assertion_type
2458 );
2459 }
2460 }
2461 }
2462 return;
2463 }
2464 }
2465 }
2466
2467 if !result_is_simple {
2470 if let Some(f) = &assertion.field {
2471 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2472 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2473 return;
2474 }
2475 }
2476 }
2477
2478 let field_expr = if result_is_simple {
2479 result_var.to_string()
2481 } else {
2482 match &assertion.field {
2483 Some(f) if !f.is_empty() => {
2484 if let Some(local_var) = optional_locals.get(f.as_str()) {
2486 local_var.clone()
2487 } else {
2488 field_resolver.accessor(f, "go", result_var)
2489 }
2490 }
2491 _ => result_var.to_string(),
2492 }
2493 };
2494
2495 let is_optional = assertion
2499 .field
2500 .as_ref()
2501 .map(|f| {
2502 let resolved = field_resolver.resolve(f);
2503 let check_path = resolved
2504 .strip_suffix(".length")
2505 .or_else(|| resolved.strip_suffix(".count"))
2506 .or_else(|| resolved.strip_suffix(".size"))
2507 .unwrap_or(resolved);
2508 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2509 })
2510 .unwrap_or(false);
2511
2512 let field_is_array_for_len = assertion
2516 .field
2517 .as_ref()
2518 .map(|f| {
2519 let resolved = field_resolver.resolve(f);
2520 let check_path = resolved
2521 .strip_suffix(".length")
2522 .or_else(|| resolved.strip_suffix(".count"))
2523 .or_else(|| resolved.strip_suffix(".size"))
2524 .unwrap_or(resolved);
2525 field_resolver.is_array(check_path)
2526 })
2527 .unwrap_or(false);
2528 let field_expr =
2529 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2530 let inner = &field_expr[4..field_expr.len() - 1];
2531 format!("len(*{inner})")
2532 } else {
2533 field_expr
2534 };
2535 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2537 Some(field_expr[5..field_expr.len() - 1].to_string())
2538 } else {
2539 None
2540 };
2541
2542 let field_is_slice = assertion
2546 .field
2547 .as_ref()
2548 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2549 .unwrap_or(false);
2550 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2551 format!("*{field_expr}")
2552 } else {
2553 field_expr.clone()
2554 };
2555
2556 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2561 let mut array_expr = field_expr[..idx].to_string();
2562 if let Some(stripped) = array_expr.strip_prefix("len(") {
2563 array_expr = stripped.to_string();
2564 }
2565 Some(array_expr)
2566 } else {
2567 None
2568 };
2569
2570 let mut assertion_buf = String::new();
2573 let out_ref = &mut assertion_buf;
2574
2575 match assertion.assertion_type.as_str() {
2576 "equals" => {
2577 if let Some(expected) = &assertion.value {
2578 let go_val = json_to_go(expected);
2579 if expected.is_string() {
2581 let resolved_name = assertion
2585 .field
2586 .as_ref()
2587 .map(|f| field_resolver.resolve(f))
2588 .unwrap_or_default();
2589 let is_struct = resolved_name.contains("FormatMetadata");
2590 let trimmed_field = if is_struct {
2591 if is_optional && !field_expr.starts_with("len(") {
2593 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2594 } else {
2595 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2596 }
2597 } else if is_optional && !field_expr.starts_with("len(") {
2598 format!("strings.TrimSpace(string(*{field_expr}))")
2599 } else {
2600 format!("strings.TrimSpace(string({field_expr}))")
2601 };
2602 if is_optional && !field_expr.starts_with("len(") {
2603 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2604 } else {
2605 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2606 }
2607 } else if is_optional && !field_expr.starts_with("len(") {
2608 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2609 } else {
2610 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2611 }
2612 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2613 let _ = writeln!(out_ref, "\t}}");
2614 }
2615 }
2616 "contains" => {
2617 if let Some(expected) = &assertion.value {
2618 let go_val = json_to_go(expected);
2619 let resolved_field = assertion.field.as_deref().unwrap_or("");
2625 let resolved_name = field_resolver.resolve(resolved_field);
2626 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2627 let is_opt =
2628 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2629 let field_for_contains = if is_opt && field_is_array {
2630 format!("jsonString({field_expr})")
2632 } else if is_opt {
2633 format!("fmt.Sprint(*{field_expr})")
2634 } else if field_is_array {
2635 format!("jsonString({field_expr})")
2636 } else {
2637 format!("fmt.Sprint({field_expr})")
2638 };
2639 if is_opt {
2640 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2641 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2642 let _ = writeln!(
2643 out_ref,
2644 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2645 );
2646 let _ = writeln!(out_ref, "\t}}");
2647 let _ = writeln!(out_ref, "\t}}");
2648 } else {
2649 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2650 let _ = writeln!(
2651 out_ref,
2652 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2653 );
2654 let _ = writeln!(out_ref, "\t}}");
2655 }
2656 }
2657 }
2658 "contains_all" => {
2659 if let Some(values) = &assertion.values {
2660 let resolved_field = assertion.field.as_deref().unwrap_or("");
2661 let resolved_name = field_resolver.resolve(resolved_field);
2662 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2663 let is_opt =
2664 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2665 for val in values {
2666 let go_val = json_to_go(val);
2667 let field_for_contains = if is_opt && field_is_array {
2668 format!("jsonString({field_expr})")
2670 } else if is_opt {
2671 format!("fmt.Sprint(*{field_expr})")
2672 } else if field_is_array {
2673 format!("jsonString({field_expr})")
2674 } else {
2675 format!("fmt.Sprint({field_expr})")
2676 };
2677 if is_opt {
2678 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2679 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2680 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2681 let _ = writeln!(out_ref, "\t}}");
2682 let _ = writeln!(out_ref, "\t}}");
2683 } else {
2684 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2685 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2686 let _ = writeln!(out_ref, "\t}}");
2687 }
2688 }
2689 }
2690 }
2691 "not_contains" => {
2692 if let Some(expected) = &assertion.value {
2693 let go_val = json_to_go(expected);
2694 let resolved_field = assertion.field.as_deref().unwrap_or("");
2695 let resolved_name = field_resolver.resolve(resolved_field);
2696 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2697 let is_opt =
2698 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2699 let field_for_contains = if is_opt && field_is_array {
2700 format!("jsonString({field_expr})")
2702 } else if is_opt {
2703 format!("fmt.Sprint(*{field_expr})")
2704 } else if field_is_array {
2705 format!("jsonString({field_expr})")
2706 } else {
2707 format!("fmt.Sprint({field_expr})")
2708 };
2709 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2710 let _ = writeln!(
2711 out_ref,
2712 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2713 );
2714 let _ = writeln!(out_ref, "\t}}");
2715 }
2716 }
2717 "not_empty" => {
2718 let field_is_array = {
2721 let rf = assertion.field.as_deref().unwrap_or("");
2722 let rn = field_resolver.resolve(rf);
2723 field_resolver.is_array(rn)
2724 };
2725 if is_optional && !field_is_array {
2726 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2728 } else if is_optional && field_is_slice {
2729 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2731 } else if is_optional {
2732 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2734 } else if result_is_simple && result_is_array {
2735 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2737 } else {
2738 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2739 }
2740 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2741 let _ = writeln!(out_ref, "\t}}");
2742 }
2743 "is_empty" => {
2744 let field_is_array = {
2745 let rf = assertion.field.as_deref().unwrap_or("");
2746 let rn = field_resolver.resolve(rf);
2747 field_resolver.is_array(rn)
2748 };
2749 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2752 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2754 } else if is_optional && !field_is_array {
2755 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2757 } else if is_optional && field_is_slice {
2758 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2760 } else if is_optional {
2761 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2763 } else {
2764 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2765 }
2766 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2767 let _ = writeln!(out_ref, "\t}}");
2768 }
2769 "contains_any" => {
2770 if let Some(values) = &assertion.values {
2771 let resolved_field = assertion.field.as_deref().unwrap_or("");
2772 let resolved_name = field_resolver.resolve(resolved_field);
2773 let field_is_array = field_resolver.is_array(resolved_name);
2774 let is_opt =
2775 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2776 let field_for_contains = if is_opt && field_is_array {
2777 format!("jsonString({field_expr})")
2779 } else if is_opt {
2780 format!("fmt.Sprint(*{field_expr})")
2781 } else if field_is_array {
2782 format!("jsonString({field_expr})")
2783 } else {
2784 format!("fmt.Sprint({field_expr})")
2785 };
2786 let _ = writeln!(out_ref, "\t{{");
2787 let _ = writeln!(out_ref, "\t\tfound := false");
2788 for val in values {
2789 let go_val = json_to_go(val);
2790 let _ = writeln!(
2791 out_ref,
2792 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2793 );
2794 }
2795 let _ = writeln!(out_ref, "\t\tif !found {{");
2796 let _ = writeln!(
2797 out_ref,
2798 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2799 );
2800 let _ = writeln!(out_ref, "\t\t}}");
2801 let _ = writeln!(out_ref, "\t}}");
2802 }
2803 }
2804 "greater_than" => {
2805 if let Some(val) = &assertion.value {
2806 let go_val = json_to_go(val);
2807 if is_optional {
2811 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2812 if let Some(n) = val.as_u64() {
2813 let next = n + 1;
2814 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2815 } else {
2816 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2817 }
2818 let _ = writeln!(
2819 out_ref,
2820 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2821 );
2822 let _ = writeln!(out_ref, "\t\t}}");
2823 let _ = writeln!(out_ref, "\t}}");
2824 } else if let Some(n) = val.as_u64() {
2825 let next = n + 1;
2826 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2827 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
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" => {
2837 if let Some(val) = &assertion.value {
2838 let go_val = json_to_go(val);
2839 if let Some(ref guard) = nil_guard_expr {
2840 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2841 let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
2842 let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2843 let _ = writeln!(out_ref, "\t\t}}");
2844 let _ = writeln!(out_ref, "\t}}");
2845 } else if is_optional && !field_expr.starts_with("len(") {
2846 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2848 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
2849 let _ = writeln!(
2850 out_ref,
2851 "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
2852 );
2853 let _ = writeln!(out_ref, "\t\t}}");
2854 let _ = writeln!(out_ref, "\t}}");
2855 } else {
2856 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2857 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2858 let _ = writeln!(out_ref, "\t}}");
2859 }
2860 }
2861 }
2862 "greater_than_or_equal" => {
2863 if let Some(val) = &assertion.value {
2864 let go_val = json_to_go(val);
2865 if let Some(ref guard) = nil_guard_expr {
2866 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2867 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2868 let _ = writeln!(
2869 out_ref,
2870 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2871 );
2872 let _ = writeln!(out_ref, "\t\t}}");
2873 let _ = writeln!(out_ref, "\t}}");
2874 } else if is_optional && !field_expr.starts_with("len(") {
2875 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2877 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2878 let _ = writeln!(
2879 out_ref,
2880 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2881 );
2882 let _ = writeln!(out_ref, "\t\t}}");
2883 let _ = writeln!(out_ref, "\t}}");
2884 } else {
2885 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2886 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2887 let _ = writeln!(out_ref, "\t}}");
2888 }
2889 }
2890 }
2891 "less_than_or_equal" => {
2892 if let Some(val) = &assertion.value {
2893 let go_val = json_to_go(val);
2894 if is_optional && !field_expr.starts_with("len(") {
2895 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2897 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2898 let _ = writeln!(
2899 out_ref,
2900 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2901 );
2902 let _ = writeln!(out_ref, "\t\t}}");
2903 let _ = writeln!(out_ref, "\t}}");
2904 } else {
2905 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2906 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2907 let _ = writeln!(out_ref, "\t}}");
2908 }
2909 }
2910 }
2911 "starts_with" => {
2912 if let Some(expected) = &assertion.value {
2913 let go_val = json_to_go(expected);
2914 let field_for_prefix = if is_optional
2915 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2916 {
2917 format!("string(*{field_expr})")
2918 } else {
2919 format!("string({field_expr})")
2920 };
2921 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2922 let _ = writeln!(
2923 out_ref,
2924 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2925 );
2926 let _ = writeln!(out_ref, "\t}}");
2927 }
2928 }
2929 "count_min" => {
2930 if let Some(val) = &assertion.value {
2931 if let Some(n) = val.as_u64() {
2932 if is_optional {
2933 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2934 let len_expr = if field_is_slice {
2936 format!("len({field_expr})")
2937 } else {
2938 format!("len(*{field_expr})")
2939 };
2940 let _ = writeln!(
2941 out_ref,
2942 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2943 );
2944 let _ = writeln!(out_ref, "\t}}");
2945 } else {
2946 let _ = writeln!(
2947 out_ref,
2948 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2949 );
2950 }
2951 }
2952 }
2953 }
2954 "count_equals" => {
2955 if let Some(val) = &assertion.value {
2956 if let Some(n) = val.as_u64() {
2957 if is_optional {
2958 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2959 let len_expr = if field_is_slice {
2961 format!("len({field_expr})")
2962 } else {
2963 format!("len(*{field_expr})")
2964 };
2965 let _ = writeln!(
2966 out_ref,
2967 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2968 );
2969 let _ = writeln!(out_ref, "\t}}");
2970 } else {
2971 let _ = writeln!(
2972 out_ref,
2973 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2974 );
2975 }
2976 }
2977 }
2978 }
2979 "is_true" => {
2980 if is_optional {
2981 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2982 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2983 let _ = writeln!(out_ref, "\t}}");
2984 } else {
2985 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2986 }
2987 }
2988 "is_false" => {
2989 if is_optional {
2990 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2991 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2992 let _ = writeln!(out_ref, "\t}}");
2993 } else {
2994 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2995 }
2996 }
2997 "method_result" => {
2998 if let Some(method_name) = &assertion.method {
2999 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
3000 let check = assertion.check.as_deref().unwrap_or("is_true");
3001 let deref_expr = if info.is_pointer {
3004 format!("*{}", info.call_expr)
3005 } else {
3006 info.call_expr.clone()
3007 };
3008 match check {
3009 "equals" => {
3010 if let Some(val) = &assertion.value {
3011 if val.is_boolean() {
3012 if val.as_bool() == Some(true) {
3013 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3014 } else {
3015 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3016 }
3017 } else {
3018 let go_val = if let Some(cast) = info.value_cast {
3022 if val.is_number() {
3023 format!("{cast}({})", json_to_go(val))
3024 } else {
3025 json_to_go(val)
3026 }
3027 } else {
3028 json_to_go(val)
3029 };
3030 let _ = writeln!(
3031 out_ref,
3032 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
3033 );
3034 }
3035 }
3036 }
3037 "is_true" => {
3038 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
3039 }
3040 "is_false" => {
3041 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
3042 }
3043 "greater_than_or_equal" => {
3044 if let Some(val) = &assertion.value {
3045 let n = val.as_u64().unwrap_or(0);
3046 let cast = info.value_cast.unwrap_or("uint");
3048 let _ = writeln!(
3049 out_ref,
3050 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
3051 );
3052 }
3053 }
3054 "count_min" => {
3055 if let Some(val) = &assertion.value {
3056 let n = val.as_u64().unwrap_or(0);
3057 let _ = writeln!(
3058 out_ref,
3059 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
3060 );
3061 }
3062 }
3063 "contains" => {
3064 if let Some(val) = &assertion.value {
3065 let go_val = json_to_go(val);
3066 let _ = writeln!(
3067 out_ref,
3068 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
3069 );
3070 }
3071 }
3072 "is_error" => {
3073 let _ = writeln!(out_ref, "\t{{");
3074 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3075 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3076 let _ = writeln!(out_ref, "\t}}");
3077 }
3078 other_check => {
3079 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3080 }
3081 }
3082 } else {
3083 panic!("Go e2e generator: method_result assertion missing 'method' field");
3084 }
3085 }
3086 "min_length" => {
3087 if let Some(val) = &assertion.value {
3088 if let Some(n) = val.as_u64() {
3089 if is_optional {
3090 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3091 let _ = writeln!(
3092 out_ref,
3093 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3094 );
3095 let _ = writeln!(out_ref, "\t}}");
3096 } else if field_expr.starts_with("len(") {
3097 let _ = writeln!(
3098 out_ref,
3099 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3100 );
3101 } else {
3102 let _ = writeln!(
3103 out_ref,
3104 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3105 );
3106 }
3107 }
3108 }
3109 }
3110 "max_length" => {
3111 if let Some(val) = &assertion.value {
3112 if let Some(n) = val.as_u64() {
3113 if is_optional {
3114 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3115 let _ = writeln!(
3116 out_ref,
3117 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3118 );
3119 let _ = writeln!(out_ref, "\t}}");
3120 } else if field_expr.starts_with("len(") {
3121 let _ = writeln!(
3122 out_ref,
3123 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3124 );
3125 } else {
3126 let _ = writeln!(
3127 out_ref,
3128 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3129 );
3130 }
3131 }
3132 }
3133 }
3134 "ends_with" => {
3135 if let Some(expected) = &assertion.value {
3136 let go_val = json_to_go(expected);
3137 let field_for_suffix = if is_optional
3138 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3139 {
3140 format!("string(*{field_expr})")
3141 } else {
3142 format!("string({field_expr})")
3143 };
3144 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3145 let _ = writeln!(
3146 out_ref,
3147 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3148 );
3149 let _ = writeln!(out_ref, "\t}}");
3150 }
3151 }
3152 "matches_regex" => {
3153 if let Some(expected) = &assertion.value {
3154 let go_val = json_to_go(expected);
3155 let field_for_regex = if is_optional
3156 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3157 {
3158 format!("*{field_expr}")
3159 } else {
3160 field_expr.clone()
3161 };
3162 let _ = writeln!(
3163 out_ref,
3164 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3165 );
3166 }
3167 }
3168 "not_error" => {
3169 }
3171 "error" => {
3172 }
3174 other => {
3175 panic!("Go e2e generator: unsupported assertion type: {other}");
3176 }
3177 }
3178
3179 if let Some(ref arr) = array_guard {
3182 if !assertion_buf.is_empty() {
3183 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3184 for line in assertion_buf.lines() {
3186 let _ = writeln!(out, "\t{line}");
3187 }
3188 let _ = writeln!(out, "\t}}");
3189 }
3190 } else {
3191 out.push_str(&assertion_buf);
3192 }
3193}
3194
3195struct GoMethodCallInfo {
3197 call_expr: String,
3199 is_pointer: bool,
3201 value_cast: Option<&'static str>,
3204}
3205
3206fn build_go_method_call(
3221 result_var: &str,
3222 method_name: &str,
3223 args: Option<&serde_json::Value>,
3224 import_alias: &str,
3225) -> GoMethodCallInfo {
3226 match method_name {
3227 "root_node_type" => GoMethodCallInfo {
3228 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3229 is_pointer: false,
3230 value_cast: None,
3231 },
3232 "named_children_count" => GoMethodCallInfo {
3233 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3234 is_pointer: false,
3235 value_cast: Some("uint"),
3236 },
3237 "has_error_nodes" => GoMethodCallInfo {
3238 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3239 is_pointer: true,
3240 value_cast: None,
3241 },
3242 "error_count" | "tree_error_count" => GoMethodCallInfo {
3243 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3244 is_pointer: true,
3245 value_cast: Some("uint"),
3246 },
3247 "tree_to_sexp" => GoMethodCallInfo {
3248 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3249 is_pointer: true,
3250 value_cast: None,
3251 },
3252 "contains_node_type" => {
3253 let node_type = args
3254 .and_then(|a| a.get("node_type"))
3255 .and_then(|v| v.as_str())
3256 .unwrap_or("");
3257 GoMethodCallInfo {
3258 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3259 is_pointer: true,
3260 value_cast: None,
3261 }
3262 }
3263 "find_nodes_by_type" => {
3264 let node_type = args
3265 .and_then(|a| a.get("node_type"))
3266 .and_then(|v| v.as_str())
3267 .unwrap_or("");
3268 GoMethodCallInfo {
3269 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3270 is_pointer: true,
3271 value_cast: None,
3272 }
3273 }
3274 "run_query" => {
3275 let query_source = args
3276 .and_then(|a| a.get("query_source"))
3277 .and_then(|v| v.as_str())
3278 .unwrap_or("");
3279 let language = args
3280 .and_then(|a| a.get("language"))
3281 .and_then(|v| v.as_str())
3282 .unwrap_or("");
3283 let query_lit = go_string_literal(query_source);
3284 let lang_lit = go_string_literal(language);
3285 GoMethodCallInfo {
3287 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3288 is_pointer: false,
3289 value_cast: None,
3290 }
3291 }
3292 other => {
3293 let method_pascal = other.to_upper_camel_case();
3294 GoMethodCallInfo {
3295 call_expr: format!("{result_var}.{method_pascal}()"),
3296 is_pointer: false,
3297 value_cast: None,
3298 }
3299 }
3300 }
3301}
3302
3303fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3313 match value {
3314 serde_json::Value::Object(map) => {
3315 let new_map: serde_json::Map<String, serde_json::Value> = map
3316 .into_iter()
3317 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3318 .collect();
3319 serde_json::Value::Object(new_map)
3320 }
3321 serde_json::Value::Array(arr) => {
3322 if is_byte_array(&arr) {
3325 let bytes: Vec<u8> = arr
3326 .iter()
3327 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3328 .collect();
3329 let encoded = base64_encode(&bytes);
3331 serde_json::Value::String(encoded)
3332 } else {
3333 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3334 }
3335 }
3336 serde_json::Value::String(s) => {
3337 serde_json::Value::String(pascal_to_snake_case(&s))
3340 }
3341 other => other,
3342 }
3343}
3344
3345fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3347 if arr.is_empty() {
3348 return false;
3349 }
3350 arr.iter().all(|v| {
3351 if let serde_json::Value::Number(n) = v {
3352 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3353 } else {
3354 false
3355 }
3356 })
3357}
3358
3359fn base64_encode(bytes: &[u8]) -> String {
3362 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3363 let mut result = String::new();
3364 let mut i = 0;
3365
3366 while i + 2 < bytes.len() {
3367 let b1 = bytes[i];
3368 let b2 = bytes[i + 1];
3369 let b3 = bytes[i + 2];
3370
3371 result.push(TABLE[(b1 >> 2) as usize] as char);
3372 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3373 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3374 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3375
3376 i += 3;
3377 }
3378
3379 if i < bytes.len() {
3381 let b1 = bytes[i];
3382 result.push(TABLE[(b1 >> 2) as usize] as char);
3383
3384 if i + 1 < bytes.len() {
3385 let b2 = bytes[i + 1];
3386 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3387 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3388 result.push('=');
3389 } else {
3390 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3391 result.push_str("==");
3392 }
3393 }
3394
3395 result
3396}
3397
3398fn camel_to_snake_case(s: &str) -> String {
3400 let mut result = String::new();
3401 let mut prev_upper = false;
3402 for (i, c) in s.char_indices() {
3403 if c.is_uppercase() {
3404 if i > 0 && !prev_upper {
3405 result.push('_');
3406 }
3407 result.push(c.to_lowercase().next().unwrap_or(c));
3408 prev_upper = true;
3409 } else {
3410 if prev_upper && i > 1 {
3411 }
3415 result.push(c);
3416 prev_upper = false;
3417 }
3418 }
3419 result
3420}
3421
3422fn pascal_to_snake_case(s: &str) -> String {
3427 let first_char = s.chars().next();
3429 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3430 return s.to_string();
3431 }
3432 camel_to_snake_case(s)
3433}
3434
3435fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3439 let elem = element_type.unwrap_or("String").trim();
3440 let go_elem = rust_type_to_go(elem, import_alias);
3441 format!("[]{go_elem}")
3442}
3443
3444fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3447 let trimmed = rust.trim();
3448 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3449 return format!("[]{}", rust_type_to_go(inner, import_alias));
3450 }
3451 match trimmed {
3452 "String" | "&str" | "str" => "string".to_string(),
3453 "bool" => "bool".to_string(),
3454 "f32" => "float32".to_string(),
3455 "f64" => "float64".to_string(),
3456 "i8" => "int8".to_string(),
3457 "i16" => "int16".to_string(),
3458 "i32" => "int32".to_string(),
3459 "i64" | "isize" => "int64".to_string(),
3460 "u8" => "uint8".to_string(),
3461 "u16" => "uint16".to_string(),
3462 "u32" => "uint32".to_string(),
3463 "u64" | "usize" => "uint64".to_string(),
3464 _ => format!("{import_alias}.{trimmed}"),
3465 }
3466}
3467
3468fn json_to_go(value: &serde_json::Value) -> String {
3469 match value {
3470 serde_json::Value::String(s) => go_string_literal(s),
3471 serde_json::Value::Bool(b) => b.to_string(),
3472 serde_json::Value::Number(n) => n.to_string(),
3473 serde_json::Value::Null => "nil".to_string(),
3474 other => go_string_literal(&other.to_string()),
3476 }
3477}
3478
3479fn visitor_struct_name(fixture_id: &str) -> String {
3488 use heck::ToUpperCamelCase;
3489 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3491}
3492
3493fn emit_go_visitor_struct(
3498 out: &mut String,
3499 struct_name: &str,
3500 visitor_spec: &crate::fixture::VisitorSpec,
3501 import_alias: &str,
3502) {
3503 let _ = writeln!(out, "type {struct_name} struct{{");
3504 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3505 let _ = writeln!(out, "}}");
3506 for (method_name, action) in &visitor_spec.callbacks {
3507 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3508 }
3509}
3510
3511fn emit_go_visitor_method(
3513 out: &mut String,
3514 struct_name: &str,
3515 method_name: &str,
3516 action: &CallbackAction,
3517 import_alias: &str,
3518) {
3519 let camel_method = method_to_camel(method_name);
3520 let params = match method_name {
3523 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3524 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3525 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3526 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3527 "visit_code_inline"
3528 | "visit_strong"
3529 | "visit_emphasis"
3530 | "visit_strikethrough"
3531 | "visit_underline"
3532 | "visit_subscript"
3533 | "visit_superscript"
3534 | "visit_mark"
3535 | "visit_button"
3536 | "visit_summary"
3537 | "visit_figcaption"
3538 | "visit_definition_term"
3539 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3540 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3541 "visit_list_item" => {
3542 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3543 }
3544 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3545 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3546 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3547 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3548 "visit_input" => {
3549 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3550 }
3551 "visit_audio" | "visit_video" | "visit_iframe" => {
3552 format!("_ {import_alias}.NodeContext, src *string")
3553 }
3554 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3555 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3556 format!("_ {import_alias}.NodeContext, output string")
3557 }
3558 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3559 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3560 _ => format!("_ {import_alias}.NodeContext"),
3561 };
3562
3563 let _ = writeln!(
3564 out,
3565 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3566 );
3567 match action {
3568 CallbackAction::Skip => {
3569 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3570 }
3571 CallbackAction::Continue => {
3572 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3573 }
3574 CallbackAction::PreserveHtml => {
3575 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3576 }
3577 CallbackAction::Custom { output } => {
3578 let escaped = go_string_literal(output);
3579 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3580 }
3581 CallbackAction::CustomTemplate { template, .. } => {
3582 let ptr_params = go_visitor_ptr_params(method_name);
3589 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3590 let escaped_fmt = go_string_literal(&fmt_str);
3591 if fmt_args.is_empty() {
3592 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3593 } else {
3594 let args_str = fmt_args.join(", ");
3595 let _ = writeln!(
3596 out,
3597 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3598 );
3599 }
3600 }
3601 }
3602 let _ = writeln!(out, "}}");
3603}
3604
3605fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3608 match method_name {
3609 "visit_link" => ["title"].into(),
3610 "visit_image" => ["title"].into(),
3611 "visit_heading" => ["id"].into(),
3612 "visit_code_block" => ["lang"].into(),
3613 "visit_form" => ["action", "method"].into(),
3614 "visit_input" => ["name", "value"].into(),
3615 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3616 _ => std::collections::HashSet::new(),
3617 }
3618}
3619
3620fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3632 let mut fmt_str = String::new();
3633 let mut args: Vec<String> = Vec::new();
3634 let mut chars = template.chars().peekable();
3635 while let Some(c) = chars.next() {
3636 if c == '{' {
3637 let mut name = String::new();
3639 for inner in chars.by_ref() {
3640 if inner == '}' {
3641 break;
3642 }
3643 name.push(inner);
3644 }
3645 fmt_str.push_str("%s");
3646 let go_name = go_param_name(&name);
3648 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3650 format!("*{go_name}")
3651 } else {
3652 go_name
3653 };
3654 args.push(arg_expr);
3655 } else {
3656 fmt_str.push(c);
3657 }
3658 }
3659 (fmt_str, args)
3660}
3661
3662fn method_to_camel(snake: &str) -> String {
3664 use heck::ToUpperCamelCase;
3665 snake.to_upper_camel_case()
3666}
3667
3668#[cfg(test)]
3669mod tests {
3670 use super::*;
3671 use crate::config::{CallConfig, E2eConfig};
3672 use crate::fixture::{Assertion, Fixture};
3673
3674 fn make_fixture(id: &str) -> Fixture {
3675 Fixture {
3676 id: id.to_string(),
3677 category: None,
3678 description: "test fixture".to_string(),
3679 tags: vec![],
3680 skip: None,
3681 env: None,
3682 call: None,
3683 input: serde_json::Value::Null,
3684 mock_response: Some(crate::fixture::MockResponse {
3685 status: 200,
3686 body: Some(serde_json::Value::Null),
3687 stream_chunks: None,
3688 headers: std::collections::HashMap::new(),
3689 }),
3690 source: String::new(),
3691 http: None,
3692 assertions: vec![Assertion {
3693 assertion_type: "not_error".to_string(),
3694 ..Default::default()
3695 }],
3696 visitor: None,
3697 }
3698 }
3699
3700 #[test]
3704 fn test_go_method_name_uses_go_casing() {
3705 let e2e_config = E2eConfig {
3706 call: CallConfig {
3707 function: "clean_extracted_text".to_string(),
3708 module: "github.com/example/mylib".to_string(),
3709 result_var: "result".to_string(),
3710 returns_result: true,
3711 ..CallConfig::default()
3712 },
3713 ..E2eConfig::default()
3714 };
3715
3716 let fixture = make_fixture("basic_text");
3717 let mut out = String::new();
3718 render_test_function(
3719 &mut out,
3720 &fixture,
3721 "kreuzberg",
3722 &e2e_config,
3723 &[],
3724 &std::collections::HashSet::new(),
3725 );
3726
3727 assert!(
3728 out.contains("kreuzberg.CleanExtractedText("),
3729 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3730 );
3731 assert!(
3732 !out.contains("kreuzberg.clean_extracted_text("),
3733 "must not emit raw snake_case method name, got:\n{out}"
3734 );
3735 }
3736
3737 #[test]
3738 fn test_streaming_fixture_emits_collect_snippet() {
3739 let streaming_fixture_json = r#"{
3741 "id": "basic_stream",
3742 "description": "basic streaming test",
3743 "call": "chat_stream",
3744 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3745 "mock_response": {
3746 "status": 200,
3747 "stream_chunks": [{"delta": "hello"}]
3748 },
3749 "assertions": [
3750 {"type": "count_min", "field": "chunks", "value": 1}
3751 ]
3752 }"#;
3753 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3754 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3755
3756 let e2e_config = E2eConfig {
3757 call: CallConfig {
3758 function: "chat_stream".to_string(),
3759 module: "github.com/example/mylib".to_string(),
3760 result_var: "result".to_string(),
3761 returns_result: true,
3762 r#async: true,
3763 ..CallConfig::default()
3764 },
3765 ..E2eConfig::default()
3766 };
3767
3768 let mut out = String::new();
3769 render_test_function(
3770 &mut out,
3771 &fixture,
3772 "pkg",
3773 &e2e_config,
3774 &[],
3775 &std::collections::HashSet::new(),
3776 );
3777
3778 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3779 assert!(
3780 out.contains("for chunk := range stream"),
3781 "should emit collect loop, got:\n{out}"
3782 );
3783 }
3784
3785 #[test]
3786 fn test_streaming_with_client_factory_and_json_arg() {
3787 use alef_core::config::e2e::{ArgMapping, CallOverride};
3791 let streaming_fixture_json = r#"{
3792 "id": "basic_stream_client",
3793 "description": "basic streaming test with client",
3794 "call": "chat_stream",
3795 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3796 "mock_response": {
3797 "status": 200,
3798 "stream_chunks": [{"delta": "hello"}]
3799 },
3800 "assertions": [
3801 {"type": "count_min", "field": "chunks", "value": 1}
3802 ]
3803 }"#;
3804 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3805 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3806
3807 let go_override = CallOverride {
3808 client_factory: Some("CreateClient".to_string()),
3809 ..Default::default()
3810 };
3811
3812 let mut call_overrides = std::collections::HashMap::new();
3813 call_overrides.insert("go".to_string(), go_override);
3814
3815 let e2e_config = E2eConfig {
3816 call: CallConfig {
3817 function: "chat_stream".to_string(),
3818 module: "github.com/example/mylib".to_string(),
3819 result_var: "result".to_string(),
3820 returns_result: false, r#async: true,
3822 args: vec![ArgMapping {
3823 name: "request".to_string(),
3824 field: "input".to_string(),
3825 arg_type: "json_object".to_string(),
3826 optional: false,
3827 owned: true,
3828 element_type: None,
3829 go_type: None,
3830 }],
3831 overrides: call_overrides,
3832 ..CallConfig::default()
3833 },
3834 ..E2eConfig::default()
3835 };
3836
3837 let mut out = String::new();
3838 render_test_function(
3839 &mut out,
3840 &fixture,
3841 "pkg",
3842 &e2e_config,
3843 &[],
3844 &std::collections::HashSet::new(),
3845 );
3846
3847 eprintln!("generated:\n{out}");
3848 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3849 assert!(
3850 out.contains("for chunk := range stream"),
3851 "should emit collect loop, got:\n{out}"
3852 );
3853 }
3854
3855 #[test]
3859 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3860 let mut optional_fields = std::collections::HashSet::new();
3861 optional_fields.insert("segments".to_string());
3862 let mut array_fields = std::collections::HashSet::new();
3863 array_fields.insert("segments".to_string());
3864
3865 let e2e_config = E2eConfig {
3866 call: CallConfig {
3867 function: "transcribe".to_string(),
3868 module: "github.com/example/mylib".to_string(),
3869 result_var: "result".to_string(),
3870 returns_result: true,
3871 ..CallConfig::default()
3872 },
3873 fields_optional: optional_fields,
3874 fields_array: array_fields,
3875 ..E2eConfig::default()
3876 };
3877
3878 let fixture = Fixture {
3879 id: "edge_transcribe_with_timestamps".to_string(),
3880 category: None,
3881 description: "Transcription with timestamp segments".to_string(),
3882 tags: vec![],
3883 skip: None,
3884 env: None,
3885 call: None,
3886 input: serde_json::Value::Null,
3887 mock_response: Some(crate::fixture::MockResponse {
3888 status: 200,
3889 body: Some(serde_json::Value::Null),
3890 stream_chunks: None,
3891 headers: std::collections::HashMap::new(),
3892 }),
3893 source: String::new(),
3894 http: None,
3895 assertions: vec![
3896 Assertion {
3897 assertion_type: "not_error".to_string(),
3898 ..Default::default()
3899 },
3900 Assertion {
3901 assertion_type: "equals".to_string(),
3902 field: Some("segments[0].id".to_string()),
3903 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3904 ..Default::default()
3905 },
3906 ],
3907 visitor: None,
3908 };
3909
3910 let mut out = String::new();
3911 render_test_function(
3912 &mut out,
3913 &fixture,
3914 "pkg",
3915 &e2e_config,
3916 &[],
3917 &std::collections::HashSet::new(),
3918 );
3919
3920 eprintln!("generated:\n{out}");
3921
3922 assert!(
3927 out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
3928 "guard must be on Segments (the slice), not an element; got:\n{out}"
3929 );
3930 assert!(
3932 !out.contains("result.Segments[0] != nil"),
3933 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3934 );
3935 }
3936
3937 #[test]
3943 fn test_result_is_simple_contains_binds_result_and_emits_imports() {
3944 use alef_core::config::e2e::ArgMapping;
3945
3946 let e2e_config = E2eConfig {
3947 call: CallConfig {
3948 function: "detect_mime_type_from_bytes".to_string(),
3949 module: "github.com/example/mylib".to_string(),
3950 result_var: "result".to_string(),
3951 returns_result: true,
3952 result_is_simple: true,
3953 args: vec![ArgMapping {
3954 name: "content".to_string(),
3955 field: "input.data".to_string(),
3956 arg_type: "bytes".to_string(),
3957 optional: false,
3958 owned: false,
3959 element_type: None,
3960 go_type: None,
3961 }],
3962 ..CallConfig::default()
3963 },
3964 ..E2eConfig::default()
3965 };
3966
3967 let fixture = Fixture {
3968 id: "mime_detect_bytes".to_string(),
3969 category: None,
3970 description: "Detect MIME type from file bytes".to_string(),
3971 tags: vec![],
3972 skip: None,
3973 env: None,
3974 call: None,
3975 input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
3976 mock_response: None,
3977 source: String::new(),
3978 http: None,
3979 assertions: vec![Assertion {
3980 assertion_type: "contains".to_string(),
3981 field: Some("result".to_string()),
3982 value: Some(serde_json::Value::String("pdf".to_string())),
3983 ..Default::default()
3984 }],
3985 visitor: None,
3986 };
3987
3988 let out = render_test_file(
3989 "mime_utilities",
3990 &[&fixture],
3991 "github.com/example/mylib",
3992 "kreuzberg",
3993 &e2e_config,
3994 &[],
3995 &std::collections::HashSet::new(),
3996 );
3997
3998 assert!(
3999 out.contains("result, err := kreuzberg.DetectMimeTypeFromBytes("),
4000 "expected the call to bind to `result`, not `_`; got:\n{out}"
4001 );
4002 assert!(
4003 out.contains("strings.Contains(") && out.contains("fmt.Sprint("),
4004 "expected `strings.Contains(fmt.Sprint(...))` rendering; got:\n{out}"
4005 );
4006 assert!(
4007 out.contains("\t\"fmt\""),
4008 "expected the `fmt` import to be emitted; got:\n{out}"
4009 );
4010 assert!(
4011 out.contains("\t\"strings\""),
4012 "expected the `strings` import to be emitted; got:\n{out}"
4013 );
4014 }
4015}