1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::Language;
10use alef_core::config::ResolvedCrateConfig;
11use alef_core::hash::{self, CommentStyle};
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::fmt::Write as FmtWrite;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use super::client;
19
20pub struct GoCodegen;
22
23impl E2eCodegen for GoCodegen {
24 fn generate(
25 &self,
26 groups: &[FixtureGroup],
27 e2e_config: &E2eConfig,
28 config: &ResolvedCrateConfig,
29 _type_defs: &[alef_core::ir::TypeDef],
30 _enums: &[alef_core::ir::EnumDef],
31 ) -> Result<Vec<GeneratedFile>> {
32 let lang = self.language_name();
33 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
34
35 let mut files = Vec::new();
36
37 let call = &e2e_config.call;
39 let overrides = call.overrides.get(lang);
40 let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
41 let module_path = overrides
42 .and_then(|o| o.module.as_ref())
43 .cloned()
44 .or_else(|| configured_go_module_path.clone())
45 .unwrap_or_else(|| call.module.clone());
46 let import_alias = overrides
47 .and_then(|o| o.alias.as_ref())
48 .cloned()
49 .unwrap_or_else(|| "pkg".to_string());
50
51 let go_pkg = e2e_config.resolve_package("go");
53 let go_module_path = go_pkg
54 .as_ref()
55 .and_then(|p| p.module.as_ref())
56 .cloned()
57 .or_else(|| configured_go_module_path.clone())
58 .unwrap_or_else(|| module_path.clone());
59 let replace_path = go_pkg
60 .as_ref()
61 .and_then(|p| p.path.as_ref())
62 .cloned()
63 .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
64 let go_version = go_pkg
65 .as_ref()
66 .and_then(|p| p.version.as_ref())
67 .cloned()
68 .unwrap_or_else(|| {
69 config
70 .resolved_version()
71 .map(|v| format!("v{v}"))
72 .unwrap_or_else(|| "v0.0.0".to_string())
73 });
74 let effective_replace = match e2e_config.dep_mode {
77 crate::config::DependencyMode::Registry => None,
78 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
79 };
80 let effective_go_version = if effective_replace.is_some() {
86 fix_go_major_version(&go_module_path, &go_version)
87 } else {
88 go_version.clone()
89 };
90 files.push(GeneratedFile {
91 path: output_base.join("go.mod"),
92 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
93 generated_header: false,
94 });
95
96 let emits_executable_test =
98 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
99 let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
100 emits_executable_test(f)
101 && f.assertions.iter().any(|a| {
102 matches!(
103 a.assertion_type.as_str(),
104 "contains" | "contains_all" | "contains_any" | "not_contains"
105 ) && {
106 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
107 e2e_config
108 .resolve_call_for_fixture(
109 f.call.as_deref(),
110 &f.id,
111 &f.resolved_category(),
112 &f.tags,
113 &f.input,
114 )
115 .result_is_array
116 } else {
117 let cc = e2e_config.resolve_call_for_fixture(
118 f.call.as_deref(),
119 &f.id,
120 &f.resolved_category(),
121 &f.tags,
122 &f.input,
123 );
124 let per_call_resolver = FieldResolver::new(
125 e2e_config.effective_fields(cc),
126 e2e_config.effective_fields_optional(cc),
127 e2e_config.effective_result_fields(cc),
128 e2e_config.effective_fields_array(cc),
129 &std::collections::HashSet::new(),
130 );
131 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
132 per_call_resolver.is_array(resolved_name)
133 }
134 }
135 })
136 });
137
138 if needs_json_stringify {
140 files.push(GeneratedFile {
141 path: output_base.join("helpers_test.go"),
142 content: render_helpers_test_go(),
143 generated_header: true,
144 });
145 }
146
147 let has_file_fixtures = groups
155 .iter()
156 .flat_map(|g| g.fixtures.iter())
157 .any(|f| f.http.is_none() && !f.needs_mock_server());
158
159 let needs_main_test = has_file_fixtures
160 || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
161 if f.needs_mock_server() {
162 return true;
163 }
164 let cc = e2e_config.resolve_call_for_fixture(
165 f.call.as_deref(),
166 &f.id,
167 &f.resolved_category(),
168 &f.tags,
169 &f.input,
170 );
171 let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
172 go_override.and_then(|o| o.client_factory.as_deref()).is_some()
173 });
174
175 if needs_main_test {
176 files.push(GeneratedFile {
177 path: output_base.join("main_test.go"),
178 content: render_main_test_go(&e2e_config.test_documents_dir),
179 generated_header: true,
180 });
181 }
182
183 for group in groups {
185 let active: Vec<&Fixture> = group
186 .fixtures
187 .iter()
188 .filter(|f| super::should_include_fixture(f, lang, e2e_config))
189 .collect();
190
191 if active.is_empty() {
192 continue;
193 }
194
195 let filename = format!("{}_test.go", sanitize_filename(&group.category));
196 let content = render_test_file(&group.category, &active, &module_path, &import_alias, e2e_config);
197 files.push(GeneratedFile {
198 path: output_base.join(filename),
199 content,
200 generated_header: true,
201 });
202 }
203
204 Ok(files)
205 }
206
207 fn language_name(&self) -> &'static str {
208 "go"
209 }
210}
211
212fn fix_go_major_version(module_path: &str, version: &str) -> String {
219 let major = module_path
221 .rsplit('/')
222 .next()
223 .and_then(|seg| seg.strip_prefix('v'))
224 .and_then(|n| n.parse::<u64>().ok())
225 .filter(|&n| n >= 2);
226
227 let Some(n) = major else {
228 return version.to_string();
229 };
230
231 let expected_prefix = format!("v{n}.");
233 if version.starts_with(&expected_prefix) {
234 return version.to_string();
235 }
236
237 format!("v{n}.0.0")
238}
239
240fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
241 let mut out = String::new();
242 let _ = writeln!(out, "module e2e_go");
243 let _ = writeln!(out);
244 let _ = writeln!(out, "go 1.26");
245 let _ = writeln!(out);
246 let _ = writeln!(out, "require (");
247 let _ = writeln!(out, "\t{go_module_path} {version}");
248 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
249 let _ = writeln!(out, ")");
250
251 if let Some(path) = replace_path {
252 let _ = writeln!(out);
253 let _ = writeln!(out, "replace {go_module_path} => {path}");
254 }
255
256 out
257}
258
259fn render_main_test_go(test_documents_dir: &str) -> String {
265 let mut out = String::new();
267 let _ = writeln!(out, "package e2e_test");
268 let _ = writeln!(out);
269 let _ = writeln!(out, "import (");
270 let _ = writeln!(out, "\t\"bufio\"");
271 let _ = writeln!(out, "\t\"encoding/json\"");
272 let _ = writeln!(out, "\t\"io\"");
273 let _ = writeln!(out, "\t\"os\"");
274 let _ = writeln!(out, "\t\"os/exec\"");
275 let _ = writeln!(out, "\t\"path/filepath\"");
276 let _ = writeln!(out, "\t\"runtime\"");
277 let _ = writeln!(out, "\t\"strings\"");
278 let _ = writeln!(out, "\t\"testing\"");
279 let _ = writeln!(out, ")");
280 let _ = writeln!(out);
281 let _ = writeln!(out, "func TestMain(m *testing.M) {{");
282 let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
283 let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
284 let _ = writeln!(out);
285 let _ = writeln!(
286 out,
287 "\t// Change to the configured test-documents directory (if it exists) so that fixture"
288 );
289 let _ = writeln!(
290 out,
291 "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
292 );
293 let _ = writeln!(
294 out,
295 "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
296 );
297 let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
298 let _ = writeln!(
299 out,
300 "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
301 );
302 let _ = writeln!(
303 out,
304 "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
305 );
306 let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
307 let _ = writeln!(out, "\t\t\tpanic(err)");
308 let _ = writeln!(out, "\t\t}}");
309 let _ = writeln!(out, "\t}}");
310 let _ = writeln!(out);
311 let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
312 let _ = writeln!(
313 out,
314 "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
315 );
316 let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
317 let _ = writeln!(
318 out,
319 "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
320 );
321 let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
322 let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
323 let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
324 let _ = writeln!(out, "\t\tif err != nil {{");
325 let _ = writeln!(out, "\t\t\tpanic(err)");
326 let _ = writeln!(out, "\t\t}}");
327 let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
328 let _ = writeln!(
329 out,
330 "\t\t// server does not see EOF and exit immediately. The mock-server"
331 );
332 let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
333 let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
334 let _ = writeln!(out, "\t\tif err != nil {{");
335 let _ = writeln!(out, "\t\t\tpanic(err)");
336 let _ = writeln!(out, "\t\t}}");
337 let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
338 let _ = writeln!(out, "\t\t\tpanic(err)");
339 let _ = writeln!(out, "\t\t}}");
340 let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
341 let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
342 let _ = writeln!(out, "\t\t\tline := scanner.Text()");
343 let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
344 let _ = writeln!(
345 out,
346 "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
347 );
348 let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
349 let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
350 let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
351 let _ = writeln!(
352 out,
353 "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
354 );
355 let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
356 let _ = writeln!(
357 out,
358 "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
359 );
360 let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
361 let _ = writeln!(
362 out,
363 "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
364 );
365 let _ = writeln!(out, "\t\t\t\t\t}}");
366 let _ = writeln!(out, "\t\t\t\t}}");
367 let _ = writeln!(out, "\t\t\t\tbreak");
368 let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
369 let _ = writeln!(out, "\t\t\t\tbreak");
370 let _ = writeln!(out, "\t\t\t}}");
371 let _ = writeln!(out, "\t\t}}");
372 let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
373 let _ = writeln!(out, "\t\tcode := m.Run()");
374 let _ = writeln!(out, "\t\t_ = stdin.Close()");
375 let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
376 let _ = writeln!(out, "\t\t_ = cmd.Wait()");
377 let _ = writeln!(out, "\t\tos.Exit(code)");
378 let _ = writeln!(out, "\t}} else {{");
379 let _ = writeln!(out, "\t\tcode := m.Run()");
380 let _ = writeln!(out, "\t\tos.Exit(code)");
381 let _ = writeln!(out, "\t}}");
382 let _ = writeln!(out, "}}");
383 out
384}
385
386fn render_helpers_test_go() -> String {
389 let mut out = String::new();
390 let _ = writeln!(out, "package e2e_test");
391 let _ = writeln!(out);
392 let _ = writeln!(out, "import \"encoding/json\"");
393 let _ = writeln!(out);
394 let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
395 let _ = writeln!(
396 out,
397 "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
398 );
399 let _ = writeln!(out, "func jsonString(value any) string {{");
400 let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
401 let _ = writeln!(out, "\tif err != nil {{");
402 let _ = writeln!(out, "\t\treturn \"\"");
403 let _ = writeln!(out, "\t}}");
404 let _ = writeln!(out, "\treturn string(encoded)");
405 let _ = writeln!(out, "}}");
406 out
407}
408
409fn render_test_file(
410 category: &str,
411 fixtures: &[&Fixture],
412 go_module_path: &str,
413 import_alias: &str,
414 e2e_config: &crate::config::E2eConfig,
415) -> String {
416 let mut out = String::new();
417 let emits_executable_test =
418 |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
419
420 out.push_str(&hash::header(CommentStyle::DoubleSlash));
422 let _ = writeln!(out);
423
424 let needs_pkg = fixtures
433 .iter()
434 .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
435
436 let needs_os = fixtures.iter().any(|f| {
439 if f.is_http_test() {
440 return true;
441 }
442 if !emits_executable_test(f) {
443 return false;
444 }
445 let call_config =
446 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
447 let go_override = call_config
448 .overrides
449 .get("go")
450 .or_else(|| e2e_config.call.overrides.get("go"));
451 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
452 return true;
453 }
454 let call_args = &call_config.args;
455 if call_args
458 .iter()
459 .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
460 {
461 return true;
462 }
463 call_args.iter().any(|a| {
464 if a.arg_type != "bytes" {
465 return false;
466 }
467 let mut current = &f.input;
470 let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
471 for segment in path.split('.') {
472 match current.get(segment) {
473 Some(next) => current = next,
474 None => return false,
475 }
476 }
477 current.is_string()
478 })
479 });
480
481 let needs_filepath = false;
484
485 let _needs_json_stringify = fixtures.iter().any(|f| {
486 emits_executable_test(f)
487 && f.assertions.iter().any(|a| {
488 matches!(
489 a.assertion_type.as_str(),
490 "contains" | "contains_all" | "contains_any" | "not_contains"
491 ) && {
492 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
495 e2e_config
497 .resolve_call_for_fixture(
498 f.call.as_deref(),
499 &f.id,
500 &f.resolved_category(),
501 &f.tags,
502 &f.input,
503 )
504 .result_is_array
505 } else {
506 let cc = e2e_config.resolve_call_for_fixture(
508 f.call.as_deref(),
509 &f.id,
510 &f.resolved_category(),
511 &f.tags,
512 &f.input,
513 );
514 let per_call_resolver = FieldResolver::new(
515 e2e_config.effective_fields(cc),
516 e2e_config.effective_fields_optional(cc),
517 e2e_config.effective_result_fields(cc),
518 e2e_config.effective_fields_array(cc),
519 &std::collections::HashSet::new(),
520 );
521 let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
522 per_call_resolver.is_array(resolved_name)
523 }
524 }
525 })
526 });
527
528 let needs_json = fixtures.iter().any(|f| {
532 if let Some(http) = &f.http {
535 let body_needs_json = http
536 .expected_response
537 .body
538 .as_ref()
539 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
540 let partial_needs_json = http.expected_response.body_partial.is_some();
541 let ve_needs_json = http
542 .expected_response
543 .validation_errors
544 .as_ref()
545 .is_some_and(|v| !v.is_empty());
546 if body_needs_json || partial_needs_json || ve_needs_json {
547 return true;
548 }
549 }
550 if !emits_executable_test(f) {
551 return false;
552 }
553
554 let call =
555 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
556 let call_args = &call.args;
557 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
559 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
560 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
561 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
562 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
563 })
564 };
565 let go_override = call.overrides.get("go");
567 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
568 e2e_config
569 .call
570 .overrides
571 .get("go")
572 .and_then(|o| o.options_type.as_deref())
573 });
574 let has_json_obj = call_args.iter().any(|a| {
575 if a.arg_type != "json_object" {
576 return false;
577 }
578 let v = if a.field == "input" {
579 &f.input
580 } else {
581 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
582 f.input.get(field).unwrap_or(&serde_json::Value::Null)
583 };
584 if v.is_array() {
585 return true;
586 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
588 });
589 has_handle || has_json_obj
590 });
591
592 let needs_base64 = false;
597
598 let needs_fmt = fixtures.iter().any(|f| {
604 if f.visitor.as_ref().is_some_and(|v| {
606 v.callbacks.values().any(|action| {
607 if let CallbackAction::CustomTemplate { template, .. } = action {
608 template.contains('{')
609 } else {
610 false
611 }
612 })
613 }) {
614 return true;
615 }
616
617 if !emits_executable_test(f) {
618 return false;
619 }
620
621 if f.assertions.iter().any(|a| {
623 matches!(
624 a.assertion_type.as_str(),
625 "contains" | "contains_all" | "contains_any" | "not_contains"
626 ) && {
627 if a.field.as_ref().is_none_or(|f| f.is_empty()) {
628 !e2e_config
630 .resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input)
631 .result_is_array
632 } else {
633 let field = a.field.as_deref().unwrap_or("");
636 let cc = e2e_config.resolve_call_for_fixture(
637 f.call.as_deref(),
638 &f.id,
639 &f.resolved_category(),
640 &f.tags,
641 &f.input,
642 );
643 let per_call_resolver = FieldResolver::new(
644 e2e_config.effective_fields(cc),
645 e2e_config.effective_fields_optional(cc),
646 e2e_config.effective_result_fields(cc),
647 e2e_config.effective_fields_array(cc),
648 &std::collections::HashSet::new(),
649 );
650 let resolved_name = per_call_resolver.resolve(field);
651 !per_call_resolver.is_array(resolved_name) && per_call_resolver.is_valid_for_result(field)
652 }
653 }
654 }) {
655 return true;
656 }
657
658 f.assertions.iter().any(|a| {
660 if let Some(field) = &a.field {
661 if !field.is_empty() && a.value.as_ref().is_some_and(|v| v.is_string()) {
662 let cc = e2e_config.resolve_call_for_fixture(
663 f.call.as_deref(),
664 &f.id,
665 &f.resolved_category(),
666 &f.tags,
667 &f.input,
668 );
669 let per_call_resolver = FieldResolver::new(
670 e2e_config.effective_fields(cc),
671 e2e_config.effective_fields_optional(cc),
672 e2e_config.effective_result_fields(cc),
673 e2e_config.effective_fields_array(cc),
674 &std::collections::HashSet::new(),
675 );
676 let resolved = per_call_resolver.resolve(field);
677 per_call_resolver.is_optional(resolved)
679 && !per_call_resolver.is_array(resolved)
680 && !per_call_resolver.has_map_access(field)
681 && per_call_resolver.is_valid_for_result(field)
682 } else {
683 false
684 }
685 } else {
686 false
687 }
688 })
689 });
690
691 let needs_strings = fixtures.iter().any(|f| {
695 if !emits_executable_test(f) {
696 return false;
697 }
698 let cc =
700 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
701 if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
702 return true;
703 }
704 let per_call_resolver = FieldResolver::new(
705 e2e_config.effective_fields(cc),
706 e2e_config.effective_fields_optional(cc),
707 e2e_config.effective_result_fields(cc),
708 e2e_config.effective_fields_array(cc),
709 &std::collections::HashSet::new(),
710 );
711 f.assertions.iter().any(|a| {
712 let type_needs_strings = if a.assertion_type == "equals" {
713 a.value.as_ref().is_some_and(|v| v.is_string())
715 } else {
716 matches!(
717 a.assertion_type.as_str(),
718 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
719 )
720 };
721 let field_valid = a
722 .field
723 .as_ref()
724 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
725 .unwrap_or(true);
726 type_needs_strings && field_valid
727 })
728 });
729
730 let needs_assert = fixtures.iter().any(|f| {
732 if !emits_executable_test(f) {
733 return false;
734 }
735 if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
739 return true;
740 }
741 let is_streaming_fixture = f.is_streaming_mock();
746 let cc =
747 e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
748 let per_call_resolver = FieldResolver::new(
749 e2e_config.effective_fields(cc),
750 e2e_config.effective_fields_optional(cc),
751 e2e_config.effective_result_fields(cc),
752 e2e_config.effective_fields_array(cc),
753 &std::collections::HashSet::new(),
754 );
755 f.assertions.iter().any(|a| {
756 let field_is_streaming_virtual = a
757 .field
758 .as_deref()
759 .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
760 let field_valid = a
761 .field
762 .as_ref()
763 .map(|f| f.is_empty() || per_call_resolver.is_valid_for_result(f))
764 .unwrap_or(true)
765 || (is_streaming_fixture && field_is_streaming_virtual);
766 let synthetic_field_needs_assert = match a.field.as_deref() {
767 Some(
768 "chunks_have_content"
769 | "chunks_have_embeddings"
770 | "chunks_have_heading_context"
771 | "first_chunk_starts_with_heading",
772 ) => {
773 matches!(a.assertion_type.as_str(), "is_true" | "is_false")
774 }
775 Some("embeddings") => {
776 matches!(
777 a.assertion_type.as_str(),
778 "count_equals" | "count_min" | "not_empty" | "is_empty"
779 )
780 }
781 _ => false,
782 };
783 let type_needs_assert = matches!(
784 a.assertion_type.as_str(),
785 "count_equals"
786 | "count_min"
787 | "count_max"
788 | "is_true"
789 | "is_false"
790 | "method_result"
791 | "min_length"
792 | "max_length"
793 | "matches_regex"
794 );
795 synthetic_field_needs_assert || type_needs_assert && field_valid
796 })
797 });
798
799 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
801 let needs_http = has_http_fixtures;
802 let needs_io = has_http_fixtures;
804
805 let needs_reflect = fixtures.iter().any(|f| {
808 if let Some(http) = &f.http {
809 let body_needs_reflect = http
810 .expected_response
811 .body
812 .as_ref()
813 .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
814 let partial_needs_reflect = http.expected_response.body_partial.is_some();
815 body_needs_reflect || partial_needs_reflect
816 } else {
817 false
818 }
819 });
820
821 let _ = writeln!(out, "// E2e tests for category: {category}");
822 let _ = writeln!(out, "package e2e_test");
823 let _ = writeln!(out);
824 let _ = writeln!(out, "import (");
825 if needs_base64 {
826 let _ = writeln!(out, "\t\"encoding/base64\"");
827 }
828 if needs_json || needs_reflect {
829 let _ = writeln!(out, "\t\"encoding/json\"");
830 }
831 if needs_fmt {
832 let _ = writeln!(out, "\t\"fmt\"");
833 }
834 if needs_io {
835 let _ = writeln!(out, "\t\"io\"");
836 }
837 if needs_http {
838 let _ = writeln!(out, "\t\"net/http\"");
839 }
840 if needs_os {
841 let _ = writeln!(out, "\t\"os\"");
842 }
843 let _ = needs_filepath; if needs_reflect {
845 let _ = writeln!(out, "\t\"reflect\"");
846 }
847 if needs_strings {
848 let _ = writeln!(out, "\t\"strings\"");
849 }
850 let _ = writeln!(out, "\t\"testing\"");
851 if needs_assert {
852 let _ = writeln!(out);
853 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
854 }
855 if needs_pkg {
856 let _ = writeln!(out);
857 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
858 }
859 let _ = writeln!(out, ")");
860 let _ = writeln!(out);
861
862 for fixture in fixtures.iter() {
864 if let Some(visitor_spec) = &fixture.visitor {
865 let struct_name = visitor_struct_name(&fixture.id);
866 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
867 let _ = writeln!(out);
868 }
869 }
870
871 for (i, fixture) in fixtures.iter().enumerate() {
872 render_test_function(&mut out, fixture, import_alias, e2e_config);
873 if i + 1 < fixtures.len() {
874 let _ = writeln!(out);
875 }
876 }
877
878 while out.ends_with("\n\n") {
880 out.pop();
881 }
882 if !out.ends_with('\n') {
883 out.push('\n');
884 }
885 out
886}
887
888fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
897 if fixture.is_http_test() {
899 return false;
900 }
901 let call_config = e2e_config.resolve_call_for_fixture(
902 fixture.call.as_deref(),
903 &fixture.id,
904 &fixture.resolved_category(),
905 &fixture.tags,
906 &fixture.input,
907 );
908 if call_config.skip_languages.iter().any(|l| l == "go") {
911 return false;
912 }
913 let go_override = call_config
914 .overrides
915 .get("go")
916 .or_else(|| e2e_config.call.overrides.get("go"));
917 if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
920 return true;
921 }
922 let fn_name = go_override
926 .and_then(|o| o.function.as_deref())
927 .filter(|s| !s.is_empty())
928 .unwrap_or(call_config.function.as_str());
929 !fn_name.is_empty()
930}
931
932fn render_test_function(
933 out: &mut String,
934 fixture: &Fixture,
935 import_alias: &str,
936 e2e_config: &crate::config::E2eConfig,
937) {
938 let fn_name = fixture.id.to_upper_camel_case();
939 let description = &fixture.description;
940
941 if fixture.http.is_some() {
943 render_http_test_function(out, fixture);
944 return;
945 }
946
947 if !fixture_has_go_callable(fixture, e2e_config) {
952 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
953 let _ = writeln!(out, "\t// {description}");
954 let _ = writeln!(
955 out,
956 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
957 );
958 let _ = writeln!(out, "}}");
959 return;
960 }
961
962 let call_config = e2e_config.resolve_call_for_fixture(
964 fixture.call.as_deref(),
965 &fixture.id,
966 &fixture.resolved_category(),
967 &fixture.tags,
968 &fixture.input,
969 );
970 let call_field_resolver = FieldResolver::new(
972 e2e_config.effective_fields(call_config),
973 e2e_config.effective_fields_optional(call_config),
974 e2e_config.effective_result_fields(call_config),
975 e2e_config.effective_fields_array(call_config),
976 &std::collections::HashSet::new(),
977 );
978 let field_resolver = &call_field_resolver;
979 let lang = "go";
980 let overrides = call_config.overrides.get(lang);
981
982 let base_function_name = overrides
986 .and_then(|o| o.function.as_deref())
987 .unwrap_or(&call_config.function);
988 let function_name = to_go_name(base_function_name);
989 let result_var = &call_config.result_var;
990 let args = &call_config.args;
991
992 let returns_result = overrides
995 .and_then(|o| o.returns_result)
996 .unwrap_or(call_config.returns_result);
997
998 let returns_void = call_config.returns_void;
1001
1002 let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
1008 || call_config.result_is_simple
1009 || call_config
1010 .overrides
1011 .get("rust")
1012 .map(|o| o.result_is_simple)
1013 .unwrap_or(false);
1014
1015 let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;
1021
1022 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
1024 e2e_config
1025 .call
1026 .overrides
1027 .get("go")
1028 .and_then(|o| o.options_type.as_deref())
1029 });
1030
1031 let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
1033 e2e_config
1034 .call
1035 .overrides
1036 .get("go")
1037 .map(|o| o.options_ptr)
1038 .unwrap_or(false)
1039 });
1040
1041 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
1042 let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
1046
1047 let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
1050 e2e_config
1051 .call
1052 .overrides
1053 .get(lang)
1054 .and_then(|o| o.client_factory.as_deref())
1055 });
1056
1057 let (mut setup_lines, args_str) = build_args_and_setup(
1058 &fixture.input,
1059 args,
1060 import_alias,
1061 call_options_type,
1062 fixture,
1063 call_options_ptr,
1064 validation_creation_failure,
1065 );
1066
1067 let mut visitor_opts_var: Option<String> = None;
1070 if fixture.visitor.is_some() {
1071 let struct_name = visitor_struct_name(&fixture.id);
1072 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
1073 let opts_type = call_options_type.unwrap_or("ConversionOptions");
1075 let opts_var = "opts".to_string();
1076 setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
1077 setup_lines.push("opts.Visitor = visitor".to_string());
1078 visitor_opts_var = Some(opts_var);
1079 }
1080
1081 let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
1082 let final_args = {
1083 let mut parts: Vec<String> = Vec::new();
1084 if !args_str.is_empty() {
1085 let processed_args = if let Some(ref opts_var) = visitor_opts_var {
1087 args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
1088 } else {
1089 args_str
1090 };
1091 parts.push(processed_args);
1092 }
1093 parts.extend(go_extra_args);
1094 parts.join(", ")
1095 };
1096
1097 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1098 let _ = writeln!(out, "\t// {description}");
1099
1100 let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
1104 let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
1105 if let Some(var) = api_key_var {
1106 if has_mock {
1107 let fixture_id = &fixture.id;
1111 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1112 let _ = writeln!(out, "\tvar baseURL *string");
1113 let _ = writeln!(out, "\tif apiKey != \"\" {{");
1114 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
1115 let _ = writeln!(out, "\t}} else {{");
1116 let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
1117 let _ = writeln!(
1118 out,
1119 "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1120 );
1121 let _ = writeln!(out, "\t\tbaseURL = &u");
1122 let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
1123 let _ = writeln!(out, "\t}}");
1124 } else {
1125 let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
1126 let _ = writeln!(out, "\tif apiKey == \"\" {{");
1127 let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
1128 let _ = writeln!(out, "\t}}");
1129 }
1130 }
1131
1132 for line in &setup_lines {
1133 let _ = writeln!(out, "\t{line}");
1134 }
1135
1136 let call_prefix = if let Some(factory) = client_factory {
1140 let factory_name = to_go_name(factory);
1141 let fixture_id = &fixture.id;
1142 let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
1145 ("apiKey".to_string(), "baseURL".to_string())
1147 } else if api_key_var.is_some() {
1148 ("apiKey".to_string(), "nil".to_string())
1150 } else if fixture.has_host_root_route() {
1151 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1152 let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1153 let _ = writeln!(out, "\tif mockURL == \"\" {{");
1154 let _ = writeln!(
1155 out,
1156 "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1157 );
1158 let _ = writeln!(out, "\t}}");
1159 ("\"test-key\"".to_string(), "&mockURL".to_string())
1160 } else {
1161 let _ = writeln!(
1162 out,
1163 "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1164 );
1165 ("\"test-key\"".to_string(), "&mockURL".to_string())
1166 };
1167 let _ = writeln!(
1168 out,
1169 "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1170 );
1171 let _ = writeln!(out, "\tif clientErr != nil {{");
1172 let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1173 let _ = writeln!(out, "\t}}");
1174 "client".to_string()
1175 } else {
1176 import_alias.to_string()
1177 };
1178
1179 let binding_returns_error_pre = args
1184 .iter()
1185 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1186 let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1187
1188 if expects_error {
1189 if effective_returns_result_pre && !returns_void {
1190 let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1191 } else {
1192 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1193 }
1194 let _ = writeln!(out, "\tif err == nil {{");
1195 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1196 let _ = writeln!(out, "\t}}");
1197 let _ = writeln!(out, "}}");
1198 return;
1199 }
1200
1201 let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1203
1204 let has_usable_assertion = fixture.assertions.iter().any(|a| {
1209 if a.assertion_type == "not_error" || a.assertion_type == "error" {
1210 return false;
1211 }
1212 if a.assertion_type == "method_result" {
1214 return true;
1215 }
1216 match &a.field {
1217 Some(f) if !f.is_empty() => {
1218 if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1219 return true;
1220 }
1221 field_resolver.is_valid_for_result(f)
1222 }
1223 _ => true,
1224 }
1225 });
1226
1227 let binding_returns_error = args
1234 .iter()
1235 .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1236 let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1238
1239 if !effective_returns_result && result_is_simple {
1245 let result_binding = if has_usable_assertion {
1247 result_var.to_string()
1248 } else {
1249 "_".to_string()
1250 };
1251 let assign_op = if result_binding == "_" { "=" } else { ":=" };
1253 let _ = writeln!(
1254 out,
1255 "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1256 );
1257 if has_usable_assertion && result_binding != "_" {
1258 if result_is_array {
1259 let _ = writeln!(out, "\tvalue := {result_var}");
1261 } else {
1262 let only_nil_assertions = fixture
1265 .assertions
1266 .iter()
1267 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1268 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1269 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1270
1271 if !only_nil_assertions {
1272 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1275 if result_is_ptr {
1276 let _ = writeln!(out, "\tif {result_var} == nil {{");
1277 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1278 let _ = writeln!(out, "\t}}");
1279 let _ = writeln!(out, "\tvalue := *{result_var}");
1280 } else {
1281 let _ = writeln!(out, "\tvalue := {result_var}");
1283 }
1284 }
1285 }
1286 }
1287 } else if !effective_returns_result || returns_void {
1288 let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1291 let _ = writeln!(out, "\tif err != nil {{");
1292 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1293 let _ = writeln!(out, "\t}}");
1294 let _ = writeln!(out, "}}");
1296 return;
1297 } else {
1298 let result_binding = if is_streaming {
1301 "stream".to_string()
1302 } else if has_usable_assertion {
1303 result_var.to_string()
1304 } else {
1305 "_".to_string()
1306 };
1307 let _ = writeln!(
1308 out,
1309 "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1310 );
1311 let _ = writeln!(out, "\tif err != nil {{");
1312 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1313 let _ = writeln!(out, "\t}}");
1314 if is_streaming {
1316 let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1317 let _ = writeln!(out, "\tfor chunk := range stream {{");
1318 let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1319 let _ = writeln!(out, "\t}}");
1320 }
1321 if result_is_simple && has_usable_assertion && result_binding != "_" {
1322 if result_is_array {
1323 let _ = writeln!(out, "\tvalue := {}", result_var);
1325 } else {
1326 let only_nil_assertions = fixture
1329 .assertions
1330 .iter()
1331 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1332 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1333 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1334
1335 if !only_nil_assertions {
1336 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1339 if result_is_ptr {
1340 let _ = writeln!(out, "\tif {} == nil {{", result_var);
1341 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1342 let _ = writeln!(out, "\t}}");
1343 let _ = writeln!(out, "\tvalue := *{}", result_var);
1344 } else {
1345 let _ = writeln!(out, "\tvalue := {}", result_var);
1347 }
1348 }
1349 }
1350 }
1351 }
1352
1353 let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
1357 let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
1358 let only_nil_assertions = fixture
1359 .assertions
1360 .iter()
1361 .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1362 .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1363 .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1364 !only_nil_assertions
1365 } else if result_is_simple && has_usable_assertion && result_is_ptr {
1366 true
1367 } else {
1368 result_is_simple && has_usable_assertion
1369 };
1370
1371 let effective_result_var = if has_deref_value {
1372 "value".to_string()
1373 } else {
1374 result_var.to_string()
1375 };
1376
1377 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1382 for assertion in &fixture.assertions {
1383 if let Some(f) = &assertion.field {
1384 if !f.is_empty() {
1385 let resolved = field_resolver.resolve(f);
1386 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1387 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1392 let is_array_field = field_resolver.is_array(resolved);
1393 if !is_string_field || is_array_field {
1394 continue;
1397 }
1398 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1399 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1400 if field_resolver.has_map_access(f) {
1401 let _ = writeln!(out, "\t{local_var} := {field_expr}");
1404 } else {
1405 let _ = writeln!(out, "\tvar {local_var} string");
1406 let _ = writeln!(out, "\tif {field_expr} != nil {{");
1407 let _ = writeln!(out, "\t\t{local_var} = fmt.Sprintf(\"%v\", *{field_expr})");
1411 let _ = writeln!(out, "\t}}");
1412 }
1413 optional_locals.insert(f.clone(), local_var);
1414 }
1415 }
1416 }
1417 }
1418
1419 for assertion in &fixture.assertions {
1421 if let Some(f) = &assertion.field {
1422 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1423 let parts: Vec<&str> = f.split('.').collect();
1426 let mut guard_expr: Option<String> = None;
1427 for i in 1..parts.len() {
1428 let prefix = parts[..i].join(".");
1429 let resolved_prefix = field_resolver.resolve(&prefix);
1430 if field_resolver.is_optional(resolved_prefix) {
1431 let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1437 let suffix = &resolved_prefix[bracket_pos + 1..];
1438 let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1439 if is_numeric_index {
1440 &resolved_prefix[..bracket_pos]
1441 } else {
1442 resolved_prefix
1443 }
1444 } else {
1445 resolved_prefix
1446 };
1447 let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1448 guard_expr = Some(accessor);
1449 break;
1450 }
1451 }
1452 if let Some(guard) = guard_expr {
1453 if field_resolver.is_valid_for_result(f) {
1456 let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
1462 if is_struct_value {
1463 render_assertion(
1466 out,
1467 assertion,
1468 &effective_result_var,
1469 import_alias,
1470 field_resolver,
1471 &optional_locals,
1472 result_is_simple,
1473 result_is_array,
1474 is_streaming,
1475 );
1476 continue;
1477 }
1478 let _ = writeln!(out, "\tif {guard} != nil {{");
1479 let mut nil_buf = String::new();
1482 render_assertion(
1483 &mut nil_buf,
1484 assertion,
1485 &effective_result_var,
1486 import_alias,
1487 field_resolver,
1488 &optional_locals,
1489 result_is_simple,
1490 result_is_array,
1491 is_streaming,
1492 );
1493 for line in nil_buf.lines() {
1494 let _ = writeln!(out, "\t{line}");
1495 }
1496 let _ = writeln!(out, "\t}}");
1497 } else {
1498 render_assertion(
1499 out,
1500 assertion,
1501 &effective_result_var,
1502 import_alias,
1503 field_resolver,
1504 &optional_locals,
1505 result_is_simple,
1506 result_is_array,
1507 is_streaming,
1508 );
1509 }
1510 continue;
1511 }
1512 }
1513 }
1514 render_assertion(
1515 out,
1516 assertion,
1517 &effective_result_var,
1518 import_alias,
1519 field_resolver,
1520 &optional_locals,
1521 result_is_simple,
1522 result_is_array,
1523 is_streaming,
1524 );
1525 }
1526
1527 let _ = writeln!(out, "}}");
1528}
1529
1530fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1536 client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1537}
1538
1539struct GoTestClientRenderer;
1551
1552impl client::TestClientRenderer for GoTestClientRenderer {
1553 fn language_name(&self) -> &'static str {
1554 "go"
1555 }
1556
1557 fn sanitize_test_name(&self, id: &str) -> String {
1561 id.to_upper_camel_case()
1562 }
1563
1564 fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1567 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1568 let _ = writeln!(out, "\t// {description}");
1569 if let Some(reason) = skip_reason {
1570 let escaped = go_string_literal(reason);
1571 let _ = writeln!(out, "\tt.Skip({escaped})");
1572 }
1573 }
1574
1575 fn render_test_close(&self, out: &mut String) {
1576 let _ = writeln!(out, "}}");
1577 }
1578
1579 fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1585 let method = ctx.method.to_uppercase();
1586 let path = ctx.path;
1587
1588 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1589 let _ = writeln!(out, "\tif baseURL == \"\" {{");
1590 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1591 let _ = writeln!(out, "\t}}");
1592
1593 let body_expr = if let Some(body) = ctx.body {
1595 let json = serde_json::to_string(body).unwrap_or_default();
1596 let escaped = go_string_literal(&json);
1597 format!("strings.NewReader({})", escaped)
1598 } else {
1599 "strings.NewReader(\"\")".to_string()
1600 };
1601
1602 let _ = writeln!(out, "\tbody := {body_expr}");
1603 let _ = writeln!(
1604 out,
1605 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1606 );
1607 let _ = writeln!(out, "\tif err != nil {{");
1608 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1609 let _ = writeln!(out, "\t}}");
1610
1611 if ctx.body.is_some() {
1613 let content_type = ctx.content_type.unwrap_or("application/json");
1614 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1615 }
1616
1617 let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1619 header_names.sort();
1620 for name in header_names {
1621 let value = &ctx.headers[name];
1622 let escaped_name = go_string_literal(name);
1623 let escaped_value = go_string_literal(value);
1624 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1625 }
1626
1627 if !ctx.cookies.is_empty() {
1629 let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1630 cookie_names.sort();
1631 for name in cookie_names {
1632 let value = &ctx.cookies[name];
1633 let escaped_name = go_string_literal(name);
1634 let escaped_value = go_string_literal(value);
1635 let _ = writeln!(
1636 out,
1637 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1638 );
1639 }
1640 }
1641
1642 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1644 let _ = writeln!(
1645 out,
1646 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1647 );
1648 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1649 let _ = writeln!(out, "\t\t}},");
1650 let _ = writeln!(out, "\t}}");
1651 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1652 let _ = writeln!(out, "\tif err != nil {{");
1653 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1654 let _ = writeln!(out, "\t}}");
1655 let _ = writeln!(out, "\tdefer resp.Body.Close()");
1656
1657 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1661 let _ = writeln!(out, "\tif err != nil {{");
1662 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1663 let _ = writeln!(out, "\t}}");
1664 let _ = writeln!(out, "\t_ = bodyBytes");
1665 }
1666
1667 fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1668 let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1669 let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1670 let _ = writeln!(out, "\t}}");
1671 }
1672
1673 fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1676 if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1678 return;
1679 }
1680 if name.eq_ignore_ascii_case("connection") {
1682 return;
1683 }
1684 let escaped_name = go_string_literal(name);
1685 let escaped_value = go_string_literal(expected);
1686 let _ = writeln!(
1687 out,
1688 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1689 );
1690 let _ = writeln!(
1691 out,
1692 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1693 );
1694 let _ = writeln!(out, "\t}}");
1695 }
1696
1697 fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1702 match expected {
1703 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1704 let json_str = serde_json::to_string(expected).unwrap_or_default();
1705 let escaped = go_string_literal(&json_str);
1706 let _ = writeln!(out, "\tvar got any");
1707 let _ = writeln!(out, "\tvar want any");
1708 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1709 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1710 let _ = writeln!(out, "\t}}");
1711 let _ = writeln!(
1712 out,
1713 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1714 );
1715 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1716 let _ = writeln!(out, "\t}}");
1717 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1718 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1719 let _ = writeln!(out, "\t}}");
1720 }
1721 serde_json::Value::String(s) => {
1722 let escaped = go_string_literal(s);
1723 let _ = writeln!(out, "\twant := {escaped}");
1724 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1725 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1726 let _ = writeln!(out, "\t}}");
1727 }
1728 other => {
1729 let escaped = go_string_literal(&other.to_string());
1730 let _ = writeln!(out, "\twant := {escaped}");
1731 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1732 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1733 let _ = writeln!(out, "\t}}");
1734 }
1735 }
1736 }
1737
1738 fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1741 if let Some(obj) = expected.as_object() {
1742 let _ = writeln!(out, "\tvar _partialGot map[string]any");
1743 let _ = writeln!(
1744 out,
1745 "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1746 );
1747 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1748 let _ = writeln!(out, "\t}}");
1749 for (key, val) in obj {
1750 let escaped_key = go_string_literal(key);
1751 let json_val = serde_json::to_string(val).unwrap_or_default();
1752 let escaped_val = go_string_literal(&json_val);
1753 let _ = writeln!(out, "\t{{");
1754 let _ = writeln!(out, "\t\tvar _wantVal any");
1755 let _ = writeln!(
1756 out,
1757 "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1758 );
1759 let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1760 let _ = writeln!(out, "\t\t}}");
1761 let _ = writeln!(
1762 out,
1763 "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1764 );
1765 let _ = writeln!(
1766 out,
1767 "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1768 );
1769 let _ = writeln!(out, "\t\t}}");
1770 let _ = writeln!(out, "\t}}");
1771 }
1772 }
1773 }
1774
1775 fn render_assert_validation_errors(
1780 &self,
1781 out: &mut String,
1782 _response_var: &str,
1783 errors: &[ValidationErrorExpectation],
1784 ) {
1785 let _ = writeln!(out, "\tvar _veBody map[string]any");
1786 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1787 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1788 let _ = writeln!(out, "\t}}");
1789 let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1790 for ve in errors {
1791 let escaped_msg = go_string_literal(&ve.msg);
1792 let _ = writeln!(out, "\t{{");
1793 let _ = writeln!(out, "\t\t_found := false");
1794 let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1795 let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1796 let _ = writeln!(
1797 out,
1798 "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1799 );
1800 let _ = writeln!(out, "\t\t\t\t\t_found = true");
1801 let _ = writeln!(out, "\t\t\t\t\tbreak");
1802 let _ = writeln!(out, "\t\t\t\t}}");
1803 let _ = writeln!(out, "\t\t\t}}");
1804 let _ = writeln!(out, "\t\t}}");
1805 let _ = writeln!(out, "\t\tif !_found {{");
1806 let _ = writeln!(
1807 out,
1808 "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1809 );
1810 let _ = writeln!(out, "\t\t}}");
1811 let _ = writeln!(out, "\t}}");
1812 }
1813 }
1814}
1815
1816fn build_args_and_setup(
1824 input: &serde_json::Value,
1825 args: &[crate::config::ArgMapping],
1826 import_alias: &str,
1827 options_type: Option<&str>,
1828 fixture: &crate::fixture::Fixture,
1829 options_ptr: bool,
1830 expects_error: bool,
1831) -> (Vec<String>, String) {
1832 let fixture_id = &fixture.id;
1833 use heck::ToUpperCamelCase;
1834
1835 if args.is_empty() {
1836 return (Vec::new(), String::new());
1837 }
1838
1839 let mut setup_lines: Vec<String> = Vec::new();
1840 let mut parts: Vec<String> = Vec::new();
1841
1842 for arg in args {
1843 if arg.arg_type == "mock_url" {
1844 if fixture.has_host_root_route() {
1845 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1846 setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1847 setup_lines.push(format!(
1848 "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1849 arg.name, arg.name
1850 ));
1851 } else {
1852 setup_lines.push(format!(
1853 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1854 arg.name,
1855 ));
1856 }
1857 parts.push(arg.name.clone());
1858 continue;
1859 }
1860
1861 if arg.arg_type == "mock_url_list" {
1862 let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1867 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1868 let val = input.get(field).unwrap_or(&serde_json::Value::Null);
1869
1870 let paths: Vec<String> = if let Some(arr) = val.as_array() {
1871 arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
1872 } else {
1873 Vec::new()
1874 };
1875
1876 let paths_literal = paths.join(", ");
1877 let var_name = &arg.name;
1878
1879 setup_lines.push(format!(
1880 "{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}}"
1881 ));
1882 setup_lines.push(format!(
1883 "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}}"
1884 ));
1885 parts.push(var_name.to_string());
1886 continue;
1887 }
1888
1889 if arg.arg_type == "handle" {
1890 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1892 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1893 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1894 let create_err_handler = if expects_error {
1898 "assert.Error(t, createErr)\n\t\treturn".to_string()
1899 } else {
1900 "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1901 };
1902 if config_value.is_null()
1903 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1904 {
1905 setup_lines.push(format!(
1906 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1907 name = arg.name,
1908 ));
1909 } else {
1910 let json_str = serde_json::to_string(config_value).unwrap_or_default();
1911 let go_literal = go_string_literal(&json_str);
1912 let name = &arg.name;
1913 setup_lines.push(format!(
1914 "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}}"
1915 ));
1916 setup_lines.push(format!(
1917 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1918 ));
1919 }
1920 parts.push(arg.name.clone());
1921 continue;
1922 }
1923
1924 let val: Option<&serde_json::Value> = if arg.field == "input" {
1925 Some(input)
1926 } else {
1927 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1928 input.get(field)
1929 };
1930
1931 if arg.arg_type == "bytes" {
1938 let var_name = format!("{}Bytes", arg.name);
1939 match val {
1940 None | Some(serde_json::Value::Null) => {
1941 if arg.optional {
1942 parts.push("nil".to_string());
1943 } else {
1944 parts.push("[]byte{}".to_string());
1945 }
1946 }
1947 Some(serde_json::Value::String(s)) => {
1948 let go_path = go_string_literal(s);
1953 setup_lines.push(format!(
1954 "{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}}"
1955 ));
1956 parts.push(var_name);
1957 }
1958 Some(other) => {
1959 parts.push(format!("[]byte({})", json_to_go(other)));
1960 }
1961 }
1962 continue;
1963 }
1964
1965 match val {
1966 None | Some(serde_json::Value::Null) if arg.optional => {
1967 match arg.arg_type.as_str() {
1969 "string" => {
1970 parts.push("nil".to_string());
1972 }
1973 "json_object" => {
1974 if options_ptr {
1975 parts.push("nil".to_string());
1977 } else if let Some(opts_type) = options_type {
1978 parts.push(format!("{import_alias}.{opts_type}{{}}"));
1980 } else {
1981 parts.push("nil".to_string());
1982 }
1983 }
1984 _ => {
1985 parts.push("nil".to_string());
1986 }
1987 }
1988 }
1989 None | Some(serde_json::Value::Null) => {
1990 let default_val = match arg.arg_type.as_str() {
1992 "string" => "\"\"".to_string(),
1993 "int" | "integer" | "i64" => "0".to_string(),
1994 "float" | "number" => "0.0".to_string(),
1995 "bool" | "boolean" => "false".to_string(),
1996 "json_object" => {
1997 if options_ptr {
1998 "nil".to_string()
2000 } else if let Some(opts_type) = options_type {
2001 format!("{import_alias}.{opts_type}{{}}")
2002 } else {
2003 "nil".to_string()
2004 }
2005 }
2006 _ => "nil".to_string(),
2007 };
2008 parts.push(default_val);
2009 }
2010 Some(v) => {
2011 match arg.arg_type.as_str() {
2012 "json_object" => {
2013 let is_array = v.is_array();
2016 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
2017 if is_empty_obj {
2018 if options_ptr {
2019 parts.push("nil".to_string());
2021 } else if let Some(opts_type) = options_type {
2022 parts.push(format!("{import_alias}.{opts_type}{{}}"));
2023 } else {
2024 parts.push("nil".to_string());
2025 }
2026 } else if is_array {
2027 let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
2032 if go_t.starts_with('[') {
2036 go_t.to_string()
2037 } else {
2038 let qualified = if go_t.contains('.') {
2040 go_t.to_string()
2041 } else {
2042 format!("{import_alias}.{go_t}")
2043 };
2044 format!("[]{qualified}")
2045 }
2046 } else {
2047 element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
2048 };
2049 let converted_v = convert_json_for_go(v.clone());
2051 let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
2052 let go_literal = go_string_literal(&json_str);
2053 let var_name = &arg.name;
2054 setup_lines.push(format!(
2055 "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}}"
2056 ));
2057 parts.push(var_name.to_string());
2058 } else if let Some(opts_type) = options_type {
2059 let remapped_v = if options_ptr {
2064 convert_json_for_go(v.clone())
2065 } else {
2066 v.clone()
2067 };
2068 let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
2069 let go_literal = go_string_literal(&json_str);
2070 let var_name = &arg.name;
2071 setup_lines.push(format!(
2072 "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}}"
2073 ));
2074 let arg_expr = if options_ptr {
2076 format!("&{var_name}")
2077 } else {
2078 var_name.to_string()
2079 };
2080 parts.push(arg_expr);
2081 } else {
2082 parts.push(json_to_go(v));
2083 }
2084 }
2085 "string" if arg.optional => {
2086 let var_name = format!("{}Val", arg.name);
2088 let go_val = json_to_go(v);
2089 setup_lines.push(format!("{var_name} := {go_val}"));
2090 parts.push(format!("&{var_name}"));
2091 }
2092 _ => {
2093 parts.push(json_to_go(v));
2094 }
2095 }
2096 }
2097 }
2098 }
2099
2100 (setup_lines, parts.join(", "))
2101}
2102
2103#[allow(clippy::too_many_arguments)]
2104fn render_assertion(
2105 out: &mut String,
2106 assertion: &Assertion,
2107 result_var: &str,
2108 import_alias: &str,
2109 field_resolver: &FieldResolver,
2110 optional_locals: &std::collections::HashMap<String, String>,
2111 result_is_simple: bool,
2112 result_is_array: bool,
2113 is_streaming: bool,
2114) {
2115 if !result_is_simple {
2118 if let Some(f) = &assertion.field {
2119 let embed_deref = format!("(*{result_var})");
2122 match f.as_str() {
2123 "chunks_have_content" => {
2124 let pred = format!(
2125 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
2126 );
2127 match assertion.assertion_type.as_str() {
2128 "is_true" => {
2129 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2130 }
2131 "is_false" => {
2132 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2133 }
2134 _ => {
2135 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2136 }
2137 }
2138 return;
2139 }
2140 "chunks_have_embeddings" => {
2141 let pred = format!(
2142 "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 }}()"
2143 );
2144 match assertion.assertion_type.as_str() {
2145 "is_true" => {
2146 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2147 }
2148 "is_false" => {
2149 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2150 }
2151 _ => {
2152 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2153 }
2154 }
2155 return;
2156 }
2157 "chunks_have_heading_context" => {
2158 let pred = format!(
2159 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
2160 );
2161 match assertion.assertion_type.as_str() {
2162 "is_true" => {
2163 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2164 }
2165 "is_false" => {
2166 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2167 }
2168 _ => {
2169 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2170 }
2171 }
2172 return;
2173 }
2174 "first_chunk_starts_with_heading" => {
2175 let pred = format!(
2176 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
2177 );
2178 match assertion.assertion_type.as_str() {
2179 "is_true" => {
2180 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2181 }
2182 "is_false" => {
2183 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2184 }
2185 _ => {
2186 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2187 }
2188 }
2189 return;
2190 }
2191 "embeddings" => {
2192 match assertion.assertion_type.as_str() {
2193 "count_equals" => {
2194 if let Some(val) = &assertion.value {
2195 if let Some(n) = val.as_u64() {
2196 let _ = writeln!(
2197 out,
2198 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
2199 );
2200 }
2201 }
2202 }
2203 "count_min" => {
2204 if let Some(val) = &assertion.value {
2205 if let Some(n) = val.as_u64() {
2206 let _ = writeln!(
2207 out,
2208 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
2209 );
2210 }
2211 }
2212 }
2213 "not_empty" => {
2214 let _ = writeln!(
2215 out,
2216 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
2217 );
2218 }
2219 "is_empty" => {
2220 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
2221 }
2222 _ => {
2223 let _ = writeln!(
2224 out,
2225 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
2226 );
2227 }
2228 }
2229 return;
2230 }
2231 "embedding_dimensions" => {
2232 let expr = format!(
2233 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
2234 );
2235 match assertion.assertion_type.as_str() {
2236 "equals" => {
2237 if let Some(val) = &assertion.value {
2238 if let Some(n) = val.as_u64() {
2239 let _ = writeln!(
2240 out,
2241 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
2242 );
2243 }
2244 }
2245 }
2246 "greater_than" => {
2247 if let Some(val) = &assertion.value {
2248 if let Some(n) = val.as_u64() {
2249 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2250 }
2251 }
2252 }
2253 _ => {
2254 let _ = writeln!(
2255 out,
2256 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2257 );
2258 }
2259 }
2260 return;
2261 }
2262 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2263 let pred = match f.as_str() {
2264 "embeddings_valid" => {
2265 format!(
2266 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2267 )
2268 }
2269 "embeddings_finite" => {
2270 format!(
2271 "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 }}()"
2272 )
2273 }
2274 "embeddings_non_zero" => {
2275 format!(
2276 "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 }}()"
2277 )
2278 }
2279 "embeddings_normalized" => {
2280 format!(
2281 "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 }}()"
2282 )
2283 }
2284 _ => unreachable!(),
2285 };
2286 match assertion.assertion_type.as_str() {
2287 "is_true" => {
2288 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2289 }
2290 "is_false" => {
2291 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2292 }
2293 _ => {
2294 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2295 }
2296 }
2297 return;
2298 }
2299 "keywords" | "keywords_count" => {
2302 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2303 return;
2304 }
2305 _ => {}
2306 }
2307 }
2308 }
2309
2310 if !result_is_simple && is_streaming {
2317 if let Some(f) = &assertion.field {
2318 if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2319 if let Some(expr) =
2320 crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2321 {
2322 match assertion.assertion_type.as_str() {
2323 "count_min" => {
2324 if let Some(val) = &assertion.value {
2325 if let Some(n) = val.as_u64() {
2326 let _ = writeln!(
2327 out,
2328 "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2329 );
2330 }
2331 }
2332 }
2333 "count_equals" => {
2334 if let Some(val) = &assertion.value {
2335 if let Some(n) = val.as_u64() {
2336 let _ = writeln!(
2337 out,
2338 "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2339 );
2340 }
2341 }
2342 }
2343 "equals" => {
2344 if let Some(serde_json::Value::String(s)) = &assertion.value {
2345 let escaped = crate::escape::go_string_literal(s);
2346 let is_deep_path = f.contains('.') || f.contains('[');
2351 let safe_expr = if is_deep_path {
2352 format!(
2353 "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2354 )
2355 } else {
2356 expr.clone()
2357 };
2358 let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2359 } else if let Some(val) = &assertion.value {
2360 if let Some(n) = val.as_u64() {
2361 let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2362 }
2363 }
2364 }
2365 "not_empty" => {
2366 let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2367 }
2368 "is_empty" => {
2369 let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2370 }
2371 "is_true" => {
2372 let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2373 }
2374 "is_false" => {
2375 let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2376 }
2377 "greater_than" => {
2378 if let Some(val) = &assertion.value {
2379 if let Some(n) = val.as_u64() {
2380 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2381 }
2382 }
2383 }
2384 "greater_than_or_equal" => {
2385 if let Some(val) = &assertion.value {
2386 if let Some(n) = val.as_u64() {
2387 let _ =
2388 writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2389 }
2390 }
2391 }
2392 "contains" => {
2393 if let Some(serde_json::Value::String(s)) = &assertion.value {
2394 let escaped = crate::escape::go_string_literal(s);
2395 let _ =
2396 writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2397 }
2398 }
2399 _ => {
2400 let _ = writeln!(
2401 out,
2402 "\t// streaming field '{f}': assertion type '{}' not rendered",
2403 assertion.assertion_type
2404 );
2405 }
2406 }
2407 }
2408 return;
2409 }
2410 }
2411 }
2412
2413 if !result_is_simple {
2416 if let Some(f) = &assertion.field {
2417 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2418 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2419 return;
2420 }
2421 }
2422 }
2423
2424 let field_expr = if result_is_simple {
2425 result_var.to_string()
2427 } else {
2428 match &assertion.field {
2429 Some(f) if !f.is_empty() => {
2430 if let Some(local_var) = optional_locals.get(f.as_str()) {
2432 local_var.clone()
2433 } else {
2434 field_resolver.accessor(f, "go", result_var)
2435 }
2436 }
2437 _ => result_var.to_string(),
2438 }
2439 };
2440
2441 let is_optional = assertion
2445 .field
2446 .as_ref()
2447 .map(|f| {
2448 let resolved = field_resolver.resolve(f);
2449 let check_path = resolved
2450 .strip_suffix(".length")
2451 .or_else(|| resolved.strip_suffix(".count"))
2452 .or_else(|| resolved.strip_suffix(".size"))
2453 .unwrap_or(resolved);
2454 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2455 })
2456 .unwrap_or(false);
2457
2458 let field_is_array_for_len = assertion
2462 .field
2463 .as_ref()
2464 .map(|f| {
2465 let resolved = field_resolver.resolve(f);
2466 let check_path = resolved
2467 .strip_suffix(".length")
2468 .or_else(|| resolved.strip_suffix(".count"))
2469 .or_else(|| resolved.strip_suffix(".size"))
2470 .unwrap_or(resolved);
2471 field_resolver.is_array(check_path)
2472 })
2473 .unwrap_or(false);
2474 let field_expr =
2475 if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2476 let inner = &field_expr[4..field_expr.len() - 1];
2477 format!("len(*{inner})")
2478 } else {
2479 field_expr
2480 };
2481 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2483 Some(field_expr[5..field_expr.len() - 1].to_string())
2484 } else {
2485 None
2486 };
2487
2488 let field_is_slice = assertion
2492 .field
2493 .as_ref()
2494 .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2495 .unwrap_or(false);
2496 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2497 format!("*{field_expr}")
2498 } else {
2499 field_expr.clone()
2500 };
2501
2502 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2507 let mut array_expr = field_expr[..idx].to_string();
2508 if let Some(stripped) = array_expr.strip_prefix("len(") {
2509 array_expr = stripped.to_string();
2510 }
2511 Some(array_expr)
2512 } else {
2513 None
2514 };
2515
2516 let mut assertion_buf = String::new();
2519 let out_ref = &mut assertion_buf;
2520
2521 match assertion.assertion_type.as_str() {
2522 "equals" => {
2523 if let Some(expected) = &assertion.value {
2524 let go_val = json_to_go(expected);
2525 if expected.is_string() {
2527 let resolved_name = assertion
2531 .field
2532 .as_ref()
2533 .map(|f| field_resolver.resolve(f))
2534 .unwrap_or_default();
2535 let is_struct = resolved_name.contains("FormatMetadata");
2536 let trimmed_field = if is_struct {
2537 if is_optional && !field_expr.starts_with("len(") {
2539 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", *{field_expr}))")
2540 } else {
2541 format!("strings.TrimSpace(fmt.Sprintf(\"%v\", {field_expr}))")
2542 }
2543 } else if is_optional && !field_expr.starts_with("len(") {
2544 format!("strings.TrimSpace(string(*{field_expr}))")
2545 } else {
2546 format!("strings.TrimSpace(string({field_expr}))")
2547 };
2548 if is_optional && !field_expr.starts_with("len(") {
2549 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2550 } else {
2551 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2552 }
2553 } else if is_optional && !field_expr.starts_with("len(") {
2554 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2555 } else {
2556 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2557 }
2558 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2559 let _ = writeln!(out_ref, "\t}}");
2560 }
2561 }
2562 "contains" => {
2563 if let Some(expected) = &assertion.value {
2564 let go_val = json_to_go(expected);
2565 let resolved_field = assertion.field.as_deref().unwrap_or("");
2571 let resolved_name = field_resolver.resolve(resolved_field);
2572 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2573 let is_opt =
2574 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2575 let field_for_contains = if is_opt && field_is_array {
2576 format!("jsonString({field_expr})")
2578 } else if is_opt {
2579 format!("fmt.Sprint(*{field_expr})")
2580 } else if field_is_array {
2581 format!("jsonString({field_expr})")
2582 } else {
2583 format!("fmt.Sprint({field_expr})")
2584 };
2585 if is_opt {
2586 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2587 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2588 let _ = writeln!(
2589 out_ref,
2590 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2591 );
2592 let _ = writeln!(out_ref, "\t}}");
2593 let _ = writeln!(out_ref, "\t}}");
2594 } else {
2595 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2596 let _ = writeln!(
2597 out_ref,
2598 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2599 );
2600 let _ = writeln!(out_ref, "\t}}");
2601 }
2602 }
2603 }
2604 "contains_all" => {
2605 if let Some(values) = &assertion.values {
2606 let resolved_field = assertion.field.as_deref().unwrap_or("");
2607 let resolved_name = field_resolver.resolve(resolved_field);
2608 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2609 let is_opt =
2610 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2611 for val in values {
2612 let go_val = json_to_go(val);
2613 let field_for_contains = if is_opt && field_is_array {
2614 format!("jsonString({field_expr})")
2616 } else if is_opt {
2617 format!("fmt.Sprint(*{field_expr})")
2618 } else if field_is_array {
2619 format!("jsonString({field_expr})")
2620 } else {
2621 format!("fmt.Sprint({field_expr})")
2622 };
2623 if is_opt {
2624 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2625 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2626 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2627 let _ = writeln!(out_ref, "\t}}");
2628 let _ = writeln!(out_ref, "\t}}");
2629 } else {
2630 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2631 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2632 let _ = writeln!(out_ref, "\t}}");
2633 }
2634 }
2635 }
2636 }
2637 "not_contains" => {
2638 if let Some(expected) = &assertion.value {
2639 let go_val = json_to_go(expected);
2640 let resolved_field = assertion.field.as_deref().unwrap_or("");
2641 let resolved_name = field_resolver.resolve(resolved_field);
2642 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2643 let is_opt =
2644 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2645 let field_for_contains = if is_opt && field_is_array {
2646 format!("jsonString({field_expr})")
2648 } else if is_opt {
2649 format!("fmt.Sprint(*{field_expr})")
2650 } else if field_is_array {
2651 format!("jsonString({field_expr})")
2652 } else {
2653 format!("fmt.Sprint({field_expr})")
2654 };
2655 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2656 let _ = writeln!(
2657 out_ref,
2658 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2659 );
2660 let _ = writeln!(out_ref, "\t}}");
2661 }
2662 }
2663 "not_empty" => {
2664 let field_is_array = {
2667 let rf = assertion.field.as_deref().unwrap_or("");
2668 let rn = field_resolver.resolve(rf);
2669 field_resolver.is_array(rn)
2670 };
2671 if is_optional && !field_is_array {
2672 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2674 } else if is_optional && field_is_slice {
2675 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2677 } else if is_optional {
2678 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2680 } else if result_is_simple && result_is_array {
2681 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2683 } else {
2684 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2685 }
2686 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2687 let _ = writeln!(out_ref, "\t}}");
2688 }
2689 "is_empty" => {
2690 let field_is_array = {
2691 let rf = assertion.field.as_deref().unwrap_or("");
2692 let rn = field_resolver.resolve(rf);
2693 field_resolver.is_array(rn)
2694 };
2695 if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2698 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2700 } else if is_optional && !field_is_array {
2701 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2703 } else if is_optional && field_is_slice {
2704 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2706 } else if is_optional {
2707 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2709 } else {
2710 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2711 }
2712 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2713 let _ = writeln!(out_ref, "\t}}");
2714 }
2715 "contains_any" => {
2716 if let Some(values) = &assertion.values {
2717 let resolved_field = assertion.field.as_deref().unwrap_or("");
2718 let resolved_name = field_resolver.resolve(resolved_field);
2719 let field_is_array = field_resolver.is_array(resolved_name);
2720 let is_opt =
2721 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2722 let field_for_contains = if is_opt && field_is_array {
2723 format!("jsonString({field_expr})")
2725 } else if is_opt {
2726 format!("fmt.Sprint(*{field_expr})")
2727 } else if field_is_array {
2728 format!("jsonString({field_expr})")
2729 } else {
2730 format!("fmt.Sprint({field_expr})")
2731 };
2732 let _ = writeln!(out_ref, "\t{{");
2733 let _ = writeln!(out_ref, "\t\tfound := false");
2734 for val in values {
2735 let go_val = json_to_go(val);
2736 let _ = writeln!(
2737 out_ref,
2738 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2739 );
2740 }
2741 let _ = writeln!(out_ref, "\t\tif !found {{");
2742 let _ = writeln!(
2743 out_ref,
2744 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2745 );
2746 let _ = writeln!(out_ref, "\t\t}}");
2747 let _ = writeln!(out_ref, "\t}}");
2748 }
2749 }
2750 "greater_than" => {
2751 if let Some(val) = &assertion.value {
2752 let go_val = json_to_go(val);
2753 if is_optional {
2757 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2758 if let Some(n) = val.as_u64() {
2759 let next = n + 1;
2760 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2761 } else {
2762 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2763 }
2764 let _ = writeln!(
2765 out_ref,
2766 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2767 );
2768 let _ = writeln!(out_ref, "\t\t}}");
2769 let _ = writeln!(out_ref, "\t}}");
2770 } else if let Some(n) = val.as_u64() {
2771 let next = n + 1;
2772 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2773 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2774 let _ = writeln!(out_ref, "\t}}");
2775 } else {
2776 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2777 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2778 let _ = writeln!(out_ref, "\t}}");
2779 }
2780 }
2781 }
2782 "less_than" => {
2783 if let Some(val) = &assertion.value {
2784 let go_val = json_to_go(val);
2785 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2786 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2787 let _ = writeln!(out_ref, "\t}}");
2788 }
2789 }
2790 "greater_than_or_equal" => {
2791 if let Some(val) = &assertion.value {
2792 let go_val = json_to_go(val);
2793 if let Some(ref guard) = nil_guard_expr {
2794 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2795 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2796 let _ = writeln!(
2797 out_ref,
2798 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2799 );
2800 let _ = writeln!(out_ref, "\t\t}}");
2801 let _ = writeln!(out_ref, "\t}}");
2802 } else if is_optional && !field_expr.starts_with("len(") {
2803 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2805 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2806 let _ = writeln!(
2807 out_ref,
2808 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2809 );
2810 let _ = writeln!(out_ref, "\t\t}}");
2811 let _ = writeln!(out_ref, "\t}}");
2812 } else {
2813 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2814 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2815 let _ = writeln!(out_ref, "\t}}");
2816 }
2817 }
2818 }
2819 "less_than_or_equal" => {
2820 if let Some(val) = &assertion.value {
2821 let go_val = json_to_go(val);
2822 if is_optional && !field_expr.starts_with("len(") {
2823 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2825 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
2826 let _ = writeln!(
2827 out_ref,
2828 "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
2829 );
2830 let _ = writeln!(out_ref, "\t\t}}");
2831 let _ = writeln!(out_ref, "\t}}");
2832 } else {
2833 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2834 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2835 let _ = writeln!(out_ref, "\t}}");
2836 }
2837 }
2838 }
2839 "starts_with" => {
2840 if let Some(expected) = &assertion.value {
2841 let go_val = json_to_go(expected);
2842 let field_for_prefix = if is_optional
2843 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2844 {
2845 format!("string(*{field_expr})")
2846 } else {
2847 format!("string({field_expr})")
2848 };
2849 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2850 let _ = writeln!(
2851 out_ref,
2852 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2853 );
2854 let _ = writeln!(out_ref, "\t}}");
2855 }
2856 }
2857 "count_min" => {
2858 if let Some(val) = &assertion.value {
2859 if let Some(n) = val.as_u64() {
2860 if is_optional {
2861 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2862 let len_expr = if field_is_slice {
2864 format!("len({field_expr})")
2865 } else {
2866 format!("len(*{field_expr})")
2867 };
2868 let _ = writeln!(
2869 out_ref,
2870 "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2871 );
2872 let _ = writeln!(out_ref, "\t}}");
2873 } else {
2874 let _ = writeln!(
2875 out_ref,
2876 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2877 );
2878 }
2879 }
2880 }
2881 }
2882 "count_equals" => {
2883 if let Some(val) = &assertion.value {
2884 if let Some(n) = val.as_u64() {
2885 if is_optional {
2886 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2887 let len_expr = if field_is_slice {
2889 format!("len({field_expr})")
2890 } else {
2891 format!("len(*{field_expr})")
2892 };
2893 let _ = writeln!(
2894 out_ref,
2895 "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2896 );
2897 let _ = writeln!(out_ref, "\t}}");
2898 } else {
2899 let _ = writeln!(
2900 out_ref,
2901 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2902 );
2903 }
2904 }
2905 }
2906 }
2907 "is_true" => {
2908 if is_optional {
2909 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2910 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2911 let _ = writeln!(out_ref, "\t}}");
2912 } else {
2913 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2914 }
2915 }
2916 "is_false" => {
2917 if is_optional {
2918 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2919 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2920 let _ = writeln!(out_ref, "\t}}");
2921 } else {
2922 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2923 }
2924 }
2925 "method_result" => {
2926 if let Some(method_name) = &assertion.method {
2927 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2928 let check = assertion.check.as_deref().unwrap_or("is_true");
2929 let deref_expr = if info.is_pointer {
2932 format!("*{}", info.call_expr)
2933 } else {
2934 info.call_expr.clone()
2935 };
2936 match check {
2937 "equals" => {
2938 if let Some(val) = &assertion.value {
2939 if val.is_boolean() {
2940 if val.as_bool() == Some(true) {
2941 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2942 } else {
2943 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2944 }
2945 } else {
2946 let go_val = if let Some(cast) = info.value_cast {
2950 if val.is_number() {
2951 format!("{cast}({})", json_to_go(val))
2952 } else {
2953 json_to_go(val)
2954 }
2955 } else {
2956 json_to_go(val)
2957 };
2958 let _ = writeln!(
2959 out_ref,
2960 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2961 );
2962 }
2963 }
2964 }
2965 "is_true" => {
2966 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2967 }
2968 "is_false" => {
2969 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2970 }
2971 "greater_than_or_equal" => {
2972 if let Some(val) = &assertion.value {
2973 let n = val.as_u64().unwrap_or(0);
2974 let cast = info.value_cast.unwrap_or("uint");
2976 let _ = writeln!(
2977 out_ref,
2978 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2979 );
2980 }
2981 }
2982 "count_min" => {
2983 if let Some(val) = &assertion.value {
2984 let n = val.as_u64().unwrap_or(0);
2985 let _ = writeln!(
2986 out_ref,
2987 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2988 );
2989 }
2990 }
2991 "contains" => {
2992 if let Some(val) = &assertion.value {
2993 let go_val = json_to_go(val);
2994 let _ = writeln!(
2995 out_ref,
2996 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2997 );
2998 }
2999 }
3000 "is_error" => {
3001 let _ = writeln!(out_ref, "\t{{");
3002 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
3003 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
3004 let _ = writeln!(out_ref, "\t}}");
3005 }
3006 other_check => {
3007 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
3008 }
3009 }
3010 } else {
3011 panic!("Go e2e generator: method_result assertion missing 'method' field");
3012 }
3013 }
3014 "min_length" => {
3015 if let Some(val) = &assertion.value {
3016 if let Some(n) = val.as_u64() {
3017 if is_optional {
3018 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3019 let _ = writeln!(
3020 out_ref,
3021 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
3022 );
3023 let _ = writeln!(out_ref, "\t}}");
3024 } else if field_expr.starts_with("len(") {
3025 let _ = writeln!(
3026 out_ref,
3027 "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
3028 );
3029 } else {
3030 let _ = writeln!(
3031 out_ref,
3032 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
3033 );
3034 }
3035 }
3036 }
3037 }
3038 "max_length" => {
3039 if let Some(val) = &assertion.value {
3040 if let Some(n) = val.as_u64() {
3041 if is_optional {
3042 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
3043 let _ = writeln!(
3044 out_ref,
3045 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
3046 );
3047 let _ = writeln!(out_ref, "\t}}");
3048 } else if field_expr.starts_with("len(") {
3049 let _ = writeln!(
3050 out_ref,
3051 "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
3052 );
3053 } else {
3054 let _ = writeln!(
3055 out_ref,
3056 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
3057 );
3058 }
3059 }
3060 }
3061 }
3062 "ends_with" => {
3063 if let Some(expected) = &assertion.value {
3064 let go_val = json_to_go(expected);
3065 let field_for_suffix = if is_optional
3066 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3067 {
3068 format!("string(*{field_expr})")
3069 } else {
3070 format!("string({field_expr})")
3071 };
3072 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
3073 let _ = writeln!(
3074 out_ref,
3075 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
3076 );
3077 let _ = writeln!(out_ref, "\t}}");
3078 }
3079 }
3080 "matches_regex" => {
3081 if let Some(expected) = &assertion.value {
3082 let go_val = json_to_go(expected);
3083 let field_for_regex = if is_optional
3084 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
3085 {
3086 format!("*{field_expr}")
3087 } else {
3088 field_expr.clone()
3089 };
3090 let _ = writeln!(
3091 out_ref,
3092 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
3093 );
3094 }
3095 }
3096 "not_error" => {
3097 }
3099 "error" => {
3100 }
3102 other => {
3103 panic!("Go e2e generator: unsupported assertion type: {other}");
3104 }
3105 }
3106
3107 if let Some(ref arr) = array_guard {
3110 if !assertion_buf.is_empty() {
3111 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
3112 for line in assertion_buf.lines() {
3114 let _ = writeln!(out, "\t{line}");
3115 }
3116 let _ = writeln!(out, "\t}}");
3117 }
3118 } else {
3119 out.push_str(&assertion_buf);
3120 }
3121}
3122
3123struct GoMethodCallInfo {
3125 call_expr: String,
3127 is_pointer: bool,
3129 value_cast: Option<&'static str>,
3132}
3133
3134fn build_go_method_call(
3149 result_var: &str,
3150 method_name: &str,
3151 args: Option<&serde_json::Value>,
3152 import_alias: &str,
3153) -> GoMethodCallInfo {
3154 match method_name {
3155 "root_node_type" => GoMethodCallInfo {
3156 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
3157 is_pointer: false,
3158 value_cast: None,
3159 },
3160 "named_children_count" => GoMethodCallInfo {
3161 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
3162 is_pointer: false,
3163 value_cast: Some("uint"),
3164 },
3165 "has_error_nodes" => GoMethodCallInfo {
3166 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
3167 is_pointer: true,
3168 value_cast: None,
3169 },
3170 "error_count" | "tree_error_count" => GoMethodCallInfo {
3171 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
3172 is_pointer: true,
3173 value_cast: Some("uint"),
3174 },
3175 "tree_to_sexp" => GoMethodCallInfo {
3176 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
3177 is_pointer: true,
3178 value_cast: None,
3179 },
3180 "contains_node_type" => {
3181 let node_type = args
3182 .and_then(|a| a.get("node_type"))
3183 .and_then(|v| v.as_str())
3184 .unwrap_or("");
3185 GoMethodCallInfo {
3186 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
3187 is_pointer: true,
3188 value_cast: None,
3189 }
3190 }
3191 "find_nodes_by_type" => {
3192 let node_type = args
3193 .and_then(|a| a.get("node_type"))
3194 .and_then(|v| v.as_str())
3195 .unwrap_or("");
3196 GoMethodCallInfo {
3197 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
3198 is_pointer: true,
3199 value_cast: None,
3200 }
3201 }
3202 "run_query" => {
3203 let query_source = args
3204 .and_then(|a| a.get("query_source"))
3205 .and_then(|v| v.as_str())
3206 .unwrap_or("");
3207 let language = args
3208 .and_then(|a| a.get("language"))
3209 .and_then(|v| v.as_str())
3210 .unwrap_or("");
3211 let query_lit = go_string_literal(query_source);
3212 let lang_lit = go_string_literal(language);
3213 GoMethodCallInfo {
3215 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
3216 is_pointer: false,
3217 value_cast: None,
3218 }
3219 }
3220 other => {
3221 let method_pascal = other.to_upper_camel_case();
3222 GoMethodCallInfo {
3223 call_expr: format!("{result_var}.{method_pascal}()"),
3224 is_pointer: false,
3225 value_cast: None,
3226 }
3227 }
3228 }
3229}
3230
3231fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
3241 match value {
3242 serde_json::Value::Object(map) => {
3243 let new_map: serde_json::Map<String, serde_json::Value> = map
3244 .into_iter()
3245 .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
3246 .collect();
3247 serde_json::Value::Object(new_map)
3248 }
3249 serde_json::Value::Array(arr) => {
3250 if is_byte_array(&arr) {
3253 let bytes: Vec<u8> = arr
3254 .iter()
3255 .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
3256 .collect();
3257 let encoded = base64_encode(&bytes);
3259 serde_json::Value::String(encoded)
3260 } else {
3261 serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
3262 }
3263 }
3264 serde_json::Value::String(s) => {
3265 serde_json::Value::String(pascal_to_snake_case(&s))
3268 }
3269 other => other,
3270 }
3271}
3272
3273fn is_byte_array(arr: &[serde_json::Value]) -> bool {
3275 if arr.is_empty() {
3276 return false;
3277 }
3278 arr.iter().all(|v| {
3279 if let serde_json::Value::Number(n) = v {
3280 n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3281 } else {
3282 false
3283 }
3284 })
3285}
3286
3287fn base64_encode(bytes: &[u8]) -> String {
3290 const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3291 let mut result = String::new();
3292 let mut i = 0;
3293
3294 while i + 2 < bytes.len() {
3295 let b1 = bytes[i];
3296 let b2 = bytes[i + 1];
3297 let b3 = bytes[i + 2];
3298
3299 result.push(TABLE[(b1 >> 2) as usize] as char);
3300 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3301 result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3302 result.push(TABLE[(b3 & 0x3f) as usize] as char);
3303
3304 i += 3;
3305 }
3306
3307 if i < bytes.len() {
3309 let b1 = bytes[i];
3310 result.push(TABLE[(b1 >> 2) as usize] as char);
3311
3312 if i + 1 < bytes.len() {
3313 let b2 = bytes[i + 1];
3314 result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3315 result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3316 result.push('=');
3317 } else {
3318 result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3319 result.push_str("==");
3320 }
3321 }
3322
3323 result
3324}
3325
3326fn camel_to_snake_case(s: &str) -> String {
3328 let mut result = String::new();
3329 let mut prev_upper = false;
3330 for (i, c) in s.char_indices() {
3331 if c.is_uppercase() {
3332 if i > 0 && !prev_upper {
3333 result.push('_');
3334 }
3335 result.push(c.to_lowercase().next().unwrap_or(c));
3336 prev_upper = true;
3337 } else {
3338 if prev_upper && i > 1 {
3339 }
3343 result.push(c);
3344 prev_upper = false;
3345 }
3346 }
3347 result
3348}
3349
3350fn pascal_to_snake_case(s: &str) -> String {
3355 let first_char = s.chars().next();
3357 if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3358 return s.to_string();
3359 }
3360 camel_to_snake_case(s)
3361}
3362
3363fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3367 let elem = element_type.unwrap_or("String").trim();
3368 let go_elem = rust_type_to_go(elem, import_alias);
3369 format!("[]{go_elem}")
3370}
3371
3372fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3375 let trimmed = rust.trim();
3376 if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3377 return format!("[]{}", rust_type_to_go(inner, import_alias));
3378 }
3379 match trimmed {
3380 "String" | "&str" | "str" => "string".to_string(),
3381 "bool" => "bool".to_string(),
3382 "f32" => "float32".to_string(),
3383 "f64" => "float64".to_string(),
3384 "i8" => "int8".to_string(),
3385 "i16" => "int16".to_string(),
3386 "i32" => "int32".to_string(),
3387 "i64" | "isize" => "int64".to_string(),
3388 "u8" => "uint8".to_string(),
3389 "u16" => "uint16".to_string(),
3390 "u32" => "uint32".to_string(),
3391 "u64" | "usize" => "uint64".to_string(),
3392 _ => format!("{import_alias}.{trimmed}"),
3393 }
3394}
3395
3396fn json_to_go(value: &serde_json::Value) -> String {
3397 match value {
3398 serde_json::Value::String(s) => go_string_literal(s),
3399 serde_json::Value::Bool(b) => b.to_string(),
3400 serde_json::Value::Number(n) => n.to_string(),
3401 serde_json::Value::Null => "nil".to_string(),
3402 other => go_string_literal(&other.to_string()),
3404 }
3405}
3406
3407fn visitor_struct_name(fixture_id: &str) -> String {
3416 use heck::ToUpperCamelCase;
3417 format!("testVisitor{}", fixture_id.to_upper_camel_case())
3419}
3420
3421fn emit_go_visitor_struct(
3426 out: &mut String,
3427 struct_name: &str,
3428 visitor_spec: &crate::fixture::VisitorSpec,
3429 import_alias: &str,
3430) {
3431 let _ = writeln!(out, "type {struct_name} struct{{");
3432 let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3433 let _ = writeln!(out, "}}");
3434 for (method_name, action) in &visitor_spec.callbacks {
3435 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3436 }
3437}
3438
3439fn emit_go_visitor_method(
3441 out: &mut String,
3442 struct_name: &str,
3443 method_name: &str,
3444 action: &CallbackAction,
3445 import_alias: &str,
3446) {
3447 let camel_method = method_to_camel(method_name);
3448 let params = match method_name {
3451 "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3452 "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3453 "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3454 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3455 "visit_code_inline"
3456 | "visit_strong"
3457 | "visit_emphasis"
3458 | "visit_strikethrough"
3459 | "visit_underline"
3460 | "visit_subscript"
3461 | "visit_superscript"
3462 | "visit_mark"
3463 | "visit_button"
3464 | "visit_summary"
3465 | "visit_figcaption"
3466 | "visit_definition_term"
3467 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3468 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3469 "visit_list_item" => {
3470 format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3471 }
3472 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3473 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3474 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3475 "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3476 "visit_input" => {
3477 format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3478 }
3479 "visit_audio" | "visit_video" | "visit_iframe" => {
3480 format!("_ {import_alias}.NodeContext, src *string")
3481 }
3482 "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3483 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3484 format!("_ {import_alias}.NodeContext, output string")
3485 }
3486 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3487 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3488 _ => format!("_ {import_alias}.NodeContext"),
3489 };
3490
3491 let _ = writeln!(
3492 out,
3493 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3494 );
3495 match action {
3496 CallbackAction::Skip => {
3497 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3498 }
3499 CallbackAction::Continue => {
3500 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3501 }
3502 CallbackAction::PreserveHtml => {
3503 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3504 }
3505 CallbackAction::Custom { output } => {
3506 let escaped = go_string_literal(output);
3507 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3508 }
3509 CallbackAction::CustomTemplate { template, .. } => {
3510 let ptr_params = go_visitor_ptr_params(method_name);
3517 let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3518 let escaped_fmt = go_string_literal(&fmt_str);
3519 if fmt_args.is_empty() {
3520 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3521 } else {
3522 let args_str = fmt_args.join(", ");
3523 let _ = writeln!(
3524 out,
3525 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3526 );
3527 }
3528 }
3529 }
3530 let _ = writeln!(out, "}}");
3531}
3532
3533fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3536 match method_name {
3537 "visit_link" => ["title"].into(),
3538 "visit_image" => ["title"].into(),
3539 "visit_heading" => ["id"].into(),
3540 "visit_code_block" => ["lang"].into(),
3541 "visit_form" => ["action", "method"].into(),
3542 "visit_input" => ["name", "value"].into(),
3543 "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3544 _ => std::collections::HashSet::new(),
3545 }
3546}
3547
3548fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3560 let mut fmt_str = String::new();
3561 let mut args: Vec<String> = Vec::new();
3562 let mut chars = template.chars().peekable();
3563 while let Some(c) = chars.next() {
3564 if c == '{' {
3565 let mut name = String::new();
3567 for inner in chars.by_ref() {
3568 if inner == '}' {
3569 break;
3570 }
3571 name.push(inner);
3572 }
3573 fmt_str.push_str("%s");
3574 let go_name = go_param_name(&name);
3576 let arg_expr = if ptr_params.contains(go_name.as_str()) {
3578 format!("*{go_name}")
3579 } else {
3580 go_name
3581 };
3582 args.push(arg_expr);
3583 } else {
3584 fmt_str.push(c);
3585 }
3586 }
3587 (fmt_str, args)
3588}
3589
3590fn method_to_camel(snake: &str) -> String {
3592 use heck::ToUpperCamelCase;
3593 snake.to_upper_camel_case()
3594}
3595
3596#[cfg(test)]
3597mod tests {
3598 use super::*;
3599 use crate::config::{CallConfig, E2eConfig};
3600 use crate::fixture::{Assertion, Fixture};
3601
3602 fn make_fixture(id: &str) -> Fixture {
3603 Fixture {
3604 id: id.to_string(),
3605 category: None,
3606 description: "test fixture".to_string(),
3607 tags: vec![],
3608 skip: None,
3609 env: None,
3610 call: None,
3611 input: serde_json::Value::Null,
3612 mock_response: Some(crate::fixture::MockResponse {
3613 status: 200,
3614 body: Some(serde_json::Value::Null),
3615 stream_chunks: None,
3616 headers: std::collections::HashMap::new(),
3617 }),
3618 source: String::new(),
3619 http: None,
3620 assertions: vec![Assertion {
3621 assertion_type: "not_error".to_string(),
3622 ..Default::default()
3623 }],
3624 visitor: None,
3625 }
3626 }
3627
3628 #[test]
3632 fn test_go_method_name_uses_go_casing() {
3633 let e2e_config = E2eConfig {
3634 call: CallConfig {
3635 function: "clean_extracted_text".to_string(),
3636 module: "github.com/example/mylib".to_string(),
3637 result_var: "result".to_string(),
3638 returns_result: true,
3639 ..CallConfig::default()
3640 },
3641 ..E2eConfig::default()
3642 };
3643
3644 let fixture = make_fixture("basic_text");
3645 let mut out = String::new();
3646 render_test_function(&mut out, &fixture, "kreuzberg", &e2e_config);
3647
3648 assert!(
3649 out.contains("kreuzberg.CleanExtractedText("),
3650 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3651 );
3652 assert!(
3653 !out.contains("kreuzberg.clean_extracted_text("),
3654 "must not emit raw snake_case method name, got:\n{out}"
3655 );
3656 }
3657
3658 #[test]
3659 fn test_streaming_fixture_emits_collect_snippet() {
3660 let streaming_fixture_json = r#"{
3662 "id": "basic_stream",
3663 "description": "basic streaming test",
3664 "call": "chat_stream",
3665 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3666 "mock_response": {
3667 "status": 200,
3668 "stream_chunks": [{"delta": "hello"}]
3669 },
3670 "assertions": [
3671 {"type": "count_min", "field": "chunks", "value": 1}
3672 ]
3673 }"#;
3674 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3675 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3676
3677 let e2e_config = E2eConfig {
3678 call: CallConfig {
3679 function: "chat_stream".to_string(),
3680 module: "github.com/example/mylib".to_string(),
3681 result_var: "result".to_string(),
3682 returns_result: true,
3683 r#async: true,
3684 ..CallConfig::default()
3685 },
3686 ..E2eConfig::default()
3687 };
3688
3689 let mut out = String::new();
3690 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3691
3692 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3693 assert!(
3694 out.contains("for chunk := range stream"),
3695 "should emit collect loop, got:\n{out}"
3696 );
3697 }
3698
3699 #[test]
3700 fn test_streaming_with_client_factory_and_json_arg() {
3701 use alef_core::config::e2e::{ArgMapping, CallOverride};
3705 let streaming_fixture_json = r#"{
3706 "id": "basic_stream_client",
3707 "description": "basic streaming test with client",
3708 "call": "chat_stream",
3709 "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3710 "mock_response": {
3711 "status": 200,
3712 "stream_chunks": [{"delta": "hello"}]
3713 },
3714 "assertions": [
3715 {"type": "count_min", "field": "chunks", "value": 1}
3716 ]
3717 }"#;
3718 let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3719 assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3720
3721 let go_override = CallOverride {
3722 client_factory: Some("CreateClient".to_string()),
3723 ..Default::default()
3724 };
3725
3726 let mut call_overrides = std::collections::HashMap::new();
3727 call_overrides.insert("go".to_string(), go_override);
3728
3729 let e2e_config = E2eConfig {
3730 call: CallConfig {
3731 function: "chat_stream".to_string(),
3732 module: "github.com/example/mylib".to_string(),
3733 result_var: "result".to_string(),
3734 returns_result: false, r#async: true,
3736 args: vec![ArgMapping {
3737 name: "request".to_string(),
3738 field: "input".to_string(),
3739 arg_type: "json_object".to_string(),
3740 optional: false,
3741 owned: true,
3742 element_type: None,
3743 go_type: None,
3744 }],
3745 overrides: call_overrides,
3746 ..CallConfig::default()
3747 },
3748 ..E2eConfig::default()
3749 };
3750
3751 let mut out = String::new();
3752 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3753
3754 eprintln!("generated:\n{out}");
3755 assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3756 assert!(
3757 out.contains("for chunk := range stream"),
3758 "should emit collect loop, got:\n{out}"
3759 );
3760 }
3761
3762 #[test]
3766 fn test_indexed_element_prefix_guard_uses_array_not_element() {
3767 let mut optional_fields = std::collections::HashSet::new();
3768 optional_fields.insert("segments".to_string());
3769 let mut array_fields = std::collections::HashSet::new();
3770 array_fields.insert("segments".to_string());
3771
3772 let e2e_config = E2eConfig {
3773 call: CallConfig {
3774 function: "transcribe".to_string(),
3775 module: "github.com/example/mylib".to_string(),
3776 result_var: "result".to_string(),
3777 returns_result: true,
3778 ..CallConfig::default()
3779 },
3780 fields_optional: optional_fields,
3781 fields_array: array_fields,
3782 ..E2eConfig::default()
3783 };
3784
3785 let fixture = Fixture {
3786 id: "edge_transcribe_with_timestamps".to_string(),
3787 category: None,
3788 description: "Transcription with timestamp segments".to_string(),
3789 tags: vec![],
3790 skip: None,
3791 env: None,
3792 call: None,
3793 input: serde_json::Value::Null,
3794 mock_response: Some(crate::fixture::MockResponse {
3795 status: 200,
3796 body: Some(serde_json::Value::Null),
3797 stream_chunks: None,
3798 headers: std::collections::HashMap::new(),
3799 }),
3800 source: String::new(),
3801 http: None,
3802 assertions: vec![
3803 Assertion {
3804 assertion_type: "not_error".to_string(),
3805 ..Default::default()
3806 },
3807 Assertion {
3808 assertion_type: "equals".to_string(),
3809 field: Some("segments[0].id".to_string()),
3810 value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3811 ..Default::default()
3812 },
3813 ],
3814 visitor: None,
3815 };
3816
3817 let mut out = String::new();
3818 render_test_function(&mut out, &fixture, "pkg", &e2e_config);
3819
3820 eprintln!("generated:\n{out}");
3821
3822 assert!(
3824 out.contains("result.Segments != nil"),
3825 "guard must be on Segments (the slice), not an element; got:\n{out}"
3826 );
3827 assert!(
3829 !out.contains("result.Segments[0] != nil"),
3830 "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3831 );
3832 }
3833}