1use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, 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_strings = fixtures.iter().any(|f| {
168 f.assertions.iter().any(|a| {
169 let type_needs_strings = if a.assertion_type == "equals" {
170 a.value.as_ref().is_some_and(|v| v.is_string())
172 } else {
173 matches!(
174 a.assertion_type.as_str(),
175 "contains" | "contains_all" | "not_contains" | "starts_with"
176 )
177 };
178 let field_valid = a
179 .field
180 .as_ref()
181 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
182 .unwrap_or(true);
183 type_needs_strings && field_valid
184 })
185 });
186
187 let needs_assert = fixtures.iter().any(|f| {
189 f.assertions.iter().any(|a| {
190 let field_valid = a
191 .field
192 .as_ref()
193 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
194 .unwrap_or(true);
195 matches!(a.assertion_type.as_str(), "count_min" | "count_max") && field_valid
196 })
197 });
198
199 let _ = writeln!(out, "// E2e tests for category: {category}");
200 let _ = writeln!(out, "package e2e_test");
201 let _ = writeln!(out);
202 let _ = writeln!(out, "import (");
203 if needs_json {
204 let _ = writeln!(out, "\t\"encoding/json\"");
205 }
206 if needs_os {
207 let _ = writeln!(out, "\t\"os\"");
208 }
209 if needs_strings {
210 let _ = writeln!(out, "\t\"strings\"");
211 }
212 let _ = writeln!(out, "\t\"testing\"");
213 if needs_assert {
214 let _ = writeln!(out);
215 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
216 }
217 let _ = writeln!(out);
218 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
219 let _ = writeln!(out, ")");
220 let _ = writeln!(out);
221
222 for (i, fixture) in fixtures.iter().enumerate() {
223 render_test_function(
224 &mut out,
225 fixture,
226 import_alias,
227 function_name,
228 result_var,
229 args,
230 field_resolver,
231 e2e_config,
232 );
233 if i + 1 < fixtures.len() {
234 let _ = writeln!(out);
235 }
236 }
237
238 while out.ends_with("\n\n") {
240 out.pop();
241 }
242 if !out.ends_with('\n') {
243 out.push('\n');
244 }
245 out
246}
247
248#[allow(clippy::too_many_arguments)]
249fn render_test_function(
250 out: &mut String,
251 fixture: &Fixture,
252 import_alias: &str,
253 function_name: &str,
254 result_var: &str,
255 args: &[crate::config::ArgMapping],
256 field_resolver: &FieldResolver,
257 e2e_config: &crate::config::E2eConfig,
258) {
259 let fn_name = fixture.id.to_upper_camel_case();
260 let description = &fixture.description;
261
262 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
263
264 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
265
266 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
267 let _ = writeln!(out, "\t// {description}");
268
269 for line in &setup_lines {
270 let _ = writeln!(out, "\t{line}");
271 }
272
273 if expects_error {
274 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({args_str})");
275 let _ = writeln!(out, "\tif err == nil {{");
276 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
277 let _ = writeln!(out, "\t}}");
278 let _ = writeln!(out, "}}");
279 return;
280 }
281
282 let has_usable_assertion = fixture.assertions.iter().any(|a| {
286 if a.assertion_type == "not_error" || a.assertion_type == "error" {
287 return false;
288 }
289 match &a.field {
290 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
291 _ => true,
292 }
293 });
294
295 let result_binding = if has_usable_assertion {
296 result_var.to_string()
297 } else {
298 "_".to_string()
299 };
300
301 let _ = writeln!(
303 out,
304 "\t{result_binding}, err := {import_alias}.{function_name}({args_str})"
305 );
306 let _ = writeln!(out, "\tif err != nil {{");
307 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
308 let _ = writeln!(out, "\t}}");
309
310 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
315 for assertion in &fixture.assertions {
316 if let Some(f) = &assertion.field {
317 if !f.is_empty() {
318 let resolved = field_resolver.resolve(f);
319 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
320 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
323 if !is_string_field {
324 continue;
327 }
328 let field_expr = field_resolver.accessor(f, "go", result_var);
329 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
330 if field_resolver.has_map_access(f) {
331 let _ = writeln!(out, "\t{local_var} := {field_expr}");
334 } else {
335 let _ = writeln!(out, "\tvar {local_var} string");
336 let _ = writeln!(out, "\tif {field_expr} != nil {{");
337 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
338 let _ = writeln!(out, "\t}}");
339 }
340 optional_locals.insert(f.clone(), local_var);
341 }
342 }
343 }
344 }
345
346 for assertion in &fixture.assertions {
348 if let Some(f) = &assertion.field {
349 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
350 let parts: Vec<&str> = f.split('.').collect();
353 let mut guard_expr: Option<String> = None;
354 for i in 1..parts.len() {
355 let prefix = parts[..i].join(".");
356 let resolved_prefix = field_resolver.resolve(&prefix);
357 if field_resolver.is_optional(resolved_prefix) {
358 let accessor = field_resolver.accessor(&prefix, "go", result_var);
359 guard_expr = Some(accessor);
360 break;
361 }
362 }
363 if let Some(guard) = guard_expr {
364 if field_resolver.is_valid_for_result(f) {
367 let _ = writeln!(out, "\tif {guard} != nil {{");
368 let mut nil_buf = String::new();
371 render_assertion(&mut nil_buf, assertion, result_var, field_resolver, &optional_locals);
372 for line in nil_buf.lines() {
373 let _ = writeln!(out, "\t{line}");
374 }
375 let _ = writeln!(out, "\t}}");
376 } else {
377 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
378 }
379 continue;
380 }
381 }
382 }
383 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
384 }
385
386 let _ = writeln!(out, "}}");
387}
388
389fn build_args_and_setup(
393 input: &serde_json::Value,
394 args: &[crate::config::ArgMapping],
395 import_alias: &str,
396 e2e_config: &crate::config::E2eConfig,
397 fixture_id: &str,
398) -> (Vec<String>, String) {
399 use heck::ToUpperCamelCase;
400
401 if args.is_empty() {
402 return (Vec::new(), json_to_go(input));
403 }
404
405 let overrides = e2e_config.call.overrides.get("go");
406 let options_type = overrides.and_then(|o| o.options_type.as_deref());
407
408 let mut setup_lines: Vec<String> = Vec::new();
409 let mut parts: Vec<String> = Vec::new();
410
411 for arg in args {
412 if arg.arg_type == "mock_url" {
413 setup_lines.push(format!(
414 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
415 arg.name,
416 ));
417 parts.push(arg.name.clone());
418 continue;
419 }
420
421 if arg.arg_type == "handle" {
422 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
424 let config_value = input.get(&arg.field).unwrap_or(&serde_json::Value::Null);
425 if config_value.is_null()
426 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
427 {
428 setup_lines.push(format!(
429 "{name}, createErr := {import_alias}.{constructor_name}()\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
430 name = arg.name,
431 ));
432 } else {
433 let json_str = serde_json::to_string(config_value).unwrap_or_default();
434 let go_literal = go_string_literal(&json_str);
435 let name = &arg.name;
436 setup_lines.push(format!(
437 "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}}"
438 ));
439 setup_lines.push(format!(
440 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
441 ));
442 }
443 parts.push(arg.name.clone());
444 continue;
445 }
446
447 let val = input.get(&arg.field);
448 match val {
449 None | Some(serde_json::Value::Null) if arg.optional => {
450 continue;
452 }
453 None | Some(serde_json::Value::Null) => {
454 let default_val = match arg.arg_type.as_str() {
456 "string" => "\"\"".to_string(),
457 "int" | "integer" => "0".to_string(),
458 "float" | "number" => "0.0".to_string(),
459 "bool" | "boolean" => "false".to_string(),
460 _ => "nil".to_string(),
461 };
462 parts.push(default_val);
463 }
464 Some(v) => {
465 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
467 if let Some(obj) = v.as_object() {
468 let with_calls: Vec<String> = obj
469 .iter()
470 .map(|(k, vv)| {
471 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
472 let go_val = json_to_go(vv);
473 format!("htmd.{func_name}({go_val})")
474 })
475 .collect();
476 let new_fn = format!("New{opts_type}");
477 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
478 continue;
479 }
480 }
481 parts.push(json_to_go(v));
482 }
483 }
484 }
485
486 (setup_lines, parts.join(", "))
487}
488
489fn render_assertion(
490 out: &mut String,
491 assertion: &Assertion,
492 result_var: &str,
493 field_resolver: &FieldResolver,
494 optional_locals: &std::collections::HashMap<String, String>,
495) {
496 if let Some(f) = &assertion.field {
498 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
499 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
500 return;
501 }
502 }
503
504 let field_expr = match &assertion.field {
505 Some(f) if !f.is_empty() => {
506 if let Some(local_var) = optional_locals.get(f.as_str()) {
508 local_var.clone()
509 } else {
510 field_resolver.accessor(f, "go", result_var)
511 }
512 }
513 _ => result_var.to_string(),
514 };
515
516 let is_optional = assertion
520 .field
521 .as_ref()
522 .map(|f| {
523 let resolved = field_resolver.resolve(f);
524 let check_path = resolved
525 .strip_suffix(".length")
526 .or_else(|| resolved.strip_suffix(".count"))
527 .or_else(|| resolved.strip_suffix(".size"))
528 .unwrap_or(resolved);
529 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
530 })
531 .unwrap_or(false);
532
533 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
536 let inner = &field_expr[4..field_expr.len() - 1];
537 format!("len(*{inner})")
538 } else {
539 field_expr
540 };
541 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
543 Some(field_expr[5..field_expr.len() - 1].to_string())
544 } else {
545 None
546 };
547
548 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
551 format!("*{field_expr}")
552 } else {
553 field_expr.clone()
554 };
555
556 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
561 let array_expr = &field_expr[..idx];
562 Some(array_expr.to_string())
563 } else {
564 None
565 };
566
567 let mut assertion_buf = String::new();
570 let out_ref = &mut assertion_buf;
571
572 match assertion.assertion_type.as_str() {
573 "equals" => {
574 if let Some(expected) = &assertion.value {
575 let go_val = json_to_go(expected);
576 if expected.is_string() {
578 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
580 format!("strings.TrimSpace(*{field_expr})")
581 } else {
582 format!("strings.TrimSpace({field_expr})")
583 };
584 if is_optional && !field_expr.starts_with("len(") {
585 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
586 } else {
587 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
588 }
589 } else {
590 if is_optional && !field_expr.starts_with("len(") {
591 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
592 } else {
593 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
594 }
595 }
596 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
597 let _ = writeln!(out_ref, "\t}}");
598 }
599 }
600 "contains" => {
601 if let Some(expected) = &assertion.value {
602 let go_val = json_to_go(expected);
603 let field_for_contains = if is_optional
604 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
605 {
606 format!("string(*{field_expr})")
607 } else {
608 format!("string({field_expr})")
609 };
610 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
611 let _ = writeln!(
612 out_ref,
613 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
614 );
615 let _ = writeln!(out_ref, "\t}}");
616 }
617 }
618 "contains_all" => {
619 if let Some(values) = &assertion.values {
620 for val in values {
621 let go_val = json_to_go(val);
622 let field_for_contains = if is_optional
623 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
624 {
625 format!("string(*{field_expr})")
626 } else {
627 format!("string({field_expr})")
628 };
629 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
630 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
631 let _ = writeln!(out_ref, "\t}}");
632 }
633 }
634 }
635 "not_contains" => {
636 if let Some(expected) = &assertion.value {
637 let go_val = json_to_go(expected);
638 let field_for_contains = if is_optional
639 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
640 {
641 format!("string(*{field_expr})")
642 } else {
643 format!("string({field_expr})")
644 };
645 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
646 let _ = writeln!(
647 out_ref,
648 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
649 );
650 let _ = writeln!(out_ref, "\t}}");
651 }
652 }
653 "not_empty" => {
654 if is_optional {
655 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
656 } else {
657 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
658 }
659 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
660 let _ = writeln!(out_ref, "\t}}");
661 }
662 "is_empty" => {
663 if is_optional {
664 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
665 } else {
666 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
667 }
668 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
669 let _ = writeln!(out_ref, "\t}}");
670 }
671 "contains_any" => {
672 if let Some(values) = &assertion.values {
673 let field_for_contains = if is_optional
674 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
675 {
676 format!("*{field_expr}")
677 } else {
678 field_expr.clone()
679 };
680 let _ = writeln!(out_ref, "\t{{");
681 let _ = writeln!(out_ref, "\t\tfound := false");
682 for val in values {
683 let go_val = json_to_go(val);
684 let _ = writeln!(
685 out_ref,
686 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
687 );
688 }
689 let _ = writeln!(out_ref, "\t\tif !found {{");
690 let _ = writeln!(
691 out_ref,
692 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
693 );
694 let _ = writeln!(out_ref, "\t\t}}");
695 let _ = writeln!(out_ref, "\t}}");
696 }
697 }
698 "greater_than" => {
699 if let Some(val) = &assertion.value {
700 let go_val = json_to_go(val);
701 if let Some(n) = val.as_u64() {
704 let next = n + 1;
705 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
706 } else {
707 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
708 }
709 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
710 let _ = writeln!(out_ref, "\t}}");
711 }
712 }
713 "less_than" => {
714 if let Some(val) = &assertion.value {
715 let go_val = json_to_go(val);
716 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
717 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
718 let _ = writeln!(out_ref, "\t}}");
719 }
720 }
721 "greater_than_or_equal" => {
722 if let Some(val) = &assertion.value {
723 let go_val = json_to_go(val);
724 if let Some(ref guard) = nil_guard_expr {
725 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
726 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
727 let _ = writeln!(
728 out_ref,
729 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
730 );
731 let _ = writeln!(out_ref, "\t\t}}");
732 let _ = writeln!(out_ref, "\t}}");
733 } else {
734 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
735 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
736 let _ = writeln!(out_ref, "\t}}");
737 }
738 }
739 }
740 "less_than_or_equal" => {
741 if let Some(val) = &assertion.value {
742 let go_val = json_to_go(val);
743 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
744 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
745 let _ = writeln!(out_ref, "\t}}");
746 }
747 }
748 "starts_with" => {
749 if let Some(expected) = &assertion.value {
750 let go_val = json_to_go(expected);
751 let field_for_prefix = if is_optional
752 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
753 {
754 format!("string(*{field_expr})")
755 } else {
756 format!("string({field_expr})")
757 };
758 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
759 let _ = writeln!(
760 out_ref,
761 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
762 );
763 let _ = writeln!(out_ref, "\t}}");
764 }
765 }
766 "count_min" => {
767 if let Some(val) = &assertion.value {
768 if let Some(n) = val.as_u64() {
769 if is_optional {
770 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
771 let _ = writeln!(
772 out_ref,
773 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
774 );
775 let _ = writeln!(out_ref, "\t}}");
776 } else {
777 let _ = writeln!(
778 out_ref,
779 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
780 );
781 }
782 }
783 }
784 }
785 "not_error" => {
786 }
788 "error" => {
789 }
791 other => {
792 let _ = writeln!(out_ref, "\t// TODO: unsupported assertion type: {other}");
793 }
794 }
795
796 if let Some(ref arr) = array_guard {
799 if !assertion_buf.is_empty() {
800 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
801 for line in assertion_buf.lines() {
803 let _ = writeln!(out, "\t{line}");
804 }
805 let _ = writeln!(out, "\t}}");
806 }
807 } else {
808 out.push_str(&assertion_buf);
809 }
810}
811
812const GO_INITIALISMS: &[&str] = &[
815 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
816 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
817 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
818];
819
820fn go_local_name(snake: &str) -> String {
824 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
825 if words.is_empty() {
826 return String::new();
827 }
828 let mut result = String::new();
829 for (i, word) in words.iter().enumerate() {
830 let upper = word.to_uppercase();
831 if GO_INITIALISMS.contains(&upper.as_str()) {
832 if i == 0 {
833 result.push_str(&upper.to_lowercase());
835 } else {
836 result.push_str(&upper);
837 }
838 } else if i == 0 {
839 result.push_str(&word.to_lowercase());
841 } else {
842 let mut chars = word.chars();
844 if let Some(c) = chars.next() {
845 result.extend(c.to_uppercase());
846 result.push_str(&chars.as_str().to_lowercase());
847 }
848 }
849 }
850 result
851}
852
853fn json_to_go(value: &serde_json::Value) -> String {
855 match value {
856 serde_json::Value::String(s) => go_string_literal(s),
857 serde_json::Value::Bool(b) => b.to_string(),
858 serde_json::Value::Number(n) => n.to_string(),
859 serde_json::Value::Null => "nil".to_string(),
860 other => go_string_literal(&other.to_string()),
862 }
863}