1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
7use alef_core::backend::GeneratedFile;
8use alef_core::config::AlefConfig;
9use alef_core::hash::{self, CommentStyle};
10use anyhow::Result;
11use heck::ToUpperCamelCase;
12use std::fmt::Write as FmtWrite;
13use std::path::PathBuf;
14
15use super::E2eCodegen;
16
17pub struct GoCodegen;
19
20impl E2eCodegen for GoCodegen {
21 fn generate(
22 &self,
23 groups: &[FixtureGroup],
24 e2e_config: &E2eConfig,
25 alef_config: &AlefConfig,
26 ) -> Result<Vec<GeneratedFile>> {
27 let lang = self.language_name();
28 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
29
30 let mut files = Vec::new();
31
32 let call = &e2e_config.call;
34 let overrides = call.overrides.get(lang);
35 let module_path = overrides
36 .and_then(|o| o.module.as_ref())
37 .cloned()
38 .unwrap_or_else(|| call.module.clone());
39 let import_alias = overrides
40 .and_then(|o| o.alias.as_ref())
41 .cloned()
42 .unwrap_or_else(|| "pkg".to_string());
43
44 let go_pkg = e2e_config.resolve_package("go");
46 let go_module_path = go_pkg
47 .as_ref()
48 .and_then(|p| p.module.as_ref())
49 .cloned()
50 .unwrap_or_else(|| module_path.clone());
51 let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
52 let go_version = go_pkg
53 .as_ref()
54 .and_then(|p| p.version.as_ref())
55 .cloned()
56 .unwrap_or_else(|| {
57 alef_config
58 .resolved_version()
59 .map(|v| format!("v{v}"))
60 .unwrap_or_else(|| "v0.0.0".to_string())
61 });
62 let field_resolver = FieldResolver::new(
63 &e2e_config.fields,
64 &e2e_config.fields_optional,
65 &e2e_config.result_fields,
66 &e2e_config.fields_array,
67 );
68
69 let effective_replace = match e2e_config.dep_mode {
72 crate::config::DependencyMode::Registry => None,
73 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
74 };
75 files.push(GeneratedFile {
76 path: output_base.join("go.mod"),
77 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
78 generated_header: false,
79 });
80
81 for group in groups {
83 let active: Vec<&Fixture> = group
84 .fixtures
85 .iter()
86 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
87 .collect();
88
89 if active.is_empty() {
90 continue;
91 }
92
93 let filename = format!("{}_test.go", sanitize_filename(&group.category));
94 let content = render_test_file(
95 &group.category,
96 &active,
97 &module_path,
98 &import_alias,
99 &field_resolver,
100 e2e_config,
101 );
102 files.push(GeneratedFile {
103 path: output_base.join(filename),
104 content,
105 generated_header: true,
106 });
107 }
108
109 Ok(files)
110 }
111
112 fn language_name(&self) -> &'static str {
113 "go"
114 }
115}
116
117fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
118 let mut out = String::new();
119 let _ = writeln!(out, "module e2e_go");
120 let _ = writeln!(out);
121 let _ = writeln!(out, "go 1.26");
122 let _ = writeln!(out);
123 let _ = writeln!(out, "require {go_module_path} {version}");
124
125 if let Some(path) = replace_path {
126 let _ = writeln!(out);
127 let _ = writeln!(out, "replace {go_module_path} => {path}");
128 }
129
130 out
131}
132
133fn render_test_file(
134 category: &str,
135 fixtures: &[&Fixture],
136 go_module_path: &str,
137 import_alias: &str,
138 field_resolver: &FieldResolver,
139 e2e_config: &crate::config::E2eConfig,
140) -> String {
141 let mut out = String::new();
142
143 out.push_str(&hash::header(CommentStyle::DoubleSlash));
145 let _ = writeln!(out);
146
147 let needs_os = fixtures.iter().any(|f| {
150 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
151 call_args.iter().any(|a| a.arg_type == "mock_url")
152 });
153
154 let needs_json = fixtures.iter().any(|f| {
156 let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
157 call_args.iter().any(|a| a.arg_type == "handle") && {
158 call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
159 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
160 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
161 })
162 }
163 });
164
165 let needs_fmt = fixtures.iter().any(|f| {
167 f.visitor.as_ref().is_some_and(|v| {
168 v.callbacks.values().any(|action| {
169 if let CallbackAction::CustomTemplate { template } = action {
170 template.contains('{')
171 } else {
172 false
173 }
174 })
175 })
176 });
177
178 let needs_strings = fixtures.iter().any(|f| {
181 f.assertions.iter().any(|a| {
182 let type_needs_strings = if a.assertion_type == "equals" {
183 a.value.as_ref().is_some_and(|v| v.is_string())
185 } else {
186 matches!(
187 a.assertion_type.as_str(),
188 "contains" | "contains_all" | "not_contains" | "starts_with" | "ends_with"
189 )
190 };
191 let field_valid = a
192 .field
193 .as_ref()
194 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
195 .unwrap_or(true);
196 type_needs_strings && field_valid
197 })
198 });
199
200 let needs_assert = fixtures.iter().any(|f| {
203 f.assertions.iter().any(|a| {
204 let field_valid = a
205 .field
206 .as_ref()
207 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
208 .unwrap_or(true);
209 let type_needs_assert = matches!(
210 a.assertion_type.as_str(),
211 "count_min"
212 | "count_max"
213 | "is_true"
214 | "is_false"
215 | "method_result"
216 | "min_length"
217 | "max_length"
218 | "matches_regex"
219 );
220 type_needs_assert && field_valid
221 })
222 });
223
224 let _ = writeln!(out, "// E2e tests for category: {category}");
225 let _ = writeln!(out, "package e2e_test");
226 let _ = writeln!(out);
227 let _ = writeln!(out, "import (");
228 if needs_json {
229 let _ = writeln!(out, "\t\"encoding/json\"");
230 }
231 if needs_fmt {
232 let _ = writeln!(out, "\t\"fmt\"");
233 }
234 if needs_os {
235 let _ = writeln!(out, "\t\"os\"");
236 }
237 if needs_strings {
238 let _ = writeln!(out, "\t\"strings\"");
239 }
240 let _ = writeln!(out, "\t\"testing\"");
241 if needs_assert {
242 let _ = writeln!(out);
243 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
244 }
245 let _ = writeln!(out);
246 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
247 let _ = writeln!(out, ")");
248 let _ = writeln!(out);
249
250 for fixture in fixtures.iter() {
252 if let Some(visitor_spec) = &fixture.visitor {
253 let struct_name = visitor_struct_name(&fixture.id);
254 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
255 let _ = writeln!(out);
256 }
257 }
258
259 for (i, fixture) in fixtures.iter().enumerate() {
260 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
261 if i + 1 < fixtures.len() {
262 let _ = writeln!(out);
263 }
264 }
265
266 while out.ends_with("\n\n") {
268 out.pop();
269 }
270 if !out.ends_with('\n') {
271 out.push('\n');
272 }
273 out
274}
275
276fn render_test_function(
277 out: &mut String,
278 fixture: &Fixture,
279 import_alias: &str,
280 field_resolver: &FieldResolver,
281 e2e_config: &crate::config::E2eConfig,
282) {
283 let fn_name = fixture.id.to_upper_camel_case();
284 let description = &fixture.description;
285
286 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
288 let lang = "go";
289 let overrides = call_config.overrides.get(lang);
290 let function_name = overrides
291 .and_then(|o| o.function.as_ref())
292 .cloned()
293 .unwrap_or_else(|| call_config.function.clone());
294 let result_var = &call_config.result_var;
295 let args = &call_config.args;
296
297 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
298
299 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
300
301 let mut visitor_arg = String::new();
303 if fixture.visitor.is_some() {
304 let struct_name = visitor_struct_name(&fixture.id);
305 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
306 visitor_arg = "visitor".to_string();
307 }
308
309 let final_args = if visitor_arg.is_empty() {
310 args_str
311 } else {
312 format!("{args_str}, {visitor_arg}")
313 };
314
315 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
316 let _ = writeln!(out, "\t// {description}");
317
318 for line in &setup_lines {
319 let _ = writeln!(out, "\t{line}");
320 }
321
322 if expects_error {
323 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
324 let _ = writeln!(out, "\tif err == nil {{");
325 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
326 let _ = writeln!(out, "\t}}");
327 let _ = writeln!(out, "}}");
328 return;
329 }
330
331 let has_usable_assertion = fixture.assertions.iter().any(|a| {
335 if a.assertion_type == "not_error" || a.assertion_type == "error" {
336 return false;
337 }
338 if a.assertion_type == "method_result" {
340 return true;
341 }
342 match &a.field {
343 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
344 _ => true,
345 }
346 });
347
348 let result_binding = if has_usable_assertion {
349 result_var.to_string()
350 } else {
351 "_".to_string()
352 };
353
354 let _ = writeln!(
356 out,
357 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
358 );
359 let _ = writeln!(out, "\tif err != nil {{");
360 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
361 let _ = writeln!(out, "\t}}");
362
363 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
368 for assertion in &fixture.assertions {
369 if let Some(f) = &assertion.field {
370 if !f.is_empty() {
371 let resolved = field_resolver.resolve(f);
372 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
373 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
376 if !is_string_field {
377 continue;
380 }
381 let field_expr = field_resolver.accessor(f, "go", result_var);
382 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
383 if field_resolver.has_map_access(f) {
384 let _ = writeln!(out, "\t{local_var} := {field_expr}");
387 } else {
388 let _ = writeln!(out, "\tvar {local_var} string");
389 let _ = writeln!(out, "\tif {field_expr} != nil {{");
390 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
391 let _ = writeln!(out, "\t}}");
392 }
393 optional_locals.insert(f.clone(), local_var);
394 }
395 }
396 }
397 }
398
399 for assertion in &fixture.assertions {
401 if let Some(f) = &assertion.field {
402 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
403 let parts: Vec<&str> = f.split('.').collect();
406 let mut guard_expr: Option<String> = None;
407 for i in 1..parts.len() {
408 let prefix = parts[..i].join(".");
409 let resolved_prefix = field_resolver.resolve(&prefix);
410 if field_resolver.is_optional(resolved_prefix) {
411 let accessor = field_resolver.accessor(&prefix, "go", result_var);
412 guard_expr = Some(accessor);
413 break;
414 }
415 }
416 if let Some(guard) = guard_expr {
417 if field_resolver.is_valid_for_result(f) {
420 let _ = writeln!(out, "\tif {guard} != nil {{");
421 let mut nil_buf = String::new();
424 render_assertion(
425 &mut nil_buf,
426 assertion,
427 result_var,
428 import_alias,
429 field_resolver,
430 &optional_locals,
431 );
432 for line in nil_buf.lines() {
433 let _ = writeln!(out, "\t{line}");
434 }
435 let _ = writeln!(out, "\t}}");
436 } else {
437 render_assertion(
438 out,
439 assertion,
440 result_var,
441 import_alias,
442 field_resolver,
443 &optional_locals,
444 );
445 }
446 continue;
447 }
448 }
449 }
450 render_assertion(
451 out,
452 assertion,
453 result_var,
454 import_alias,
455 field_resolver,
456 &optional_locals,
457 );
458 }
459
460 let _ = writeln!(out, "}}");
461}
462
463fn build_args_and_setup(
467 input: &serde_json::Value,
468 args: &[crate::config::ArgMapping],
469 import_alias: &str,
470 e2e_config: &crate::config::E2eConfig,
471 fixture_id: &str,
472) -> (Vec<String>, String) {
473 use heck::ToUpperCamelCase;
474
475 if args.is_empty() {
476 return (Vec::new(), json_to_go(input));
477 }
478
479 let overrides = e2e_config.call.overrides.get("go");
480 let options_type = overrides.and_then(|o| o.options_type.as_deref());
481
482 let mut setup_lines: Vec<String> = Vec::new();
483 let mut parts: Vec<String> = Vec::new();
484
485 for arg in args {
486 if arg.arg_type == "mock_url" {
487 setup_lines.push(format!(
488 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
489 arg.name,
490 ));
491 parts.push(arg.name.clone());
492 continue;
493 }
494
495 if arg.arg_type == "handle" {
496 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
498 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
499 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
500 if config_value.is_null()
501 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
502 {
503 setup_lines.push(format!(
504 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
505 name = arg.name,
506 ));
507 } else {
508 let json_str = serde_json::to_string(config_value).unwrap_or_default();
509 let go_literal = go_string_literal(&json_str);
510 let name = &arg.name;
511 setup_lines.push(format!(
512 "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}}"
513 ));
514 setup_lines.push(format!(
515 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
516 ));
517 }
518 parts.push(arg.name.clone());
519 continue;
520 }
521
522 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
523 let val = input.get(field);
524 match val {
525 None | Some(serde_json::Value::Null) if arg.optional => {
526 continue;
528 }
529 None | Some(serde_json::Value::Null) => {
530 let default_val = match arg.arg_type.as_str() {
532 "string" => "\"\"".to_string(),
533 "int" | "integer" => "0".to_string(),
534 "float" | "number" => "0.0".to_string(),
535 "bool" | "boolean" => "false".to_string(),
536 _ => "nil".to_string(),
537 };
538 parts.push(default_val);
539 }
540 Some(v) => {
541 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
543 if let Some(obj) = v.as_object() {
544 let with_calls: Vec<String> = obj
545 .iter()
546 .map(|(k, vv)| {
547 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
548 let go_val = json_to_go(vv);
549 format!("htmd.{func_name}({go_val})")
550 })
551 .collect();
552 let new_fn = format!("New{opts_type}");
553 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
554 continue;
555 }
556 }
557 parts.push(json_to_go(v));
558 }
559 }
560 }
561
562 (setup_lines, parts.join(", "))
563}
564
565fn render_assertion(
566 out: &mut String,
567 assertion: &Assertion,
568 result_var: &str,
569 import_alias: &str,
570 field_resolver: &FieldResolver,
571 optional_locals: &std::collections::HashMap<String, String>,
572) {
573 if let Some(f) = &assertion.field {
575 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
576 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
577 return;
578 }
579 }
580
581 let field_expr = match &assertion.field {
582 Some(f) if !f.is_empty() => {
583 if let Some(local_var) = optional_locals.get(f.as_str()) {
585 local_var.clone()
586 } else {
587 field_resolver.accessor(f, "go", result_var)
588 }
589 }
590 _ => result_var.to_string(),
591 };
592
593 let is_optional = assertion
597 .field
598 .as_ref()
599 .map(|f| {
600 let resolved = field_resolver.resolve(f);
601 let check_path = resolved
602 .strip_suffix(".length")
603 .or_else(|| resolved.strip_suffix(".count"))
604 .or_else(|| resolved.strip_suffix(".size"))
605 .unwrap_or(resolved);
606 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
607 })
608 .unwrap_or(false);
609
610 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
613 let inner = &field_expr[4..field_expr.len() - 1];
614 format!("len(*{inner})")
615 } else {
616 field_expr
617 };
618 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
620 Some(field_expr[5..field_expr.len() - 1].to_string())
621 } else {
622 None
623 };
624
625 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
628 format!("*{field_expr}")
629 } else {
630 field_expr.clone()
631 };
632
633 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
638 let array_expr = &field_expr[..idx];
639 Some(array_expr.to_string())
640 } else {
641 None
642 };
643
644 let mut assertion_buf = String::new();
647 let out_ref = &mut assertion_buf;
648
649 match assertion.assertion_type.as_str() {
650 "equals" => {
651 if let Some(expected) = &assertion.value {
652 let go_val = json_to_go(expected);
653 if expected.is_string() {
655 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
657 format!("strings.TrimSpace(*{field_expr})")
658 } else {
659 format!("strings.TrimSpace({field_expr})")
660 };
661 if is_optional && !field_expr.starts_with("len(") {
662 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
663 } else {
664 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
665 }
666 } else if is_optional && !field_expr.starts_with("len(") {
667 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
668 } else {
669 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
670 }
671 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
672 let _ = writeln!(out_ref, "\t}}");
673 }
674 }
675 "contains" => {
676 if let Some(expected) = &assertion.value {
677 let go_val = json_to_go(expected);
678 let field_for_contains = if is_optional
679 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
680 {
681 format!("string(*{field_expr})")
682 } else {
683 format!("string({field_expr})")
684 };
685 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
686 let _ = writeln!(
687 out_ref,
688 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
689 );
690 let _ = writeln!(out_ref, "\t}}");
691 }
692 }
693 "contains_all" => {
694 if let Some(values) = &assertion.values {
695 for val in values {
696 let go_val = json_to_go(val);
697 let field_for_contains = if is_optional
698 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
699 {
700 format!("string(*{field_expr})")
701 } else {
702 format!("string({field_expr})")
703 };
704 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
705 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
706 let _ = writeln!(out_ref, "\t}}");
707 }
708 }
709 }
710 "not_contains" => {
711 if let Some(expected) = &assertion.value {
712 let go_val = json_to_go(expected);
713 let field_for_contains = if is_optional
714 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
715 {
716 format!("string(*{field_expr})")
717 } else {
718 format!("string({field_expr})")
719 };
720 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
721 let _ = writeln!(
722 out_ref,
723 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
724 );
725 let _ = writeln!(out_ref, "\t}}");
726 }
727 }
728 "not_empty" => {
729 if is_optional {
730 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
731 } else {
732 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
733 }
734 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
735 let _ = writeln!(out_ref, "\t}}");
736 }
737 "is_empty" => {
738 if is_optional {
739 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
740 } else {
741 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
742 }
743 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
744 let _ = writeln!(out_ref, "\t}}");
745 }
746 "contains_any" => {
747 if let Some(values) = &assertion.values {
748 let field_for_contains = if is_optional
749 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
750 {
751 format!("*{field_expr}")
752 } else {
753 field_expr.clone()
754 };
755 let _ = writeln!(out_ref, "\t{{");
756 let _ = writeln!(out_ref, "\t\tfound := false");
757 for val in values {
758 let go_val = json_to_go(val);
759 let _ = writeln!(
760 out_ref,
761 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
762 );
763 }
764 let _ = writeln!(out_ref, "\t\tif !found {{");
765 let _ = writeln!(
766 out_ref,
767 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
768 );
769 let _ = writeln!(out_ref, "\t\t}}");
770 let _ = writeln!(out_ref, "\t}}");
771 }
772 }
773 "greater_than" => {
774 if let Some(val) = &assertion.value {
775 let go_val = json_to_go(val);
776 if let Some(n) = val.as_u64() {
779 let next = n + 1;
780 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
781 } else {
782 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
783 }
784 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
785 let _ = writeln!(out_ref, "\t}}");
786 }
787 }
788 "less_than" => {
789 if let Some(val) = &assertion.value {
790 let go_val = json_to_go(val);
791 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
792 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
793 let _ = writeln!(out_ref, "\t}}");
794 }
795 }
796 "greater_than_or_equal" => {
797 if let Some(val) = &assertion.value {
798 let go_val = json_to_go(val);
799 if let Some(ref guard) = nil_guard_expr {
800 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
801 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
802 let _ = writeln!(
803 out_ref,
804 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
805 );
806 let _ = writeln!(out_ref, "\t\t}}");
807 let _ = writeln!(out_ref, "\t}}");
808 } else {
809 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
810 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
811 let _ = writeln!(out_ref, "\t}}");
812 }
813 }
814 }
815 "less_than_or_equal" => {
816 if let Some(val) = &assertion.value {
817 let go_val = json_to_go(val);
818 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
819 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
820 let _ = writeln!(out_ref, "\t}}");
821 }
822 }
823 "starts_with" => {
824 if let Some(expected) = &assertion.value {
825 let go_val = json_to_go(expected);
826 let field_for_prefix = if is_optional
827 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
828 {
829 format!("string(*{field_expr})")
830 } else {
831 format!("string({field_expr})")
832 };
833 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
834 let _ = writeln!(
835 out_ref,
836 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
837 );
838 let _ = writeln!(out_ref, "\t}}");
839 }
840 }
841 "count_min" => {
842 if let Some(val) = &assertion.value {
843 if let Some(n) = val.as_u64() {
844 if is_optional {
845 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
846 let _ = writeln!(
847 out_ref,
848 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
849 );
850 let _ = writeln!(out_ref, "\t}}");
851 } else {
852 let _ = writeln!(
853 out_ref,
854 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
855 );
856 }
857 }
858 }
859 }
860 "count_equals" => {
861 if let Some(val) = &assertion.value {
862 if let Some(n) = val.as_u64() {
863 if is_optional {
864 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
865 let _ = writeln!(
866 out_ref,
867 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
868 );
869 let _ = writeln!(out_ref, "\t}}");
870 } else {
871 let _ = writeln!(
872 out_ref,
873 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
874 );
875 }
876 }
877 }
878 }
879 "is_true" => {
880 if is_optional {
881 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
882 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
883 let _ = writeln!(out_ref, "\t}}");
884 } else {
885 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
886 }
887 }
888 "is_false" => {
889 if is_optional {
890 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
891 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
892 let _ = writeln!(out_ref, "\t}}");
893 } else {
894 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
895 }
896 }
897 "method_result" => {
898 if let Some(method_name) = &assertion.method {
899 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
900 let check = assertion.check.as_deref().unwrap_or("is_true");
901 let deref_expr = if info.is_pointer {
904 format!("*{}", info.call_expr)
905 } else {
906 info.call_expr.clone()
907 };
908 match check {
909 "equals" => {
910 if let Some(val) = &assertion.value {
911 if val.is_boolean() {
912 if val.as_bool() == Some(true) {
913 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
914 } else {
915 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
916 }
917 } else {
918 let go_val = if let Some(cast) = info.value_cast {
922 if val.is_number() {
923 format!("{cast}({})", json_to_go(val))
924 } else {
925 json_to_go(val)
926 }
927 } else {
928 json_to_go(val)
929 };
930 let _ = writeln!(
931 out_ref,
932 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
933 );
934 }
935 }
936 }
937 "is_true" => {
938 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
939 }
940 "is_false" => {
941 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
942 }
943 "greater_than_or_equal" => {
944 if let Some(val) = &assertion.value {
945 let n = val.as_u64().unwrap_or(0);
946 let cast = info.value_cast.unwrap_or("uint");
948 let _ = writeln!(
949 out_ref,
950 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
951 );
952 }
953 }
954 "count_min" => {
955 if let Some(val) = &assertion.value {
956 let n = val.as_u64().unwrap_or(0);
957 let _ = writeln!(
958 out_ref,
959 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
960 );
961 }
962 }
963 "contains" => {
964 if let Some(val) = &assertion.value {
965 let go_val = json_to_go(val);
966 let _ = writeln!(
967 out_ref,
968 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
969 );
970 }
971 }
972 "is_error" => {
973 let _ = writeln!(out_ref, "\t{{");
974 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
975 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
976 let _ = writeln!(out_ref, "\t}}");
977 }
978 other_check => {
979 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
980 }
981 }
982 } else {
983 panic!("Go e2e generator: method_result assertion missing 'method' field");
984 }
985 }
986 "min_length" => {
987 if let Some(val) = &assertion.value {
988 if let Some(n) = val.as_u64() {
989 if is_optional {
990 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
991 let _ = writeln!(
992 out_ref,
993 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
994 );
995 let _ = writeln!(out_ref, "\t}}");
996 } else {
997 let _ = writeln!(
998 out_ref,
999 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1000 );
1001 }
1002 }
1003 }
1004 }
1005 "max_length" => {
1006 if let Some(val) = &assertion.value {
1007 if let Some(n) = val.as_u64() {
1008 if is_optional {
1009 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1010 let _ = writeln!(
1011 out_ref,
1012 "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1013 );
1014 let _ = writeln!(out_ref, "\t}}");
1015 } else {
1016 let _ = writeln!(
1017 out_ref,
1018 "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1019 );
1020 }
1021 }
1022 }
1023 }
1024 "ends_with" => {
1025 if let Some(expected) = &assertion.value {
1026 let go_val = json_to_go(expected);
1027 let field_for_suffix = if is_optional
1028 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1029 {
1030 format!("string(*{field_expr})")
1031 } else {
1032 format!("string({field_expr})")
1033 };
1034 let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1035 let _ = writeln!(
1036 out_ref,
1037 "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1038 );
1039 let _ = writeln!(out_ref, "\t}}");
1040 }
1041 }
1042 "matches_regex" => {
1043 if let Some(expected) = &assertion.value {
1044 let go_val = json_to_go(expected);
1045 let field_for_regex = if is_optional
1046 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1047 {
1048 format!("*{field_expr}")
1049 } else {
1050 field_expr.clone()
1051 };
1052 let _ = writeln!(
1053 out_ref,
1054 "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1055 );
1056 }
1057 }
1058 "not_error" => {
1059 }
1061 "error" => {
1062 }
1064 other => {
1065 panic!("Go e2e generator: unsupported assertion type: {other}");
1066 }
1067 }
1068
1069 if let Some(ref arr) = array_guard {
1072 if !assertion_buf.is_empty() {
1073 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1074 for line in assertion_buf.lines() {
1076 let _ = writeln!(out, "\t{line}");
1077 }
1078 let _ = writeln!(out, "\t}}");
1079 }
1080 } else {
1081 out.push_str(&assertion_buf);
1082 }
1083}
1084
1085struct GoMethodCallInfo {
1087 call_expr: String,
1089 is_pointer: bool,
1091 value_cast: Option<&'static str>,
1094}
1095
1096fn build_go_method_call(
1111 result_var: &str,
1112 method_name: &str,
1113 args: Option<&serde_json::Value>,
1114 import_alias: &str,
1115) -> GoMethodCallInfo {
1116 match method_name {
1117 "root_node_type" => GoMethodCallInfo {
1118 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1119 is_pointer: false,
1120 value_cast: None,
1121 },
1122 "named_children_count" => GoMethodCallInfo {
1123 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1124 is_pointer: false,
1125 value_cast: Some("uint"),
1126 },
1127 "has_error_nodes" => GoMethodCallInfo {
1128 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1129 is_pointer: true,
1130 value_cast: None,
1131 },
1132 "error_count" | "tree_error_count" => GoMethodCallInfo {
1133 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1134 is_pointer: true,
1135 value_cast: Some("uint"),
1136 },
1137 "tree_to_sexp" => GoMethodCallInfo {
1138 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1139 is_pointer: true,
1140 value_cast: None,
1141 },
1142 "contains_node_type" => {
1143 let node_type = args
1144 .and_then(|a| a.get("node_type"))
1145 .and_then(|v| v.as_str())
1146 .unwrap_or("");
1147 GoMethodCallInfo {
1148 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1149 is_pointer: true,
1150 value_cast: None,
1151 }
1152 }
1153 "find_nodes_by_type" => {
1154 let node_type = args
1155 .and_then(|a| a.get("node_type"))
1156 .and_then(|v| v.as_str())
1157 .unwrap_or("");
1158 GoMethodCallInfo {
1159 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1160 is_pointer: true,
1161 value_cast: None,
1162 }
1163 }
1164 "run_query" => {
1165 let query_source = args
1166 .and_then(|a| a.get("query_source"))
1167 .and_then(|v| v.as_str())
1168 .unwrap_or("");
1169 let language = args
1170 .and_then(|a| a.get("language"))
1171 .and_then(|v| v.as_str())
1172 .unwrap_or("");
1173 let query_lit = go_string_literal(query_source);
1174 let lang_lit = go_string_literal(language);
1175 GoMethodCallInfo {
1177 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1178 is_pointer: false,
1179 value_cast: None,
1180 }
1181 }
1182 other => {
1183 let method_pascal = other.to_upper_camel_case();
1184 GoMethodCallInfo {
1185 call_expr: format!("{result_var}.{method_pascal}()"),
1186 is_pointer: false,
1187 value_cast: None,
1188 }
1189 }
1190 }
1191}
1192
1193const GO_INITIALISMS: &[&str] = &[
1196 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
1197 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
1198 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
1199];
1200
1201fn go_local_name(snake: &str) -> String {
1205 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
1206 if words.is_empty() {
1207 return String::new();
1208 }
1209 let mut result = String::new();
1210 for (i, word) in words.iter().enumerate() {
1211 let upper = word.to_uppercase();
1212 if GO_INITIALISMS.contains(&upper.as_str()) {
1213 if i == 0 {
1214 result.push_str(&upper.to_lowercase());
1216 } else {
1217 result.push_str(&upper);
1218 }
1219 } else if i == 0 {
1220 result.push_str(&word.to_lowercase());
1222 } else {
1223 let mut chars = word.chars();
1225 if let Some(c) = chars.next() {
1226 result.extend(c.to_uppercase());
1227 result.push_str(&chars.as_str().to_lowercase());
1228 }
1229 }
1230 }
1231 result
1232}
1233
1234fn json_to_go(value: &serde_json::Value) -> String {
1236 match value {
1237 serde_json::Value::String(s) => go_string_literal(s),
1238 serde_json::Value::Bool(b) => b.to_string(),
1239 serde_json::Value::Number(n) => n.to_string(),
1240 serde_json::Value::Null => "nil".to_string(),
1241 other => go_string_literal(&other.to_string()),
1243 }
1244}
1245
1246fn visitor_struct_name(fixture_id: &str) -> String {
1255 use heck::ToUpperCamelCase;
1256 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1258}
1259
1260fn emit_go_visitor_struct(
1262 out: &mut String,
1263 struct_name: &str,
1264 visitor_spec: &crate::fixture::VisitorSpec,
1265 import_alias: &str,
1266) {
1267 let _ = writeln!(out, "type {struct_name} struct{{}}");
1268 for (method_name, action) in &visitor_spec.callbacks {
1269 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1270 }
1271}
1272
1273fn emit_go_visitor_method(
1275 out: &mut String,
1276 struct_name: &str,
1277 method_name: &str,
1278 action: &CallbackAction,
1279 import_alias: &str,
1280) {
1281 let camel_method = method_to_camel(method_name);
1282 let params = match method_name {
1283 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1284 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1285 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1286 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1287 "visit_code_inline"
1288 | "visit_strong"
1289 | "visit_emphasis"
1290 | "visit_strikethrough"
1291 | "visit_underline"
1292 | "visit_subscript"
1293 | "visit_superscript"
1294 | "visit_mark"
1295 | "visit_button"
1296 | "visit_summary"
1297 | "visit_figcaption"
1298 | "visit_definition_term"
1299 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1300 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1301 "visit_list_item" => {
1302 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1303 }
1304 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1305 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1306 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1307 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1308 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1309 "visit_audio" | "visit_video" | "visit_iframe" => {
1310 format!("_ {import_alias}.NodeContext, src string")
1311 }
1312 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1313 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1314 format!("_ {import_alias}.NodeContext, output string")
1315 }
1316 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1317 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1318 _ => format!("_ {import_alias}.NodeContext"),
1319 };
1320
1321 let _ = writeln!(
1322 out,
1323 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1324 );
1325 match action {
1326 CallbackAction::Skip => {
1327 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1328 }
1329 CallbackAction::Continue => {
1330 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1331 }
1332 CallbackAction::PreserveHtml => {
1333 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1334 }
1335 CallbackAction::Custom { output } => {
1336 let escaped = go_string_literal(output);
1337 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1338 }
1339 CallbackAction::CustomTemplate { template } => {
1340 let (fmt_str, fmt_args) = template_to_sprintf(template);
1343 let escaped_fmt = go_string_literal(&fmt_str);
1344 if fmt_args.is_empty() {
1345 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1346 } else {
1347 let args_str = fmt_args.join(", ");
1348 let _ = writeln!(
1349 out,
1350 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1351 );
1352 }
1353 }
1354 }
1355 let _ = writeln!(out, "}}");
1356}
1357
1358fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1362 let mut fmt_str = String::new();
1363 let mut args: Vec<String> = Vec::new();
1364 let mut chars = template.chars().peekable();
1365 while let Some(c) = chars.next() {
1366 if c == '{' {
1367 let mut name = String::new();
1369 for inner in chars.by_ref() {
1370 if inner == '}' {
1371 break;
1372 }
1373 name.push(inner);
1374 }
1375 fmt_str.push_str("%s");
1376 args.push(name);
1377 } else {
1378 fmt_str.push(c);
1379 }
1380 }
1381 (fmt_str, args)
1382}
1383
1384fn method_to_camel(snake: &str) -> String {
1386 use heck::ToUpperCamelCase;
1387 snake.to_upper_camel_case()
1388}