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.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.packages.get("go");
50 let go_module_path = go_pkg
51 .and_then(|p| p.module.as_ref())
52 .cloned()
53 .unwrap_or_else(|| module_path.clone());
54 let replace_path = go_pkg.and_then(|p| p.path.as_ref()).cloned();
55 let go_version = go_pkg
56 .and_then(|p| p.version.as_ref())
57 .cloned()
58 .unwrap_or_else(|| "v0.0.0".to_string());
59 let field_resolver = FieldResolver::new(
60 &e2e_config.fields,
61 &e2e_config.fields_optional,
62 &e2e_config.result_fields,
63 &e2e_config.fields_array,
64 );
65
66 files.push(GeneratedFile {
68 path: output_base.join("go.mod"),
69 content: render_go_mod(&go_module_path, replace_path.as_deref(), &go_version),
70 generated_header: false,
71 });
72
73 for group in groups {
75 let active: Vec<&Fixture> = group
76 .fixtures
77 .iter()
78 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
79 .collect();
80
81 if active.is_empty() {
82 continue;
83 }
84
85 let filename = format!("{}_test.go", sanitize_filename(&group.category));
86 let content = render_test_file(
87 &group.category,
88 &active,
89 &module_path,
90 &import_alias,
91 &function_name,
92 result_var,
93 &e2e_config.call.args,
94 &field_resolver,
95 e2e_config,
96 );
97 files.push(GeneratedFile {
98 path: output_base.join(filename),
99 content,
100 generated_header: true,
101 });
102 }
103
104 Ok(files)
105 }
106
107 fn language_name(&self) -> &'static str {
108 "go"
109 }
110}
111
112fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
113 let mut out = String::new();
114 let _ = writeln!(out, "module e2e_go");
115 let _ = writeln!(out);
116 let _ = writeln!(out, "go 1.23");
117 let _ = writeln!(out);
118 let _ = writeln!(out, "require {go_module_path} {version}");
119
120 if let Some(path) = replace_path {
121 let _ = writeln!(out);
122 let _ = writeln!(out, "replace {go_module_path} => {path}");
123 }
124
125 out
126}
127
128#[allow(clippy::too_many_arguments)]
129fn render_test_file(
130 category: &str,
131 fixtures: &[&Fixture],
132 go_module_path: &str,
133 import_alias: &str,
134 function_name: &str,
135 result_var: &str,
136 args: &[crate::config::ArgMapping],
137 field_resolver: &FieldResolver,
138 e2e_config: &crate::config::E2eConfig,
139) -> String {
140 let mut out = String::new();
141
142 let _ = writeln!(out, "// Code generated by alef. DO NOT EDIT.");
144 let _ = writeln!(out);
145
146 let needs_os = args.iter().any(|a| a.arg_type == "mock_url");
148
149 let needs_json = args.iter().any(|a| a.arg_type == "handle")
151 && fixtures.iter().any(|f| {
152 args.iter().filter(|a| a.arg_type == "handle").any(|a| {
153 let v = f.input.get(&a.field).unwrap_or(&serde_json::Value::Null);
154 !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
155 })
156 });
157
158 let needs_strings = fixtures.iter().any(|f| {
161 f.assertions.iter().any(|a| {
162 let type_needs_strings = if a.assertion_type == "equals" {
163 a.value.as_ref().is_some_and(|v| v.is_string())
165 } else {
166 matches!(
167 a.assertion_type.as_str(),
168 "contains" | "contains_all" | "not_contains" | "starts_with"
169 )
170 };
171 let field_valid = a
172 .field
173 .as_ref()
174 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
175 .unwrap_or(true);
176 type_needs_strings && field_valid
177 })
178 });
179
180 let needs_assert = fixtures.iter().any(|f| {
182 f.assertions.iter().any(|a| {
183 let field_valid = a
184 .field
185 .as_ref()
186 .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
187 .unwrap_or(true);
188 matches!(a.assertion_type.as_str(), "count_min" | "count_max") && field_valid
189 })
190 });
191
192 let _ = writeln!(out, "// E2e tests for category: {category}");
193 let _ = writeln!(out, "package e2e_test");
194 let _ = writeln!(out);
195 let _ = writeln!(out, "import (");
196 if needs_json {
197 let _ = writeln!(out, "\t\"encoding/json\"");
198 }
199 if needs_os {
200 let _ = writeln!(out, "\t\"os\"");
201 }
202 if needs_strings {
203 let _ = writeln!(out, "\t\"strings\"");
204 }
205 let _ = writeln!(out, "\t\"testing\"");
206 if needs_assert {
207 let _ = writeln!(out);
208 let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
209 }
210 let _ = writeln!(out);
211 let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
212 let _ = writeln!(out, ")");
213 let _ = writeln!(out);
214
215 for (i, fixture) in fixtures.iter().enumerate() {
216 render_test_function(
217 &mut out,
218 fixture,
219 import_alias,
220 function_name,
221 result_var,
222 args,
223 field_resolver,
224 e2e_config,
225 );
226 if i + 1 < fixtures.len() {
227 let _ = writeln!(out);
228 }
229 }
230
231 while out.ends_with("\n\n") {
233 out.pop();
234 }
235 if !out.ends_with('\n') {
236 out.push('\n');
237 }
238 out
239}
240
241#[allow(clippy::too_many_arguments)]
242fn render_test_function(
243 out: &mut String,
244 fixture: &Fixture,
245 import_alias: &str,
246 function_name: &str,
247 result_var: &str,
248 args: &[crate::config::ArgMapping],
249 field_resolver: &FieldResolver,
250 e2e_config: &crate::config::E2eConfig,
251) {
252 let fn_name = fixture.id.to_upper_camel_case();
253 let description = &fixture.description;
254
255 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
256
257 let (setup_lines, args_str) = build_args_and_setup(&fixture.input, args, import_alias, e2e_config, &fixture.id);
258
259 let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
260 let _ = writeln!(out, "\t// {description}");
261
262 for line in &setup_lines {
263 let _ = writeln!(out, "\t{line}");
264 }
265
266 if expects_error {
267 let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({args_str})");
268 let _ = writeln!(out, "\tif err == nil {{");
269 let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
270 let _ = writeln!(out, "\t}}");
271 let _ = writeln!(out, "}}");
272 return;
273 }
274
275 let has_usable_assertion = fixture.assertions.iter().any(|a| {
279 if a.assertion_type == "not_error" || a.assertion_type == "error" {
280 return false;
281 }
282 match &a.field {
283 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
284 _ => true,
285 }
286 });
287
288 let result_binding = if has_usable_assertion {
289 result_var.to_string()
290 } else {
291 "_".to_string()
292 };
293
294 let _ = writeln!(
296 out,
297 "\t{result_binding}, err := {import_alias}.{function_name}({args_str})"
298 );
299 let _ = writeln!(out, "\tif err != nil {{");
300 let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
301 let _ = writeln!(out, "\t}}");
302
303 let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
308 for assertion in &fixture.assertions {
309 if let Some(f) = &assertion.field {
310 if !f.is_empty() {
311 let resolved = field_resolver.resolve(f);
312 if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
313 let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
316 if !is_string_field {
317 continue;
320 }
321 let field_expr = field_resolver.accessor(f, "go", result_var);
322 let local_var = go_local_name(&resolved.replace(['.', '[', ']'], "_"));
323 if field_resolver.has_map_access(f) {
324 let _ = writeln!(out, "\t{local_var} := {field_expr}");
327 } else {
328 let _ = writeln!(out, "\tvar {local_var} string");
329 let _ = writeln!(out, "\tif {field_expr} != nil {{");
330 let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
331 let _ = writeln!(out, "\t}}");
332 }
333 optional_locals.insert(f.clone(), local_var);
334 }
335 }
336 }
337 }
338
339 for assertion in &fixture.assertions {
341 if let Some(f) = &assertion.field {
342 if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
343 let parts: Vec<&str> = f.split('.').collect();
346 let mut guard_expr: Option<String> = None;
347 for i in 1..parts.len() {
348 let prefix = parts[..i].join(".");
349 let resolved_prefix = field_resolver.resolve(&prefix);
350 if field_resolver.is_optional(resolved_prefix) {
351 let accessor = field_resolver.accessor(&prefix, "go", result_var);
352 guard_expr = Some(accessor);
353 break;
354 }
355 }
356 if let Some(guard) = guard_expr {
357 if field_resolver.is_valid_for_result(f) {
360 let _ = writeln!(out, "\tif {guard} != nil {{");
361 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
362 let _ = writeln!(out, "\t}}");
363 } else {
364 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
365 }
366 continue;
367 }
368 }
369 }
370 render_assertion(out, assertion, result_var, field_resolver, &optional_locals);
371 }
372
373 let _ = writeln!(out, "}}");
374}
375
376fn build_args_and_setup(
380 input: &serde_json::Value,
381 args: &[crate::config::ArgMapping],
382 import_alias: &str,
383 e2e_config: &crate::config::E2eConfig,
384 fixture_id: &str,
385) -> (Vec<String>, String) {
386 use heck::ToUpperCamelCase;
387
388 if args.is_empty() {
389 return (Vec::new(), json_to_go(input));
390 }
391
392 let overrides = e2e_config.call.overrides.get("go");
393 let options_type = overrides.and_then(|o| o.options_type.as_deref());
394
395 let mut setup_lines: Vec<String> = Vec::new();
396 let mut parts: Vec<String> = Vec::new();
397
398 for arg in args {
399 if arg.arg_type == "mock_url" {
400 setup_lines.push(format!(
401 "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
402 arg.name,
403 ));
404 parts.push(arg.name.clone());
405 continue;
406 }
407
408 if arg.arg_type == "handle" {
409 let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
411 let config_value = input.get(&arg.field).unwrap_or(&serde_json::Value::Null);
412 if config_value.is_null()
413 || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
414 {
415 setup_lines.push(format!(
416 "{name}, createErr := {import_alias}.{constructor_name}()\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
417 name = arg.name,
418 ));
419 } else {
420 let json_str = serde_json::to_string(config_value).unwrap_or_default();
421 let go_literal = go_string_literal(&json_str);
422 let name = &arg.name;
423 setup_lines.push(format!(
424 "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}}"
425 ));
426 setup_lines.push(format!(
427 "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
428 ));
429 }
430 parts.push(arg.name.clone());
431 continue;
432 }
433
434 let val = input.get(&arg.field);
435 match val {
436 None | Some(serde_json::Value::Null) if arg.optional => {
437 continue;
439 }
440 None | Some(serde_json::Value::Null) => {
441 let default_val = match arg.arg_type.as_str() {
443 "string" => "\"\"".to_string(),
444 "int" | "integer" => "0".to_string(),
445 "float" | "number" => "0.0".to_string(),
446 "bool" | "boolean" => "false".to_string(),
447 _ => "nil".to_string(),
448 };
449 parts.push(default_val);
450 }
451 Some(v) => {
452 if let (Some(opts_type), "json_object") = (options_type, arg.arg_type.as_str()) {
454 if let Some(obj) = v.as_object() {
455 let with_calls: Vec<String> = obj
456 .iter()
457 .map(|(k, vv)| {
458 let func_name = format!("With{}{}", opts_type, k.to_upper_camel_case());
459 let go_val = json_to_go(vv);
460 format!("htmd.{func_name}({go_val})")
461 })
462 .collect();
463 let new_fn = format!("New{opts_type}");
464 parts.push(format!("htmd.{new_fn}({})", with_calls.join(", ")));
465 continue;
466 }
467 }
468 parts.push(json_to_go(v));
469 }
470 }
471 }
472
473 (setup_lines, parts.join(", "))
474}
475
476fn render_assertion(
477 out: &mut String,
478 assertion: &Assertion,
479 result_var: &str,
480 field_resolver: &FieldResolver,
481 optional_locals: &std::collections::HashMap<String, String>,
482) {
483 if let Some(f) = &assertion.field {
485 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
486 let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
487 return;
488 }
489 }
490
491 let field_expr = match &assertion.field {
492 Some(f) if !f.is_empty() => {
493 if let Some(local_var) = optional_locals.get(f.as_str()) {
495 local_var.clone()
496 } else {
497 field_resolver.accessor(f, "go", result_var)
498 }
499 }
500 _ => result_var.to_string(),
501 };
502
503 let is_optional = assertion
507 .field
508 .as_ref()
509 .map(|f| {
510 let resolved = field_resolver.resolve(f);
511 let check_path = resolved
512 .strip_suffix(".length")
513 .or_else(|| resolved.strip_suffix(".count"))
514 .or_else(|| resolved.strip_suffix(".size"))
515 .unwrap_or(resolved);
516 field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
517 })
518 .unwrap_or(false);
519
520 let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
523 let inner = &field_expr[4..field_expr.len() - 1];
524 format!("len(*{inner})")
525 } else {
526 field_expr
527 };
528 let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
530 Some(field_expr[5..field_expr.len() - 1].to_string())
531 } else {
532 None
533 };
534
535 let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
538 format!("*{field_expr}")
539 } else {
540 field_expr.clone()
541 };
542
543 let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
548 let array_expr = &field_expr[..idx];
549 Some(array_expr.to_string())
550 } else {
551 None
552 };
553
554 let mut assertion_buf = String::new();
557 let out_ref = &mut assertion_buf;
558
559 match assertion.assertion_type.as_str() {
560 "equals" => {
561 if let Some(expected) = &assertion.value {
562 let go_val = json_to_go(expected);
563 if expected.is_string() {
565 let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
567 format!("strings.TrimSpace(*{field_expr})")
568 } else {
569 format!("strings.TrimSpace({field_expr})")
570 };
571 if is_optional && !field_expr.starts_with("len(") {
572 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
573 } else {
574 let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
575 }
576 } else {
577 if is_optional && !field_expr.starts_with("len(") {
578 let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
579 } else {
580 let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
581 }
582 }
583 let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
584 let _ = writeln!(out_ref, "\t}}");
585 }
586 }
587 "contains" => {
588 if let Some(expected) = &assertion.value {
589 let go_val = json_to_go(expected);
590 let field_for_contains = if is_optional
591 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
592 {
593 format!("string(*{field_expr})")
594 } else {
595 format!("string({field_expr})")
596 };
597 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
598 let _ = writeln!(
599 out_ref,
600 "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
601 );
602 let _ = writeln!(out_ref, "\t}}");
603 }
604 }
605 "contains_all" => {
606 if let Some(values) = &assertion.values {
607 for val in values {
608 let go_val = json_to_go(val);
609 let field_for_contains = if is_optional
610 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
611 {
612 format!("string(*{field_expr})")
613 } else {
614 format!("string({field_expr})")
615 };
616 let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
617 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
618 let _ = writeln!(out_ref, "\t}}");
619 }
620 }
621 }
622 "not_contains" => {
623 if let Some(expected) = &assertion.value {
624 let go_val = json_to_go(expected);
625 let field_for_contains = if is_optional
626 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
627 {
628 format!("string(*{field_expr})")
629 } else {
630 format!("string({field_expr})")
631 };
632 let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
633 let _ = writeln!(
634 out_ref,
635 "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
636 );
637 let _ = writeln!(out_ref, "\t}}");
638 }
639 }
640 "not_empty" => {
641 if is_optional {
642 let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
643 } else {
644 let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
645 }
646 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
647 let _ = writeln!(out_ref, "\t}}");
648 }
649 "is_empty" => {
650 if is_optional {
651 let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
652 } else {
653 let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
654 }
655 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
656 let _ = writeln!(out_ref, "\t}}");
657 }
658 "contains_any" => {
659 if let Some(values) = &assertion.values {
660 let field_for_contains = if is_optional
661 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
662 {
663 format!("*{field_expr}")
664 } else {
665 field_expr.clone()
666 };
667 let _ = writeln!(out_ref, "\t{{");
668 let _ = writeln!(out_ref, "\t\tfound := false");
669 for val in values {
670 let go_val = json_to_go(val);
671 let _ = writeln!(
672 out_ref,
673 "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
674 );
675 }
676 let _ = writeln!(out_ref, "\t\tif !found {{");
677 let _ = writeln!(
678 out_ref,
679 "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
680 );
681 let _ = writeln!(out_ref, "\t\t}}");
682 let _ = writeln!(out_ref, "\t}}");
683 }
684 }
685 "greater_than" => {
686 if let Some(val) = &assertion.value {
687 let go_val = json_to_go(val);
688 if let Some(n) = val.as_u64() {
691 let next = n + 1;
692 let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
693 } else {
694 let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
695 }
696 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
697 let _ = writeln!(out_ref, "\t}}");
698 }
699 }
700 "less_than" => {
701 if let Some(val) = &assertion.value {
702 let go_val = json_to_go(val);
703 let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
704 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
705 let _ = writeln!(out_ref, "\t}}");
706 }
707 }
708 "greater_than_or_equal" => {
709 if let Some(val) = &assertion.value {
710 let go_val = json_to_go(val);
711 if let Some(ref guard) = nil_guard_expr {
712 let _ = writeln!(out_ref, "\tif {guard} != nil {{");
713 let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
714 let _ = writeln!(
715 out_ref,
716 "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
717 );
718 let _ = writeln!(out_ref, "\t\t}}");
719 let _ = writeln!(out_ref, "\t}}");
720 } else {
721 let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
722 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
723 let _ = writeln!(out_ref, "\t}}");
724 }
725 }
726 }
727 "less_than_or_equal" => {
728 if let Some(val) = &assertion.value {
729 let go_val = json_to_go(val);
730 let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
731 let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
732 let _ = writeln!(out_ref, "\t}}");
733 }
734 }
735 "starts_with" => {
736 if let Some(expected) = &assertion.value {
737 let go_val = json_to_go(expected);
738 let field_for_prefix = if is_optional
739 && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
740 {
741 format!("string(*{field_expr})")
742 } else {
743 format!("string({field_expr})")
744 };
745 let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
746 let _ = writeln!(
747 out_ref,
748 "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
749 );
750 let _ = writeln!(out_ref, "\t}}");
751 }
752 }
753 "count_min" => {
754 if let Some(val) = &assertion.value {
755 if let Some(n) = val.as_u64() {
756 if is_optional {
757 let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
758 let _ = writeln!(
759 out_ref,
760 "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
761 );
762 let _ = writeln!(out_ref, "\t}}");
763 } else {
764 let _ = writeln!(
765 out_ref,
766 "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
767 );
768 }
769 }
770 }
771 }
772 "not_error" => {
773 }
775 "error" => {
776 }
778 other => {
779 let _ = writeln!(out_ref, "\t// TODO: unsupported assertion type: {other}");
780 }
781 }
782
783 if let Some(ref arr) = array_guard {
786 if !assertion_buf.is_empty() {
787 let _ = writeln!(out, "\tif len({arr}) > 0 {{");
788 for line in assertion_buf.lines() {
790 let _ = writeln!(out, "\t{line}");
791 }
792 let _ = writeln!(out, "\t}}");
793 }
794 } else {
795 out.push_str(&assertion_buf);
796 }
797}
798
799const GO_INITIALISMS: &[&str] = &[
802 "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IDS", "IP", "JSON",
803 "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "UID", "UUID",
804 "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS",
805];
806
807fn go_local_name(snake: &str) -> String {
811 let words: Vec<&str> = snake.split('_').filter(|w| !w.is_empty()).collect();
812 if words.is_empty() {
813 return String::new();
814 }
815 let mut result = String::new();
816 for (i, word) in words.iter().enumerate() {
817 let upper = word.to_uppercase();
818 if GO_INITIALISMS.contains(&upper.as_str()) {
819 if i == 0 {
820 result.push_str(&upper.to_lowercase());
822 } else {
823 result.push_str(&upper);
824 }
825 } else if i == 0 {
826 result.push_str(&word.to_lowercase());
828 } else {
829 let mut chars = word.chars();
831 if let Some(c) = chars.next() {
832 result.extend(c.to_uppercase());
833 result.push_str(&chars.as_str().to_lowercase());
834 }
835 }
836 }
837 result
838}
839
840fn json_to_go(value: &serde_json::Value) -> String {
842 match value {
843 serde_json::Value::String(s) => go_string_literal(s),
844 serde_json::Value::Bool(b) => b.to_string(),
845 serde_json::Value::Number(n) => n.to_string(),
846 serde_json::Value::Null => "nil".to_string(),
847 other => go_string_literal(&other.to_string()),
849 }
850}