1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, HttpFixture};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::AlefConfig;
10use alef_core::hash::{self, CommentStyle};
11use anyhow::Result;
12use heck::ToUpperCamelCase;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16use super::E2eCodegen;
17
18pub struct GoCodegen;
20
21impl E2eCodegen for GoCodegen {
22 fn generate(
23 &self,
24 groups: &[FixtureGroup],
25 e2e_config: &E2eConfig,
26 alef_config: &AlefConfig,
27 ) -> Result<Vec<GeneratedFile>> {
28 let lang = self.language_name();
29 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
30
31 let mut files = Vec::new();
32
33 let call = &e2e_config.call;
35 let overrides = call.overrides.get(lang);
36 let module_path = overrides
37 .and_then(|o| o.module.as_ref())
38 .cloned()
39 .unwrap_or_else(|| call.module.clone());
40 let import_alias = overrides
41 .and_then(|o| o.alias.as_ref())
42 .cloned()
43 .unwrap_or_else(|| "pkg".to_string());
44
45 let go_pkg = e2e_config.resolve_package("go");
47 let go_module_path = go_pkg
48 .as_ref()
49 .and_then(|p| p.module.as_ref())
50 .cloned()
51 .unwrap_or_else(|| module_path.clone());
52 let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
53 let go_version = go_pkg
54 .as_ref()
55 .and_then(|p| p.version.as_ref())
56 .cloned()
57 .unwrap_or_else(|| {
58 alef_config
59 .resolved_version()
60 .map(|v| format!("v{v}"))
61 .unwrap_or_else(|| "v0.0.0".to_string())
62 });
63 let field_resolver = FieldResolver::new(
64 &e2e_config.fields,
65 &e2e_config.fields_optional,
66 &e2e_config.result_fields,
67 &e2e_config.fields_array,
68 );
69
70 let effective_replace = match e2e_config.dep_mode {
73 crate::config::DependencyMode::Registry => None,
74 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
75 };
76 files.push(GeneratedFile {
77 path: output_base.join("go.mod"),
78 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
79 generated_header: false,
80 });
81
82 for group in groups {
84 let active: Vec<&Fixture> = group
85 .fixtures
86 .iter()
87 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
88 .collect();
89
90 if active.is_empty() {
91 continue;
92 }
93
94 let filename = format!("{}_test.go", sanitize_filename(&group.category));
95 let content = render_test_file(
96 &group.category,
97 &active,
98 &module_path,
99 &import_alias,
100 &field_resolver,
101 e2e_config,
102 );
103 files.push(GeneratedFile {
104 path: output_base.join(filename),
105 content,
106 generated_header: true,
107 });
108 }
109
110 Ok(files)
111 }
112
113 fn language_name(&self) -> &'static str {
114 "go"
115 }
116}
117
118fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
119 let mut out = String::new();
120 let _ = writeln!(out, "module e2e_go");
121 let _ = writeln!(out);
122 let _ = writeln!(out, "go 1.26");
123 let _ = writeln!(out);
124 let _ = writeln!(out, "require (");
125 let _ = writeln!(out, "\t{go_module_path} {version}");
126 let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
127 let _ = writeln!(out, ")");
128
129 if let Some(path) = replace_path {
130 let _ = writeln!(out);
131 let _ = writeln!(out, "replace {go_module_path} => {path}");
132 }
133
134 out
135}
136
137fn render_test_file(
138 category: &str,
139 fixtures: &[&Fixture],
140 go_module_path: &str,
141 import_alias: &str,
142 field_resolver: &FieldResolver,
143 e2e_config: &crate::config::E2eConfig,
144) -> String {
145 let mut out = String::new();
146
147 out.push_str(&hash::header(CommentStyle::DoubleSlash));
149 let _ = writeln!(out);
150
151 let needs_pkg = fixtures.iter().any(|f| f.mock_response.is_some());
156
157 let needs_os = fixtures.iter().any(|f| {
160 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
161 call_args.iter().any(|a| a.arg_type == "mock_url")
162 });
163
164 let needs_json = fixtures.iter().any(|f| {
167 let call = e2e_config.resolve_call(f.call.as_deref());
168 let call_args = &call.args;
169 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
171 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
172 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
173 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
174 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
175 })
176 };
177 let go_override = call.overrides.get("go");
179 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
180 e2e_config
181 .call
182 .overrides
183 .get("go")
184 .and_then(|o| o.options_type.as_deref())
185 });
186 let has_json_obj = call_args.iter().any(|a| {
187 if a.arg_type != "json_object" {
188 return false;
189 }
190 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
191 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
192 if v.is_array() {
193 return true;
194 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
196 });
197 has_handle || has_json_obj
198 });
199
200 let needs_base64 = fixtures.iter().any(|f| {
202 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
203 call_args.iter().any(|a| {
204 if a.arg_type != "bytes" {
205 return false;
206 }
207 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
208 matches!(f.input.get(field), Some(serde_json::Value::String(_)))
209 })
210 });
211
212 let needs_fmt = fixtures.iter().any(|f| {
214 f.visitor.as_ref().is_some_and(|v| {
215 v.callbacks.values().any(|action| {
216 if let CallbackAction::CustomTemplate { template } = action {
217 template.contains('{')
218 } else {
219 false
220 }
221 })
222 })
223 });
224
225 let needs_strings = fixtures.iter().any(|f| {
228 f.assertions.iter().any(|a| {
229 let type_needs_strings = if a.assertion_type == "equals" {
230 a.value.as_ref().is_some_and(|v| v.is_string())
232 } else {
233 matches!(
234 a.assertion_type.as_str(),
235 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
236 )
237 };
238 let field_valid = a
239 .field
240 .as_ref()
241 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
242 .unwrap_or(true);
243 type_needs_strings && field_valid
244 })
245 });
246
247 let needs_assert = fixtures.iter().any(|f| {
250 f.assertions.iter().any(|a| {
251 let field_valid = a
252 .field
253 .as_ref()
254 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
255 .unwrap_or(true);
256 let type_needs_assert = matches!(
257 a.assertion_type.as_str(),
258 "count_min"
259 | "count_max"
260 | "is_true"
261 | "is_false"
262 | "method_result"
263 | "min_length"
264 | "max_length"
265 | "matches_regex"
266 );
267 type_needs_assert && field_valid
268 })
269 });
270
271 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
273 let needs_http = has_http_fixtures;
274
275 let needs_reflect = fixtures.iter().any(|f| {
277 if let Some(http) = &f.http {
278 if let Some(body) = &http.expected_response.body {
279 matches!(body, serde_json::Value::Object(_) | serde_json::Value::Array(_))
280 } else {
281 false
282 }
283 } else {
284 false
285 }
286 });
287
288 let _ = writeln!(out, "// E2e tests for category: {category}");
289 let _ = writeln!(out, "package e2e_test");
290 let _ = writeln!(out);
291 let _ = writeln!(out, "import (");
292 if needs_base64 {
293 let _ = writeln!(out, "\t\"encoding/base64\"");
294 }
295 if needs_json || needs_reflect {
296 let _ = writeln!(out, "\t\"encoding/json\"");
297 }
298 if needs_fmt {
299 let _ = writeln!(out, "\t\"fmt\"");
300 }
301 if needs_http {
302 let _ = writeln!(out, "\t\"io\"");
303 }
304 if needs_http {
305 let _ = writeln!(out, "\t\"net/http\"");
306 }
307 if needs_os {
308 let _ = writeln!(out, "\t\"os\"");
309 }
310 if needs_reflect {
311 let _ = writeln!(out, "\t\"reflect\"");
312 }
313 if needs_strings || needs_http {
314 let _ = writeln!(out, "\t\"strings\"");
315 }
316 let _ = writeln!(out, "\t\"testing\"");
317 if needs_assert {
318 let _ = writeln!(out);
319 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
320 }
321 if needs_pkg {
322 let _ = writeln!(out);
323 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
324 }
325 let _ = writeln!(out, ")");
326 let _ = writeln!(out);
327
328 for fixture in fixtures.iter() {
330 if let Some(visitor_spec) = &fixture.visitor {
331 let struct_name = visitor_struct_name(&fixture.id);
332 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
333 let _ = writeln!(out);
334 }
335 }
336
337 for (i, fixture) in fixtures.iter().enumerate() {
338 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
339 if i + 1 < fixtures.len() {
340 let _ = writeln!(out);
341 }
342 }
343
344 while out.ends_with("\n\n") {
346 out.pop();
347 }
348 if !out.ends_with('\n') {
349 out.push('\n');
350 }
351 out
352}
353
354fn render_test_function(
355 out: &mut String,
356 fixture: &Fixture,
357 import_alias: &str,
358 field_resolver: &FieldResolver,
359 e2e_config: &crate::config::E2eConfig,
360) {
361 let fn_name = fixture.id.to_upper_camel_case();
362 let description = &fixture.description;
363
364 if let Some(http) = &fixture.http {
366 render_http_test_function(out, fixture, http);
367 return;
368 }
369
370 if fixture.mock_response.is_none() {
374 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
375 let _ = writeln!(out, "\t// {description}");
376 let _ = writeln!(
377 out,
378 "\tt.Skip(\"TODO: implement Go e2e tests via the spikard Go binding API\")"
379 );
380 let _ = writeln!(out, "}}");
381 return;
382 }
383
384 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
386 let lang = "go";
387 let overrides = call_config.overrides.get(lang);
388 let function_name = to_go_name(
389 overrides
390 .and_then(|o| o.function.as_ref())
391 .map(String::as_str)
392 .unwrap_or(&call_config.function),
393 );
394 let result_var = &call_config.result_var;
395 let args = &call_config.args;
396
397 let returns_result = overrides
400 .and_then(|o| o.returns_result)
401 .unwrap_or(call_config.returns_result);
402
403 let returns_void = call_config.returns_void;
406
407 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
410 call_config
411 .overrides
412 .get("rust")
413 .map(|o| o.result_is_simple)
414 .unwrap_or(false)
415 });
416
417 let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
420
421 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
423 e2e_config
424 .call
425 .overrides
426 .get("go")
427 .and_then(|o| o.options_type.as_deref())
428 });
429
430 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
431
432 let (mut setup_lines, args_str) =
433 build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
434
435 let mut visitor_arg = String::new();
437 if fixture.visitor.is_some() {
438 let struct_name = visitor_struct_name(&fixture.id);
439 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
440 visitor_arg = "visitor".to_string();
441 }
442
443 let final_args = if visitor_arg.is_empty() {
444 args_str
445 } else {
446 format!("{args_str}, {visitor_arg}")
447 };
448
449 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
450 let _ = writeln!(out, "\t// {description}");
451
452 for line in &setup_lines {
453 let _ = writeln!(out, "\t{line}");
454 }
455
456 if expects_error {
457 if returns_result && !returns_void {
458 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
459 } else {
460 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
461 }
462 let _ = writeln!(out, "\tif err == nil {{");
463 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
464 let _ = writeln!(out, "\t}}");
465 let _ = writeln!(out, "}}");
466 return;
467 }
468
469 let has_usable_assertion = fixture.assertions.iter().any(|a| {
473 if a.assertion_type == "not_error" || a.assertion_type == "error" {
474 return false;
475 }
476 if a.assertion_type == "method_result" {
478 return true;
479 }
480 match &a.field {
481 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
482 _ => true,
483 }
484 });
485
486 if !returns_result && result_is_simple {
492 let result_binding = if has_usable_assertion {
494 result_var.to_string()
495 } else {
496 "_".to_string()
497 };
498 let assign_op = if result_binding == "_" { "=" } else { ":=" };
500 let _ = writeln!(
501 out,
502 "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
503 );
504 if has_usable_assertion && result_binding != "_" {
505 let _ = writeln!(out, "\tif {result_var} == nil {{");
507 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
508 let _ = writeln!(out, "\t}}");
509 let _ = writeln!(out, "\tvalue := *{result_var}");
510 }
511 } else if !returns_result || returns_void {
512 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
515 let _ = writeln!(out, "\tif err != nil {{");
516 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
517 let _ = writeln!(out, "\t}}");
518 let _ = writeln!(out, "}}");
520 return;
521 } else {
522 let result_binding = if has_usable_assertion {
524 result_var.to_string()
525 } else {
526 "_".to_string()
527 };
528 let _ = writeln!(
529 out,
530 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
531 );
532 let _ = writeln!(out, "\tif err != nil {{");
533 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
534 let _ = writeln!(out, "\t}}");
535 if result_is_simple && has_usable_assertion && result_binding != "_" {
536 let _ = writeln!(out, "\tif {result_var} == nil {{");
538 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
539 let _ = writeln!(out, "\t}}");
540 let _ = writeln!(out, "\tvalue := *{result_var}");
541 }
542 }
543
544 let effective_result_var = if result_is_simple && has_usable_assertion {
546 "value".to_string()
547 } else {
548 result_var.to_string()
549 };
550
551 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
556 for assertion in &fixture.assertions {
557 if let Some(f) = &assertion.field {
558 if !f.is_empty() {
559 let resolved = field_resolver.resolve(f);
560 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
561 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
566 let is_array_field = field_resolver.is_array(resolved);
567 if !is_string_field || is_array_field {
568 continue;
571 }
572 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
573 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
574 if field_resolver.has_map_access(f) {
575 let _ = writeln!(out, "\t{local_var} := {field_expr}");
578 } else {
579 let _ = writeln!(out, "\tvar {local_var} string");
580 let _ = writeln!(out, "\tif {field_expr} != nil {{");
581 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
582 let _ = writeln!(out, "\t}}");
583 }
584 optional_locals.insert(f.clone(), local_var);
585 }
586 }
587 }
588 }
589
590 for assertion in &fixture.assertions {
592 if let Some(f) = &assertion.field {
593 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
594 let parts: Vec<&str> = f.split('.').collect();
597 let mut guard_expr: Option<String> = None;
598 for i in 1..parts.len() {
599 let prefix = parts[..i].join(".");
600 let resolved_prefix = field_resolver.resolve(&prefix);
601 if field_resolver.is_optional(resolved_prefix) {
602 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
603 guard_expr = Some(accessor);
604 break;
605 }
606 }
607 if let Some(guard) = guard_expr {
608 if field_resolver.is_valid_for_result(f) {
611 let _ = writeln!(out, "\tif {guard} != nil {{");
612 let mut nil_buf = String::new();
615 render_assertion(
616 &mut nil_buf,
617 assertion,
618 &effective_result_var,
619 import_alias,
620 field_resolver,
621 &optional_locals,
622 result_is_simple,
623 result_is_array,
624 );
625 for line in nil_buf.lines() {
626 let _ = writeln!(out, "\t{line}");
627 }
628 let _ = writeln!(out, "\t}}");
629 } else {
630 render_assertion(
631 out,
632 assertion,
633 &effective_result_var,
634 import_alias,
635 field_resolver,
636 &optional_locals,
637 result_is_simple,
638 result_is_array,
639 );
640 }
641 continue;
642 }
643 }
644 }
645 render_assertion(
646 out,
647 assertion,
648 &effective_result_var,
649 import_alias,
650 field_resolver,
651 &optional_locals,
652 result_is_simple,
653 result_is_array,
654 );
655 }
656
657 let _ = writeln!(out, "}}");
658}
659
660fn render_http_test_function(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
665 let fn_name = fixture.id.to_upper_camel_case();
666 let description = &fixture.description;
667 let request = &http.request;
668 let expected = &http.expected_response;
669 let method = request.method.to_uppercase();
670 let fixture_id = &fixture.id;
671 let expected_status = expected.status_code;
672
673 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
674 let _ = writeln!(out, "\t// {description}");
675 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
676 let _ = writeln!(out, "\tif baseURL == \"\" {{");
677 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
678 let _ = writeln!(out, "\t}}");
679
680 let body_expr = if let Some(body) = &request.body {
682 let json = serde_json::to_string(body).unwrap_or_default();
683 let escaped = go_string_literal(&json);
684 format!("strings.NewReader({})", escaped)
685 } else {
686 "strings.NewReader(\"\")".to_string()
687 };
688
689 let _ = writeln!(out, "\tbody := {body_expr}");
690 let _ = writeln!(
691 out,
692 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"/fixtures/{fixture_id}\", body)"
693 );
694 let _ = writeln!(out, "\tif err != nil {{");
695 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
696 let _ = writeln!(out, "\t}}");
697
698 let content_type = request.content_type.as_deref().unwrap_or("application/json");
700 if request.body.is_some() {
701 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
702 }
703
704 for (name, value) in &request.headers {
705 let escaped_name = go_string_literal(name);
706 let escaped_value = go_string_literal(value);
707 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
708 }
709
710 if !request.cookies.is_empty() {
712 for (name, value) in &request.cookies {
713 let escaped_name = go_string_literal(name);
714 let escaped_value = go_string_literal(value);
715 let _ = writeln!(
716 out,
717 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
718 );
719 }
720 }
721
722 let _ = writeln!(out, "\tresp, err := http.DefaultClient.Do(req)");
724 let _ = writeln!(out, "\tif err != nil {{");
725 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
726 let _ = writeln!(out, "\t}}");
727 let _ = writeln!(out, "\tdefer resp.Body.Close()");
728
729 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
731 let _ = writeln!(out, "\tif err != nil {{");
732 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
733 let _ = writeln!(out, "\t}}");
734
735 let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
737 let _ = writeln!(
738 out,
739 "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
740 );
741 let _ = writeln!(out, "\t}}");
742
743 if let Some(expected_body) = &expected.body {
745 match expected_body {
746 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
747 let json_str = serde_json::to_string(expected_body).unwrap_or_default();
748 let escaped = go_string_literal(&json_str);
749 let _ = writeln!(out, "\tvar got map[string]any");
750 let _ = writeln!(out, "\tvar want map[string]any");
751 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
752 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
753 let _ = writeln!(out, "\t}}");
754 let _ = writeln!(
755 out,
756 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
757 );
758 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
759 let _ = writeln!(out, "\t}}");
760 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
761 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
762 let _ = writeln!(out, "\t}}");
763 }
764 serde_json::Value::String(s) => {
765 let escaped = go_string_literal(s);
766 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != {escaped} {{");
767 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %s want {escaped}\", string(bodyBytes))");
768 let _ = writeln!(out, "\t}}");
769 }
770 other => {
771 let escaped = go_string_literal(&other.to_string());
772 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != {escaped} {{");
773 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %s want {escaped}\", string(bodyBytes))");
774 let _ = writeln!(out, "\t}}");
775 }
776 }
777 }
778
779 for (name, value) in &expected.headers {
781 if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
782 continue;
784 }
785 let escaped_name = go_string_literal(name);
786 let escaped_value = go_string_literal(value);
787 let _ = writeln!(
788 out,
789 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
790 );
791 let _ = writeln!(
792 out,
793 "\t\tt.Fatalf(\"header {escaped_name} mismatch: got %s want to contain {escaped_value}\", resp.Header.Get({escaped_name}))"
794 );
795 let _ = writeln!(out, "\t}}");
796 }
797
798 let _ = writeln!(out, "}}");
799}
800
801fn build_args_and_setup(
805 input: &serde_json::Value,
806 args: &[crate::config::ArgMapping],
807 import_alias: &str,
808 options_type: Option<&str>,
809 fixture_id: &str,
810) -> (Vec<String>, String) {
811 use heck::ToUpperCamelCase;
812
813 if args.is_empty() {
814 return (Vec::new(), String::new());
815 }
816
817 let mut setup_lines: Vec<String> = Vec::new();
818 let mut parts: Vec<String> = Vec::new();
819
820 for arg in args {
821 if arg.arg_type == "mock_url" {
822 setup_lines.push(format!(
823 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
824 arg.name,
825 ));
826 parts.push(arg.name.clone());
827 continue;
828 }
829
830 if arg.arg_type == "handle" {
831 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
833 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
834 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
835 if config_value.is_null()
836 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
837 {
838 setup_lines.push(format!(
839 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
840 name = arg.name,
841 ));
842 } else {
843 let json_str = serde_json::to_string(config_value).unwrap_or_default();
844 let go_literal = go_string_literal(&json_str);
845 let name = &arg.name;
846 setup_lines.push(format!(
847 "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}}"
848 ));
849 setup_lines.push(format!(
850 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
851 ));
852 }
853 parts.push(arg.name.clone());
854 continue;
855 }
856
857 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
858 let val = input.get(field);
859
860 if arg.arg_type == "bytes" {
863 let var_name = format!("{}Bytes", arg.name);
864 match val {
865 None | Some(serde_json::Value::Null) => {
866 if arg.optional {
867 parts.push("nil".to_string());
868 } else {
869 parts.push("[]byte{}".to_string());
870 }
871 }
872 Some(serde_json::Value::String(s)) => {
873 let go_b64 = go_string_literal(s);
874 setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
875 parts.push(var_name);
876 }
877 Some(other) => {
878 parts.push(format!("[]byte({})", json_to_go(other)));
879 }
880 }
881 continue;
882 }
883
884 match val {
885 None | Some(serde_json::Value::Null) if arg.optional => {
886 match arg.arg_type.as_str() {
888 "string" => {
889 parts.push("nil".to_string());
891 }
892 "json_object" => {
893 if let Some(opts_type) = options_type {
895 parts.push(format!("{import_alias}.{opts_type}{{}}"));
896 } else {
897 parts.push("nil".to_string());
898 }
899 }
900 _ => {
901 parts.push("nil".to_string());
902 }
903 }
904 }
905 None | Some(serde_json::Value::Null) => {
906 let default_val = match arg.arg_type.as_str() {
908 "string" => "\"\"".to_string(),
909 "int" | "integer" | "i64" => "0".to_string(),
910 "float" | "number" => "0.0".to_string(),
911 "bool" | "boolean" => "false".to_string(),
912 "json_object" => {
913 if let Some(opts_type) = options_type {
914 format!("{import_alias}.{opts_type}{{}}")
915 } else {
916 "nil".to_string()
917 }
918 }
919 _ => "nil".to_string(),
920 };
921 parts.push(default_val);
922 }
923 Some(v) => {
924 match arg.arg_type.as_str() {
925 "json_object" => {
926 let is_array = v.is_array();
929 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
930 if is_empty_obj {
931 if let Some(opts_type) = options_type {
932 parts.push(format!("{import_alias}.{opts_type}{{}}"));
933 } else {
934 parts.push("nil".to_string());
935 }
936 } else if is_array {
937 let json_str = serde_json::to_string(v).unwrap_or_default();
939 let go_literal = go_string_literal(&json_str);
940 let var_name = &arg.name;
941 setup_lines.push(format!(
942 "var {var_name} []string\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
943 ));
944 parts.push(var_name.to_string());
945 } else if let Some(opts_type) = options_type {
946 let json_str = serde_json::to_string(v).unwrap_or_default();
948 let go_literal = go_string_literal(&json_str);
949 let var_name = &arg.name;
950 setup_lines.push(format!(
951 "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}}"
952 ));
953 parts.push(var_name.to_string());
954 } else {
955 parts.push(json_to_go(v));
956 }
957 }
958 "string" if arg.optional => {
959 let var_name = format!("{}Val", arg.name);
961 let go_val = json_to_go(v);
962 setup_lines.push(format!("{var_name} := {go_val}"));
963 parts.push(format!("&{var_name}"));
964 }
965 _ => {
966 parts.push(json_to_go(v));
967 }
968 }
969 }
970 }
971 }
972
973 (setup_lines, parts.join(", "))
974}
975
976#[allow(clippy::too_many_arguments)]
977fn render_assertion(
978 out: &mut String,
979 assertion: &Assertion,
980 result_var: &str,
981 import_alias: &str,
982 field_resolver: &FieldResolver,
983 optional_locals: &std::collections::HashMap<String, String>,
984 result_is_simple: bool,
985 result_is_array: bool,
986) {
987 if !result_is_simple {
990 if let Some(f) = &assertion.field {
991 let embed_deref = format!("(*{result_var})");
994 match f.as_str() {
995 "chunks_have_content" => {
996 let pred = format!(
997 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
998 );
999 match assertion.assertion_type.as_str() {
1000 "is_true" => {
1001 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1002 }
1003 "is_false" => {
1004 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1005 }
1006 _ => {
1007 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1008 }
1009 }
1010 return;
1011 }
1012 "chunks_have_embeddings" => {
1013 let pred = format!(
1014 "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 }}()"
1015 );
1016 match assertion.assertion_type.as_str() {
1017 "is_true" => {
1018 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1019 }
1020 "is_false" => {
1021 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1022 }
1023 _ => {
1024 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1025 }
1026 }
1027 return;
1028 }
1029 "embeddings" => {
1030 match assertion.assertion_type.as_str() {
1031 "count_equals" => {
1032 if let Some(val) = &assertion.value {
1033 if let Some(n) = val.as_u64() {
1034 let _ = writeln!(
1035 out,
1036 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1037 );
1038 }
1039 }
1040 }
1041 "count_min" => {
1042 if let Some(val) = &assertion.value {
1043 if let Some(n) = val.as_u64() {
1044 let _ = writeln!(
1045 out,
1046 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1047 );
1048 }
1049 }
1050 }
1051 "not_empty" => {
1052 let _ = writeln!(
1053 out,
1054 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1055 );
1056 }
1057 "is_empty" => {
1058 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1059 }
1060 _ => {
1061 let _ = writeln!(
1062 out,
1063 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1064 );
1065 }
1066 }
1067 return;
1068 }
1069 "embedding_dimensions" => {
1070 let expr = format!(
1071 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1072 );
1073 match assertion.assertion_type.as_str() {
1074 "equals" => {
1075 if let Some(val) = &assertion.value {
1076 if let Some(n) = val.as_u64() {
1077 let _ = writeln!(
1078 out,
1079 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1080 );
1081 }
1082 }
1083 }
1084 "greater_than" => {
1085 if let Some(val) = &assertion.value {
1086 if let Some(n) = val.as_u64() {
1087 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1088 }
1089 }
1090 }
1091 _ => {
1092 let _ = writeln!(
1093 out,
1094 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1095 );
1096 }
1097 }
1098 return;
1099 }
1100 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1101 let pred = match f.as_str() {
1102 "embeddings_valid" => {
1103 format!(
1104 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1105 )
1106 }
1107 "embeddings_finite" => {
1108 format!(
1109 "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 }}()"
1110 )
1111 }
1112 "embeddings_non_zero" => {
1113 format!(
1114 "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 }}()"
1115 )
1116 }
1117 "embeddings_normalized" => {
1118 format!(
1119 "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 }}()"
1120 )
1121 }
1122 _ => unreachable!(),
1123 };
1124 match assertion.assertion_type.as_str() {
1125 "is_true" => {
1126 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1127 }
1128 "is_false" => {
1129 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1130 }
1131 _ => {
1132 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1133 }
1134 }
1135 return;
1136 }
1137 "keywords" | "keywords_count" => {
1140 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1141 return;
1142 }
1143 _ => {}
1144 }
1145 }
1146 }
1147
1148 if !result_is_simple {
1151 if let Some(f) = &assertion.field {
1152 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1153 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1154 return;
1155 }
1156 }
1157 }
1158
1159 let field_expr = if result_is_simple {
1160 result_var.to_string()
1162 } else {
1163 match &assertion.field {
1164 Some(f) if !f.is_empty() => {
1165 if let Some(local_var) = optional_locals.get(f.as_str()) {
1167 local_var.clone()
1168 } else {
1169 field_resolver.accessor(f, "go", result_var)
1170 }
1171 }
1172 _ => result_var.to_string(),
1173 }
1174 };
1175
1176 let is_optional = assertion
1180 .field
1181 .as_ref()
1182 .map(|f| {
1183 let resolved = field_resolver.resolve(f);
1184 let check_path = resolved
1185 .strip_suffix(".length")
1186 .or_else(|| resolved.strip_suffix(".count"))
1187 .or_else(|| resolved.strip_suffix(".size"))
1188 .unwrap_or(resolved);
1189 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1190 })
1191 .unwrap_or(false);
1192
1193 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1196 let inner = &field_expr[4..field_expr.len() - 1];
1197 format!("len(*{inner})")
1198 } else {
1199 field_expr
1200 };
1201 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1203 Some(field_expr[5..field_expr.len() - 1].to_string())
1204 } else {
1205 None
1206 };
1207
1208 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1211 format!("*{field_expr}")
1212 } else {
1213 field_expr.clone()
1214 };
1215
1216 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1221 let array_expr = &field_expr[..idx];
1222 Some(array_expr.to_string())
1223 } else {
1224 None
1225 };
1226
1227 let mut assertion_buf = String::new();
1230 let out_ref = &mut assertion_buf;
1231
1232 match assertion.assertion_type.as_str() {
1233 "equals" => {
1234 if let Some(expected) = &assertion.value {
1235 let go_val = json_to_go(expected);
1236 if expected.is_string() {
1238 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1240 format!("strings.TrimSpace(*{field_expr})")
1241 } else {
1242 format!("strings.TrimSpace({field_expr})")
1243 };
1244 if is_optional && !field_expr.starts_with("len(") {
1245 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1246 } else {
1247 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1248 }
1249 } else if is_optional && !field_expr.starts_with("len(") {
1250 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1251 } else {
1252 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1253 }
1254 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1255 let _ = writeln!(out_ref, "\t}}");
1256 }
1257 }
1258 "contains" => {
1259 if let Some(expected) = &assertion.value {
1260 let go_val = json_to_go(expected);
1261 let resolved_field = assertion.field.as_deref().unwrap_or("");
1267 let resolved_name = field_resolver.resolve(resolved_field);
1268 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1269 let is_opt =
1270 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1271 let field_for_contains = if is_opt && field_is_array {
1272 format!("strings.Join(*{field_expr}, \" \")")
1273 } else if is_opt {
1274 format!("string(*{field_expr})")
1275 } else if field_is_array {
1276 format!("strings.Join({field_expr}, \" \")")
1277 } else {
1278 format!("string({field_expr})")
1279 };
1280 if is_opt {
1281 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1282 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1283 let _ = writeln!(
1284 out_ref,
1285 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1286 );
1287 let _ = writeln!(out_ref, "\t}}");
1288 let _ = writeln!(out_ref, "\t}}");
1289 } else {
1290 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1291 let _ = writeln!(
1292 out_ref,
1293 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1294 );
1295 let _ = writeln!(out_ref, "\t}}");
1296 }
1297 }
1298 }
1299 "contains_all" => {
1300 if let Some(values) = &assertion.values {
1301 let resolved_field = assertion.field.as_deref().unwrap_or("");
1302 let resolved_name = field_resolver.resolve(resolved_field);
1303 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1304 let is_opt =
1305 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1306 for val in values {
1307 let go_val = json_to_go(val);
1308 let field_for_contains = if is_opt && field_is_array {
1309 format!("strings.Join(*{field_expr}, \" \")")
1310 } else if is_opt {
1311 format!("string(*{field_expr})")
1312 } else if field_is_array {
1313 format!("strings.Join({field_expr}, \" \")")
1314 } else {
1315 format!("string({field_expr})")
1316 };
1317 if is_opt {
1318 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1319 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1320 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1321 let _ = writeln!(out_ref, "\t}}");
1322 let _ = writeln!(out_ref, "\t}}");
1323 } else {
1324 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1325 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1326 let _ = writeln!(out_ref, "\t}}");
1327 }
1328 }
1329 }
1330 }
1331 "not_contains" => {
1332 if let Some(expected) = &assertion.value {
1333 let go_val = json_to_go(expected);
1334 let resolved_field = assertion.field.as_deref().unwrap_or("");
1335 let resolved_name = field_resolver.resolve(resolved_field);
1336 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1337 let is_opt =
1338 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1339 let field_for_contains = if is_opt && field_is_array {
1340 format!("strings.Join(*{field_expr}, \" \")")
1341 } else if is_opt {
1342 format!("string(*{field_expr})")
1343 } else if field_is_array {
1344 format!("strings.Join({field_expr}, \" \")")
1345 } else {
1346 format!("string({field_expr})")
1347 };
1348 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1349 let _ = writeln!(
1350 out_ref,
1351 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1352 );
1353 let _ = writeln!(out_ref, "\t}}");
1354 }
1355 }
1356 "not_empty" => {
1357 let field_is_array = {
1360 let rf = assertion.field.as_deref().unwrap_or("");
1361 let rn = field_resolver.resolve(rf);
1362 field_resolver.is_array(rn)
1363 };
1364 if is_optional && !field_is_array {
1365 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1367 } else if is_optional {
1368 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1369 } else {
1370 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1371 }
1372 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1373 let _ = writeln!(out_ref, "\t}}");
1374 }
1375 "is_empty" => {
1376 let field_is_array = {
1377 let rf = assertion.field.as_deref().unwrap_or("");
1378 let rn = field_resolver.resolve(rf);
1379 field_resolver.is_array(rn)
1380 };
1381 if is_optional && !field_is_array {
1382 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1384 } else if is_optional {
1385 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1386 } else {
1387 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1388 }
1389 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1390 let _ = writeln!(out_ref, "\t}}");
1391 }
1392 "contains_any" => {
1393 if let Some(values) = &assertion.values {
1394 let resolved_field = assertion.field.as_deref().unwrap_or("");
1395 let resolved_name = field_resolver.resolve(resolved_field);
1396 let field_is_array = field_resolver.is_array(resolved_name);
1397 let is_opt =
1398 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1399 let field_for_contains = if is_opt && field_is_array {
1400 format!("strings.Join(*{field_expr}, \" \")")
1401 } else if is_opt {
1402 format!("*{field_expr}")
1403 } else if field_is_array {
1404 format!("strings.Join({field_expr}, \" \")")
1405 } else {
1406 field_expr.clone()
1407 };
1408 let _ = writeln!(out_ref, "\t{{");
1409 let _ = writeln!(out_ref, "\t\tfound := false");
1410 for val in values {
1411 let go_val = json_to_go(val);
1412 let _ = writeln!(
1413 out_ref,
1414 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1415 );
1416 }
1417 let _ = writeln!(out_ref, "\t\tif !found {{");
1418 let _ = writeln!(
1419 out_ref,
1420 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1421 );
1422 let _ = writeln!(out_ref, "\t\t}}");
1423 let _ = writeln!(out_ref, "\t}}");
1424 }
1425 }
1426 "greater_than" => {
1427 if let Some(val) = &assertion.value {
1428 let go_val = json_to_go(val);
1429 if is_optional {
1433 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1434 if let Some(n) = val.as_u64() {
1435 let next = n + 1;
1436 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1437 } else {
1438 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1439 }
1440 let _ = writeln!(
1441 out_ref,
1442 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1443 );
1444 let _ = writeln!(out_ref, "\t\t}}");
1445 let _ = writeln!(out_ref, "\t}}");
1446 } else if let Some(n) = val.as_u64() {
1447 let next = n + 1;
1448 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1449 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1450 let _ = writeln!(out_ref, "\t}}");
1451 } else {
1452 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1453 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1454 let _ = writeln!(out_ref, "\t}}");
1455 }
1456 }
1457 }
1458 "less_than" => {
1459 if let Some(val) = &assertion.value {
1460 let go_val = json_to_go(val);
1461 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1462 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1463 let _ = writeln!(out_ref, "\t}}");
1464 }
1465 }
1466 "greater_than_or_equal" => {
1467 if let Some(val) = &assertion.value {
1468 let go_val = json_to_go(val);
1469 if let Some(ref guard) = nil_guard_expr {
1470 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1471 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1472 let _ = writeln!(
1473 out_ref,
1474 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1475 );
1476 let _ = writeln!(out_ref, "\t\t}}");
1477 let _ = writeln!(out_ref, "\t}}");
1478 } else if is_optional && !field_expr.starts_with("len(") {
1479 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1481 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1482 let _ = writeln!(
1483 out_ref,
1484 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1485 );
1486 let _ = writeln!(out_ref, "\t\t}}");
1487 let _ = writeln!(out_ref, "\t}}");
1488 } else {
1489 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1490 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1491 let _ = writeln!(out_ref, "\t}}");
1492 }
1493 }
1494 }
1495 "less_than_or_equal" => {
1496 if let Some(val) = &assertion.value {
1497 let go_val = json_to_go(val);
1498 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1499 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1500 let _ = writeln!(out_ref, "\t}}");
1501 }
1502 }
1503 "starts_with" => {
1504 if let Some(expected) = &assertion.value {
1505 let go_val = json_to_go(expected);
1506 let field_for_prefix = if is_optional
1507 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1508 {
1509 format!("string(*{field_expr})")
1510 } else {
1511 format!("string({field_expr})")
1512 };
1513 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1514 let _ = writeln!(
1515 out_ref,
1516 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1517 );
1518 let _ = writeln!(out_ref, "\t}}");
1519 }
1520 }
1521 "count_min" => {
1522 if let Some(val) = &assertion.value {
1523 if let Some(n) = val.as_u64() {
1524 if is_optional {
1525 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1526 let _ = writeln!(
1527 out_ref,
1528 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1529 );
1530 let _ = writeln!(out_ref, "\t}}");
1531 } else {
1532 let _ = writeln!(
1533 out_ref,
1534 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1535 );
1536 }
1537 }
1538 }
1539 }
1540 "count_equals" => {
1541 if let Some(val) = &assertion.value {
1542 if let Some(n) = val.as_u64() {
1543 if is_optional {
1544 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1545 let _ = writeln!(
1546 out_ref,
1547 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1548 );
1549 let _ = writeln!(out_ref, "\t}}");
1550 } else {
1551 let _ = writeln!(
1552 out_ref,
1553 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1554 );
1555 }
1556 }
1557 }
1558 }
1559 "is_true" => {
1560 if is_optional {
1561 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1562 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1563 let _ = writeln!(out_ref, "\t}}");
1564 } else {
1565 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1566 }
1567 }
1568 "is_false" => {
1569 if is_optional {
1570 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1571 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1572 let _ = writeln!(out_ref, "\t}}");
1573 } else {
1574 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1575 }
1576 }
1577 "method_result" => {
1578 if let Some(method_name) = &assertion.method {
1579 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1580 let check = assertion.check.as_deref().unwrap_or("is_true");
1581 let deref_expr = if info.is_pointer {
1584 format!("*{}", info.call_expr)
1585 } else {
1586 info.call_expr.clone()
1587 };
1588 match check {
1589 "equals" => {
1590 if let Some(val) = &assertion.value {
1591 if val.is_boolean() {
1592 if val.as_bool() == Some(true) {
1593 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1594 } else {
1595 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1596 }
1597 } else {
1598 let go_val = if let Some(cast) = info.value_cast {
1602 if val.is_number() {
1603 format!("{cast}({})", json_to_go(val))
1604 } else {
1605 json_to_go(val)
1606 }
1607 } else {
1608 json_to_go(val)
1609 };
1610 let _ = writeln!(
1611 out_ref,
1612 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1613 );
1614 }
1615 }
1616 }
1617 "is_true" => {
1618 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1619 }
1620 "is_false" => {
1621 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1622 }
1623 "greater_than_or_equal" => {
1624 if let Some(val) = &assertion.value {
1625 let n = val.as_u64().unwrap_or(0);
1626 let cast = info.value_cast.unwrap_or("uint");
1628 let _ = writeln!(
1629 out_ref,
1630 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1631 );
1632 }
1633 }
1634 "count_min" => {
1635 if let Some(val) = &assertion.value {
1636 let n = val.as_u64().unwrap_or(0);
1637 let _ = writeln!(
1638 out_ref,
1639 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1640 );
1641 }
1642 }
1643 "contains" => {
1644 if let Some(val) = &assertion.value {
1645 let go_val = json_to_go(val);
1646 let _ = writeln!(
1647 out_ref,
1648 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1649 );
1650 }
1651 }
1652 "is_error" => {
1653 let _ = writeln!(out_ref, "\t{{");
1654 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1655 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1656 let _ = writeln!(out_ref, "\t}}");
1657 }
1658 other_check => {
1659 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1660 }
1661 }
1662 } else {
1663 panic!("Go e2e generator: method_result assertion missing 'method' field");
1664 }
1665 }
1666 "min_length" => {
1667 if let Some(val) = &assertion.value {
1668 if let Some(n) = val.as_u64() {
1669 if is_optional {
1670 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1671 let _ = writeln!(
1672 out_ref,
1673 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1674 );
1675 let _ = writeln!(out_ref, "\t}}");
1676 } else {
1677 let _ = writeln!(
1678 out_ref,
1679 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1680 );
1681 }
1682 }
1683 }
1684 }
1685 "max_length" => {
1686 if let Some(val) = &assertion.value {
1687 if let Some(n) = val.as_u64() {
1688 if is_optional {
1689 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1690 let _ = writeln!(
1691 out_ref,
1692 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1693 );
1694 let _ = writeln!(out_ref, "\t}}");
1695 } else {
1696 let _ = writeln!(
1697 out_ref,
1698 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1699 );
1700 }
1701 }
1702 }
1703 }
1704 "ends_with" => {
1705 if let Some(expected) = &assertion.value {
1706 let go_val = json_to_go(expected);
1707 let field_for_suffix = if is_optional
1708 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1709 {
1710 format!("string(*{field_expr})")
1711 } else {
1712 format!("string({field_expr})")
1713 };
1714 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1715 let _ = writeln!(
1716 out_ref,
1717 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1718 );
1719 let _ = writeln!(out_ref, "\t}}");
1720 }
1721 }
1722 "matches_regex" => {
1723 if let Some(expected) = &assertion.value {
1724 let go_val = json_to_go(expected);
1725 let field_for_regex = if is_optional
1726 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1727 {
1728 format!("*{field_expr}")
1729 } else {
1730 field_expr.clone()
1731 };
1732 let _ = writeln!(
1733 out_ref,
1734 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1735 );
1736 }
1737 }
1738 "not_error" => {
1739 }
1741 "error" => {
1742 }
1744 other => {
1745 panic!("Go e2e generator: unsupported assertion type: {other}");
1746 }
1747 }
1748
1749 if let Some(ref arr) = array_guard {
1752 if !assertion_buf.is_empty() {
1753 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1754 for line in assertion_buf.lines() {
1756 let _ = writeln!(out, "\t{line}");
1757 }
1758 let _ = writeln!(out, "\t}}");
1759 }
1760 } else {
1761 out.push_str(&assertion_buf);
1762 }
1763}
1764
1765struct GoMethodCallInfo {
1767 call_expr: String,
1769 is_pointer: bool,
1771 value_cast: Option<&'static str>,
1774}
1775
1776fn build_go_method_call(
1791 result_var: &str,
1792 method_name: &str,
1793 args: Option<&serde_json::Value>,
1794 import_alias: &str,
1795) -> GoMethodCallInfo {
1796 match method_name {
1797 "root_node_type" => GoMethodCallInfo {
1798 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1799 is_pointer: false,
1800 value_cast: None,
1801 },
1802 "named_children_count" => GoMethodCallInfo {
1803 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1804 is_pointer: false,
1805 value_cast: Some("uint"),
1806 },
1807 "has_error_nodes" => GoMethodCallInfo {
1808 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1809 is_pointer: true,
1810 value_cast: None,
1811 },
1812 "error_count" | "tree_error_count" => GoMethodCallInfo {
1813 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1814 is_pointer: true,
1815 value_cast: Some("uint"),
1816 },
1817 "tree_to_sexp" => GoMethodCallInfo {
1818 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1819 is_pointer: true,
1820 value_cast: None,
1821 },
1822 "contains_node_type" => {
1823 let node_type = args
1824 .and_then(|a| a.get("node_type"))
1825 .and_then(|v| v.as_str())
1826 .unwrap_or("");
1827 GoMethodCallInfo {
1828 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1829 is_pointer: true,
1830 value_cast: None,
1831 }
1832 }
1833 "find_nodes_by_type" => {
1834 let node_type = args
1835 .and_then(|a| a.get("node_type"))
1836 .and_then(|v| v.as_str())
1837 .unwrap_or("");
1838 GoMethodCallInfo {
1839 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1840 is_pointer: true,
1841 value_cast: None,
1842 }
1843 }
1844 "run_query" => {
1845 let query_source = args
1846 .and_then(|a| a.get("query_source"))
1847 .and_then(|v| v.as_str())
1848 .unwrap_or("");
1849 let language = args
1850 .and_then(|a| a.get("language"))
1851 .and_then(|v| v.as_str())
1852 .unwrap_or("");
1853 let query_lit = go_string_literal(query_source);
1854 let lang_lit = go_string_literal(language);
1855 GoMethodCallInfo {
1857 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1858 is_pointer: false,
1859 value_cast: None,
1860 }
1861 }
1862 other => {
1863 let method_pascal = other.to_upper_camel_case();
1864 GoMethodCallInfo {
1865 call_expr: format!("{result_var}.{method_pascal}()"),
1866 is_pointer: false,
1867 value_cast: None,
1868 }
1869 }
1870 }
1871}
1872
1873fn json_to_go(value: &serde_json::Value) -> String {
1875 match value {
1876 serde_json::Value::String(s) => go_string_literal(s),
1877 serde_json::Value::Bool(b) => b.to_string(),
1878 serde_json::Value::Number(n) => n.to_string(),
1879 serde_json::Value::Null => "nil".to_string(),
1880 other => go_string_literal(&other.to_string()),
1882 }
1883}
1884
1885fn visitor_struct_name(fixture_id: &str) -> String {
1894 use heck::ToUpperCamelCase;
1895 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1897}
1898
1899fn emit_go_visitor_struct(
1901 out: &mut String,
1902 struct_name: &str,
1903 visitor_spec: &crate::fixture::VisitorSpec,
1904 import_alias: &str,
1905) {
1906 let _ = writeln!(out, "type {struct_name} struct{{}}");
1907 for (method_name, action) in &visitor_spec.callbacks {
1908 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1909 }
1910}
1911
1912fn emit_go_visitor_method(
1914 out: &mut String,
1915 struct_name: &str,
1916 method_name: &str,
1917 action: &CallbackAction,
1918 import_alias: &str,
1919) {
1920 let camel_method = method_to_camel(method_name);
1921 let params = match method_name {
1922 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1923 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1924 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1925 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1926 "visit_code_inline"
1927 | "visit_strong"
1928 | "visit_emphasis"
1929 | "visit_strikethrough"
1930 | "visit_underline"
1931 | "visit_subscript"
1932 | "visit_superscript"
1933 | "visit_mark"
1934 | "visit_button"
1935 | "visit_summary"
1936 | "visit_figcaption"
1937 | "visit_definition_term"
1938 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1939 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1940 "visit_list_item" => {
1941 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1942 }
1943 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1944 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1945 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1946 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1947 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1948 "visit_audio" | "visit_video" | "visit_iframe" => {
1949 format!("_ {import_alias}.NodeContext, src string")
1950 }
1951 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1952 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1953 format!("_ {import_alias}.NodeContext, output string")
1954 }
1955 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1956 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1957 _ => format!("_ {import_alias}.NodeContext"),
1958 };
1959
1960 let _ = writeln!(
1961 out,
1962 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1963 );
1964 match action {
1965 CallbackAction::Skip => {
1966 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1967 }
1968 CallbackAction::Continue => {
1969 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1970 }
1971 CallbackAction::PreserveHtml => {
1972 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1973 }
1974 CallbackAction::Custom { output } => {
1975 let escaped = go_string_literal(output);
1976 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1977 }
1978 CallbackAction::CustomTemplate { template } => {
1979 let (fmt_str, fmt_args) = template_to_sprintf(template);
1982 let escaped_fmt = go_string_literal(&fmt_str);
1983 if fmt_args.is_empty() {
1984 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1985 } else {
1986 let args_str = fmt_args.join(", ");
1987 let _ = writeln!(
1988 out,
1989 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1990 );
1991 }
1992 }
1993 }
1994 let _ = writeln!(out, "}}");
1995}
1996
1997fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2001 let mut fmt_str = String::new();
2002 let mut args: Vec<String> = Vec::new();
2003 let mut chars = template.chars().peekable();
2004 while let Some(c) = chars.next() {
2005 if c == '{' {
2006 let mut name = String::new();
2008 for inner in chars.by_ref() {
2009 if inner == '}' {
2010 break;
2011 }
2012 name.push(inner);
2013 }
2014 fmt_str.push_str("%s");
2015 args.push(name);
2016 } else {
2017 fmt_str.push(c);
2018 }
2019 }
2020 (fmt_str, args)
2021}
2022
2023fn method_to_camel(snake: &str) -> String {
2025 use heck::ToUpperCamelCase;
2026 snake.to_upper_camel_case()
2027}
2028
2029#[cfg(test)]
2030mod tests {
2031 use super::*;
2032 use crate::config::{CallConfig, E2eConfig};
2033 use crate::field_access::FieldResolver;
2034 use crate::fixture::{Assertion, Fixture};
2035
2036 fn make_fixture(id: &str) -> Fixture {
2037 Fixture {
2038 id: id.to_string(),
2039 category: None,
2040 description: "test fixture".to_string(),
2041 tags: vec![],
2042 skip: None,
2043 call: None,
2044 input: serde_json::Value::Null,
2045 mock_response: None,
2046 source: String::new(),
2047 http: None,
2048 assertions: vec![Assertion {
2049 assertion_type: "not_error".to_string(),
2050 field: None,
2051 value: None,
2052 values: None,
2053 method: None,
2054 args: None,
2055 check: None,
2056 }],
2057 visitor: None,
2058 }
2059 }
2060
2061 #[test]
2065 fn test_go_method_name_uses_go_casing() {
2066 let e2e_config = E2eConfig {
2067 call: CallConfig {
2068 function: "clean_extracted_text".to_string(),
2069 module: "github.com/example/mylib".to_string(),
2070 result_var: "result".to_string(),
2071 r#async: false,
2072 path: None,
2073 method: None,
2074 args: vec![],
2075 overrides: std::collections::HashMap::new(),
2076 returns_result: true,
2077 returns_void: false,
2078 skip_languages: vec![],
2079 },
2080 ..E2eConfig::default()
2081 };
2082
2083 let fixture = make_fixture("basic_text");
2084 let resolver = FieldResolver::new(
2085 &std::collections::HashMap::new(),
2086 &std::collections::HashSet::new(),
2087 &std::collections::HashSet::new(),
2088 &std::collections::HashSet::new(),
2089 );
2090 let mut out = String::new();
2091 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2092
2093 assert!(
2094 out.contains("kreuzberg.CleanExtractedText("),
2095 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2096 );
2097 assert!(
2098 !out.contains("kreuzberg.clean_extracted_text("),
2099 "must not emit raw snake_case method name, got:\n{out}"
2100 );
2101 }
2102}