1use crate::config::E2eConfig;
7use crate::escape::{escape_rust, rust_raw_string, sanitize_filename, sanitize_ident};
8use crate::field_access::FieldResolver;
9use crate::fixture::{Assertion, Fixture, FixtureGroup};
10use alef_core::backend::GeneratedFile;
11use alef_core::config::AlefConfig;
12use anyhow::Result;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16pub struct RustE2eCodegen;
18
19impl super::E2eCodegen for RustE2eCodegen {
20 fn generate(
21 &self,
22 groups: &[FixtureGroup],
23 e2e_config: &E2eConfig,
24 alef_config: &AlefConfig,
25 ) -> Result<Vec<GeneratedFile>> {
26 let mut files = Vec::new();
27 let output_base = PathBuf::from(e2e_config.effective_output()).join("rust");
28
29 let crate_name = resolve_crate_name(e2e_config, alef_config);
31 let crate_path = resolve_crate_path(e2e_config, &crate_name);
32 let dep_name = crate_name.replace('-', "_");
33
34 let needs_serde_json = e2e_config
37 .call
38 .args
39 .iter()
40 .any(|a| a.arg_type == "json_object" || a.arg_type == "handle");
41 let crate_version = resolve_crate_version(e2e_config);
42 files.push(GeneratedFile {
43 path: output_base.join("Cargo.toml"),
44 content: render_cargo_toml(
45 &crate_name,
46 &dep_name,
47 &crate_path,
48 needs_serde_json,
49 e2e_config.dep_mode,
50 crate_version.as_deref(),
51 ),
52 generated_header: true,
53 });
54
55 for group in groups {
57 let fixtures: Vec<&Fixture> = group.fixtures.iter().filter(|f| !is_skipped(f, "rust")).collect();
58
59 if fixtures.is_empty() {
60 continue;
61 }
62
63 let filename = format!("{}_test.rs", sanitize_filename(&group.category));
64 let content = render_test_file(&group.category, &fixtures, e2e_config, &dep_name);
65
66 files.push(GeneratedFile {
67 path: output_base.join("tests").join(filename),
68 content,
69 generated_header: true,
70 });
71 }
72
73 Ok(files)
74 }
75
76 fn language_name(&self) -> &'static str {
77 "rust"
78 }
79}
80
81fn resolve_crate_name(_e2e_config: &E2eConfig, alef_config: &AlefConfig) -> String {
86 alef_config.crate_config.name.clone()
90}
91
92fn resolve_crate_path(e2e_config: &E2eConfig, crate_name: &str) -> String {
93 e2e_config
94 .resolve_package("rust")
95 .and_then(|p| p.path.clone())
96 .unwrap_or_else(|| format!("../../crates/{crate_name}"))
97}
98
99fn resolve_crate_version(e2e_config: &E2eConfig) -> Option<String> {
100 e2e_config.resolve_package("rust").and_then(|p| p.version.clone())
101}
102
103fn resolve_function_name(e2e_config: &E2eConfig) -> String {
104 e2e_config
105 .call
106 .overrides
107 .get("rust")
108 .and_then(|o| o.function.clone())
109 .unwrap_or_else(|| e2e_config.call.function.clone())
110}
111
112fn resolve_module(e2e_config: &E2eConfig, dep_name: &str) -> String {
113 let overrides = e2e_config.call.overrides.get("rust");
116 overrides
117 .and_then(|o| o.crate_name.clone())
118 .or_else(|| overrides.and_then(|o| o.module.clone()))
119 .unwrap_or_else(|| dep_name.to_string())
120}
121
122fn is_skipped(fixture: &Fixture, language: &str) -> bool {
123 fixture.skip.as_ref().is_some_and(|s| s.should_skip(language))
124}
125
126fn render_cargo_toml(
131 crate_name: &str,
132 dep_name: &str,
133 crate_path: &str,
134 needs_serde_json: bool,
135 dep_mode: crate::config::DependencyMode,
136 version: Option<&str>,
137) -> String {
138 let e2e_name = format!("{dep_name}-e2e-rust");
139 let dep_spec = match dep_mode {
140 crate::config::DependencyMode::Registry => {
141 let ver = version.unwrap_or("0.1.0");
142 if crate_name != dep_name {
143 format!("{dep_name} = {{ package = \"{crate_name}\", version = \"{ver}\" }}")
144 } else {
145 format!("{dep_name} = \"{ver}\"")
146 }
147 }
148 crate::config::DependencyMode::Local => {
149 if crate_name != dep_name {
152 format!("{dep_name} = {{ package = \"{crate_name}\", path = \"{crate_path}\" }}")
153 } else {
154 format!("{dep_name} = {{ path = \"{crate_path}\" }}")
155 }
156 }
157 };
158 let serde_line = if needs_serde_json { "\nserde_json = \"1\"" } else { "" };
159 let workspace_section = match dep_mode {
164 crate::config::DependencyMode::Registry => "\n[workspace]\n",
165 crate::config::DependencyMode::Local => "",
166 };
167 format!(
168 r#"# This file is auto-generated by alef. DO NOT EDIT.
169{workspace_section}
170[package]
171name = "{e2e_name}"
172version = "0.1.0"
173edition = "2021"
174license = "MIT"
175publish = false
176
177[dependencies]
178{dep_spec}{serde_line}
179tokio = {{ version = "1", features = ["full"] }}
180wiremock = "0.6"
181"#
182 )
183}
184
185fn render_test_file(category: &str, fixtures: &[&Fixture], e2e_config: &E2eConfig, dep_name: &str) -> String {
186 let mut out = String::new();
187 let _ = writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.");
188 let _ = writeln!(out, "//! E2e tests for category: {category}");
189 let _ = writeln!(out);
190
191 let module = resolve_module(e2e_config, dep_name);
192 let function_name = resolve_function_name(e2e_config);
193 let field_resolver = FieldResolver::new(
194 &e2e_config.fields,
195 &e2e_config.fields_optional,
196 &e2e_config.result_fields,
197 &e2e_config.fields_array,
198 );
199
200 let _ = writeln!(out, "use {module}::{function_name};");
201
202 let has_handle_args = e2e_config.call.args.iter().any(|a| a.arg_type == "handle");
204 if has_handle_args {
205 let _ = writeln!(out, "use {module}::CrawlConfig;");
206 }
207 for arg in &e2e_config.call.args {
208 if arg.arg_type == "handle" {
209 use heck::ToSnakeCase;
210 let constructor_name = format!("create_{}", arg.name.to_snake_case());
211 let _ = writeln!(out, "use {module}::{constructor_name};");
212 }
213 }
214
215 let _ = writeln!(out);
216
217 for fixture in fixtures {
218 render_test_function(&mut out, fixture, e2e_config, dep_name, &field_resolver);
219 let _ = writeln!(out);
220 }
221
222 if !out.ends_with('\n') {
223 out.push('\n');
224 }
225 out
226}
227
228fn render_test_function(
229 out: &mut String,
230 fixture: &Fixture,
231 e2e_config: &E2eConfig,
232 dep_name: &str,
233 field_resolver: &FieldResolver,
234) {
235 let fn_name = sanitize_ident(&fixture.id);
236 let description = &fixture.description;
237 let function_name = resolve_function_name(e2e_config);
238 let module = resolve_module(e2e_config, dep_name);
239 let result_var = &e2e_config.call.result_var;
240
241 let is_async = e2e_config.call.r#async;
242 if is_async {
243 let _ = writeln!(out, "#[tokio::test]");
244 let _ = writeln!(out, "async fn test_{fn_name}() {{");
245 } else {
246 let _ = writeln!(out, "#[test]");
247 let _ = writeln!(out, "fn test_{fn_name}() {{");
248 }
249 let _ = writeln!(out, " // {description}");
250
251 let has_error_assertion = fixture.assertions.iter().any(|a| a.assertion_type == "error");
253
254 let mut arg_exprs: Vec<String> = Vec::new();
256 for arg in &e2e_config.call.args {
257 let value = resolve_field(&fixture.input, &arg.field);
258 let var_name = &arg.name;
259 let (bindings, expr) = render_rust_arg(var_name, value, &arg.arg_type, arg.optional, &module, &fixture.id);
260 for binding in &bindings {
261 let _ = writeln!(out, " {binding}");
262 }
263 arg_exprs.push(expr);
264 }
265
266 let args_str = arg_exprs.join(", ");
267
268 let await_suffix = if is_async { ".await" } else { "" };
269
270 if has_error_assertion {
271 let _ = writeln!(out, " let {result_var} = {function_name}({args_str}){await_suffix};");
272 for assertion in &fixture.assertions {
274 render_assertion(out, assertion, result_var, dep_name, true, &[], field_resolver);
275 }
276 let _ = writeln!(out, "}}");
277 return;
278 }
279
280 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
282
283 let has_usable_assertion = fixture.assertions.iter().any(|a| {
287 if a.assertion_type == "not_error" || a.assertion_type == "error" {
288 return false;
289 }
290 match &a.field {
291 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
292 _ => true,
293 }
294 });
295
296 let result_binding = if has_usable_assertion {
297 result_var.to_string()
298 } else {
299 "_".to_string()
300 };
301
302 if has_not_error || !fixture.assertions.is_empty() {
303 let _ = writeln!(
304 out,
305 " let {result_binding} = {function_name}({args_str}){await_suffix}.expect(\"should succeed\");"
306 );
307 } else {
308 let _ = writeln!(
309 out,
310 " let {result_binding} = {function_name}({args_str}){await_suffix};"
311 );
312 }
313
314 let string_assertion_types = [
317 "equals",
318 "contains",
319 "contains_all",
320 "contains_any",
321 "not_contains",
322 "starts_with",
323 "ends_with",
324 "min_length",
325 "max_length",
326 "matches_regex",
327 ];
328 let mut unwrapped_fields: Vec<(String, String)> = Vec::new(); for assertion in &fixture.assertions {
330 if let Some(f) = &assertion.field {
331 if !f.is_empty()
332 && string_assertion_types.contains(&assertion.assertion_type.as_str())
333 && !unwrapped_fields.iter().any(|(ff, _)| ff == f)
334 {
335 let is_string_assertion = assertion.value.as_ref().is_none_or(|v| v.is_string());
338 if !is_string_assertion {
339 continue;
340 }
341 if let Some((binding, local_var)) = field_resolver.rust_unwrap_binding(f, result_var) {
342 let _ = writeln!(out, " {binding}");
343 unwrapped_fields.push((f.clone(), local_var));
344 }
345 }
346 }
347 }
348
349 for assertion in &fixture.assertions {
351 if assertion.assertion_type == "not_error" {
352 continue;
354 }
355 render_assertion(
356 out,
357 assertion,
358 result_var,
359 dep_name,
360 false,
361 &unwrapped_fields,
362 field_resolver,
363 );
364 }
365
366 let _ = writeln!(out, "}}");
367}
368
369fn resolve_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
374 let mut current = input;
375 for part in field_path.split('.') {
376 current = current.get(part).unwrap_or(&serde_json::Value::Null);
377 }
378 current
379}
380
381fn render_rust_arg(
382 name: &str,
383 value: &serde_json::Value,
384 arg_type: &str,
385 optional: bool,
386 module: &str,
387 fixture_id: &str,
388) -> (Vec<String>, String) {
389 if arg_type == "mock_url" {
390 let lines = vec![format!(
391 "let {name} = format!(\"{{}}/fixtures/{{}}\", std::env::var(\"MOCK_SERVER_URL\").expect(\"MOCK_SERVER_URL not set\"), \"{fixture_id}\");"
392 )];
393 return (lines, format!("&{name}"));
394 }
395 if arg_type == "handle" {
396 use heck::ToSnakeCase;
400 let constructor_name = format!("create_{}", name.to_snake_case());
401 let mut lines = Vec::new();
402 if value.is_null() || value.is_object() && value.as_object().unwrap().is_empty() {
403 lines.push(format!(
404 "let {name} = {constructor_name}(None).expect(\"handle creation should succeed\");"
405 ));
406 } else {
407 let json_literal = serde_json::to_string(value).unwrap_or_default();
409 let escaped = json_literal.replace('\\', "\\\\").replace('"', "\\\"");
410 lines.push(format!(
411 "let {name}_config: CrawlConfig = serde_json::from_str(\"{escaped}\").expect(\"config should parse\");"
412 ));
413 lines.push(format!(
414 "let {name} = {constructor_name}(Some({name}_config)).expect(\"handle creation should succeed\");"
415 ));
416 }
417 return (lines, format!("&{name}"));
418 }
419 if arg_type == "json_object" {
420 return render_json_object_arg(name, value, optional, module);
421 }
422 if value.is_null() && !optional {
423 let default_val = match arg_type {
425 "string" => "String::new()".to_string(),
426 "int" | "integer" => "0".to_string(),
427 "float" | "number" => "0.0_f64".to_string(),
428 "bool" | "boolean" => "false".to_string(),
429 _ => "Default::default()".to_string(),
430 };
431 let expr = if arg_type == "string" {
433 format!("&{name}")
434 } else {
435 name.to_string()
436 };
437 return (vec![format!("let {name} = {default_val};")], expr);
438 }
439 let literal = json_to_rust_literal(value, arg_type);
440 let pass_by_ref = arg_type == "string";
442 let expr = |n: &str| if pass_by_ref { format!("&{n}") } else { n.to_string() };
443 if optional && value.is_null() {
444 (vec![format!("let {name} = None;")], expr(name))
445 } else if optional {
446 (vec![format!("let {name} = Some({literal});")], expr(name))
447 } else {
448 (vec![format!("let {name} = {literal};")], expr(name))
449 }
450}
451
452fn render_json_object_arg(
456 name: &str,
457 value: &serde_json::Value,
458 optional: bool,
459 _module: &str,
460) -> (Vec<String>, String) {
461 if value.is_null() && optional {
462 return (vec![format!("let {name} = None;")], name.to_string());
463 }
464
465 let normalized = super::normalize_json_keys_to_snake_case(value);
468 let json_literal = json_value_to_macro_literal(&normalized);
470 let mut lines = Vec::new();
471 lines.push(format!("let {name}_json = serde_json::json!({json_literal});"));
472 let deser_expr = format!("serde_json::from_value({name}_json).unwrap()");
474 if optional {
475 lines.push(format!("let {name} = Some({deser_expr});"));
476 } else {
477 lines.push(format!("let {name} = {deser_expr};"));
478 }
479 (lines, name.to_string())
480}
481
482fn json_value_to_macro_literal(value: &serde_json::Value) -> String {
484 match value {
485 serde_json::Value::Null => "null".to_string(),
486 serde_json::Value::Bool(b) => format!("{b}"),
487 serde_json::Value::Number(n) => n.to_string(),
488 serde_json::Value::String(s) => {
489 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
490 format!("\"{escaped}\"")
491 }
492 serde_json::Value::Array(arr) => {
493 let items: Vec<String> = arr.iter().map(json_value_to_macro_literal).collect();
494 format!("[{}]", items.join(", "))
495 }
496 serde_json::Value::Object(obj) => {
497 let entries: Vec<String> = obj
498 .iter()
499 .map(|(k, v)| {
500 let escaped_key = k.replace('\\', "\\\\").replace('"', "\\\"");
501 format!("\"{escaped_key}\": {}", json_value_to_macro_literal(v))
502 })
503 .collect();
504 format!("{{{}}}", entries.join(", "))
505 }
506 }
507}
508
509fn json_to_rust_literal(value: &serde_json::Value, arg_type: &str) -> String {
510 match value {
511 serde_json::Value::Null => "None".to_string(),
512 serde_json::Value::Bool(b) => format!("{b}"),
513 serde_json::Value::Number(n) => {
514 if arg_type.contains("float") || arg_type.contains("f64") || arg_type.contains("f32") {
515 if let Some(f) = n.as_f64() {
516 return format!("{f}_f64");
517 }
518 }
519 n.to_string()
520 }
521 serde_json::Value::String(s) => rust_raw_string(s),
522 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
523 let json_str = serde_json::to_string(value).unwrap_or_default();
524 let literal = rust_raw_string(&json_str);
525 format!("serde_json::from_str({literal}).unwrap()")
526 }
527 }
528}
529
530fn render_assertion(
535 out: &mut String,
536 assertion: &Assertion,
537 result_var: &str,
538 _dep_name: &str,
539 is_error_context: bool,
540 unwrapped_fields: &[(String, String)], field_resolver: &FieldResolver,
542) {
543 if let Some(f) = &assertion.field {
545 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
546 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
547 return;
548 }
549 }
550
551 let field_access = match &assertion.field {
555 Some(f) if !f.is_empty() => {
556 if let Some((_, local_var)) = unwrapped_fields.iter().find(|(ff, _)| ff == f) {
557 local_var.clone()
558 } else {
559 field_resolver.accessor(f, "rust", result_var)
560 }
561 }
562 _ => result_var.to_string(),
563 };
564
565 let is_unwrapped = assertion
567 .field
568 .as_ref()
569 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
570
571 match assertion.assertion_type.as_str() {
572 "error" => {
573 let _ = writeln!(out, " assert!({result_var}.is_err(), \"expected call to fail\");");
574 if let Some(serde_json::Value::String(msg)) = &assertion.value {
575 let escaped = escape_rust(msg);
576 let _ = writeln!(
577 out,
578 " assert!({result_var}.as_ref().unwrap_err().to_string().contains(\"{escaped}\"), \"error message mismatch\");"
579 );
580 }
581 }
582 "not_error" => {
583 }
585 "equals" => {
586 if let Some(val) = &assertion.value {
587 let expected = value_to_rust_string(val);
588 if is_error_context {
589 return;
590 }
591 if val.is_string() {
594 let _ = writeln!(
595 out,
596 " assert_eq!({field_access}.trim(), {expected}, \"equals assertion failed\");"
597 );
598 } else if val.is_boolean() {
599 if val.as_bool() == Some(true) {
601 let _ = writeln!(out, " assert!({field_access}, \"equals assertion failed\");");
602 } else {
603 let _ = writeln!(out, " assert!(!{field_access}, \"equals assertion failed\");");
604 }
605 } else {
606 let is_opt = assertion.field.as_ref().is_some_and(|f| {
608 let resolved = field_resolver.resolve(f);
609 field_resolver.is_optional(resolved)
610 });
611 if is_opt
612 && !unwrapped_fields
613 .iter()
614 .any(|(ff, _)| assertion.field.as_ref() == Some(ff))
615 {
616 let _ = writeln!(
617 out,
618 " assert_eq!({field_access}, Some({expected}), \"equals assertion failed\");"
619 );
620 } else {
621 let _ = writeln!(
622 out,
623 " assert_eq!({field_access}, {expected}, \"equals assertion failed\");"
624 );
625 }
626 }
627 }
628 }
629 "contains" => {
630 if let Some(val) = &assertion.value {
631 let expected = value_to_rust_string(val);
632 let line = format!(
633 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
634 );
635 let _ = writeln!(out, "{line}");
636 }
637 }
638 "contains_all" => {
639 if let Some(values) = &assertion.values {
640 for val in values {
641 let expected = value_to_rust_string(val);
642 let line = format!(
643 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
644 );
645 let _ = writeln!(out, "{line}");
646 }
647 }
648 }
649 "not_contains" => {
650 if let Some(val) = &assertion.value {
651 let expected = value_to_rust_string(val);
652 let line = format!(
653 " assert!(!format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected NOT to contain: {{}}\", {expected});"
654 );
655 let _ = writeln!(out, "{line}");
656 }
657 }
658 "not_empty" => {
659 if let Some(f) = &assertion.field {
660 let resolved = field_resolver.resolve(f);
661 if !is_unwrapped && field_resolver.is_optional(resolved) {
662 let accessor = field_resolver.accessor(f, "rust", result_var);
664 let _ = writeln!(
665 out,
666 " assert!({accessor}.is_some(), \"expected {f} to be present\");"
667 );
668 } else {
669 let _ = writeln!(
670 out,
671 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
672 );
673 }
674 } else {
675 let _ = writeln!(
676 out,
677 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
678 );
679 }
680 }
681 "is_empty" => {
682 if let Some(f) = &assertion.field {
683 let resolved = field_resolver.resolve(f);
684 if !is_unwrapped && field_resolver.is_optional(resolved) {
685 let accessor = field_resolver.accessor(f, "rust", result_var);
686 let _ = writeln!(out, " assert!({accessor}.is_none(), \"expected {f} to be absent\");");
687 } else {
688 let _ = writeln!(out, " assert!({field_access}.is_empty(), \"expected empty value\");");
689 }
690 } else {
691 let _ = writeln!(out, " assert!({field_access}.is_empty(), \"expected empty value\");");
692 }
693 }
694 "contains_any" => {
695 if let Some(values) = &assertion.values {
696 let checks: Vec<String> = values
697 .iter()
698 .map(|v| {
699 let expected = value_to_rust_string(v);
700 format!("{field_access}.contains({expected})")
701 })
702 .collect();
703 let joined = checks.join(" || ");
704 let _ = writeln!(
705 out,
706 " assert!({joined}, \"expected to contain at least one of the specified values\");"
707 );
708 }
709 }
710 "greater_than" => {
711 if let Some(val) = &assertion.value {
712 if val.as_f64().is_some_and(|n| n < 0.0) {
714 let _ = writeln!(
715 out,
716 " // skipped: greater_than with negative value is always true for unsigned types"
717 );
718 } else if val.as_u64() == Some(0) {
719 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
721 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected > 0\");");
722 } else {
723 let lit = numeric_literal(val);
724 let _ = writeln!(out, " assert!({field_access} > {lit}, \"expected > {lit}\");");
725 }
726 }
727 }
728 "less_than" => {
729 if let Some(val) = &assertion.value {
730 let lit = numeric_literal(val);
731 let _ = writeln!(out, " assert!({field_access} < {lit}, \"expected < {lit}\");");
732 }
733 }
734 "greater_than_or_equal" => {
735 if let Some(val) = &assertion.value {
736 if val.as_u64() == Some(1) {
737 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
739 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= 1\");");
740 } else {
741 let lit = numeric_literal(val);
742 let _ = writeln!(out, " assert!({field_access} >= {lit}, \"expected >= {lit}\");");
743 }
744 }
745 }
746 "less_than_or_equal" => {
747 if let Some(val) = &assertion.value {
748 let lit = numeric_literal(val);
749 let _ = writeln!(out, " assert!({field_access} <= {lit}, \"expected <= {lit}\");");
750 }
751 }
752 "starts_with" => {
753 if let Some(val) = &assertion.value {
754 let expected = value_to_rust_string(val);
755 let _ = writeln!(
756 out,
757 " assert!({field_access}.starts_with({expected}), \"expected to start with: {{}}\", {expected});"
758 );
759 }
760 }
761 "ends_with" => {
762 if let Some(val) = &assertion.value {
763 let expected = value_to_rust_string(val);
764 let _ = writeln!(
765 out,
766 " assert!({field_access}.ends_with({expected}), \"expected to end with: {{}}\", {expected});"
767 );
768 }
769 }
770 "min_length" => {
771 if let Some(val) = &assertion.value {
772 if let Some(n) = val.as_u64() {
773 let _ = writeln!(
774 out,
775 " assert!({field_access}.len() >= {n}, \"expected length >= {n}, got {{}}\", {field_access}.len());"
776 );
777 }
778 }
779 }
780 "max_length" => {
781 if let Some(val) = &assertion.value {
782 if let Some(n) = val.as_u64() {
783 let _ = writeln!(
784 out,
785 " assert!({field_access}.len() <= {n}, \"expected length <= {n}, got {{}}\", {field_access}.len());"
786 );
787 }
788 }
789 }
790 "count_min" => {
791 if let Some(val) = &assertion.value {
792 if let Some(n) = val.as_u64() {
793 if n <= 1 {
794 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
796 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= {n}\");");
797 } else {
798 let _ = writeln!(
799 out,
800 " assert!({field_access}.len() >= {n}, \"expected at least {n} elements, got {{}}\", {field_access}.len());"
801 );
802 }
803 }
804 }
805 }
806 other => {
807 let _ = writeln!(out, " // TODO: unsupported assertion type: {other}");
808 }
809 }
810}
811
812fn numeric_literal(value: &serde_json::Value) -> String {
818 if let Some(n) = value.as_f64() {
819 if n.fract() == 0.0 {
820 return format!("{}", n as i64);
823 }
824 return format!("{n}_f64");
825 }
826 value.to_string()
828}
829
830fn value_to_rust_string(value: &serde_json::Value) -> String {
831 match value {
832 serde_json::Value::String(s) => rust_raw_string(s),
833 serde_json::Value::Bool(b) => format!("{b}"),
834 serde_json::Value::Number(n) => n.to_string(),
835 other => {
836 let s = other.to_string();
837 format!("\"{s}\"")
838 }
839 }
840}