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(|| {
61 alef_config
62 .resolved_version()
63 .map(|v| format!("v{v}"))
64 .unwrap_or_else(|| "v0.0.0".to_string())
65 });
66 let field_resolver = FieldResolver::new(
67 &e2e_config.fields,
68 &e2e_config.fields_optional,
69 &e2e_config.result_fields,
70 &e2e_config.fields_array,
71 );
72
73 let effective_replace = match e2e_config.dep_mode {
76 crate::config::DependencyMode::Registry => None,
77 crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
78 };
79 files.push(GeneratedFile {
80 path: output_base.join("go.mod"),
81 content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
82 generated_header: false,
83 });
84
85 for group in groups {
87 let active: Vec<&Fixture> = group
88 .fixtures
89 .iter()
90 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
91 .collect();
92
93 if active.is_empty() {
94 continue;
95 }
96
97 let filename = format!("{}_test.go", sanitize_filename(&group.category));
98 let content = render_test_file(
99 &group.category,
100 &active,
101 &module_path,
102 &import_alias,
103 &function_name,
104 result_var,
105 &e2e_config.call.args,
106 &field_resolver,
107 e2e_config,
108 );
109 files.push(GeneratedFile {
110 path: output_base.join(filename),
111 content,
112 generated_header: true,
113 });
114 }
115
116 Ok(files)
117 }
118
119 fn language_name(&self) -> &'static str {
120 "go"
121 }
122}
123
124fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
125 let mut out = String::new();
126 let _ = writeln!(out, "module e2e_go");
127 let _ = writeln!(out);
128 let _ = writeln!(out, "go 1.26");
129 let _ = writeln!(out);
130 let _ = writeln!(out, "require {go_module_path} {version}");
131
132 if let Some(path) = replace_path {
133 let _ = writeln!(out);
134 let _ = writeln!(out, "replace {go_module_path} => {path}");
135 }
136
137 out
138}
139
140#[allow(clippy::too_many_arguments)]
141fn render_test_file(
142 category: &str,
143 fixtures: &[&Fixture],
144 go_module_path: &str,
145 import_alias: &str,
146 function_name: &str,
147 result_var: &str,
148 args: &[crate::config::ArgMapping],
149 field_resolver: &FieldResolver,
150 e2e_config: &crate::config::E2eConfig,
151) -> String {
152 let mut out = String::new();
153
154 let _ = writeln!(out, "// Code generated by alef. DO NOT EDIT.");
156 let _ = writeln!(out);
157
158 let needs_os = args.iter().any(|a| a.arg_type == "mock_url");
160
161 let needs_json = args.iter().any(|a| a.arg_type == "handle")
163 && fixtures.iter().any(|f| {
164 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
165 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
166 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
167 })
168 });
169
170 let needs_fmt = fixtures.iter().any(|f| {
172 f.visitor.as_ref().is_some_and(|v| {
173 v.callbacks.values().any(|action| {
174 if let CallbackAction::CustomTemplate { template } = action {
175 template.contains('{')
176 } else {
177 false
178 }
179 })
180 })
181 });
182
183 let needs_strings = fixtures.iter().any(|f| {
186 f.assertions.iter().any(|a| {
187 let type_needs_strings = if a.assertion_type == "equals" {
188 a.value.as_ref().is_some_and(|v| v.is_string())
190 } else {
191 matches!(
192 a.assertion_type.as_str(),
193 "contains" | "contains_all" | "not_contains" | "starts_with"
194 )
195 };
196 let field_valid = a
197 .field
198 .as_ref()
199 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
200 .unwrap_or(true);
201 type_needs_strings && field_valid
202 })
203 });
204
205 let needs_assert = fixtures.iter().any(|f| {
207 f.assertions.iter().any(|a| {
208 let field_valid = a
209 .field
210 .as_ref()
211 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
212 .unwrap_or(true);
213 matches!(a.assertion_type.as_str(), "count_min" | "count_max") && 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(
254 &mut out,
255 fixture,
256 import_alias,
257 function_name,
258 result_var,
259 args,
260 field_resolver,
261 e2e_config,
262 );
263 if i + 1 < fixtures.len() {
264 let _ = writeln!(out);
265 }
266 }
267
268 while out.ends_with("\n\n") {
270 out.pop();
271 }
272 if !out.ends_with('\n') {
273 out.push('\n');
274 }
275 out
276}
277
278#[allow(clippy::too_many_arguments)]
279fn render_test_function(
280 out: &mut String,
281 fixture: &Fixture,
282 import_alias: &str,
283 function_name: &str,
284 result_var: &str,
285 args: &[crate::config::ArgMapping],
286 field_resolver: &FieldResolver,
287 e2e_config: &crate::config::E2eConfig,
288) {
289 let fn_name = fixture.id.to_upper_camel_case();
290 let description = &fixture.description;
291
292 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
293
294 let (mut setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
295
296 let mut visitor_arg = String::new();
298 if fixture.visitor.is_some() {
299 let struct_name = visitor_struct_name(&fixture.id);
300 setup_lines.push(format!("visitor := &{struct_name}{{}}"));
301 visitor_arg = "visitor".to_string();
302 }
303
304 let final_args = if visitor_arg.is_empty() {
305 args_str
306 } else {
307 format!("{args_str}, {visitor_arg}")
308 };
309
310 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
311 let _ = writeln!(out, "\t// {description}");
312
313 for line in &setup_lines {
314 let _ = writeln!(out, "\t{line}");
315 }
316
317 if expects_error {
318 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
319 let _ = writeln!(out, "\tif err == nil {{");
320 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
321 let _ = writeln!(out, "\t}}");
322 let _ = writeln!(out, "}}");
323 return;
324 }
325
326 let has_usable_assertion = fixture.assertions.iter().any(|a| {
330 if a.assertion_type == "not_error" || a.assertion_type == "error" {
331 return false;
332 }
333 match &a.field {
334 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
335 _ => true,
336 }
337 });
338
339 let result_binding = if has_usable_assertion {
340 result_var.to_string()
341 } else {
342 "_".to_string()
343 };
344
345 let _ = writeln!(
347 out,
348 "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
349 );
350 let _ = writeln!(out, "\tif err != nil {{");
351 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
352 let _ = writeln!(out, "\t}}");
353
354 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
359 for assertion in &fixture.assertions {
360 if let Some(f) = &assertion.field {
361 if !f.is_empty() {
362 let resolved = field_resolver.resolve(f);
363 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
364 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
367 if !is_string_field {
368 continue;
371 }
372 let field_expr = field_resolver.accessor(f, "go", result_var);
373 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
374 if field_resolver.has_map_access(f) {
375 let _ = writeln!(out, "\t{local_var} := {field_expr}");
378 } else {
379 let _ = writeln!(out, "\tvar {local_var} string");
380 let _ = writeln!(out, "\tif {field_expr} != nil {{");
381 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
382 let _ = writeln!(out, "\t}}");
383 }
384 optional_locals.insert(f.clone(), local_var);
385 }
386 }
387 }
388 }
389
390 for assertion in &fixture.assertions {
392 if let Some(f) = &assertion.field {
393 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
394 let parts: Vec<&str> = f.split('.').collect();
397 let mut guard_expr: Option<String> = None;
398 for i in 1..parts.len() {
399 let prefix = parts[..i].join(".");
400 let resolved_prefix = field_resolver.resolve(&prefix);
401 if field_resolver.is_optional(resolved_prefix) {
402 let accessor = field_resolver.accessor(&prefix, "go", result_var);
403 guard_expr = Some(accessor);
404 break;
405 }
406 }
407 if let Some(guard) = guard_expr {
408 if field_resolver.is_valid_for_result(f) {
411 let _ = writeln!(out, "\tif {guard} != nil {{");
412 let mut nil_buf = String::new();
415 render_assertion(&mut nil_buf, assertion, result_var, field_resolver, &optional_locals);
416 for line in nil_buf.lines() {
417 let _ = writeln!(out, "\t{line}");
418 }
419 let _ = writeln!(out, "\t}}");
420 } else {
421 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
422 }
423 continue;
424 }
425 }
426 }
427 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
428 }
429
430 let _ = writeln!(out, "}}");
431}
432
433fn build_args_and_setup(
437 input: &serde_json::Value,
438 args: &[crate::config::ArgMapping],
439 import_alias: &str,
440 e2e_config: &crate::config::E2eConfig,
441 fixture_id: &str,
442) -> (Vec<String>, String) {
443 use heck::ToUpperCamelCase;
444
445 if args.is_empty() {
446 return (Vec::new(), json_to_go(input));
447 }
448
449 let overrides = e2e_config.call.overrides.get("go");
450 let options_type = overrides.and_then(|o| o.options_type.as_deref());
451
452 let mut setup_lines: Vec<String> = Vec::new();
453 let mut parts: Vec<String> = Vec::new();
454
455 for arg in args {
456 if arg.arg_type == "mock_url" {
457 setup_lines.push(format!(
458 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
459 arg.name,
460 ));
461 parts.push(arg.name.clone());
462 continue;
463 }
464
465 if arg.arg_type == "handle" {
466 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
468 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
469 let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
470 if config_value.is_null()
471 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
472 {
473 setup_lines.push(format!(
474 "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
475 name = arg.name,
476 ));
477 } else {
478 let json_str = serde_json::to_string(config_value).unwrap_or_default();
479 let go_literal = go_string_literal(&json_str);
480 let name = &arg.name;
481 setup_lines.push(format!(
482 "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}}"
483 ));
484 setup_lines.push(format!(
485 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
486 ));
487 }
488 parts.push(arg.name.clone());
489 continue;
490 }
491
492 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
493 let val = input.get(field);
494 match val {
495 None | Some(serde_json::Value::Null) if arg.optional => {
496 continue;
498 }
499 None | Some(serde_json::Value::Null) => {
500 let default_val = match arg.arg_type.as_str() {
502 "string" => "\"\"".to_string(),
503 "int" | "integer" => "0".to_string(),
504 "float" | "number" => "0.0".to_string(),
505 "bool" | "boolean" => "false".to_string(),
506 _ => "nil".to_string(),
507 };
508 parts.push(default_val);
509 }
510 Some(v) => {
511 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
513 if let Some(obj) = v.as_object() {
514 let with_calls: Vec<String> = obj
515 .iter()
516 .map(|(k, vv)| {
517 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
518 let go_val = json_to_go(vv);
519 format!("htmd.{func_name}({go_val})")
520 })
521 .collect();
522 let new_fn = format!("New{opts_type}");
523 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
524 continue;
525 }
526 }
527 parts.push(json_to_go(v));
528 }
529 }
530 }
531
532 (setup_lines, parts.join(", "))
533}
534
535fn render_assertion(
536 out: &mut String,
537 assertion: &Assertion,
538 result_var: &str,
539 field_resolver: &FieldResolver,
540 optional_locals: &std::collections::HashMap<String, String>,
541) {
542 if let Some(f) = &assertion.field {
544 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
545 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
546 return;
547 }
548 }
549
550 let field_expr = match &assertion.field {
551 Some(f) if !f.is_empty() => {
552 if let Some(local_var) = optional_locals.get(f.as_str()) {
554 local_var.clone()
555 } else {
556 field_resolver.accessor(f, "go", result_var)
557 }
558 }
559 _ => result_var.to_string(),
560 };
561
562 let is_optional = assertion
566 .field
567 .as_ref()
568 .map(|f| {
569 let resolved = field_resolver.resolve(f);
570 let check_path = resolved
571 .strip_suffix(".length")
572 .or_else(|| resolved.strip_suffix(".count"))
573 .or_else(|| resolved.strip_suffix(".size"))
574 .unwrap_or(resolved);
575 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
576 })
577 .unwrap_or(false);
578
579 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
582 let inner = &field_expr[4..field_expr.len() - 1];
583 format!("len(*{inner})")
584 } else {
585 field_expr
586 };
587 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
589 Some(field_expr[5..field_expr.len() - 1].to_string())
590 } else {
591 None
592 };
593
594 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
597 format!("*{field_expr}")
598 } else {
599 field_expr.clone()
600 };
601
602 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
607 let array_expr = &field_expr[..idx];
608 Some(array_expr.to_string())
609 } else {
610 None
611 };
612
613 let mut assertion_buf = String::new();
616 let out_ref = &mut assertion_buf;
617
618 match assertion.assertion_type.as_str() {
619 "equals" => {
620 if let Some(expected) = &assertion.value {
621 let go_val = json_to_go(expected);
622 if expected.is_string() {
624 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
626 format!("strings.TrimSpace(*{field_expr})")
627 } else {
628 format!("strings.TrimSpace({field_expr})")
629 };
630 if is_optional && !field_expr.starts_with("len(") {
631 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
632 } else {
633 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
634 }
635 } else if is_optional && !field_expr.starts_with("len(") {
636 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
637 } else {
638 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
639 }
640 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
641 let _ = writeln!(out_ref, "\t}}");
642 }
643 }
644 "contains" => {
645 if let Some(expected) = &assertion.value {
646 let go_val = json_to_go(expected);
647 let field_for_contains = if is_optional
648 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
649 {
650 format!("string(*{field_expr})")
651 } else {
652 format!("string({field_expr})")
653 };
654 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
655 let _ = writeln!(
656 out_ref,
657 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
658 );
659 let _ = writeln!(out_ref, "\t}}");
660 }
661 }
662 "contains_all" => {
663 if let Some(values) = &assertion.values {
664 for val in values {
665 let go_val = json_to_go(val);
666 let field_for_contains = if is_optional
667 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
668 {
669 format!("string(*{field_expr})")
670 } else {
671 format!("string({field_expr})")
672 };
673 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
674 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
675 let _ = writeln!(out_ref, "\t}}");
676 }
677 }
678 }
679 "not_contains" => {
680 if let Some(expected) = &assertion.value {
681 let go_val = json_to_go(expected);
682 let field_for_contains = if is_optional
683 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
684 {
685 format!("string(*{field_expr})")
686 } else {
687 format!("string({field_expr})")
688 };
689 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
690 let _ = writeln!(
691 out_ref,
692 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
693 );
694 let _ = writeln!(out_ref, "\t}}");
695 }
696 }
697 "not_empty" => {
698 if is_optional {
699 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
700 } else {
701 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
702 }
703 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
704 let _ = writeln!(out_ref, "\t}}");
705 }
706 "is_empty" => {
707 if is_optional {
708 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
709 } else {
710 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
711 }
712 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
713 let _ = writeln!(out_ref, "\t}}");
714 }
715 "contains_any" => {
716 if let Some(values) = &assertion.values {
717 let field_for_contains = if is_optional
718 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
719 {
720 format!("*{field_expr}")
721 } else {
722 field_expr.clone()
723 };
724 let _ = writeln!(out_ref, "\t{{");
725 let _ = writeln!(out_ref, "\t\tfound := false");
726 for val in values {
727 let go_val = json_to_go(val);
728 let _ = writeln!(
729 out_ref,
730 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
731 );
732 }
733 let _ = writeln!(out_ref, "\t\tif !found {{");
734 let _ = writeln!(
735 out_ref,
736 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
737 );
738 let _ = writeln!(out_ref, "\t\t}}");
739 let _ = writeln!(out_ref, "\t}}");
740 }
741 }
742 "greater_than" => {
743 if let Some(val) = &assertion.value {
744 let go_val = json_to_go(val);
745 if let Some(n) = val.as_u64() {
748 let next = n + 1;
749 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
750 } else {
751 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
752 }
753 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
754 let _ = writeln!(out_ref, "\t}}");
755 }
756 }
757 "less_than" => {
758 if let Some(val) = &assertion.value {
759 let go_val = json_to_go(val);
760 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
761 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
762 let _ = writeln!(out_ref, "\t}}");
763 }
764 }
765 "greater_than_or_equal" => {
766 if let Some(val) = &assertion.value {
767 let go_val = json_to_go(val);
768 if let Some(ref guard) = nil_guard_expr {
769 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
770 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
771 let _ = writeln!(
772 out_ref,
773 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
774 );
775 let _ = writeln!(out_ref, "\t\t}}");
776 let _ = writeln!(out_ref, "\t}}");
777 } else {
778 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
779 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
780 let _ = writeln!(out_ref, "\t}}");
781 }
782 }
783 }
784 "less_than_or_equal" => {
785 if let Some(val) = &assertion.value {
786 let go_val = json_to_go(val);
787 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
788 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
789 let _ = writeln!(out_ref, "\t}}");
790 }
791 }
792 "starts_with" => {
793 if let Some(expected) = &assertion.value {
794 let go_val = json_to_go(expected);
795 let field_for_prefix = if is_optional
796 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
797 {
798 format!("string(*{field_expr})")
799 } else {
800 format!("string({field_expr})")
801 };
802 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
803 let _ = writeln!(
804 out_ref,
805 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
806 );
807 let _ = writeln!(out_ref, "\t}}");
808 }
809 }
810 "count_min" => {
811 if let Some(val) = &assertion.value {
812 if let Some(n) = val.as_u64() {
813 if is_optional {
814 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
815 let _ = writeln!(
816 out_ref,
817 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
818 );
819 let _ = writeln!(out_ref, "\t}}");
820 } else {
821 let _ = writeln!(
822 out_ref,
823 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
824 );
825 }
826 }
827 }
828 }
829 "count_equals" => {
830 if let Some(val) = &assertion.value {
831 if let Some(n) = val.as_u64() {
832 if is_optional {
833 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
834 let _ = writeln!(
835 out_ref,
836 "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
837 );
838 let _ = writeln!(out_ref, "\t}}");
839 } else {
840 let _ = writeln!(
841 out_ref,
842 "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
843 );
844 }
845 }
846 }
847 }
848 "is_true" => {
849 if is_optional {
850 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
851 let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
852 let _ = writeln!(out_ref, "\t}}");
853 } else {
854 let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
855 }
856 }
857 "not_error" => {
858 }
860 "error" => {
861 }
863 other => {
864 let _ = writeln!(out_ref, "\t// TODO: unsupported assertion type: {other}");
865 }
866 }
867
868 if let Some(ref arr) = array_guard {
871 if !assertion_buf.is_empty() {
872 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
873 for line in assertion_buf.lines() {
875 let _ = writeln!(out, "\t{line}");
876 }
877 let _ = writeln!(out, "\t}}");
878 }
879 } else {
880 out.push_str(&assertion_buf);
881 }
882}
883
884const GO_INITIALISMS: &[&str] = &[
887 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
888 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
889 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
890];
891
892fn go_local_name(snake: &str) -> String {
896 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
897 if words.is_empty() {
898 return String::new();
899 }
900 let mut result = String::new();
901 for (i, word) in words.iter().enumerate() {
902 let upper = word.to_uppercase();
903 if GO_INITIALISMS.contains(&upper.as_str()) {
904 if i == 0 {
905 result.push_str(&upper.to_lowercase());
907 } else {
908 result.push_str(&upper);
909 }
910 } else if i == 0 {
911 result.push_str(&word.to_lowercase());
913 } else {
914 let mut chars = word.chars();
916 if let Some(c) = chars.next() {
917 result.extend(c.to_uppercase());
918 result.push_str(&chars.as_str().to_lowercase());
919 }
920 }
921 }
922 result
923}
924
925fn json_to_go(value: &serde_json::Value) -> String {
927 match value {
928 serde_json::Value::String(s) => go_string_literal(s),
929 serde_json::Value::Bool(b) => b.to_string(),
930 serde_json::Value::Number(n) => n.to_string(),
931 serde_json::Value::Null => "nil".to_string(),
932 other => go_string_literal(&other.to_string()),
934 }
935}
936
937fn visitor_struct_name(fixture_id: &str) -> String {
946 use heck::ToUpperCamelCase;
947 format!("testVisitor{}", fixture_id.to_upper_camel_case())
949}
950
951fn emit_go_visitor_struct(
953 out: &mut String,
954 struct_name: &str,
955 visitor_spec: &crate::fixture::VisitorSpec,
956 import_alias: &str,
957) {
958 let _ = writeln!(out, "type {struct_name} struct{{}}");
959 for (method_name, action) in &visitor_spec.callbacks {
960 emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
961 }
962}
963
964fn emit_go_visitor_method(
966 out: &mut String,
967 struct_name: &str,
968 method_name: &str,
969 action: &CallbackAction,
970 import_alias: &str,
971) {
972 let camel_method = method_to_camel(method_name);
973 let params = match method_name {
974 "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
975 "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
976 "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
977 "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
978 "visit_code_inline"
979 | "visit_strong"
980 | "visit_emphasis"
981 | "visit_strikethrough"
982 | "visit_underline"
983 | "visit_subscript"
984 | "visit_superscript"
985 | "visit_mark"
986 | "visit_button"
987 | "visit_summary"
988 | "visit_figcaption"
989 | "visit_definition_term"
990 | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
991 "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
992 "visit_list_item" => {
993 format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
994 }
995 "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
996 "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
997 "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
998 "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
999 "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1000 "visit_audio" | "visit_video" | "visit_iframe" => {
1001 format!("_ {import_alias}.NodeContext, src string")
1002 }
1003 "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1004 _ => format!("_ {import_alias}.NodeContext"),
1005 };
1006
1007 let _ = writeln!(
1008 out,
1009 "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1010 );
1011 match action {
1012 CallbackAction::Skip => {
1013 let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1014 }
1015 CallbackAction::Continue => {
1016 let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1017 }
1018 CallbackAction::PreserveHtml => {
1019 let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1020 }
1021 CallbackAction::Custom { output } => {
1022 let escaped = go_string_literal(output);
1023 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1024 }
1025 CallbackAction::CustomTemplate { template } => {
1026 let (fmt_str, fmt_args) = template_to_sprintf(template);
1029 let escaped_fmt = go_string_literal(&fmt_str);
1030 if fmt_args.is_empty() {
1031 let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1032 } else {
1033 let args_str = fmt_args.join(", ");
1034 let _ = writeln!(
1035 out,
1036 "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
1037 );
1038 }
1039 }
1040 }
1041 let _ = writeln!(out, "}}");
1042}
1043
1044fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
1048 let mut fmt_str = String::new();
1049 let mut args: Vec<String> = Vec::new();
1050 let mut chars = template.chars().peekable();
1051 while let Some(c) = chars.next() {
1052 if c == '{' {
1053 let mut name = String::new();
1055 for inner in chars.by_ref() {
1056 if inner == '}' {
1057 break;
1058 }
1059 name.push(inner);
1060 }
1061 fmt_str.push_str("%s");
1062 args.push(name);
1063 } else {
1064 fmt_str.push(c);
1065 }
1066 }
1067 (fmt_str, args)
1068}
1069
1070fn method_to_camel(snake: &str) -> String {
1072 use heck::ToUpperCamelCase;
1073 snake.to_upper_camel_case()
1074}