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 if f.is_http_test() {
161 return true;
162 }
163 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
164 call_args.iter().any(|a| a.arg_type == "mock_url")
165 });
166
167 let needs_json = fixtures.iter().any(|f| {
170 let call = e2e_config.resolve_call(f.call.as_deref());
171 let call_args = &call.args;
172 let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
174 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
175 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
176 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
177 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
178 })
179 };
180 let go_override = call.overrides.get("go");
182 let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
183 e2e_config
184 .call
185 .overrides
186 .get("go")
187 .and_then(|o| o.options_type.as_deref())
188 });
189 let has_json_obj = call_args.iter().any(|a| {
190 if a.arg_type != "json_object" {
191 return false;
192 }
193 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
194 let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
195 if v.is_array() {
196 return true;
197 } opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
199 });
200 has_handle || has_json_obj
201 });
202
203 let needs_base64 = fixtures.iter().any(|f| {
205 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
206 call_args.iter().any(|a| {
207 if a.arg_type != "bytes" {
208 return false;
209 }
210 let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
211 matches!(f.input.get(field), Some(serde_json::Value::String(_)))
212 })
213 });
214
215 let needs_fmt = fixtures.iter().any(|f| {
217 f.visitor.as_ref().is_some_and(|v| {
218 v.callbacks.values().any(|action| {
219 if let CallbackAction::CustomTemplate { template } = action {
220 template.contains('{')
221 } else {
222 false
223 }
224 })
225 })
226 });
227
228 let needs_strings = fixtures.iter().any(|f| {
231 f.assertions.iter().any(|a| {
232 let type_needs_strings = if a.assertion_type == "equals" {
233 a.value.as_ref().is_some_and(|v| v.is_string())
235 } else {
236 matches!(
237 a.assertion_type.as_str(),
238 "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
239 )
240 };
241 let field_valid = a
242 .field
243 .as_ref()
244 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
245 .unwrap_or(true);
246 type_needs_strings && field_valid
247 })
248 });
249
250 let needs_assert = fixtures.iter().any(|f| {
253 f.assertions.iter().any(|a| {
254 let field_valid = a
255 .field
256 .as_ref()
257 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
258 .unwrap_or(true);
259 let type_needs_assert = matches!(
260 a.assertion_type.as_str(),
261 "count_min"
262 | "count_max"
263 | "is_true"
264 | "is_false"
265 | "method_result"
266 | "min_length"
267 | "max_length"
268 | "matches_regex"
269 );
270 type_needs_assert && field_valid
271 })
272 });
273
274 let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
276 let needs_http = has_http_fixtures;
277 let needs_io = fixtures
279 .iter()
280 .any(|f| f.http.as_ref().is_some_and(|h| h.expected_response.body.is_some()));
281
282 let needs_reflect = fixtures.iter().any(|f| {
284 if let Some(http) = &f.http {
285 if let Some(body) = &http.expected_response.body {
286 matches!(body, serde_json::Value::Object(_) | serde_json::Value::Array(_))
287 } else {
288 false
289 }
290 } else {
291 false
292 }
293 });
294
295 let _ = writeln!(out, "// E2e tests for category: {category}");
296 let _ = writeln!(out, "package e2e_test");
297 let _ = writeln!(out);
298 let _ = writeln!(out, "import (");
299 if needs_base64 {
300 let _ = writeln!(out, "\t\"encoding/base64\"");
301 }
302 if needs_json || needs_reflect {
303 let _ = writeln!(out, "\t\"encoding/json\"");
304 }
305 if needs_fmt {
306 let _ = writeln!(out, "\t\"fmt\"");
307 }
308 if needs_io {
309 let _ = writeln!(out, "\t\"io\"");
310 }
311 if needs_http {
312 let _ = writeln!(out, "\t\"net/http\"");
313 }
314 if needs_os {
315 let _ = writeln!(out, "\t\"os\"");
316 }
317 if needs_reflect {
318 let _ = writeln!(out, "\t\"reflect\"");
319 }
320 if needs_strings || needs_http {
321 let _ = writeln!(out, "\t\"strings\"");
322 }
323 let _ = writeln!(out, "\t\"testing\"");
324 if needs_assert {
325 let _ = writeln!(out);
326 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
327 }
328 if needs_pkg {
329 let _ = writeln!(out);
330 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
331 }
332 let _ = writeln!(out, ")");
333 let _ = writeln!(out);
334
335 for fixture in fixtures.iter() {
337 if let Some(visitor_spec) = &fixture.visitor {
338 let struct_name = visitor_struct_name(&fixture.id);
339 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
340 let _ = writeln!(out);
341 }
342 }
343
344 for (i, fixture) in fixtures.iter().enumerate() {
345 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
346 if i + 1 < fixtures.len() {
347 let _ = writeln!(out);
348 }
349 }
350
351 while out.ends_with("\n\n") {
353 out.pop();
354 }
355 if !out.ends_with('\n') {
356 out.push('\n');
357 }
358 out
359}
360
361fn render_test_function(
362 out: &mut String,
363 fixture: &Fixture,
364 import_alias: &str,
365 field_resolver: &FieldResolver,
366 e2e_config: &crate::config::E2eConfig,
367) {
368 let fn_name = fixture.id.to_upper_camel_case();
369 let description = &fixture.description;
370
371 if let Some(http) = &fixture.http {
373 render_http_test_function(out, fixture, http);
374 return;
375 }
376
377 if fixture.mock_response.is_none() {
382 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
383 let _ = writeln!(out, "\t// {description}");
384 let _ = writeln!(
385 out,
386 "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
387 );
388 let _ = writeln!(out, "}}");
389 return;
390 }
391
392 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
394 let lang = "go";
395 let overrides = call_config.overrides.get(lang);
396 let function_name = to_go_name(
397 overrides
398 .and_then(|o| o.function.as_ref())
399 .map(String::as_str)
400 .unwrap_or(&call_config.function),
401 );
402 let result_var = &call_config.result_var;
403 let args = &call_config.args;
404
405 let returns_result = overrides
408 .and_then(|o| o.returns_result)
409 .unwrap_or(call_config.returns_result);
410
411 let returns_void = call_config.returns_void;
414
415 let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
418 call_config
419 .overrides
420 .get("rust")
421 .map(|o| o.result_is_simple)
422 .unwrap_or(false)
423 });
424
425 let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
428
429 let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
431 e2e_config
432 .call
433 .overrides
434 .get("go")
435 .and_then(|o| o.options_type.as_deref())
436 });
437
438 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
439
440 let (mut setup_lines, args_str) =
441 build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
442
443 let mut visitor_arg = String::new();
445 if fixture.visitor.is_some() {
446 let struct_name = visitor_struct_name(&fixture.id);
447 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
448 visitor_arg = "visitor".to_string();
449 }
450
451 let final_args = if visitor_arg.is_empty() {
452 args_str
453 } else {
454 format!("{args_str}, {visitor_arg}")
455 };
456
457 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
458 let _ = writeln!(out, "\t// {description}");
459
460 for line in &setup_lines {
461 let _ = writeln!(out, "\t{line}");
462 }
463
464 if expects_error {
465 if returns_result && !returns_void {
466 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
467 } else {
468 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
469 }
470 let _ = writeln!(out, "\tif err == nil {{");
471 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
472 let _ = writeln!(out, "\t}}");
473 let _ = writeln!(out, "}}");
474 return;
475 }
476
477 let has_usable_assertion = fixture.assertions.iter().any(|a| {
481 if a.assertion_type == "not_error" || a.assertion_type == "error" {
482 return false;
483 }
484 if a.assertion_type == "method_result" {
486 return true;
487 }
488 match &a.field {
489 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
490 _ => true,
491 }
492 });
493
494 if !returns_result && result_is_simple {
500 let result_binding = if has_usable_assertion {
502 result_var.to_string()
503 } else {
504 "_".to_string()
505 };
506 let assign_op = if result_binding == "_" { "=" } else { ":=" };
508 let _ = writeln!(
509 out,
510 "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
511 );
512 if has_usable_assertion && result_binding != "_" {
513 let _ = writeln!(out, "\tif {result_var} == nil {{");
515 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
516 let _ = writeln!(out, "\t}}");
517 let _ = writeln!(out, "\tvalue := *{result_var}");
518 }
519 } else if !returns_result || returns_void {
520 let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
523 let _ = writeln!(out, "\tif err != nil {{");
524 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
525 let _ = writeln!(out, "\t}}");
526 let _ = writeln!(out, "}}");
528 return;
529 } else {
530 let result_binding = if has_usable_assertion {
532 result_var.to_string()
533 } else {
534 "_".to_string()
535 };
536 let _ = writeln!(
537 out,
538 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
539 );
540 let _ = writeln!(out, "\tif err != nil {{");
541 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
542 let _ = writeln!(out, "\t}}");
543 if result_is_simple && has_usable_assertion && result_binding != "_" {
544 let _ = writeln!(out, "\tif {result_var} == nil {{");
546 let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
547 let _ = writeln!(out, "\t}}");
548 let _ = writeln!(out, "\tvalue := *{result_var}");
549 }
550 }
551
552 let effective_result_var = if result_is_simple && has_usable_assertion {
554 "value".to_string()
555 } else {
556 result_var.to_string()
557 };
558
559 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
564 for assertion in &fixture.assertions {
565 if let Some(f) = &assertion.field {
566 if !f.is_empty() {
567 let resolved = field_resolver.resolve(f);
568 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
569 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
574 let is_array_field = field_resolver.is_array(resolved);
575 if !is_string_field || is_array_field {
576 continue;
579 }
580 let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
581 let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
582 if field_resolver.has_map_access(f) {
583 let _ = writeln!(out, "\t{local_var} := {field_expr}");
586 } else {
587 let _ = writeln!(out, "\tvar {local_var} string");
588 let _ = writeln!(out, "\tif {field_expr} != nil {{");
589 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
590 let _ = writeln!(out, "\t}}");
591 }
592 optional_locals.insert(f.clone(), local_var);
593 }
594 }
595 }
596 }
597
598 for assertion in &fixture.assertions {
600 if let Some(f) = &assertion.field {
601 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
602 let parts: Vec<&str> = f.split('.').collect();
605 let mut guard_expr: Option<String> = None;
606 for i in 1..parts.len() {
607 let prefix = parts[..i].join(".");
608 let resolved_prefix = field_resolver.resolve(&prefix);
609 if field_resolver.is_optional(resolved_prefix) {
610 let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
611 guard_expr = Some(accessor);
612 break;
613 }
614 }
615 if let Some(guard) = guard_expr {
616 if field_resolver.is_valid_for_result(f) {
619 let _ = writeln!(out, "\tif {guard} != nil {{");
620 let mut nil_buf = String::new();
623 render_assertion(
624 &mut nil_buf,
625 assertion,
626 &effective_result_var,
627 import_alias,
628 field_resolver,
629 &optional_locals,
630 result_is_simple,
631 result_is_array,
632 );
633 for line in nil_buf.lines() {
634 let _ = writeln!(out, "\t{line}");
635 }
636 let _ = writeln!(out, "\t}}");
637 } else {
638 render_assertion(
639 out,
640 assertion,
641 &effective_result_var,
642 import_alias,
643 field_resolver,
644 &optional_locals,
645 result_is_simple,
646 result_is_array,
647 );
648 }
649 continue;
650 }
651 }
652 }
653 render_assertion(
654 out,
655 assertion,
656 &effective_result_var,
657 import_alias,
658 field_resolver,
659 &optional_locals,
660 result_is_simple,
661 result_is_array,
662 );
663 }
664
665 let _ = writeln!(out, "}}");
666}
667
668fn render_http_test_function(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
673 let fn_name = fixture.id.to_upper_camel_case();
674 let description = &fixture.description;
675 let request = &http.request;
676 let expected = &http.expected_response;
677 let method = request.method.to_uppercase();
678 let fixture_id = &fixture.id;
679 let expected_status = expected.status_code;
680
681 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
682 let _ = writeln!(out, "\t// {description}");
683 let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
684 let _ = writeln!(out, "\tif baseURL == \"\" {{");
685 let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
686 let _ = writeln!(out, "\t}}");
687
688 let body_expr = if let Some(body) = &request.body {
690 let json = serde_json::to_string(body).unwrap_or_default();
691 let escaped = go_string_literal(&json);
692 format!("strings.NewReader({})", escaped)
693 } else {
694 "strings.NewReader(\"\")".to_string()
695 };
696
697 let _ = writeln!(out, "\tbody := {body_expr}");
698 let _ = writeln!(
699 out,
700 "\treq, err := http.NewRequest(\"{method}\", baseURL+\"/fixtures/{fixture_id}\", body)"
701 );
702 let _ = writeln!(out, "\tif err != nil {{");
703 let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
704 let _ = writeln!(out, "\t}}");
705
706 let content_type = request.content_type.as_deref().unwrap_or("application/json");
708 if request.body.is_some() {
709 let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
710 }
711
712 for (name, value) in &request.headers {
713 let escaped_name = go_string_literal(name);
714 let escaped_value = go_string_literal(value);
715 let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
716 }
717
718 if !request.cookies.is_empty() {
720 for (name, value) in &request.cookies {
721 let escaped_name = go_string_literal(name);
722 let escaped_value = go_string_literal(value);
723 let _ = writeln!(
724 out,
725 "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
726 );
727 }
728 }
729
730 let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
734 let _ = writeln!(
735 out,
736 "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
737 );
738 let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
739 let _ = writeln!(out, "\t\t}},");
740 let _ = writeln!(out, "\t}}");
741 let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
742 let _ = writeln!(out, "\tif err != nil {{");
743 let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
744 let _ = writeln!(out, "\t}}");
745 let _ = writeln!(out, "\tdefer resp.Body.Close()");
746
747 let body_used = expected.body.is_some();
749 if body_used {
750 let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
751 let _ = writeln!(out, "\tif err != nil {{");
752 let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
753 let _ = writeln!(out, "\t}}");
754 }
755
756 let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
758 let _ = writeln!(
759 out,
760 "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
761 );
762 let _ = writeln!(out, "\t}}");
763
764 if let Some(expected_body) = &expected.body {
766 match expected_body {
767 serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
768 let json_str = serde_json::to_string(expected_body).unwrap_or_default();
769 let escaped = go_string_literal(&json_str);
770 let _ = writeln!(out, "\tvar got any");
772 let _ = writeln!(out, "\tvar want any");
773 let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
774 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
775 let _ = writeln!(out, "\t}}");
776 let _ = writeln!(
777 out,
778 "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
779 );
780 let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
781 let _ = writeln!(out, "\t}}");
782 let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
783 let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
784 let _ = writeln!(out, "\t}}");
785 }
786 serde_json::Value::String(s) => {
787 let escaped = go_string_literal(s);
788 let _ = writeln!(out, "\twant := {escaped}");
789 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
790 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
791 let _ = writeln!(out, "\t}}");
792 }
793 other => {
794 let escaped = go_string_literal(&other.to_string());
795 let _ = writeln!(out, "\twant := {escaped}");
796 let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
797 let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
798 let _ = writeln!(out, "\t}}");
799 }
800 }
801 }
802
803 for (name, value) in &expected.headers {
805 if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
806 continue;
808 }
809 let lower = name.to_ascii_lowercase();
813 if lower == "content-encoding" || lower == "connection" {
814 continue;
815 }
816 let escaped_name = go_string_literal(name);
817 let escaped_value = go_string_literal(value);
818 let _ = writeln!(
819 out,
820 "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
821 );
822 let _ = writeln!(
823 out,
824 "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
825 );
826 let _ = writeln!(out, "\t}}");
827 }
828
829 let _ = writeln!(out, "}}");
830}
831
832fn build_args_and_setup(
836 input: &serde_json::Value,
837 args: &[crate::config::ArgMapping],
838 import_alias: &str,
839 options_type: Option<&str>,
840 fixture_id: &str,
841) -> (Vec<String>, String) {
842 use heck::ToUpperCamelCase;
843
844 if args.is_empty() {
845 return (Vec::new(), String::new());
846 }
847
848 let mut setup_lines: Vec<String> = Vec::new();
849 let mut parts: Vec<String> = Vec::new();
850
851 for arg in args {
852 if arg.arg_type == "mock_url" {
853 setup_lines.push(format!(
854 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
855 arg.name,
856 ));
857 parts.push(arg.name.clone());
858 continue;
859 }
860
861 if arg.arg_type == "handle" {
862 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
864 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
865 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
866 if config_value.is_null()
867 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
868 {
869 setup_lines.push(format!(
870 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
871 name = arg.name,
872 ));
873 } else {
874 let json_str = serde_json::to_string(config_value).unwrap_or_default();
875 let go_literal = go_string_literal(&json_str);
876 let name = &arg.name;
877 setup_lines.push(format!(
878 "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}}"
879 ));
880 setup_lines.push(format!(
881 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
882 ));
883 }
884 parts.push(arg.name.clone());
885 continue;
886 }
887
888 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
889 let val = input.get(field);
890
891 if arg.arg_type == "bytes" {
894 let var_name = format!("{}Bytes", arg.name);
895 match val {
896 None | Some(serde_json::Value::Null) => {
897 if arg.optional {
898 parts.push("nil".to_string());
899 } else {
900 parts.push("[]byte{}".to_string());
901 }
902 }
903 Some(serde_json::Value::String(s)) => {
904 let go_b64 = go_string_literal(s);
905 setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
906 parts.push(var_name);
907 }
908 Some(other) => {
909 parts.push(format!("[]byte({})", json_to_go(other)));
910 }
911 }
912 continue;
913 }
914
915 match val {
916 None | Some(serde_json::Value::Null) if arg.optional => {
917 match arg.arg_type.as_str() {
919 "string" => {
920 parts.push("nil".to_string());
922 }
923 "json_object" => {
924 if let Some(opts_type) = options_type {
926 parts.push(format!("{import_alias}.{opts_type}{{}}"));
927 } else {
928 parts.push("nil".to_string());
929 }
930 }
931 _ => {
932 parts.push("nil".to_string());
933 }
934 }
935 }
936 None | Some(serde_json::Value::Null) => {
937 let default_val = match arg.arg_type.as_str() {
939 "string" => "\"\"".to_string(),
940 "int" | "integer" | "i64" => "0".to_string(),
941 "float" | "number" => "0.0".to_string(),
942 "bool" | "boolean" => "false".to_string(),
943 "json_object" => {
944 if let Some(opts_type) = options_type {
945 format!("{import_alias}.{opts_type}{{}}")
946 } else {
947 "nil".to_string()
948 }
949 }
950 _ => "nil".to_string(),
951 };
952 parts.push(default_val);
953 }
954 Some(v) => {
955 match arg.arg_type.as_str() {
956 "json_object" => {
957 let is_array = v.is_array();
960 let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
961 if is_empty_obj {
962 if let Some(opts_type) = options_type {
963 parts.push(format!("{import_alias}.{opts_type}{{}}"));
964 } else {
965 parts.push("nil".to_string());
966 }
967 } else if is_array {
968 let json_str = serde_json::to_string(v).unwrap_or_default();
970 let go_literal = go_string_literal(&json_str);
971 let var_name = &arg.name;
972 setup_lines.push(format!(
973 "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}}"
974 ));
975 parts.push(var_name.to_string());
976 } else if let Some(opts_type) = options_type {
977 let json_str = serde_json::to_string(v).unwrap_or_default();
979 let go_literal = go_string_literal(&json_str);
980 let var_name = &arg.name;
981 setup_lines.push(format!(
982 "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}}"
983 ));
984 parts.push(var_name.to_string());
985 } else {
986 parts.push(json_to_go(v));
987 }
988 }
989 "string" if arg.optional => {
990 let var_name = format!("{}Val", arg.name);
992 let go_val = json_to_go(v);
993 setup_lines.push(format!("{var_name} := {go_val}"));
994 parts.push(format!("&{var_name}"));
995 }
996 _ => {
997 parts.push(json_to_go(v));
998 }
999 }
1000 }
1001 }
1002 }
1003
1004 (setup_lines, parts.join(", "))
1005}
1006
1007#[allow(clippy::too_many_arguments)]
1008fn render_assertion(
1009 out: &mut String,
1010 assertion: &Assertion,
1011 result_var: &str,
1012 import_alias: &str,
1013 field_resolver: &FieldResolver,
1014 optional_locals: &std::collections::HashMap<String, String>,
1015 result_is_simple: bool,
1016 result_is_array: bool,
1017) {
1018 if !result_is_simple {
1021 if let Some(f) = &assertion.field {
1022 let embed_deref = format!("(*{result_var})");
1025 match f.as_str() {
1026 "chunks_have_content" => {
1027 let pred = format!(
1028 "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1029 );
1030 match assertion.assertion_type.as_str() {
1031 "is_true" => {
1032 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1033 }
1034 "is_false" => {
1035 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1036 }
1037 _ => {
1038 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1039 }
1040 }
1041 return;
1042 }
1043 "chunks_have_embeddings" => {
1044 let pred = format!(
1045 "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 }}()"
1046 );
1047 match assertion.assertion_type.as_str() {
1048 "is_true" => {
1049 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1050 }
1051 "is_false" => {
1052 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1053 }
1054 _ => {
1055 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1056 }
1057 }
1058 return;
1059 }
1060 "embeddings" => {
1061 match assertion.assertion_type.as_str() {
1062 "count_equals" => {
1063 if let Some(val) = &assertion.value {
1064 if let Some(n) = val.as_u64() {
1065 let _ = writeln!(
1066 out,
1067 "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1068 );
1069 }
1070 }
1071 }
1072 "count_min" => {
1073 if let Some(val) = &assertion.value {
1074 if let Some(n) = val.as_u64() {
1075 let _ = writeln!(
1076 out,
1077 "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1078 );
1079 }
1080 }
1081 }
1082 "not_empty" => {
1083 let _ = writeln!(
1084 out,
1085 "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1086 );
1087 }
1088 "is_empty" => {
1089 let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1090 }
1091 _ => {
1092 let _ = writeln!(
1093 out,
1094 "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1095 );
1096 }
1097 }
1098 return;
1099 }
1100 "embedding_dimensions" => {
1101 let expr = format!(
1102 "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1103 );
1104 match assertion.assertion_type.as_str() {
1105 "equals" => {
1106 if let Some(val) = &assertion.value {
1107 if let Some(n) = val.as_u64() {
1108 let _ = writeln!(
1109 out,
1110 "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1111 );
1112 }
1113 }
1114 }
1115 "greater_than" => {
1116 if let Some(val) = &assertion.value {
1117 if let Some(n) = val.as_u64() {
1118 let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1119 }
1120 }
1121 }
1122 _ => {
1123 let _ = writeln!(
1124 out,
1125 "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1126 );
1127 }
1128 }
1129 return;
1130 }
1131 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1132 let pred = match f.as_str() {
1133 "embeddings_valid" => {
1134 format!(
1135 "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1136 )
1137 }
1138 "embeddings_finite" => {
1139 format!(
1140 "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 }}()"
1141 )
1142 }
1143 "embeddings_non_zero" => {
1144 format!(
1145 "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 }}()"
1146 )
1147 }
1148 "embeddings_normalized" => {
1149 format!(
1150 "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 }}()"
1151 )
1152 }
1153 _ => unreachable!(),
1154 };
1155 match assertion.assertion_type.as_str() {
1156 "is_true" => {
1157 let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1158 }
1159 "is_false" => {
1160 let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1161 }
1162 _ => {
1163 let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1164 }
1165 }
1166 return;
1167 }
1168 "keywords" | "keywords_count" => {
1171 let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1172 return;
1173 }
1174 _ => {}
1175 }
1176 }
1177 }
1178
1179 if !result_is_simple {
1182 if let Some(f) = &assertion.field {
1183 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1184 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1185 return;
1186 }
1187 }
1188 }
1189
1190 let field_expr = if result_is_simple {
1191 result_var.to_string()
1193 } else {
1194 match &assertion.field {
1195 Some(f) if !f.is_empty() => {
1196 if let Some(local_var) = optional_locals.get(f.as_str()) {
1198 local_var.clone()
1199 } else {
1200 field_resolver.accessor(f, "go", result_var)
1201 }
1202 }
1203 _ => result_var.to_string(),
1204 }
1205 };
1206
1207 let is_optional = assertion
1211 .field
1212 .as_ref()
1213 .map(|f| {
1214 let resolved = field_resolver.resolve(f);
1215 let check_path = resolved
1216 .strip_suffix(".length")
1217 .or_else(|| resolved.strip_suffix(".count"))
1218 .or_else(|| resolved.strip_suffix(".size"))
1219 .unwrap_or(resolved);
1220 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1221 })
1222 .unwrap_or(false);
1223
1224 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1227 let inner = &field_expr[4..field_expr.len() - 1];
1228 format!("len(*{inner})")
1229 } else {
1230 field_expr
1231 };
1232 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1234 Some(field_expr[5..field_expr.len() - 1].to_string())
1235 } else {
1236 None
1237 };
1238
1239 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1242 format!("*{field_expr}")
1243 } else {
1244 field_expr.clone()
1245 };
1246
1247 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1252 let array_expr = &field_expr[..idx];
1253 Some(array_expr.to_string())
1254 } else {
1255 None
1256 };
1257
1258 let mut assertion_buf = String::new();
1261 let out_ref = &mut assertion_buf;
1262
1263 match assertion.assertion_type.as_str() {
1264 "equals" => {
1265 if let Some(expected) = &assertion.value {
1266 let go_val = json_to_go(expected);
1267 if expected.is_string() {
1269 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1271 format!("strings.TrimSpace(*{field_expr})")
1272 } else {
1273 format!("strings.TrimSpace({field_expr})")
1274 };
1275 if is_optional && !field_expr.starts_with("len(") {
1276 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1277 } else {
1278 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1279 }
1280 } else if is_optional && !field_expr.starts_with("len(") {
1281 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1282 } else {
1283 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1284 }
1285 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1286 let _ = writeln!(out_ref, "\t}}");
1287 }
1288 }
1289 "contains" => {
1290 if let Some(expected) = &assertion.value {
1291 let go_val = json_to_go(expected);
1292 let resolved_field = assertion.field.as_deref().unwrap_or("");
1298 let resolved_name = field_resolver.resolve(resolved_field);
1299 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1300 let is_opt =
1301 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1302 let field_for_contains = if is_opt && field_is_array {
1303 format!("strings.Join(*{field_expr}, \" \")")
1304 } else if is_opt {
1305 format!("string(*{field_expr})")
1306 } else if field_is_array {
1307 format!("strings.Join({field_expr}, \" \")")
1308 } else {
1309 format!("string({field_expr})")
1310 };
1311 if is_opt {
1312 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1313 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1314 let _ = writeln!(
1315 out_ref,
1316 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1317 );
1318 let _ = writeln!(out_ref, "\t}}");
1319 let _ = writeln!(out_ref, "\t}}");
1320 } else {
1321 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1322 let _ = writeln!(
1323 out_ref,
1324 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1325 );
1326 let _ = writeln!(out_ref, "\t}}");
1327 }
1328 }
1329 }
1330 "contains_all" => {
1331 if let Some(values) = &assertion.values {
1332 let resolved_field = assertion.field.as_deref().unwrap_or("");
1333 let resolved_name = field_resolver.resolve(resolved_field);
1334 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1335 let is_opt =
1336 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1337 for val in values {
1338 let go_val = json_to_go(val);
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 if is_opt {
1349 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1350 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1351 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1352 let _ = writeln!(out_ref, "\t}}");
1353 let _ = writeln!(out_ref, "\t}}");
1354 } else {
1355 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1356 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1357 let _ = writeln!(out_ref, "\t}}");
1358 }
1359 }
1360 }
1361 }
1362 "not_contains" => {
1363 if let Some(expected) = &assertion.value {
1364 let go_val = json_to_go(expected);
1365 let resolved_field = assertion.field.as_deref().unwrap_or("");
1366 let resolved_name = field_resolver.resolve(resolved_field);
1367 let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1368 let is_opt =
1369 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1370 let field_for_contains = if is_opt && field_is_array {
1371 format!("strings.Join(*{field_expr}, \" \")")
1372 } else if is_opt {
1373 format!("string(*{field_expr})")
1374 } else if field_is_array {
1375 format!("strings.Join({field_expr}, \" \")")
1376 } else {
1377 format!("string({field_expr})")
1378 };
1379 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1380 let _ = writeln!(
1381 out_ref,
1382 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1383 );
1384 let _ = writeln!(out_ref, "\t}}");
1385 }
1386 }
1387 "not_empty" => {
1388 let field_is_array = {
1391 let rf = assertion.field.as_deref().unwrap_or("");
1392 let rn = field_resolver.resolve(rf);
1393 field_resolver.is_array(rn)
1394 };
1395 if is_optional && !field_is_array {
1396 let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1398 } else if is_optional {
1399 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1400 } else {
1401 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1402 }
1403 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1404 let _ = writeln!(out_ref, "\t}}");
1405 }
1406 "is_empty" => {
1407 let field_is_array = {
1408 let rf = assertion.field.as_deref().unwrap_or("");
1409 let rn = field_resolver.resolve(rf);
1410 field_resolver.is_array(rn)
1411 };
1412 if is_optional && !field_is_array {
1413 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1415 } else if is_optional {
1416 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1417 } else {
1418 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1419 }
1420 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1421 let _ = writeln!(out_ref, "\t}}");
1422 }
1423 "contains_any" => {
1424 if let Some(values) = &assertion.values {
1425 let resolved_field = assertion.field.as_deref().unwrap_or("");
1426 let resolved_name = field_resolver.resolve(resolved_field);
1427 let field_is_array = field_resolver.is_array(resolved_name);
1428 let is_opt =
1429 is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1430 let field_for_contains = if is_opt && field_is_array {
1431 format!("strings.Join(*{field_expr}, \" \")")
1432 } else if is_opt {
1433 format!("*{field_expr}")
1434 } else if field_is_array {
1435 format!("strings.Join({field_expr}, \" \")")
1436 } else {
1437 field_expr.clone()
1438 };
1439 let _ = writeln!(out_ref, "\t{{");
1440 let _ = writeln!(out_ref, "\t\tfound := false");
1441 for val in values {
1442 let go_val = json_to_go(val);
1443 let _ = writeln!(
1444 out_ref,
1445 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1446 );
1447 }
1448 let _ = writeln!(out_ref, "\t\tif !found {{");
1449 let _ = writeln!(
1450 out_ref,
1451 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1452 );
1453 let _ = writeln!(out_ref, "\t\t}}");
1454 let _ = writeln!(out_ref, "\t}}");
1455 }
1456 }
1457 "greater_than" => {
1458 if let Some(val) = &assertion.value {
1459 let go_val = json_to_go(val);
1460 if is_optional {
1464 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1465 if let Some(n) = val.as_u64() {
1466 let next = n + 1;
1467 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1468 } else {
1469 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1470 }
1471 let _ = writeln!(
1472 out_ref,
1473 "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1474 );
1475 let _ = writeln!(out_ref, "\t\t}}");
1476 let _ = writeln!(out_ref, "\t}}");
1477 } else if let Some(n) = val.as_u64() {
1478 let next = n + 1;
1479 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1480 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1481 let _ = writeln!(out_ref, "\t}}");
1482 } else {
1483 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1484 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1485 let _ = writeln!(out_ref, "\t}}");
1486 }
1487 }
1488 }
1489 "less_than" => {
1490 if let Some(val) = &assertion.value {
1491 let go_val = json_to_go(val);
1492 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1493 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1494 let _ = writeln!(out_ref, "\t}}");
1495 }
1496 }
1497 "greater_than_or_equal" => {
1498 if let Some(val) = &assertion.value {
1499 let go_val = json_to_go(val);
1500 if let Some(ref guard) = nil_guard_expr {
1501 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1502 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1503 let _ = writeln!(
1504 out_ref,
1505 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1506 );
1507 let _ = writeln!(out_ref, "\t\t}}");
1508 let _ = writeln!(out_ref, "\t}}");
1509 } else if is_optional && !field_expr.starts_with("len(") {
1510 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1512 let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1513 let _ = writeln!(
1514 out_ref,
1515 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1516 );
1517 let _ = writeln!(out_ref, "\t\t}}");
1518 let _ = writeln!(out_ref, "\t}}");
1519 } else {
1520 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1521 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1522 let _ = writeln!(out_ref, "\t}}");
1523 }
1524 }
1525 }
1526 "less_than_or_equal" => {
1527 if let Some(val) = &assertion.value {
1528 let go_val = json_to_go(val);
1529 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1530 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1531 let _ = writeln!(out_ref, "\t}}");
1532 }
1533 }
1534 "starts_with" => {
1535 if let Some(expected) = &assertion.value {
1536 let go_val = json_to_go(expected);
1537 let field_for_prefix = if is_optional
1538 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1539 {
1540 format!("string(*{field_expr})")
1541 } else {
1542 format!("string({field_expr})")
1543 };
1544 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1545 let _ = writeln!(
1546 out_ref,
1547 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1548 );
1549 let _ = writeln!(out_ref, "\t}}");
1550 }
1551 }
1552 "count_min" => {
1553 if let Some(val) = &assertion.value {
1554 if let Some(n) = val.as_u64() {
1555 if is_optional {
1556 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1557 let _ = writeln!(
1558 out_ref,
1559 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1560 );
1561 let _ = writeln!(out_ref, "\t}}");
1562 } else {
1563 let _ = writeln!(
1564 out_ref,
1565 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1566 );
1567 }
1568 }
1569 }
1570 }
1571 "count_equals" => {
1572 if let Some(val) = &assertion.value {
1573 if let Some(n) = val.as_u64() {
1574 if is_optional {
1575 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1576 let _ = writeln!(
1577 out_ref,
1578 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1579 );
1580 let _ = writeln!(out_ref, "\t}}");
1581 } else {
1582 let _ = writeln!(
1583 out_ref,
1584 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1585 );
1586 }
1587 }
1588 }
1589 }
1590 "is_true" => {
1591 if is_optional {
1592 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1593 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1594 let _ = writeln!(out_ref, "\t}}");
1595 } else {
1596 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1597 }
1598 }
1599 "is_false" => {
1600 if is_optional {
1601 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1602 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1603 let _ = writeln!(out_ref, "\t}}");
1604 } else {
1605 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1606 }
1607 }
1608 "method_result" => {
1609 if let Some(method_name) = &assertion.method {
1610 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1611 let check = assertion.check.as_deref().unwrap_or("is_true");
1612 let deref_expr = if info.is_pointer {
1615 format!("*{}", info.call_expr)
1616 } else {
1617 info.call_expr.clone()
1618 };
1619 match check {
1620 "equals" => {
1621 if let Some(val) = &assertion.value {
1622 if val.is_boolean() {
1623 if val.as_bool() == Some(true) {
1624 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1625 } else {
1626 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1627 }
1628 } else {
1629 let go_val = if let Some(cast) = info.value_cast {
1633 if val.is_number() {
1634 format!("{cast}({})", json_to_go(val))
1635 } else {
1636 json_to_go(val)
1637 }
1638 } else {
1639 json_to_go(val)
1640 };
1641 let _ = writeln!(
1642 out_ref,
1643 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1644 );
1645 }
1646 }
1647 }
1648 "is_true" => {
1649 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1650 }
1651 "is_false" => {
1652 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1653 }
1654 "greater_than_or_equal" => {
1655 if let Some(val) = &assertion.value {
1656 let n = val.as_u64().unwrap_or(0);
1657 let cast = info.value_cast.unwrap_or("uint");
1659 let _ = writeln!(
1660 out_ref,
1661 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1662 );
1663 }
1664 }
1665 "count_min" => {
1666 if let Some(val) = &assertion.value {
1667 let n = val.as_u64().unwrap_or(0);
1668 let _ = writeln!(
1669 out_ref,
1670 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1671 );
1672 }
1673 }
1674 "contains" => {
1675 if let Some(val) = &assertion.value {
1676 let go_val = json_to_go(val);
1677 let _ = writeln!(
1678 out_ref,
1679 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1680 );
1681 }
1682 }
1683 "is_error" => {
1684 let _ = writeln!(out_ref, "\t{{");
1685 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1686 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1687 let _ = writeln!(out_ref, "\t}}");
1688 }
1689 other_check => {
1690 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1691 }
1692 }
1693 } else {
1694 panic!("Go e2e generator: method_result assertion missing 'method' field");
1695 }
1696 }
1697 "min_length" => {
1698 if let Some(val) = &assertion.value {
1699 if let Some(n) = val.as_u64() {
1700 if is_optional {
1701 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1702 let _ = writeln!(
1703 out_ref,
1704 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1705 );
1706 let _ = writeln!(out_ref, "\t}}");
1707 } else {
1708 let _ = writeln!(
1709 out_ref,
1710 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1711 );
1712 }
1713 }
1714 }
1715 }
1716 "max_length" => {
1717 if let Some(val) = &assertion.value {
1718 if let Some(n) = val.as_u64() {
1719 if is_optional {
1720 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1721 let _ = writeln!(
1722 out_ref,
1723 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1724 );
1725 let _ = writeln!(out_ref, "\t}}");
1726 } else {
1727 let _ = writeln!(
1728 out_ref,
1729 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1730 );
1731 }
1732 }
1733 }
1734 }
1735 "ends_with" => {
1736 if let Some(expected) = &assertion.value {
1737 let go_val = json_to_go(expected);
1738 let field_for_suffix = if is_optional
1739 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1740 {
1741 format!("string(*{field_expr})")
1742 } else {
1743 format!("string({field_expr})")
1744 };
1745 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1746 let _ = writeln!(
1747 out_ref,
1748 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1749 );
1750 let _ = writeln!(out_ref, "\t}}");
1751 }
1752 }
1753 "matches_regex" => {
1754 if let Some(expected) = &assertion.value {
1755 let go_val = json_to_go(expected);
1756 let field_for_regex = if is_optional
1757 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1758 {
1759 format!("*{field_expr}")
1760 } else {
1761 field_expr.clone()
1762 };
1763 let _ = writeln!(
1764 out_ref,
1765 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1766 );
1767 }
1768 }
1769 "not_error" => {
1770 }
1772 "error" => {
1773 }
1775 other => {
1776 panic!("Go e2e generator: unsupported assertion type: {other}");
1777 }
1778 }
1779
1780 if let Some(ref arr) = array_guard {
1783 if !assertion_buf.is_empty() {
1784 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1785 for line in assertion_buf.lines() {
1787 let _ = writeln!(out, "\t{line}");
1788 }
1789 let _ = writeln!(out, "\t}}");
1790 }
1791 } else {
1792 out.push_str(&assertion_buf);
1793 }
1794}
1795
1796struct GoMethodCallInfo {
1798 call_expr: String,
1800 is_pointer: bool,
1802 value_cast: Option<&'static str>,
1805}
1806
1807fn build_go_method_call(
1822 result_var: &str,
1823 method_name: &str,
1824 args: Option<&serde_json::Value>,
1825 import_alias: &str,
1826) -> GoMethodCallInfo {
1827 match method_name {
1828 "root_node_type" => GoMethodCallInfo {
1829 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1830 is_pointer: false,
1831 value_cast: None,
1832 },
1833 "named_children_count" => GoMethodCallInfo {
1834 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1835 is_pointer: false,
1836 value_cast: Some("uint"),
1837 },
1838 "has_error_nodes" => GoMethodCallInfo {
1839 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1840 is_pointer: true,
1841 value_cast: None,
1842 },
1843 "error_count" | "tree_error_count" => GoMethodCallInfo {
1844 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1845 is_pointer: true,
1846 value_cast: Some("uint"),
1847 },
1848 "tree_to_sexp" => GoMethodCallInfo {
1849 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1850 is_pointer: true,
1851 value_cast: None,
1852 },
1853 "contains_node_type" => {
1854 let node_type = args
1855 .and_then(|a| a.get("node_type"))
1856 .and_then(|v| v.as_str())
1857 .unwrap_or("");
1858 GoMethodCallInfo {
1859 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1860 is_pointer: true,
1861 value_cast: None,
1862 }
1863 }
1864 "find_nodes_by_type" => {
1865 let node_type = args
1866 .and_then(|a| a.get("node_type"))
1867 .and_then(|v| v.as_str())
1868 .unwrap_or("");
1869 GoMethodCallInfo {
1870 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1871 is_pointer: true,
1872 value_cast: None,
1873 }
1874 }
1875 "run_query" => {
1876 let query_source = args
1877 .and_then(|a| a.get("query_source"))
1878 .and_then(|v| v.as_str())
1879 .unwrap_or("");
1880 let language = args
1881 .and_then(|a| a.get("language"))
1882 .and_then(|v| v.as_str())
1883 .unwrap_or("");
1884 let query_lit = go_string_literal(query_source);
1885 let lang_lit = go_string_literal(language);
1886 GoMethodCallInfo {
1888 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1889 is_pointer: false,
1890 value_cast: None,
1891 }
1892 }
1893 other => {
1894 let method_pascal = other.to_upper_camel_case();
1895 GoMethodCallInfo {
1896 call_expr: format!("{result_var}.{method_pascal}()"),
1897 is_pointer: false,
1898 value_cast: None,
1899 }
1900 }
1901 }
1902}
1903
1904fn json_to_go(value: &serde_json::Value) -> String {
1906 match value {
1907 serde_json::Value::String(s) => go_string_literal(s),
1908 serde_json::Value::Bool(b) => b.to_string(),
1909 serde_json::Value::Number(n) => n.to_string(),
1910 serde_json::Value::Null => "nil".to_string(),
1911 other => go_string_literal(&other.to_string()),
1913 }
1914}
1915
1916fn visitor_struct_name(fixture_id: &str) -> String {
1925 use heck::ToUpperCamelCase;
1926 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1928}
1929
1930fn emit_go_visitor_struct(
1932 out: &mut String,
1933 struct_name: &str,
1934 visitor_spec: &crate::fixture::VisitorSpec,
1935 import_alias: &str,
1936) {
1937 let _ = writeln!(out, "type {struct_name} struct{{}}");
1938 for (method_name, action) in &visitor_spec.callbacks {
1939 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1940 }
1941}
1942
1943fn emit_go_visitor_method(
1945 out: &mut String,
1946 struct_name: &str,
1947 method_name: &str,
1948 action: &CallbackAction,
1949 import_alias: &str,
1950) {
1951 let camel_method = method_to_camel(method_name);
1952 let params = match method_name {
1953 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1954 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1955 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1956 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1957 "visit_code_inline"
1958 | "visit_strong"
1959 | "visit_emphasis"
1960 | "visit_strikethrough"
1961 | "visit_underline"
1962 | "visit_subscript"
1963 | "visit_superscript"
1964 | "visit_mark"
1965 | "visit_button"
1966 | "visit_summary"
1967 | "visit_figcaption"
1968 | "visit_definition_term"
1969 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1970 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1971 "visit_list_item" => {
1972 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1973 }
1974 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1975 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1976 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1977 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1978 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1979 "visit_audio" | "visit_video" | "visit_iframe" => {
1980 format!("_ {import_alias}.NodeContext, src string")
1981 }
1982 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1983 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1984 format!("_ {import_alias}.NodeContext, output string")
1985 }
1986 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1987 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1988 _ => format!("_ {import_alias}.NodeContext"),
1989 };
1990
1991 let _ = writeln!(
1992 out,
1993 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1994 );
1995 match action {
1996 CallbackAction::Skip => {
1997 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1998 }
1999 CallbackAction::Continue => {
2000 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
2001 }
2002 CallbackAction::PreserveHtml => {
2003 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
2004 }
2005 CallbackAction::Custom { output } => {
2006 let escaped = go_string_literal(output);
2007 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
2008 }
2009 CallbackAction::CustomTemplate { template } => {
2010 let (fmt_str, fmt_args) = template_to_sprintf(template);
2013 let escaped_fmt = go_string_literal(&fmt_str);
2014 if fmt_args.is_empty() {
2015 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
2016 } else {
2017 let args_str = fmt_args.join(", ");
2018 let _ = writeln!(
2019 out,
2020 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
2021 );
2022 }
2023 }
2024 }
2025 let _ = writeln!(out, "}}");
2026}
2027
2028fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2032 let mut fmt_str = String::new();
2033 let mut args: Vec<String> = Vec::new();
2034 let mut chars = template.chars().peekable();
2035 while let Some(c) = chars.next() {
2036 if c == '{' {
2037 let mut name = String::new();
2039 for inner in chars.by_ref() {
2040 if inner == '}' {
2041 break;
2042 }
2043 name.push(inner);
2044 }
2045 fmt_str.push_str("%s");
2046 args.push(name);
2047 } else {
2048 fmt_str.push(c);
2049 }
2050 }
2051 (fmt_str, args)
2052}
2053
2054fn method_to_camel(snake: &str) -> String {
2056 use heck::ToUpperCamelCase;
2057 snake.to_upper_camel_case()
2058}
2059
2060#[cfg(test)]
2061mod tests {
2062 use super::*;
2063 use crate::config::{CallConfig, E2eConfig};
2064 use crate::field_access::FieldResolver;
2065 use crate::fixture::{Assertion, Fixture};
2066
2067 fn make_fixture(id: &str) -> Fixture {
2068 Fixture {
2069 id: id.to_string(),
2070 category: None,
2071 description: "test fixture".to_string(),
2072 tags: vec![],
2073 skip: None,
2074 call: None,
2075 input: serde_json::Value::Null,
2076 mock_response: Some(crate::fixture::MockResponse {
2077 status: 200,
2078 body: Some(serde_json::Value::Null),
2079 stream_chunks: None,
2080 headers: std::collections::HashMap::new(),
2081 }),
2082 source: String::new(),
2083 http: None,
2084 assertions: vec![Assertion {
2085 assertion_type: "not_error".to_string(),
2086 field: None,
2087 value: None,
2088 values: None,
2089 method: None,
2090 args: None,
2091 check: None,
2092 }],
2093 visitor: None,
2094 }
2095 }
2096
2097 #[test]
2101 fn test_go_method_name_uses_go_casing() {
2102 let e2e_config = E2eConfig {
2103 call: CallConfig {
2104 function: "clean_extracted_text".to_string(),
2105 module: "github.com/example/mylib".to_string(),
2106 result_var: "result".to_string(),
2107 r#async: false,
2108 path: None,
2109 method: None,
2110 args: vec![],
2111 overrides: std::collections::HashMap::new(),
2112 returns_result: true,
2113 returns_void: false,
2114 skip_languages: vec![],
2115 },
2116 ..E2eConfig::default()
2117 };
2118
2119 let fixture = make_fixture("basic_text");
2120 let resolver = FieldResolver::new(
2121 &std::collections::HashMap::new(),
2122 &std::collections::HashSet::new(),
2123 &std::collections::HashSet::new(),
2124 &std::collections::HashSet::new(),
2125 );
2126 let mut out = String::new();
2127 render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2128
2129 assert!(
2130 out.contains("kreuzberg.CleanExtractedText("),
2131 "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2132 );
2133 assert!(
2134 !out.contains("kreuzberg.clean_extracted_text("),
2135 "must not emit raw snake_case method name, got:\n{out}"
2136 );
2137 }
2138}