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"] }}
180"#
181 )
182}
183
184fn render_test_file(category: &str, fixtures: &[&Fixture], e2e_config: &E2eConfig, dep_name: &str) -> String {
185 let mut out = String::new();
186 let _ = writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.");
187 let _ = writeln!(out, "//! E2e tests for category: {category}");
188 let _ = writeln!(out);
189
190 let module = resolve_module(e2e_config, dep_name);
191 let function_name = resolve_function_name(e2e_config);
192 let field_resolver = FieldResolver::new(
193 &e2e_config.fields,
194 &e2e_config.fields_optional,
195 &e2e_config.result_fields,
196 &e2e_config.fields_array,
197 );
198
199 let _ = writeln!(out, "use {module}::{function_name};");
200
201 let has_handle_args = e2e_config.call.args.iter().any(|a| a.arg_type == "handle");
203 if has_handle_args {
204 let _ = writeln!(out, "use {module}::CrawlConfig;");
205 }
206 for arg in &e2e_config.call.args {
207 if arg.arg_type == "handle" {
208 use heck::ToSnakeCase;
209 let constructor_name = format!("create_{}", arg.name.to_snake_case());
210 let _ = writeln!(out, "use {module}::{constructor_name};");
211 }
212 }
213
214 let _ = writeln!(out);
215
216 for fixture in fixtures {
217 render_test_function(&mut out, fixture, e2e_config, dep_name, &field_resolver);
218 let _ = writeln!(out);
219 }
220
221 if !out.ends_with('\n') {
222 out.push('\n');
223 }
224 out
225}
226
227fn render_test_function(
228 out: &mut String,
229 fixture: &Fixture,
230 e2e_config: &E2eConfig,
231 dep_name: &str,
232 field_resolver: &FieldResolver,
233) {
234 let fn_name = sanitize_ident(&fixture.id);
235 let description = &fixture.description;
236 let function_name = resolve_function_name(e2e_config);
237 let module = resolve_module(e2e_config, dep_name);
238 let result_var = &e2e_config.call.result_var;
239
240 let is_async = e2e_config.call.r#async;
241 if is_async {
242 let _ = writeln!(out, "#[tokio::test]");
243 let _ = writeln!(out, "async fn test_{fn_name}() {{");
244 } else {
245 let _ = writeln!(out, "#[test]");
246 let _ = writeln!(out, "fn test_{fn_name}() {{");
247 }
248 let _ = writeln!(out, " // {description}");
249
250 let has_error_assertion = fixture.assertions.iter().any(|a| a.assertion_type == "error");
252
253 let mut arg_exprs: Vec<String> = Vec::new();
255 for arg in &e2e_config.call.args {
256 let value = resolve_field(&fixture.input, &arg.field);
257 let var_name = &arg.name;
258 let (bindings, expr) = render_rust_arg(var_name, value, &arg.arg_type, arg.optional, &module, &fixture.id);
259 for binding in &bindings {
260 let _ = writeln!(out, " {binding}");
261 }
262 arg_exprs.push(expr);
263 }
264
265 let args_str = arg_exprs.join(", ");
266
267 let await_suffix = if is_async { ".await" } else { "" };
268
269 if has_error_assertion {
270 let _ = writeln!(out, " let {result_var} = {function_name}({args_str}){await_suffix};");
271 for assertion in &fixture.assertions {
273 render_assertion(out, assertion, result_var, dep_name, true, &[], field_resolver);
274 }
275 let _ = writeln!(out, "}}");
276 return;
277 }
278
279 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
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 if has_not_error || !fixture.assertions.is_empty() {
302 let _ = writeln!(
303 out,
304 " let {result_binding} = {function_name}({args_str}){await_suffix}.expect(\"should succeed\");"
305 );
306 } else {
307 let _ = writeln!(
308 out,
309 " let {result_binding} = {function_name}({args_str}){await_suffix};"
310 );
311 }
312
313 let string_assertion_types = [
316 "equals",
317 "contains",
318 "contains_all",
319 "contains_any",
320 "not_contains",
321 "starts_with",
322 "ends_with",
323 "min_length",
324 "max_length",
325 "matches_regex",
326 ];
327 let mut unwrapped_fields: Vec<(String, String)> = Vec::new(); for assertion in &fixture.assertions {
329 if let Some(f) = &assertion.field {
330 if !f.is_empty()
331 && string_assertion_types.contains(&assertion.assertion_type.as_str())
332 && !unwrapped_fields.iter().any(|(ff, _)| ff == f)
333 {
334 let is_string_assertion = assertion.value.as_ref().is_none_or(|v| v.is_string());
337 if !is_string_assertion {
338 continue;
339 }
340 if let Some((binding, local_var)) = field_resolver.rust_unwrap_binding(f, result_var) {
341 let _ = writeln!(out, " {binding}");
342 unwrapped_fields.push((f.clone(), local_var));
343 }
344 }
345 }
346 }
347
348 for assertion in &fixture.assertions {
350 if assertion.assertion_type == "not_error" {
351 continue;
353 }
354 render_assertion(
355 out,
356 assertion,
357 result_var,
358 dep_name,
359 false,
360 &unwrapped_fields,
361 field_resolver,
362 );
363 }
364
365 let _ = writeln!(out, "}}");
366}
367
368fn resolve_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
373 let mut current = input;
374 for part in field_path.split('.') {
375 current = current.get(part).unwrap_or(&serde_json::Value::Null);
376 }
377 current
378}
379
380fn render_rust_arg(
381 name: &str,
382 value: &serde_json::Value,
383 arg_type: &str,
384 optional: bool,
385 module: &str,
386 fixture_id: &str,
387) -> (Vec<String>, String) {
388 if arg_type == "mock_url" {
389 let lines = vec![format!(
390 "let {name} = format!(\"{{}}/fixtures/{{}}\", std::env::var(\"MOCK_SERVER_URL\").expect(\"MOCK_SERVER_URL not set\"), \"{fixture_id}\");"
391 )];
392 return (lines, format!("&{name}"));
393 }
394 if arg_type == "handle" {
395 use heck::ToSnakeCase;
399 let constructor_name = format!("create_{}", name.to_snake_case());
400 let mut lines = Vec::new();
401 if value.is_null() || value.is_object() && value.as_object().unwrap().is_empty() {
402 lines.push(format!(
403 "let {name} = {constructor_name}(None).expect(\"handle creation should succeed\");"
404 ));
405 } else {
406 let json_literal = serde_json::to_string(value).unwrap_or_default();
408 let escaped = json_literal.replace('\\', "\\\\").replace('"', "\\\"");
409 lines.push(format!(
410 "let {name}_config: CrawlConfig = serde_json::from_str(\"{escaped}\").expect(\"config should parse\");"
411 ));
412 lines.push(format!(
413 "let {name} = {constructor_name}(Some({name}_config)).expect(\"handle creation should succeed\");"
414 ));
415 }
416 return (lines, format!("&{name}"));
417 }
418 if arg_type == "json_object" {
419 return render_json_object_arg(name, value, optional, module);
420 }
421 if value.is_null() && !optional {
422 let default_val = match arg_type {
424 "string" => "String::new()".to_string(),
425 "int" | "integer" => "0".to_string(),
426 "float" | "number" => "0.0_f64".to_string(),
427 "bool" | "boolean" => "false".to_string(),
428 _ => "Default::default()".to_string(),
429 };
430 let expr = if arg_type == "string" {
432 format!("&{name}")
433 } else {
434 name.to_string()
435 };
436 return (vec![format!("let {name} = {default_val};")], expr);
437 }
438 let literal = json_to_rust_literal(value, arg_type);
439 let pass_by_ref = arg_type == "string";
441 let expr = |n: &str| if pass_by_ref { format!("&{n}") } else { n.to_string() };
442 if optional && value.is_null() {
443 (vec![format!("let {name} = None;")], expr(name))
444 } else if optional {
445 (vec![format!("let {name} = Some({literal});")], expr(name))
446 } else {
447 (vec![format!("let {name} = {literal};")], expr(name))
448 }
449}
450
451fn render_json_object_arg(
455 name: &str,
456 value: &serde_json::Value,
457 optional: bool,
458 _module: &str,
459) -> (Vec<String>, String) {
460 if value.is_null() && optional {
461 return (vec![format!("let {name} = None;")], name.to_string());
462 }
463
464 let normalized = super::normalize_json_keys_to_snake_case(value);
467 let json_literal = json_value_to_macro_literal(&normalized);
469 let mut lines = Vec::new();
470 lines.push(format!("let {name}_json = serde_json::json!({json_literal});"));
471 let deser_expr = format!("serde_json::from_value({name}_json).unwrap()");
473 if optional {
474 lines.push(format!("let {name} = Some({deser_expr});"));
475 } else {
476 lines.push(format!("let {name} = {deser_expr};"));
477 }
478 (lines, name.to_string())
479}
480
481fn json_value_to_macro_literal(value: &serde_json::Value) -> String {
483 match value {
484 serde_json::Value::Null => "null".to_string(),
485 serde_json::Value::Bool(b) => format!("{b}"),
486 serde_json::Value::Number(n) => n.to_string(),
487 serde_json::Value::String(s) => {
488 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
489 format!("\"{escaped}\"")
490 }
491 serde_json::Value::Array(arr) => {
492 let items: Vec<String> = arr.iter().map(json_value_to_macro_literal).collect();
493 format!("[{}]", items.join(", "))
494 }
495 serde_json::Value::Object(obj) => {
496 let entries: Vec<String> = obj
497 .iter()
498 .map(|(k, v)| {
499 let escaped_key = k.replace('\\', "\\\\").replace('"', "\\\"");
500 format!("\"{escaped_key}\": {}", json_value_to_macro_literal(v))
501 })
502 .collect();
503 format!("{{{}}}", entries.join(", "))
504 }
505 }
506}
507
508fn json_to_rust_literal(value: &serde_json::Value, arg_type: &str) -> String {
509 match value {
510 serde_json::Value::Null => "None".to_string(),
511 serde_json::Value::Bool(b) => format!("{b}"),
512 serde_json::Value::Number(n) => {
513 if arg_type.contains("float") || arg_type.contains("f64") || arg_type.contains("f32") {
514 if let Some(f) = n.as_f64() {
515 return format!("{f}_f64");
516 }
517 }
518 n.to_string()
519 }
520 serde_json::Value::String(s) => rust_raw_string(s),
521 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
522 let json_str = serde_json::to_string(value).unwrap_or_default();
523 let literal = rust_raw_string(&json_str);
524 format!("serde_json::from_str({literal}).unwrap()")
525 }
526 }
527}
528
529fn render_assertion(
534 out: &mut String,
535 assertion: &Assertion,
536 result_var: &str,
537 _dep_name: &str,
538 is_error_context: bool,
539 unwrapped_fields: &[(String, String)], field_resolver: &FieldResolver,
541) {
542 if let Some(f) = &assertion.field {
544 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
545 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
546 return;
547 }
548 }
549
550 let field_access = match &assertion.field {
554 Some(f) if !f.is_empty() => {
555 if let Some((_, local_var)) = unwrapped_fields.iter().find(|(ff, _)| ff == f) {
556 local_var.clone()
557 } else {
558 field_resolver.accessor(f, "rust", result_var)
559 }
560 }
561 _ => result_var.to_string(),
562 };
563
564 let is_unwrapped = assertion
566 .field
567 .as_ref()
568 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
569
570 match assertion.assertion_type.as_str() {
571 "error" => {
572 let _ = writeln!(out, " assert!({result_var}.is_err(), \"expected call to fail\");");
573 if let Some(serde_json::Value::String(msg)) = &assertion.value {
574 let escaped = escape_rust(msg);
575 let _ = writeln!(
576 out,
577 " assert!({result_var}.as_ref().unwrap_err().to_string().contains(\"{escaped}\"), \"error message mismatch\");"
578 );
579 }
580 }
581 "not_error" => {
582 }
584 "equals" => {
585 if let Some(val) = &assertion.value {
586 let expected = value_to_rust_string(val);
587 if is_error_context {
588 return;
589 }
590 if val.is_string() {
593 let _ = writeln!(
594 out,
595 " assert_eq!({field_access}.trim(), {expected}, \"equals assertion failed\");"
596 );
597 } else if val.is_boolean() {
598 if val.as_bool() == Some(true) {
600 let _ = writeln!(out, " assert!({field_access}, \"equals assertion failed\");");
601 } else {
602 let _ = writeln!(out, " assert!(!{field_access}, \"equals assertion failed\");");
603 }
604 } else {
605 let is_opt = assertion.field.as_ref().is_some_and(|f| {
607 let resolved = field_resolver.resolve(f);
608 field_resolver.is_optional(resolved)
609 });
610 if is_opt
611 && !unwrapped_fields
612 .iter()
613 .any(|(ff, _)| assertion.field.as_ref() == Some(ff))
614 {
615 let _ = writeln!(
616 out,
617 " assert_eq!({field_access}, Some({expected}), \"equals assertion failed\");"
618 );
619 } else {
620 let _ = writeln!(
621 out,
622 " assert_eq!({field_access}, {expected}, \"equals assertion failed\");"
623 );
624 }
625 }
626 }
627 }
628 "contains" => {
629 if let Some(val) = &assertion.value {
630 let expected = value_to_rust_string(val);
631 let line = format!(
632 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
633 );
634 let _ = writeln!(out, "{line}");
635 }
636 }
637 "contains_all" => {
638 if let Some(values) = &assertion.values {
639 for val in values {
640 let expected = value_to_rust_string(val);
641 let line = format!(
642 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
643 );
644 let _ = writeln!(out, "{line}");
645 }
646 }
647 }
648 "not_contains" => {
649 if let Some(val) = &assertion.value {
650 let expected = value_to_rust_string(val);
651 let line = format!(
652 " assert!(!format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected NOT to contain: {{}}\", {expected});"
653 );
654 let _ = writeln!(out, "{line}");
655 }
656 }
657 "not_empty" => {
658 if let Some(f) = &assertion.field {
659 let resolved = field_resolver.resolve(f);
660 if !is_unwrapped && field_resolver.is_optional(resolved) {
661 let accessor = field_resolver.accessor(f, "rust", result_var);
663 let _ = writeln!(
664 out,
665 " assert!({accessor}.is_some(), \"expected {f} to be present\");"
666 );
667 } else {
668 let _ = writeln!(
669 out,
670 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
671 );
672 }
673 } else {
674 let _ = writeln!(
675 out,
676 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
677 );
678 }
679 }
680 "is_empty" => {
681 if let Some(f) = &assertion.field {
682 let resolved = field_resolver.resolve(f);
683 if !is_unwrapped && field_resolver.is_optional(resolved) {
684 let accessor = field_resolver.accessor(f, "rust", result_var);
685 let _ = writeln!(out, " assert!({accessor}.is_none(), \"expected {f} to be absent\");");
686 } else {
687 let _ = writeln!(out, " assert!({field_access}.is_empty(), \"expected empty value\");");
688 }
689 } else {
690 let _ = writeln!(out, " assert!({field_access}.is_empty(), \"expected empty value\");");
691 }
692 }
693 "contains_any" => {
694 if let Some(values) = &assertion.values {
695 let checks: Vec<String> = values
696 .iter()
697 .map(|v| {
698 let expected = value_to_rust_string(v);
699 format!("{field_access}.contains({expected})")
700 })
701 .collect();
702 let joined = checks.join(" || ");
703 let _ = writeln!(
704 out,
705 " assert!({joined}, \"expected to contain at least one of the specified values\");"
706 );
707 }
708 }
709 "greater_than" => {
710 if let Some(val) = &assertion.value {
711 if val.as_f64().is_some_and(|n| n < 0.0) {
713 let _ = writeln!(
714 out,
715 " // skipped: greater_than with negative value is always true for unsigned types"
716 );
717 } else if val.as_u64() == Some(0) {
718 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
720 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected > 0\");");
721 } else {
722 let lit = numeric_literal(val);
723 let _ = writeln!(out, " assert!({field_access} > {lit}, \"expected > {lit}\");");
724 }
725 }
726 }
727 "less_than" => {
728 if let Some(val) = &assertion.value {
729 let lit = numeric_literal(val);
730 let _ = writeln!(out, " assert!({field_access} < {lit}, \"expected < {lit}\");");
731 }
732 }
733 "greater_than_or_equal" => {
734 if let Some(val) = &assertion.value {
735 if val.as_u64() == Some(1) {
736 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
738 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= 1\");");
739 } else {
740 let lit = numeric_literal(val);
741 let _ = writeln!(out, " assert!({field_access} >= {lit}, \"expected >= {lit}\");");
742 }
743 }
744 }
745 "less_than_or_equal" => {
746 if let Some(val) = &assertion.value {
747 let lit = numeric_literal(val);
748 let _ = writeln!(out, " assert!({field_access} <= {lit}, \"expected <= {lit}\");");
749 }
750 }
751 "starts_with" => {
752 if let Some(val) = &assertion.value {
753 let expected = value_to_rust_string(val);
754 let _ = writeln!(
755 out,
756 " assert!({field_access}.starts_with({expected}), \"expected to start with: {{}}\", {expected});"
757 );
758 }
759 }
760 "ends_with" => {
761 if let Some(val) = &assertion.value {
762 let expected = value_to_rust_string(val);
763 let _ = writeln!(
764 out,
765 " assert!({field_access}.ends_with({expected}), \"expected to end with: {{}}\", {expected});"
766 );
767 }
768 }
769 "min_length" => {
770 if let Some(val) = &assertion.value {
771 if let Some(n) = val.as_u64() {
772 let _ = writeln!(
773 out,
774 " assert!({field_access}.len() >= {n}, \"expected length >= {n}, got {{}}\", {field_access}.len());"
775 );
776 }
777 }
778 }
779 "max_length" => {
780 if let Some(val) = &assertion.value {
781 if let Some(n) = val.as_u64() {
782 let _ = writeln!(
783 out,
784 " assert!({field_access}.len() <= {n}, \"expected length <= {n}, got {{}}\", {field_access}.len());"
785 );
786 }
787 }
788 }
789 "count_min" => {
790 if let Some(val) = &assertion.value {
791 if let Some(n) = val.as_u64() {
792 if n <= 1 {
793 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
795 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= {n}\");");
796 } else {
797 let _ = writeln!(
798 out,
799 " assert!({field_access}.len() >= {n}, \"expected at least {n} elements, got {{}}\", {field_access}.len());"
800 );
801 }
802 }
803 }
804 }
805 other => {
806 let _ = writeln!(out, " // TODO: unsupported assertion type: {other}");
807 }
808 }
809}
810
811fn numeric_literal(value: &serde_json::Value) -> String {
817 if let Some(n) = value.as_f64() {
818 if n.fract() == 0.0 {
819 return format!("{}", n as i64);
822 }
823 return format!("{n}_f64");
824 }
825 value.to_string()
827}
828
829fn value_to_rust_string(value: &serde_json::Value) -> String {
830 match value {
831 serde_json::Value::String(s) => rust_raw_string(s),
832 serde_json::Value::Bool(b) => format!("{b}"),
833 serde_json::Value::Number(n) => n.to_string(),
834 other => {
835 let s = other.to_string();
836 format!("\"{s}\"")
837 }
838 }
839}