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"
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" | "count_max" | "is_true" | "is_false" | "method_result"
212 );
213 type_needs_assert && field_valid
214 })
215 });
216
217 let _ = writeln!(out, "// E2e tests for category: {category}");
218 let _ = writeln!(out, "package e2e_test");
219 let _ = writeln!(out);
220 let _ = writeln!(out, "import (");
221 if needs_json {
222 let _ = writeln!(out, "\t\"encoding/json\"");
223 }
224 if needs_fmt {
225 let _ = writeln!(out, "\t\"fmt\"");
226 }
227 if needs_os {
228 let _ = writeln!(out, "\t\"os\"");
229 }
230 if needs_strings {
231 let _ = writeln!(out, "\t\"strings\"");
232 }
233 let _ = writeln!(out, "\t\"testing\"");
234 if needs_assert {
235 let _ = writeln!(out);
236 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
237 }
238 let _ = writeln!(out);
239 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
240 let _ = writeln!(out, ")");
241 let _ = writeln!(out);
242
243 for fixture in fixtures.iter() {
245 if let Some(visitor_spec) = &fixture.visitor {
246 let struct_name = visitor_struct_name(&fixture.id);
247 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
248 let _ = writeln!(out);
249 }
250 }
251
252 for (i, fixture) in fixtures.iter().enumerate() {
253 render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
254 if i + 1 < fixtures.len() {
255 let _ = writeln!(out);
256 }
257 }
258
259 while out.ends_with("\n\n") {
261 out.pop();
262 }
263 if !out.ends_with('\n') {
264 out.push('\n');
265 }
266 out
267}
268
269fn render_test_function(
270 out: &mut String,
271 fixture: &Fixture,
272 import_alias: &str,
273 field_resolver: &FieldResolver,
274 e2e_config: &crate::config::E2eConfig,
275) {
276 let fn_name = fixture.id.to_upper_camel_case();
277 let description = &fixture.description;
278
279 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
281 let lang = "go";
282 let overrides = call_config.overrides.get(lang);
283 let function_name = overrides
284 .and_then(|o| o.function.as_ref())
285 .cloned()
286 .unwrap_or_else(|| call_config.function.clone());
287 let result_var = &call_config.result_var;
288 let args = &call_config.args;
289
290 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
291
292 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
293
294 let mut visitor_arg = String::new();
296 if fixture.visitor.is_some() {
297 let struct_name = visitor_struct_name(&fixture.id);
298 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
299 visitor_arg = "visitor".to_string();
300 }
301
302 let final_args = if visitor_arg.is_empty() {
303 args_str
304 } else {
305 format!("{args_str}, {visitor_arg}")
306 };
307
308 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
309 let _ = writeln!(out, "\t// {description}");
310
311 for line in &setup_lines {
312 let _ = writeln!(out, "\t{line}");
313 }
314
315 if expects_error {
316 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
317 let _ = writeln!(out, "\tif err == nil {{");
318 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
319 let _ = writeln!(out, "\t}}");
320 let _ = writeln!(out, "}}");
321 return;
322 }
323
324 let has_usable_assertion = fixture.assertions.iter().any(|a| {
328 if a.assertion_type == "not_error" || a.assertion_type == "error" {
329 return false;
330 }
331 if a.assertion_type == "method_result" {
333 return true;
334 }
335 match &a.field {
336 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
337 _ => true,
338 }
339 });
340
341 let result_binding = if has_usable_assertion {
342 result_var.to_string()
343 } else {
344 "_".to_string()
345 };
346
347 let _ = writeln!(
349 out,
350 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
351 );
352 let _ = writeln!(out, "\tif err != nil {{");
353 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
354 let _ = writeln!(out, "\t}}");
355
356 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
361 for assertion in &fixture.assertions {
362 if let Some(f) = &assertion.field {
363 if !f.is_empty() {
364 let resolved = field_resolver.resolve(f);
365 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
366 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
369 if !is_string_field {
370 continue;
373 }
374 let field_expr = field_resolver.accessor(f, "go", result_var);
375 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
376 if field_resolver.has_map_access(f) {
377 let _ = writeln!(out, "\t{local_var} := {field_expr}");
380 } else {
381 let _ = writeln!(out, "\tvar {local_var} string");
382 let _ = writeln!(out, "\tif {field_expr} != nil {{");
383 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
384 let _ = writeln!(out, "\t}}");
385 }
386 optional_locals.insert(f.clone(), local_var);
387 }
388 }
389 }
390 }
391
392 for assertion in &fixture.assertions {
394 if let Some(f) = &assertion.field {
395 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
396 let parts: Vec<&str> = f.split('.').collect();
399 let mut guard_expr: Option<String> = None;
400 for i in 1..parts.len() {
401 let prefix = parts[..i].join(".");
402 let resolved_prefix = field_resolver.resolve(&prefix);
403 if field_resolver.is_optional(resolved_prefix) {
404 let accessor = field_resolver.accessor(&prefix, "go", result_var);
405 guard_expr = Some(accessor);
406 break;
407 }
408 }
409 if let Some(guard) = guard_expr {
410 if field_resolver.is_valid_for_result(f) {
413 let _ = writeln!(out, "\tif {guard} != nil {{");
414 let mut nil_buf = String::new();
417 render_assertion(
418 &mut nil_buf,
419 assertion,
420 result_var,
421 import_alias,
422 field_resolver,
423 &optional_locals,
424 );
425 for line in nil_buf.lines() {
426 let _ = writeln!(out, "\t{line}");
427 }
428 let _ = writeln!(out, "\t}}");
429 } else {
430 render_assertion(
431 out,
432 assertion,
433 result_var,
434 import_alias,
435 field_resolver,
436 &optional_locals,
437 );
438 }
439 continue;
440 }
441 }
442 }
443 render_assertion(
444 out,
445 assertion,
446 result_var,
447 import_alias,
448 field_resolver,
449 &optional_locals,
450 );
451 }
452
453 let _ = writeln!(out, "}}");
454}
455
456fn build_args_and_setup(
460 input: &serde_json::Value,
461 args: &[crate::config::ArgMapping],
462 import_alias: &str,
463 e2e_config: &crate::config::E2eConfig,
464 fixture_id: &str,
465) -> (Vec<String>, String) {
466 use heck::ToUpperCamelCase;
467
468 if args.is_empty() {
469 return (Vec::new(), json_to_go(input));
470 }
471
472 let overrides = e2e_config.call.overrides.get("go");
473 let options_type = overrides.and_then(|o| o.options_type.as_deref());
474
475 let mut setup_lines: Vec<String> = Vec::new();
476 let mut parts: Vec<String> = Vec::new();
477
478 for arg in args {
479 if arg.arg_type == "mock_url" {
480 setup_lines.push(format!(
481 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
482 arg.name,
483 ));
484 parts.push(arg.name.clone());
485 continue;
486 }
487
488 if arg.arg_type == "handle" {
489 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
491 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
492 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
493 if config_value.is_null()
494 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
495 {
496 setup_lines.push(format!(
497 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
498 name = arg.name,
499 ));
500 } else {
501 let json_str = serde_json::to_string(config_value).unwrap_or_default();
502 let go_literal = go_string_literal(&json_str);
503 let name = &arg.name;
504 setup_lines.push(format!(
505 "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}}"
506 ));
507 setup_lines.push(format!(
508 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
509 ));
510 }
511 parts.push(arg.name.clone());
512 continue;
513 }
514
515 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
516 let val = input.get(field);
517 match val {
518 None | Some(serde_json::Value::Null) if arg.optional => {
519 continue;
521 }
522 None | Some(serde_json::Value::Null) => {
523 let default_val = match arg.arg_type.as_str() {
525 "string" => "\"\"".to_string(),
526 "int" | "integer" => "0".to_string(),
527 "float" | "number" => "0.0".to_string(),
528 "bool" | "boolean" => "false".to_string(),
529 _ => "nil".to_string(),
530 };
531 parts.push(default_val);
532 }
533 Some(v) => {
534 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
536 if let Some(obj) = v.as_object() {
537 let with_calls: Vec<String> = obj
538 .iter()
539 .map(|(k, vv)| {
540 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
541 let go_val = json_to_go(vv);
542 format!("htmd.{func_name}({go_val})")
543 })
544 .collect();
545 let new_fn = format!("New{opts_type}");
546 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
547 continue;
548 }
549 }
550 parts.push(json_to_go(v));
551 }
552 }
553 }
554
555 (setup_lines, parts.join(", "))
556}
557
558fn render_assertion(
559 out: &mut String,
560 assertion: &Assertion,
561 result_var: &str,
562 import_alias: &str,
563 field_resolver: &FieldResolver,
564 optional_locals: &std::collections::HashMap<String, String>,
565) {
566 if let Some(f) = &assertion.field {
568 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
569 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
570 return;
571 }
572 }
573
574 let field_expr = match &assertion.field {
575 Some(f) if !f.is_empty() => {
576 if let Some(local_var) = optional_locals.get(f.as_str()) {
578 local_var.clone()
579 } else {
580 field_resolver.accessor(f, "go", result_var)
581 }
582 }
583 _ => result_var.to_string(),
584 };
585
586 let is_optional = assertion
590 .field
591 .as_ref()
592 .map(|f| {
593 let resolved = field_resolver.resolve(f);
594 let check_path = resolved
595 .strip_suffix(".length")
596 .or_else(|| resolved.strip_suffix(".count"))
597 .or_else(|| resolved.strip_suffix(".size"))
598 .unwrap_or(resolved);
599 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
600 })
601 .unwrap_or(false);
602
603 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
606 let inner = &field_expr[4..field_expr.len() - 1];
607 format!("len(*{inner})")
608 } else {
609 field_expr
610 };
611 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
613 Some(field_expr[5..field_expr.len() - 1].to_string())
614 } else {
615 None
616 };
617
618 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
621 format!("*{field_expr}")
622 } else {
623 field_expr.clone()
624 };
625
626 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
631 let array_expr = &field_expr[..idx];
632 Some(array_expr.to_string())
633 } else {
634 None
635 };
636
637 let mut assertion_buf = String::new();
640 let out_ref = &mut assertion_buf;
641
642 match assertion.assertion_type.as_str() {
643 "equals" => {
644 if let Some(expected) = &assertion.value {
645 let go_val = json_to_go(expected);
646 if expected.is_string() {
648 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
650 format!("strings.TrimSpace(*{field_expr})")
651 } else {
652 format!("strings.TrimSpace({field_expr})")
653 };
654 if is_optional && !field_expr.starts_with("len(") {
655 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
656 } else {
657 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
658 }
659 } else if is_optional && !field_expr.starts_with("len(") {
660 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
661 } else {
662 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
663 }
664 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
665 let _ = writeln!(out_ref, "\t}}");
666 }
667 }
668 "contains" => {
669 if let Some(expected) = &assertion.value {
670 let go_val = json_to_go(expected);
671 let field_for_contains = if is_optional
672 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
673 {
674 format!("string(*{field_expr})")
675 } else {
676 format!("string({field_expr})")
677 };
678 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
679 let _ = writeln!(
680 out_ref,
681 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
682 );
683 let _ = writeln!(out_ref, "\t}}");
684 }
685 }
686 "contains_all" => {
687 if let Some(values) = &assertion.values {
688 for val in values {
689 let go_val = json_to_go(val);
690 let field_for_contains = if is_optional
691 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
692 {
693 format!("string(*{field_expr})")
694 } else {
695 format!("string({field_expr})")
696 };
697 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
698 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
699 let _ = writeln!(out_ref, "\t}}");
700 }
701 }
702 }
703 "not_contains" => {
704 if let Some(expected) = &assertion.value {
705 let go_val = json_to_go(expected);
706 let field_for_contains = if is_optional
707 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
708 {
709 format!("string(*{field_expr})")
710 } else {
711 format!("string({field_expr})")
712 };
713 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
714 let _ = writeln!(
715 out_ref,
716 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
717 );
718 let _ = writeln!(out_ref, "\t}}");
719 }
720 }
721 "not_empty" => {
722 if is_optional {
723 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
724 } else {
725 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
726 }
727 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
728 let _ = writeln!(out_ref, "\t}}");
729 }
730 "is_empty" => {
731 if is_optional {
732 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
733 } else {
734 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
735 }
736 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
737 let _ = writeln!(out_ref, "\t}}");
738 }
739 "contains_any" => {
740 if let Some(values) = &assertion.values {
741 let field_for_contains = if is_optional
742 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
743 {
744 format!("*{field_expr}")
745 } else {
746 field_expr.clone()
747 };
748 let _ = writeln!(out_ref, "\t{{");
749 let _ = writeln!(out_ref, "\t\tfound := false");
750 for val in values {
751 let go_val = json_to_go(val);
752 let _ = writeln!(
753 out_ref,
754 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
755 );
756 }
757 let _ = writeln!(out_ref, "\t\tif !found {{");
758 let _ = writeln!(
759 out_ref,
760 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
761 );
762 let _ = writeln!(out_ref, "\t\t}}");
763 let _ = writeln!(out_ref, "\t}}");
764 }
765 }
766 "greater_than" => {
767 if let Some(val) = &assertion.value {
768 let go_val = json_to_go(val);
769 if let Some(n) = val.as_u64() {
772 let next = n + 1;
773 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
774 } else {
775 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
776 }
777 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
778 let _ = writeln!(out_ref, "\t}}");
779 }
780 }
781 "less_than" => {
782 if let Some(val) = &assertion.value {
783 let go_val = json_to_go(val);
784 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
785 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
786 let _ = writeln!(out_ref, "\t}}");
787 }
788 }
789 "greater_than_or_equal" => {
790 if let Some(val) = &assertion.value {
791 let go_val = json_to_go(val);
792 if let Some(ref guard) = nil_guard_expr {
793 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
794 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
795 let _ = writeln!(
796 out_ref,
797 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
798 );
799 let _ = writeln!(out_ref, "\t\t}}");
800 let _ = writeln!(out_ref, "\t}}");
801 } else {
802 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
803 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
804 let _ = writeln!(out_ref, "\t}}");
805 }
806 }
807 }
808 "less_than_or_equal" => {
809 if let Some(val) = &assertion.value {
810 let go_val = json_to_go(val);
811 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
812 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
813 let _ = writeln!(out_ref, "\t}}");
814 }
815 }
816 "starts_with" => {
817 if let Some(expected) = &assertion.value {
818 let go_val = json_to_go(expected);
819 let field_for_prefix = if is_optional
820 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
821 {
822 format!("string(*{field_expr})")
823 } else {
824 format!("string({field_expr})")
825 };
826 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
827 let _ = writeln!(
828 out_ref,
829 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
830 );
831 let _ = writeln!(out_ref, "\t}}");
832 }
833 }
834 "count_min" => {
835 if let Some(val) = &assertion.value {
836 if let Some(n) = val.as_u64() {
837 if is_optional {
838 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
839 let _ = writeln!(
840 out_ref,
841 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
842 );
843 let _ = writeln!(out_ref, "\t}}");
844 } else {
845 let _ = writeln!(
846 out_ref,
847 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
848 );
849 }
850 }
851 }
852 }
853 "count_equals" => {
854 if let Some(val) = &assertion.value {
855 if let Some(n) = val.as_u64() {
856 if is_optional {
857 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
858 let _ = writeln!(
859 out_ref,
860 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
861 );
862 let _ = writeln!(out_ref, "\t}}");
863 } else {
864 let _ = writeln!(
865 out_ref,
866 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
867 );
868 }
869 }
870 }
871 }
872 "is_true" => {
873 if is_optional {
874 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
875 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
876 let _ = writeln!(out_ref, "\t}}");
877 } else {
878 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
879 }
880 }
881 "is_false" => {
882 if is_optional {
883 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
884 let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
885 let _ = writeln!(out_ref, "\t}}");
886 } else {
887 let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
888 }
889 }
890 "method_result" => {
891 if let Some(method_name) = &assertion.method {
892 let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
893 let check = assertion.check.as_deref().unwrap_or("is_true");
894 let deref_expr = if info.is_pointer {
897 format!("*{}", info.call_expr)
898 } else {
899 info.call_expr.clone()
900 };
901 match check {
902 "equals" => {
903 if let Some(val) = &assertion.value {
904 if val.is_boolean() {
905 if val.as_bool() == Some(true) {
906 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
907 } else {
908 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
909 }
910 } else {
911 let go_val = if let Some(cast) = info.value_cast {
915 if val.is_number() {
916 format!("{cast}({})", json_to_go(val))
917 } else {
918 json_to_go(val)
919 }
920 } else {
921 json_to_go(val)
922 };
923 let _ = writeln!(
924 out_ref,
925 "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
926 );
927 }
928 }
929 }
930 "is_true" => {
931 let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
932 }
933 "is_false" => {
934 let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
935 }
936 "greater_than_or_equal" => {
937 if let Some(val) = &assertion.value {
938 let n = val.as_u64().unwrap_or(0);
939 let cast = info.value_cast.unwrap_or("uint");
941 let _ = writeln!(
942 out_ref,
943 "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
944 );
945 }
946 }
947 "count_min" => {
948 if let Some(val) = &assertion.value {
949 let n = val.as_u64().unwrap_or(0);
950 let _ = writeln!(
951 out_ref,
952 "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
953 );
954 }
955 }
956 "contains" => {
957 if let Some(val) = &assertion.value {
958 let go_val = json_to_go(val);
959 let _ = writeln!(
960 out_ref,
961 "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
962 );
963 }
964 }
965 "is_error" => {
966 let _ = writeln!(out_ref, "\t{{");
967 let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
968 let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
969 let _ = writeln!(out_ref, "\t}}");
970 }
971 other_check => {
972 panic!("Go e2e generator: unsupported method_result check type: {other_check}");
973 }
974 }
975 } else {
976 panic!("Go e2e generator: method_result assertion missing 'method' field");
977 }
978 }
979 "not_error" => {
980 }
982 "error" => {
983 }
985 other => {
986 panic!("Go e2e generator: unsupported assertion type: {other}");
987 }
988 }
989
990 if let Some(ref arr) = array_guard {
993 if !assertion_buf.is_empty() {
994 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
995 for line in assertion_buf.lines() {
997 let _ = writeln!(out, "\t{line}");
998 }
999 let _ = writeln!(out, "\t}}");
1000 }
1001 } else {
1002 out.push_str(&assertion_buf);
1003 }
1004}
1005
1006struct GoMethodCallInfo {
1008 call_expr: String,
1010 is_pointer: bool,
1012 value_cast: Option<&'static str>,
1015}
1016
1017fn build_go_method_call(
1032 result_var: &str,
1033 method_name: &str,
1034 args: Option<&serde_json::Value>,
1035 import_alias: &str,
1036) -> GoMethodCallInfo {
1037 match method_name {
1038 "root_node_type" => GoMethodCallInfo {
1039 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1040 is_pointer: false,
1041 value_cast: None,
1042 },
1043 "named_children_count" => GoMethodCallInfo {
1044 call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1045 is_pointer: false,
1046 value_cast: Some("uint"),
1047 },
1048 "has_error_nodes" => GoMethodCallInfo {
1049 call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1050 is_pointer: true,
1051 value_cast: None,
1052 },
1053 "error_count" | "tree_error_count" => GoMethodCallInfo {
1054 call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1055 is_pointer: true,
1056 value_cast: Some("uint"),
1057 },
1058 "tree_to_sexp" => GoMethodCallInfo {
1059 call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1060 is_pointer: true,
1061 value_cast: None,
1062 },
1063 "contains_node_type" => {
1064 let node_type = args
1065 .and_then(|a| a.get("node_type"))
1066 .and_then(|v| v.as_str())
1067 .unwrap_or("");
1068 GoMethodCallInfo {
1069 call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1070 is_pointer: true,
1071 value_cast: None,
1072 }
1073 }
1074 "find_nodes_by_type" => {
1075 let node_type = args
1076 .and_then(|a| a.get("node_type"))
1077 .and_then(|v| v.as_str())
1078 .unwrap_or("");
1079 GoMethodCallInfo {
1080 call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1081 is_pointer: true,
1082 value_cast: None,
1083 }
1084 }
1085 "run_query" => {
1086 let query_source = args
1087 .and_then(|a| a.get("query_source"))
1088 .and_then(|v| v.as_str())
1089 .unwrap_or("");
1090 let language = args
1091 .and_then(|a| a.get("language"))
1092 .and_then(|v| v.as_str())
1093 .unwrap_or("");
1094 let query_lit = go_string_literal(query_source);
1095 let lang_lit = go_string_literal(language);
1096 GoMethodCallInfo {
1098 call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1099 is_pointer: false,
1100 value_cast: None,
1101 }
1102 }
1103 other => {
1104 let method_pascal = other.to_upper_camel_case();
1105 GoMethodCallInfo {
1106 call_expr: format!("{result_var}.{method_pascal}()"),
1107 is_pointer: false,
1108 value_cast: None,
1109 }
1110 }
1111 }
1112}
1113
1114const GO_INITIALISMS: &[&str] = &[
1117 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
1118 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
1119 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
1120];
1121
1122fn go_local_name(snake: &str) -> String {
1126 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
1127 if words.is_empty() {
1128 return String::new();
1129 }
1130 let mut result = String::new();
1131 for (i, word) in words.iter().enumerate() {
1132 let upper = word.to_uppercase();
1133 if GO_INITIALISMS.contains(&upper.as_str()) {
1134 if i == 0 {
1135 result.push_str(&upper.to_lowercase());
1137 } else {
1138 result.push_str(&upper);
1139 }
1140 } else if i == 0 {
1141 result.push_str(&word.to_lowercase());
1143 } else {
1144 let mut chars = word.chars();
1146 if let Some(c) = chars.next() {
1147 result.extend(c.to_uppercase());
1148 result.push_str(&chars.as_str().to_lowercase());
1149 }
1150 }
1151 }
1152 result
1153}
1154
1155fn json_to_go(value: &serde_json::Value) -> String {
1157 match value {
1158 serde_json::Value::String(s) => go_string_literal(s),
1159 serde_json::Value::Bool(b) => b.to_string(),
1160 serde_json::Value::Number(n) => n.to_string(),
1161 serde_json::Value::Null => "nil".to_string(),
1162 other => go_string_literal(&other.to_string()),
1164 }
1165}
1166
1167fn visitor_struct_name(fixture_id: &str) -> String {
1176 use heck::ToUpperCamelCase;
1177 format!("testVisitor{}", fixture_id.to_upper_camel_case())
1179}
1180
1181fn emit_go_visitor_struct(
1183 out: &mut String,
1184 struct_name: &str,
1185 visitor_spec: &crate::fixture::VisitorSpec,
1186 import_alias: &str,
1187) {
1188 let _ = writeln!(out, "type {struct_name} struct{{}}");
1189 for (method_name, action) in &visitor_spec.callbacks {
1190 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1191 }
1192}
1193
1194fn emit_go_visitor_method(
1196 out: &mut String,
1197 struct_name: &str,
1198 method_name: &str,
1199 action: &CallbackAction,
1200 import_alias: &str,
1201) {
1202 let camel_method = method_to_camel(method_name);
1203 let params = match method_name {
1204 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1205 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1206 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1207 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1208 "visit_code_inline"
1209 | "visit_strong"
1210 | "visit_emphasis"
1211 | "visit_strikethrough"
1212 | "visit_underline"
1213 | "visit_subscript"
1214 | "visit_superscript"
1215 | "visit_mark"
1216 | "visit_button"
1217 | "visit_summary"
1218 | "visit_figcaption"
1219 | "visit_definition_term"
1220 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1221 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1222 "visit_list_item" => {
1223 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1224 }
1225 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1226 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1227 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1228 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1229 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1230 "visit_audio" | "visit_video" | "visit_iframe" => {
1231 format!("_ {import_alias}.NodeContext, src string")
1232 }
1233 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1234 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1235 format!("_ {import_alias}.NodeContext, output string")
1236 }
1237 "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1238 "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1239 _ => format!("_ {import_alias}.NodeContext"),
1240 };
1241
1242 let _ = writeln!(
1243 out,
1244 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1245 );
1246 match action {
1247 CallbackAction::Skip => {
1248 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1249 }
1250 CallbackAction::Continue => {
1251 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1252 }
1253 CallbackAction::PreserveHtml => {
1254 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1255 }
1256 CallbackAction::Custom { output } => {
1257 let escaped = go_string_literal(output);
1258 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1259 }
1260 CallbackAction::CustomTemplate { template } => {
1261 let (fmt_str, fmt_args) = template_to_sprintf(template);
1264 let escaped_fmt = go_string_literal(&fmt_str);
1265 if fmt_args.is_empty() {
1266 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1267 } else {
1268 let args_str = fmt_args.join(", ");
1269 let _ = writeln!(
1270 out,
1271 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1272 );
1273 }
1274 }
1275 }
1276 let _ = writeln!(out, "}}");
1277}
1278
1279fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1283 let mut fmt_str = String::new();
1284 let mut args: Vec<String> = Vec::new();
1285 let mut chars = template.chars().peekable();
1286 while let Some(c) = chars.next() {
1287 if c == '{' {
1288 let mut name = String::new();
1290 for inner in chars.by_ref() {
1291 if inner == '}' {
1292 break;
1293 }
1294 name.push(inner);
1295 }
1296 fmt_str.push_str("%s");
1297 args.push(name);
1298 } else {
1299 fmt_str.push(c);
1300 }
1301 }
1302 (fmt_str, args)
1303}
1304
1305fn method_to_camel(snake: &str) -> String {
1307 use heck::ToUpperCamelCase;
1308 snake.to_upper_camel_case()
1309}