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 anyhow::Result;
10use heck::ToUpperCamelCase;
11use std::fmt::Write as FmtWrite;
12use std::path::PathBuf;
13
14use super::E2eCodegen;
15
16pub struct GoCodegen;
18
19impl E2eCodegen for GoCodegen {
20 fn generate(
21 &self,
22 groups: &[FixtureGroup],
23 e2e_config: &E2eConfig,
24 _alef_config: &AlefConfig,
25 ) -> Result<Vec<GeneratedFile>> {
26 let lang = self.language_name();
27 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
28
29 let mut files = Vec::new();
30
31 let call = &e2e_config.call;
33 let overrides = call.overrides.get(lang);
34 let module_path = overrides
35 .and_then(|o| o.module.as_ref())
36 .cloned()
37 .unwrap_or_else(|| call.module.clone());
38 let function_name = overrides
39 .and_then(|o| o.function.as_ref())
40 .cloned()
41 .unwrap_or_else(|| call.function.clone());
42 let import_alias = overrides
43 .and_then(|o| o.alias.as_ref())
44 .cloned()
45 .unwrap_or_else(|| "pkg".to_string());
46 let result_var = &call.result_var;
47
48 let go_pkg = e2e_config.resolve_package("go");
50 let go_module_path = go_pkg
51 .as_ref()
52 .and_then(|p| p.module.as_ref())
53 .cloned()
54 .unwrap_or_else(|| module_path.clone());
55 let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
56 let go_version = go_pkg
57 .as_ref()
58 .and_then(|p| p.version.as_ref())
59 .cloned()
60 .unwrap_or_else(|| "v0.0.0".to_string());
61 let field_resolver = FieldResolver::new(
62 &e2e_config.fields,
63 &e2e_config.fields_optional,
64 &e2e_config.result_fields,
65 &e2e_config.fields_array,
66 );
67
68 let effective_replace = match e2e_config.dep_mode {
71 crate::config::DependencyMode::Registry => None,
72 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
73 };
74 files.push(GeneratedFile {
75 path: output_base.join("go.mod"),
76 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
77 generated_header: false,
78 });
79
80 for group in groups {
82 let active: Vec<&Fixture> = group
83 .fixtures
84 .iter()
85 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
86 .collect();
87
88 if active.is_empty() {
89 continue;
90 }
91
92 let filename = format!("{}_test.go", sanitize_filename(&group.category));
93 let content = render_test_file(
94 &group.category,
95 &active,
96 &module_path,
97 &import_alias,
98 &function_name,
99 result_var,
100 &e2e_config.call.args,
101 &field_resolver,
102 e2e_config,
103 );
104 files.push(GeneratedFile {
105 path: output_base.join(filename),
106 content,
107 generated_header: true,
108 });
109 }
110
111 Ok(files)
112 }
113
114 fn language_name(&self) -> &'static str {
115 "go"
116 }
117}
118
119fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
120 let mut out = String::new();
121 let _ = writeln!(out, "module e2e_go");
122 let _ = writeln!(out);
123 let _ = writeln!(out, "go 1.26");
124 let _ = writeln!(out);
125 let _ = writeln!(out, "require {go_module_path} {version}");
126
127 if let Some(path) = replace_path {
128 let _ = writeln!(out);
129 let _ = writeln!(out, "replace {go_module_path} => {path}");
130 }
131
132 out
133}
134
135#[allow(clippy::too_many_arguments)]
136fn render_test_file(
137 category: &str,
138 fixtures: &[&Fixture],
139 go_module_path: &str,
140 import_alias: &str,
141 function_name: &str,
142 result_var: &str,
143 args: &[crate::config::ArgMapping],
144 field_resolver: &FieldResolver,
145 e2e_config: &crate::config::E2eConfig,
146) -> String {
147 let mut out = String::new();
148
149 let _ = writeln!(out, "// Code generated by alef. DO NOT EDIT.");
151 let _ = writeln!(out);
152
153 let needs_os = args.iter().any(|a| a.arg_type == "mock_url");
155
156 let needs_json = args.iter().any(|a| a.arg_type == "handle")
158 && fixtures.iter().any(|f| {
159 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
160 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
161 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
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| {
202 f.assertions.iter().any(|a| {
203 let field_valid = a
204 .field
205 .as_ref()
206 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
207 .unwrap_or(true);
208 matches!(a.assertion_type.as_str(), "count_min" | "count_max") && field_valid
209 })
210 });
211
212 let _ = writeln!(out, "// E2e tests for category: {category}");
213 let _ = writeln!(out, "package e2e_test");
214 let _ = writeln!(out);
215 let _ = writeln!(out, "import (");
216 if needs_json {
217 let _ = writeln!(out, "\t\"encoding/json\"");
218 }
219 if needs_fmt {
220 let _ = writeln!(out, "\t\"fmt\"");
221 }
222 if needs_os {
223 let _ = writeln!(out, "\t\"os\"");
224 }
225 if needs_strings {
226 let _ = writeln!(out, "\t\"strings\"");
227 }
228 let _ = writeln!(out, "\t\"testing\"");
229 if needs_assert {
230 let _ = writeln!(out);
231 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
232 }
233 let _ = writeln!(out);
234 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
235 let _ = writeln!(out, ")");
236 let _ = writeln!(out);
237
238 for fixture in fixtures.iter() {
240 if let Some(visitor_spec) = &fixture.visitor {
241 let struct_name = visitor_struct_name(&fixture.id);
242 emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
243 let _ = writeln!(out);
244 }
245 }
246
247 for (i, fixture) in fixtures.iter().enumerate() {
248 render_test_function(
249 &mut out,
250 fixture,
251 import_alias,
252 function_name,
253 result_var,
254 args,
255 field_resolver,
256 e2e_config,
257 );
258 if i + 1 < fixtures.len() {
259 let _ = writeln!(out);
260 }
261 }
262
263 while out.ends_with("\n\n") {
265 out.pop();
266 }
267 if !out.ends_with('\n') {
268 out.push('\n');
269 }
270 out
271}
272
273#[allow(clippy::too_many_arguments)]
274fn render_test_function(
275 out: &mut String,
276 fixture: &Fixture,
277 import_alias: &str,
278 function_name: &str,
279 result_var: &str,
280 args: &[crate::config::ArgMapping],
281 field_resolver: &FieldResolver,
282 e2e_config: &crate::config::E2eConfig,
283) {
284 let fn_name = fixture.id.to_upper_camel_case();
285 let description = &fixture.description;
286
287 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
288
289 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
290
291 let mut visitor_arg = String::new();
293 if fixture.visitor.is_some() {
294 let struct_name = visitor_struct_name(&fixture.id);
295 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
296 visitor_arg = "visitor".to_string();
297 }
298
299 let final_args = if visitor_arg.is_empty() {
300 args_str
301 } else {
302 format!("{args_str}, {visitor_arg}")
303 };
304
305 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
306 let _ = writeln!(out, "\t// {description}");
307
308 for line in &setup_lines {
309 let _ = writeln!(out, "\t{line}");
310 }
311
312 if expects_error {
313 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
314 let _ = writeln!(out, "\tif err == nil {{");
315 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
316 let _ = writeln!(out, "\t}}");
317 let _ = writeln!(out, "}}");
318 return;
319 }
320
321 let has_usable_assertion = fixture.assertions.iter().any(|a| {
325 if a.assertion_type == "not_error" || a.assertion_type == "error" {
326 return false;
327 }
328 match &a.field {
329 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
330 _ => true,
331 }
332 });
333
334 let result_binding = if has_usable_assertion {
335 result_var.to_string()
336 } else {
337 "_".to_string()
338 };
339
340 let _ = writeln!(
342 out,
343 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
344 );
345 let _ = writeln!(out, "\tif err != nil {{");
346 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
347 let _ = writeln!(out, "\t}}");
348
349 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
354 for assertion in &fixture.assertions {
355 if let Some(f) = &assertion.field {
356 if !f.is_empty() {
357 let resolved = field_resolver.resolve(f);
358 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
359 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
362 if !is_string_field {
363 continue;
366 }
367 let field_expr = field_resolver.accessor(f, "go", result_var);
368 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
369 if field_resolver.has_map_access(f) {
370 let _ = writeln!(out, "\t{local_var} := {field_expr}");
373 } else {
374 let _ = writeln!(out, "\tvar {local_var} string");
375 let _ = writeln!(out, "\tif {field_expr} != nil {{");
376 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
377 let _ = writeln!(out, "\t}}");
378 }
379 optional_locals.insert(f.clone(), local_var);
380 }
381 }
382 }
383 }
384
385 for assertion in &fixture.assertions {
387 if let Some(f) = &assertion.field {
388 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
389 let parts: Vec<&str> = f.split('.').collect();
392 let mut guard_expr: Option<String> = None;
393 for i in 1..parts.len() {
394 let prefix = parts[..i].join(".");
395 let resolved_prefix = field_resolver.resolve(&prefix);
396 if field_resolver.is_optional(resolved_prefix) {
397 let accessor = field_resolver.accessor(&prefix, "go", result_var);
398 guard_expr = Some(accessor);
399 break;
400 }
401 }
402 if let Some(guard) = guard_expr {
403 if field_resolver.is_valid_for_result(f) {
406 let _ = writeln!(out, "\tif {guard} != nil {{");
407 let mut nil_buf = String::new();
410 render_assertion(&mut nil_buf, assertion, result_var, field_resolver, &optional_locals);
411 for line in nil_buf.lines() {
412 let _ = writeln!(out, "\t{line}");
413 }
414 let _ = writeln!(out, "\t}}");
415 } else {
416 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
417 }
418 continue;
419 }
420 }
421 }
422 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
423 }
424
425 let _ = writeln!(out, "}}");
426}
427
428fn build_args_and_setup(
432 input: &serde_json::Value,
433 args: &[crate::config::ArgMapping],
434 import_alias: &str,
435 e2e_config: &crate::config::E2eConfig,
436 fixture_id: &str,
437) -> (Vec<String>, String) {
438 use heck::ToUpperCamelCase;
439
440 if args.is_empty() {
441 return (Vec::new(), json_to_go(input));
442 }
443
444 let overrides = e2e_config.call.overrides.get("go");
445 let options_type = overrides.and_then(|o| o.options_type.as_deref());
446
447 let mut setup_lines: Vec<String> = Vec::new();
448 let mut parts: Vec<String> = Vec::new();
449
450 for arg in args {
451 if arg.arg_type == "mock_url" {
452 setup_lines.push(format!(
453 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
454 arg.name,
455 ));
456 parts.push(arg.name.clone());
457 continue;
458 }
459
460 if arg.arg_type == "handle" {
461 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
463 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
464 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
465 if config_value.is_null()
466 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
467 {
468 setup_lines.push(format!(
469 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
470 name = arg.name,
471 ));
472 } else {
473 let json_str = serde_json::to_string(config_value).unwrap_or_default();
474 let go_literal = go_string_literal(&json_str);
475 let name = &arg.name;
476 setup_lines.push(format!(
477 "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}}"
478 ));
479 setup_lines.push(format!(
480 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
481 ));
482 }
483 parts.push(arg.name.clone());
484 continue;
485 }
486
487 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
488 let val = input.get(field);
489 match val {
490 None | Some(serde_json::Value::Null) if arg.optional => {
491 continue;
493 }
494 None | Some(serde_json::Value::Null) => {
495 let default_val = match arg.arg_type.as_str() {
497 "string" => "\"\"".to_string(),
498 "int" | "integer" => "0".to_string(),
499 "float" | "number" => "0.0".to_string(),
500 "bool" | "boolean" => "false".to_string(),
501 _ => "nil".to_string(),
502 };
503 parts.push(default_val);
504 }
505 Some(v) => {
506 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
508 if let Some(obj) = v.as_object() {
509 let with_calls: Vec<String> = obj
510 .iter()
511 .map(|(k, vv)| {
512 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
513 let go_val = json_to_go(vv);
514 format!("htmd.{func_name}({go_val})")
515 })
516 .collect();
517 let new_fn = format!("New{opts_type}");
518 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
519 continue;
520 }
521 }
522 parts.push(json_to_go(v));
523 }
524 }
525 }
526
527 (setup_lines, parts.join(", "))
528}
529
530fn render_assertion(
531 out: &mut String,
532 assertion: &Assertion,
533 result_var: &str,
534 field_resolver: &FieldResolver,
535 optional_locals: &std::collections::HashMap<String, String>,
536) {
537 if let Some(f) = &assertion.field {
539 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
540 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
541 return;
542 }
543 }
544
545 let field_expr = match &assertion.field {
546 Some(f) if !f.is_empty() => {
547 if let Some(local_var) = optional_locals.get(f.as_str()) {
549 local_var.clone()
550 } else {
551 field_resolver.accessor(f, "go", result_var)
552 }
553 }
554 _ => result_var.to_string(),
555 };
556
557 let is_optional = assertion
561 .field
562 .as_ref()
563 .map(|f| {
564 let resolved = field_resolver.resolve(f);
565 let check_path = resolved
566 .strip_suffix(".length")
567 .or_else(|| resolved.strip_suffix(".count"))
568 .or_else(|| resolved.strip_suffix(".size"))
569 .unwrap_or(resolved);
570 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
571 })
572 .unwrap_or(false);
573
574 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
577 let inner = &field_expr[4..field_expr.len() - 1];
578 format!("len(*{inner})")
579 } else {
580 field_expr
581 };
582 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
584 Some(field_expr[5..field_expr.len() - 1].to_string())
585 } else {
586 None
587 };
588
589 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
592 format!("*{field_expr}")
593 } else {
594 field_expr.clone()
595 };
596
597 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
602 let array_expr = &field_expr[..idx];
603 Some(array_expr.to_string())
604 } else {
605 None
606 };
607
608 let mut assertion_buf = String::new();
611 let out_ref = &mut assertion_buf;
612
613 match assertion.assertion_type.as_str() {
614 "equals" => {
615 if let Some(expected) = &assertion.value {
616 let go_val = json_to_go(expected);
617 if expected.is_string() {
619 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
621 format!("strings.TrimSpace(*{field_expr})")
622 } else {
623 format!("strings.TrimSpace({field_expr})")
624 };
625 if is_optional && !field_expr.starts_with("len(") {
626 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
627 } else {
628 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
629 }
630 } else if is_optional && !field_expr.starts_with("len(") {
631 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
632 } else {
633 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
634 }
635 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
636 let _ = writeln!(out_ref, "\t}}");
637 }
638 }
639 "contains" => {
640 if let Some(expected) = &assertion.value {
641 let go_val = json_to_go(expected);
642 let field_for_contains = if is_optional
643 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
644 {
645 format!("string(*{field_expr})")
646 } else {
647 format!("string({field_expr})")
648 };
649 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
650 let _ = writeln!(
651 out_ref,
652 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
653 );
654 let _ = writeln!(out_ref, "\t}}");
655 }
656 }
657 "contains_all" => {
658 if let Some(values) = &assertion.values {
659 for val in values {
660 let go_val = json_to_go(val);
661 let field_for_contains = if is_optional
662 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
663 {
664 format!("string(*{field_expr})")
665 } else {
666 format!("string({field_expr})")
667 };
668 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
669 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
670 let _ = writeln!(out_ref, "\t}}");
671 }
672 }
673 }
674 "not_contains" => {
675 if let Some(expected) = &assertion.value {
676 let go_val = json_to_go(expected);
677 let field_for_contains = if is_optional
678 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
679 {
680 format!("string(*{field_expr})")
681 } else {
682 format!("string({field_expr})")
683 };
684 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
685 let _ = writeln!(
686 out_ref,
687 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
688 );
689 let _ = writeln!(out_ref, "\t}}");
690 }
691 }
692 "not_empty" => {
693 if is_optional {
694 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
695 } else {
696 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
697 }
698 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
699 let _ = writeln!(out_ref, "\t}}");
700 }
701 "is_empty" => {
702 if is_optional {
703 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
704 } else {
705 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
706 }
707 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
708 let _ = writeln!(out_ref, "\t}}");
709 }
710 "contains_any" => {
711 if let Some(values) = &assertion.values {
712 let field_for_contains = if is_optional
713 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
714 {
715 format!("*{field_expr}")
716 } else {
717 field_expr.clone()
718 };
719 let _ = writeln!(out_ref, "\t{{");
720 let _ = writeln!(out_ref, "\t\tfound := false");
721 for val in values {
722 let go_val = json_to_go(val);
723 let _ = writeln!(
724 out_ref,
725 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
726 );
727 }
728 let _ = writeln!(out_ref, "\t\tif !found {{");
729 let _ = writeln!(
730 out_ref,
731 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
732 );
733 let _ = writeln!(out_ref, "\t\t}}");
734 let _ = writeln!(out_ref, "\t}}");
735 }
736 }
737 "greater_than" => {
738 if let Some(val) = &assertion.value {
739 let go_val = json_to_go(val);
740 if let Some(n) = val.as_u64() {
743 let next = n + 1;
744 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
745 } else {
746 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
747 }
748 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
749 let _ = writeln!(out_ref, "\t}}");
750 }
751 }
752 "less_than" => {
753 if let Some(val) = &assertion.value {
754 let go_val = json_to_go(val);
755 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
756 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
757 let _ = writeln!(out_ref, "\t}}");
758 }
759 }
760 "greater_than_or_equal" => {
761 if let Some(val) = &assertion.value {
762 let go_val = json_to_go(val);
763 if let Some(ref guard) = nil_guard_expr {
764 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
765 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
766 let _ = writeln!(
767 out_ref,
768 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
769 );
770 let _ = writeln!(out_ref, "\t\t}}");
771 let _ = writeln!(out_ref, "\t}}");
772 } else {
773 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
774 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
775 let _ = writeln!(out_ref, "\t}}");
776 }
777 }
778 }
779 "less_than_or_equal" => {
780 if let Some(val) = &assertion.value {
781 let go_val = json_to_go(val);
782 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
783 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
784 let _ = writeln!(out_ref, "\t}}");
785 }
786 }
787 "starts_with" => {
788 if let Some(expected) = &assertion.value {
789 let go_val = json_to_go(expected);
790 let field_for_prefix = if is_optional
791 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
792 {
793 format!("string(*{field_expr})")
794 } else {
795 format!("string({field_expr})")
796 };
797 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
798 let _ = writeln!(
799 out_ref,
800 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
801 );
802 let _ = writeln!(out_ref, "\t}}");
803 }
804 }
805 "count_min" => {
806 if let Some(val) = &assertion.value {
807 if let Some(n) = val.as_u64() {
808 if is_optional {
809 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
810 let _ = writeln!(
811 out_ref,
812 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
813 );
814 let _ = writeln!(out_ref, "\t}}");
815 } else {
816 let _ = writeln!(
817 out_ref,
818 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
819 );
820 }
821 }
822 }
823 }
824 "count_equals" => {
825 if let Some(val) = &assertion.value {
826 if let Some(n) = val.as_u64() {
827 if is_optional {
828 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
829 let _ = writeln!(
830 out_ref,
831 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
832 );
833 let _ = writeln!(out_ref, "\t}}");
834 } else {
835 let _ = writeln!(
836 out_ref,
837 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
838 );
839 }
840 }
841 }
842 }
843 "is_true" => {
844 if is_optional {
845 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
846 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
847 let _ = writeln!(out_ref, "\t}}");
848 } else {
849 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
850 }
851 }
852 "not_error" => {
853 }
855 "error" => {
856 }
858 other => {
859 let _ = writeln!(out_ref, "\t// TODO: unsupported assertion type: {other}");
860 }
861 }
862
863 if let Some(ref arr) = array_guard {
866 if !assertion_buf.is_empty() {
867 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
868 for line in assertion_buf.lines() {
870 let _ = writeln!(out, "\t{line}");
871 }
872 let _ = writeln!(out, "\t}}");
873 }
874 } else {
875 out.push_str(&assertion_buf);
876 }
877}
878
879const GO_INITIALISMS: &[&str] = &[
882 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
883 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
884 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
885];
886
887fn go_local_name(snake: &str) -> String {
891 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
892 if words.is_empty() {
893 return String::new();
894 }
895 let mut result = String::new();
896 for (i, word) in words.iter().enumerate() {
897 let upper = word.to_uppercase();
898 if GO_INITIALISMS.contains(&upper.as_str()) {
899 if i == 0 {
900 result.push_str(&upper.to_lowercase());
902 } else {
903 result.push_str(&upper);
904 }
905 } else if i == 0 {
906 result.push_str(&word.to_lowercase());
908 } else {
909 let mut chars = word.chars();
911 if let Some(c) = chars.next() {
912 result.extend(c.to_uppercase());
913 result.push_str(&chars.as_str().to_lowercase());
914 }
915 }
916 }
917 result
918}
919
920fn json_to_go(value: &serde_json::Value) -> String {
922 match value {
923 serde_json::Value::String(s) => go_string_literal(s),
924 serde_json::Value::Bool(b) => b.to_string(),
925 serde_json::Value::Number(n) => n.to_string(),
926 serde_json::Value::Null => "nil".to_string(),
927 other => go_string_literal(&other.to_string()),
929 }
930}
931
932fn visitor_struct_name(fixture_id: &str) -> String {
941 use heck::ToUpperCamelCase;
942 format!("testVisitor{}", fixture_id.to_upper_camel_case())
944}
945
946fn emit_go_visitor_struct(
948 out: &mut String,
949 struct_name: &str,
950 visitor_spec: &crate::fixture::VisitorSpec,
951 import_alias: &str,
952) {
953 let _ = writeln!(out, "type {struct_name} struct{{}}");
954 for (method_name, action) in &visitor_spec.callbacks {
955 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
956 }
957}
958
959fn emit_go_visitor_method(
961 out: &mut String,
962 struct_name: &str,
963 method_name: &str,
964 action: &CallbackAction,
965 import_alias: &str,
966) {
967 let camel_method = method_to_camel(method_name);
968 let params = match method_name {
969 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
970 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
971 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
972 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
973 "visit_code_inline"
974 | "visit_strong"
975 | "visit_emphasis"
976 | "visit_strikethrough"
977 | "visit_underline"
978 | "visit_subscript"
979 | "visit_superscript"
980 | "visit_mark"
981 | "visit_button"
982 | "visit_summary"
983 | "visit_figcaption"
984 | "visit_definition_term"
985 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
986 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
987 "visit_list_item" => {
988 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
989 }
990 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
991 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
992 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
993 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
994 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
995 "visit_audio" | "visit_video" | "visit_iframe" => {
996 format!("_ {import_alias}.NodeContext, src string")
997 }
998 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
999 _ => format!("_ {import_alias}.NodeContext"),
1000 };
1001
1002 let _ = writeln!(
1003 out,
1004 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1005 );
1006 match action {
1007 CallbackAction::Skip => {
1008 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1009 }
1010 CallbackAction::Continue => {
1011 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1012 }
1013 CallbackAction::PreserveHtml => {
1014 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1015 }
1016 CallbackAction::Custom { output } => {
1017 let escaped = go_string_literal(output);
1018 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1019 }
1020 CallbackAction::CustomTemplate { template } => {
1021 let (fmt_str, fmt_args) = template_to_sprintf(template);
1024 let escaped_fmt = go_string_literal(&fmt_str);
1025 if fmt_args.is_empty() {
1026 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1027 } else {
1028 let args_str = fmt_args.join(", ");
1029 let _ = writeln!(
1030 out,
1031 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1032 );
1033 }
1034 }
1035 }
1036 let _ = writeln!(out, "}}");
1037}
1038
1039fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1043 let mut fmt_str = String::new();
1044 let mut args: Vec<String> = Vec::new();
1045 let mut chars = template.chars().peekable();
1046 while let Some(c) = chars.next() {
1047 if c == '{' {
1048 let mut name = String::new();
1050 for inner in chars.by_ref() {
1051 if inner == '}' {
1052 break;
1053 }
1054 name.push(inner);
1055 }
1056 fmt_str.push_str("%s");
1057 args.push(name);
1058 } else {
1059 fmt_str.push(c);
1060 }
1061 }
1062 (fmt_str, args)
1063}
1064
1065fn method_to_camel(snake: &str) -> String {
1067 use heck::ToUpperCamelCase;
1068 snake.to_upper_camel_case()
1069}