1use crate::codegen::resolve_field;
7use crate::config::E2eConfig;
8use crate::escape::{escape_rust, rust_raw_string, sanitize_filename, sanitize_ident};
9use crate::field_access::FieldResolver;
10use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
11use alef_core::backend::GeneratedFile;
12use alef_core::config::AlefConfig;
13use alef_core::hash::{self, CommentStyle};
14use alef_core::template_versions as tv;
15use anyhow::Result;
16use std::fmt::Write as FmtWrite;
17use std::path::PathBuf;
18
19pub struct RustE2eCodegen;
21
22impl super::E2eCodegen for RustE2eCodegen {
23 fn generate(
24 &self,
25 groups: &[FixtureGroup],
26 e2e_config: &E2eConfig,
27 alef_config: &AlefConfig,
28 ) -> Result<Vec<GeneratedFile>> {
29 let mut files = Vec::new();
30 let output_base = PathBuf::from(e2e_config.effective_output()).join("rust");
31
32 let crate_name = resolve_crate_name(e2e_config, alef_config);
34 let crate_path = resolve_crate_path(e2e_config, &crate_name);
35 let dep_name = crate_name.replace('-', "_");
36
37 let all_call_configs = std::iter::once(&e2e_config.call).chain(e2e_config.calls.values());
40 let needs_serde_json = all_call_configs
41 .flat_map(|c| c.args.iter())
42 .any(|a| a.arg_type == "json_object" || a.arg_type == "handle");
43
44 let needs_mock_server = groups
47 .iter()
48 .flat_map(|g| g.fixtures.iter())
49 .any(|f| !is_skipped(f, "rust") && f.mock_response.is_some());
50
51 let needs_http_tests = groups
53 .iter()
54 .flat_map(|g| g.fixtures.iter())
55 .any(|f| !is_skipped(f, "rust") && f.http.is_some());
56
57 let any_async_call = std::iter::once(&e2e_config.call)
59 .chain(e2e_config.calls.values())
60 .any(|c| c.r#async);
61 let needs_tokio = needs_mock_server || needs_http_tests || any_async_call;
62
63 let crate_version = resolve_crate_version(e2e_config);
64 files.push(GeneratedFile {
65 path: output_base.join("Cargo.toml"),
66 content: render_cargo_toml(
67 &crate_name,
68 &dep_name,
69 &crate_path,
70 needs_serde_json,
71 needs_mock_server,
72 needs_http_tests,
73 needs_tokio,
74 e2e_config.dep_mode,
75 crate_version.as_deref(),
76 &alef_config.crate_config.features,
77 ),
78 generated_header: true,
79 });
80
81 if needs_mock_server {
83 files.push(GeneratedFile {
84 path: output_base.join("tests").join("mock_server.rs"),
85 content: render_mock_server_module(),
86 generated_header: true,
87 });
88 }
89 if needs_mock_server || needs_http_tests {
92 files.push(GeneratedFile {
93 path: output_base.join("src").join("main.rs"),
94 content: render_mock_server_binary(),
95 generated_header: true,
96 });
97 }
98
99 for group in groups {
101 let fixtures: Vec<&Fixture> = group.fixtures.iter().filter(|f| !is_skipped(f, "rust")).collect();
102
103 if fixtures.is_empty() {
104 continue;
105 }
106
107 let filename = format!("{}_test.rs", sanitize_filename(&group.category));
108 let content = render_test_file(&group.category, &fixtures, e2e_config, &dep_name, needs_mock_server);
109
110 files.push(GeneratedFile {
111 path: output_base.join("tests").join(filename),
112 content,
113 generated_header: true,
114 });
115 }
116
117 Ok(files)
118 }
119
120 fn language_name(&self) -> &'static str {
121 "rust"
122 }
123}
124
125fn resolve_crate_name(_e2e_config: &E2eConfig, alef_config: &AlefConfig) -> String {
130 alef_config.crate_config.name.clone()
134}
135
136fn resolve_crate_path(e2e_config: &E2eConfig, crate_name: &str) -> String {
137 e2e_config
138 .resolve_package("rust")
139 .and_then(|p| p.path.clone())
140 .unwrap_or_else(|| format!("../../crates/{crate_name}"))
141}
142
143fn resolve_crate_version(e2e_config: &E2eConfig) -> Option<String> {
144 e2e_config.resolve_package("rust").and_then(|p| p.version.clone())
145}
146
147fn resolve_function_name_for_call(call_config: &crate::config::CallConfig) -> String {
148 call_config
149 .overrides
150 .get("rust")
151 .and_then(|o| o.function.clone())
152 .unwrap_or_else(|| call_config.function.clone())
153}
154
155fn resolve_module(e2e_config: &E2eConfig, dep_name: &str) -> String {
156 resolve_module_for_call(&e2e_config.call, dep_name)
157}
158
159fn resolve_module_for_call(call_config: &crate::config::CallConfig, dep_name: &str) -> String {
160 let overrides = call_config.overrides.get("rust");
163 overrides
164 .and_then(|o| o.crate_name.clone())
165 .or_else(|| overrides.and_then(|o| o.module.clone()))
166 .unwrap_or_else(|| dep_name.to_string())
167}
168
169fn is_skipped(fixture: &Fixture, language: &str) -> bool {
170 fixture.skip.as_ref().is_some_and(|s| s.should_skip(language))
171}
172
173#[allow(clippy::too_many_arguments)]
178pub fn render_cargo_toml(
179 crate_name: &str,
180 dep_name: &str,
181 crate_path: &str,
182 needs_serde_json: bool,
183 needs_mock_server: bool,
184 needs_http_tests: bool,
185 needs_tokio: bool,
186 dep_mode: crate::config::DependencyMode,
187 version: Option<&str>,
188 features: &[String],
189) -> String {
190 let e2e_name = format!("{dep_name}-e2e-rust");
191 let effective_features: Vec<&str> = features.iter().map(|s| s.as_str()).collect();
195 let features_str = if effective_features.is_empty() {
196 String::new()
197 } else {
198 format!(", default-features = false, features = {:?}", effective_features)
199 };
200 let dep_spec = match dep_mode {
201 crate::config::DependencyMode::Registry => {
202 let ver = version.unwrap_or("0.1.0");
203 if crate_name != dep_name {
204 format!("{dep_name} = {{ package = \"{crate_name}\", version = \"{ver}\"{features_str} }}")
205 } else if effective_features.is_empty() {
206 format!("{dep_name} = \"{ver}\"")
207 } else {
208 format!("{dep_name} = {{ version = \"{ver}\"{features_str} }}")
209 }
210 }
211 crate::config::DependencyMode::Local => {
212 if crate_name != dep_name {
213 format!("{dep_name} = {{ package = \"{crate_name}\", path = \"{crate_path}\"{features_str} }}")
214 } else if effective_features.is_empty() {
215 format!("{dep_name} = {{ path = \"{crate_path}\" }}")
216 } else {
217 format!("{dep_name} = {{ path = \"{crate_path}\"{features_str} }}")
218 }
219 }
220 };
221 let effective_needs_serde_json = needs_serde_json || needs_mock_server || needs_http_tests;
225 let serde_line = if effective_needs_serde_json {
226 "\nserde_json = \"1\""
227 } else {
228 ""
229 };
230 let needs_axum = needs_mock_server || needs_http_tests;
239 let mock_lines = if needs_axum {
240 let mut lines = format!(
241 "\naxum = \"{axum}\"\nserde = {{ version = \"1\", features = [\"derive\"] }}\nwalkdir = \"{walkdir}\"",
242 axum = tv::cargo::AXUM,
243 walkdir = tv::cargo::WALKDIR,
244 );
245 if needs_mock_server {
246 lines.push_str(&format!(
247 "\ntokio-stream = \"{tokio_stream}\"",
248 tokio_stream = tv::cargo::TOKIO_STREAM
249 ));
250 }
251 if needs_http_tests {
252 lines.push_str("\naxum-test = \"20\"\nbytes = \"1\"");
253 }
254 lines
255 } else {
256 String::new()
257 };
258 let mut machete_ignored: Vec<&str> = Vec::new();
259 if effective_needs_serde_json {
260 machete_ignored.push("\"serde_json\"");
261 }
262 if needs_axum {
263 machete_ignored.push("\"axum\"");
264 machete_ignored.push("\"serde\"");
265 machete_ignored.push("\"walkdir\"");
266 }
267 if needs_mock_server {
268 machete_ignored.push("\"tokio-stream\"");
269 }
270 if needs_http_tests {
271 machete_ignored.push("\"axum-test\"");
272 machete_ignored.push("\"bytes\"");
273 }
274 let machete_section = if machete_ignored.is_empty() {
275 String::new()
276 } else {
277 format!(
278 "\n[package.metadata.cargo-machete]\nignored = [{}]\n",
279 machete_ignored.join(", ")
280 )
281 };
282 let tokio_line = if needs_tokio {
283 "\ntokio = { version = \"1\", features = [\"full\"] }"
284 } else {
285 ""
286 };
287 let bin_section = if needs_mock_server || needs_http_tests {
288 "\n[[bin]]\nname = \"mock-server\"\npath = \"src/main.rs\"\n"
289 } else {
290 ""
291 };
292 let header = hash::header(CommentStyle::Hash);
293 format!(
294 r#"{header}
295[workspace]
296
297[package]
298name = "{e2e_name}"
299version = "0.1.0"
300edition = "2021"
301license = "MIT"
302publish = false
303{bin_section}
304[dependencies]
305{dep_spec}{serde_line}{mock_lines}{tokio_line}
306{machete_section}"#
307 )
308}
309
310fn render_test_file(
311 category: &str,
312 fixtures: &[&Fixture],
313 e2e_config: &E2eConfig,
314 dep_name: &str,
315 needs_mock_server: bool,
316) -> String {
317 let mut out = String::new();
318 out.push_str(&hash::header(CommentStyle::DoubleSlash));
319 let _ = writeln!(out, "//! E2e tests for category: {category}");
320 let _ = writeln!(out);
321
322 let module = resolve_module(e2e_config, dep_name);
323 let field_resolver = FieldResolver::new(
324 &e2e_config.fields,
325 &e2e_config.fields_optional,
326 &e2e_config.result_fields,
327 &e2e_config.fields_array,
328 );
329
330 let file_has_http = fixtures.iter().any(|f| f.http.is_some());
332 let file_has_call_based = fixtures.iter().any(|f| f.mock_response.is_some());
335
336 if file_has_call_based {
339 let mut imported: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
340 for fixture in fixtures.iter().filter(|f| f.mock_response.is_some()) {
341 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
342 let fn_name = resolve_function_name_for_call(call_config);
343 let mod_name = resolve_module_for_call(call_config, dep_name);
344 imported.insert((mod_name, fn_name));
345 }
346 let mut by_module: std::collections::BTreeMap<String, Vec<String>> = std::collections::BTreeMap::new();
348 for (mod_name, fn_name) in &imported {
349 by_module.entry(mod_name.clone()).or_default().push(fn_name.clone());
350 }
351 for (mod_name, fns) in &by_module {
352 if fns.len() == 1 {
353 let _ = writeln!(out, "use {mod_name}::{};", fns[0]);
354 } else {
355 let joined = fns.join(", ");
356 let _ = writeln!(out, "use {mod_name}::{{{joined}}};");
357 }
358 }
359 }
360
361 if file_has_http {
363 let _ = writeln!(out, "use {module}::{{App, RequestContext}};");
364 }
365
366 let has_handle_args = e2e_config.call.args.iter().any(|a| a.arg_type == "handle");
368 if has_handle_args {
369 let _ = writeln!(out, "use {module}::CrawlConfig;");
370 }
371 for arg in &e2e_config.call.args {
372 if arg.arg_type == "handle" {
373 use heck::ToSnakeCase;
374 let constructor_name = format!("create_{}", arg.name.to_snake_case());
375 let _ = writeln!(out, "use {module}::{constructor_name};");
376 }
377 }
378
379 let file_needs_mock = needs_mock_server && fixtures.iter().any(|f| f.mock_response.is_some());
381 if file_needs_mock {
382 let _ = writeln!(out, "mod mock_server;");
383 let _ = writeln!(out, "use mock_server::{{MockRoute, MockServer}};");
384 }
385
386 let file_needs_visitor = fixtures.iter().any(|f| f.visitor.is_some());
390 if file_needs_visitor {
391 let visitor_trait = resolve_visitor_trait(&module);
392 let _ = writeln!(out, "use {module}::{{{visitor_trait}, NodeContext, VisitResult}};");
393 }
394
395 let _ = writeln!(out);
396
397 for fixture in fixtures {
398 render_test_function(&mut out, fixture, e2e_config, dep_name, &field_resolver);
399 let _ = writeln!(out);
400 }
401
402 if !out.ends_with('\n') {
403 out.push('\n');
404 }
405 out
406}
407
408fn render_test_function(
409 out: &mut String,
410 fixture: &Fixture,
411 e2e_config: &E2eConfig,
412 dep_name: &str,
413 field_resolver: &FieldResolver,
414) {
415 if fixture.http.is_some() {
417 render_http_test_function(out, fixture, dep_name);
418 return;
419 }
420
421 if fixture.http.is_none() && fixture.mock_response.is_none() {
426 let fn_name = sanitize_ident(&fixture.id);
427 let description = &fixture.description;
428 let _ = writeln!(out, "#[tokio::test]");
429 let _ = writeln!(out, "async fn test_{fn_name}() {{");
430 let _ = writeln!(out, " // {description}");
431 let _ = writeln!(
432 out,
433 " // TODO: implement when a callable API is available for this fixture type."
434 );
435 let _ = writeln!(out, "}}");
436 return;
437 }
438
439 let fn_name = sanitize_ident(&fixture.id);
440 let description = &fixture.description;
441 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
442 let function_name = resolve_function_name_for_call(call_config);
443 let module = resolve_module_for_call(call_config, dep_name);
444 let result_var = &call_config.result_var;
445 let has_mock = fixture.mock_response.is_some();
446
447 let is_async = call_config.r#async || has_mock;
449 if is_async {
450 let _ = writeln!(out, "#[tokio::test]");
451 let _ = writeln!(out, "async fn test_{fn_name}() {{");
452 } else {
453 let _ = writeln!(out, "#[test]");
454 let _ = writeln!(out, "fn test_{fn_name}() {{");
455 }
456 let _ = writeln!(out, " // {description}");
457
458 if has_mock {
461 render_mock_server_setup(out, fixture, e2e_config);
462 }
463
464 let has_error_assertion = fixture.assertions.iter().any(|a| a.assertion_type == "error");
466
467 let rust_overrides = call_config.overrides.get("rust");
469 let wrap_options_in_some = rust_overrides.is_some_and(|o| o.wrap_options_in_some);
470 let extra_args: Vec<String> = rust_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
471
472 let mut arg_exprs: Vec<String> = Vec::new();
474 for arg in &call_config.args {
475 let value = resolve_field(&fixture.input, &arg.field);
476 let var_name = &arg.name;
477 let (bindings, expr) = render_rust_arg(
478 var_name,
479 value,
480 &arg.arg_type,
481 arg.optional,
482 &module,
483 &fixture.id,
484 if has_mock {
485 Some("mock_server.url.as_str()")
486 } else {
487 None
488 },
489 arg.owned,
490 arg.element_type.as_deref(),
491 );
492 for binding in &bindings {
493 let _ = writeln!(out, " {binding}");
494 }
495 let final_expr = if wrap_options_in_some && arg.arg_type == "json_object" {
499 if let Some(rest) = expr.strip_prefix('&') {
500 format!("Some({rest}.clone())")
501 } else {
502 format!("Some({expr})")
503 }
504 } else {
505 expr
506 };
507 arg_exprs.push(final_expr);
508 }
509
510 if let Some(visitor_spec) = &fixture.visitor {
512 let _ = writeln!(out, " struct _TestVisitor;");
513 let _ = writeln!(out, " impl {} for _TestVisitor {{", resolve_visitor_trait(&module));
514 for (method_name, action) in &visitor_spec.callbacks {
515 emit_rust_visitor_method(out, method_name, action);
516 }
517 let _ = writeln!(out, " }}");
518 let _ = writeln!(
519 out,
520 " let visitor = std::rc::Rc::new(std::cell::RefCell::new(_TestVisitor));"
521 );
522 arg_exprs.push("Some(visitor)".to_string());
523 } else {
524 arg_exprs.extend(extra_args);
527 }
528
529 let args_str = arg_exprs.join(", ");
530
531 let await_suffix = if is_async { ".await" } else { "" };
532
533 let result_is_tree = call_config.result_var == "tree";
534 let result_is_simple = rust_overrides.is_some_and(|o| o.result_is_simple);
537 let result_is_vec = rust_overrides.is_some_and(|o| o.result_is_vec);
540 let result_is_option = rust_overrides.is_some_and(|o| o.result_is_option);
543
544 if has_error_assertion {
545 let _ = writeln!(out, " let {result_var} = {function_name}({args_str}){await_suffix};");
546 for assertion in &fixture.assertions {
548 render_assertion(
549 out,
550 assertion,
551 result_var,
552 &module,
553 dep_name,
554 true,
555 &[],
556 field_resolver,
557 result_is_tree,
558 result_is_simple,
559 false,
560 false,
561 );
562 }
563 let _ = writeln!(out, "}}");
564 return;
565 }
566
567 let has_not_error = fixture.assertions.iter().any(|a| a.assertion_type == "not_error");
569
570 let has_usable_assertion = fixture.assertions.iter().any(|a| {
574 if a.assertion_type == "not_error" || a.assertion_type == "error" {
575 return false;
576 }
577 if a.assertion_type == "method_result" {
578 let supported_checks = [
581 "equals",
582 "is_true",
583 "is_false",
584 "greater_than_or_equal",
585 "count_min",
586 "is_error",
587 "contains",
588 "not_empty",
589 "is_empty",
590 ];
591 let check = a.check.as_deref().unwrap_or("is_true");
592 if a.method.is_none() || !supported_checks.contains(&check) {
593 return false;
594 }
595 }
596 match &a.field {
597 Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
598 _ => true,
599 }
600 });
601
602 let result_binding = if has_usable_assertion {
603 result_var.to_string()
604 } else {
605 "_".to_string()
606 };
607
608 let has_field_access = fixture
612 .assertions
613 .iter()
614 .any(|a| a.field.as_ref().is_some_and(|f| !f.is_empty()));
615 let only_emptiness_checks = !has_field_access
616 && fixture.assertions.iter().all(|a| {
617 matches!(
618 a.assertion_type.as_str(),
619 "is_empty" | "is_false" | "not_empty" | "is_true" | "not_error"
620 )
621 });
622
623 let returns_result = rust_overrides
626 .and_then(|o| o.returns_result)
627 .unwrap_or(call_config.returns_result);
628
629 let unwrap_suffix = if returns_result {
630 ".expect(\"should succeed\")"
631 } else {
632 ""
633 };
634 if only_emptiness_checks || !returns_result {
635 let _ = writeln!(
637 out,
638 " let {result_binding} = {function_name}({args_str}){await_suffix};"
639 );
640 } else if has_not_error || !fixture.assertions.is_empty() {
641 let _ = writeln!(
642 out,
643 " let {result_binding} = {function_name}({args_str}){await_suffix}{unwrap_suffix};"
644 );
645 } else {
646 let _ = writeln!(
647 out,
648 " let {result_binding} = {function_name}({args_str}){await_suffix};"
649 );
650 }
651
652 let string_assertion_types = [
658 "equals",
659 "contains",
660 "contains_all",
661 "contains_any",
662 "not_contains",
663 "starts_with",
664 "ends_with",
665 "min_length",
666 "max_length",
667 "matches_regex",
668 ];
669 let mut unwrapped_fields: Vec<(String, String)> = Vec::new(); if !result_is_vec {
671 for assertion in &fixture.assertions {
672 if let Some(f) = &assertion.field {
673 if !f.is_empty()
674 && string_assertion_types.contains(&assertion.assertion_type.as_str())
675 && !unwrapped_fields.iter().any(|(ff, _)| ff == f)
676 {
677 let is_string_assertion = assertion.value.as_ref().is_none_or(|v| v.is_string());
680 if !is_string_assertion {
681 continue;
682 }
683 if let Some((binding, local_var)) = field_resolver.rust_unwrap_binding(f, result_var) {
684 let _ = writeln!(out, " {binding}");
685 unwrapped_fields.push((f.clone(), local_var));
686 }
687 }
688 }
689 }
690 }
691
692 for assertion in &fixture.assertions {
694 if assertion.assertion_type == "not_error" {
695 continue;
697 }
698 render_assertion(
699 out,
700 assertion,
701 result_var,
702 &module,
703 dep_name,
704 false,
705 &unwrapped_fields,
706 field_resolver,
707 result_is_tree,
708 result_is_simple,
709 result_is_vec,
710 result_is_option,
711 );
712 }
713
714 let _ = writeln!(out, "}}");
715}
716
717#[allow(clippy::too_many_arguments)]
722fn render_rust_arg(
723 name: &str,
724 value: &serde_json::Value,
725 arg_type: &str,
726 optional: bool,
727 module: &str,
728 fixture_id: &str,
729 mock_base_url: Option<&str>,
730 owned: bool,
731 element_type: Option<&str>,
732) -> (Vec<String>, String) {
733 if arg_type == "mock_url" {
734 let lines = vec![format!(
735 "let {name} = format!(\"{{}}/fixtures/{{}}\", std::env::var(\"MOCK_SERVER_URL\").expect(\"MOCK_SERVER_URL not set\"), \"{fixture_id}\");"
736 )];
737 return (lines, format!("&{name}"));
738 }
739 if arg_type == "base_url" {
741 if let Some(url_expr) = mock_base_url {
742 return (vec![], url_expr.to_string());
743 }
744 }
746 if arg_type == "handle" {
747 use heck::ToSnakeCase;
751 let constructor_name = format!("create_{}", name.to_snake_case());
752 let mut lines = Vec::new();
753 if value.is_null() || value.is_object() && value.as_object().unwrap().is_empty() {
754 lines.push(format!(
755 "let {name} = {constructor_name}(None).expect(\"handle creation should succeed\");"
756 ));
757 } else {
758 let json_literal = serde_json::to_string(value).unwrap_or_default();
760 let escaped = json_literal.replace('\\', "\\\\").replace('"', "\\\"");
761 lines.push(format!(
762 "let {name}_config: CrawlConfig = serde_json::from_str(\"{escaped}\").expect(\"config should parse\");"
763 ));
764 lines.push(format!(
765 "let {name} = {constructor_name}(Some({name}_config)).expect(\"handle creation should succeed\");"
766 ));
767 }
768 return (lines, format!("&{name}"));
769 }
770 if arg_type == "json_object" {
771 return render_json_object_arg(name, value, optional, owned, element_type, module);
772 }
773 if value.is_null() && !optional {
774 let default_val = match arg_type {
776 "string" => "String::new()".to_string(),
777 "int" | "integer" => "0".to_string(),
778 "float" | "number" => "0.0_f64".to_string(),
779 "bool" | "boolean" => "false".to_string(),
780 _ => "Default::default()".to_string(),
781 };
782 let expr = if arg_type == "string" {
784 format!("&{name}")
785 } else {
786 name.to_string()
787 };
788 return (vec![format!("let {name} = {default_val};")], expr);
789 }
790 let literal = json_to_rust_literal(value, arg_type);
791 let pass_by_ref = arg_type == "bytes";
794 let optional_expr = |n: &str| {
795 if arg_type == "string" {
796 format!("{n}.as_deref()")
797 } else if arg_type == "bytes" {
798 format!("{n}.as_deref().map(|v| v.as_slice())")
799 } else {
800 n.to_string()
804 }
805 };
806 let expr = |n: &str| {
807 if arg_type == "bytes" {
808 format!("{n}.as_bytes()")
809 } else if pass_by_ref {
810 format!("&{n}")
811 } else {
812 n.to_string()
813 }
814 };
815 if optional && value.is_null() {
816 let none_decl = match arg_type {
817 "string" => format!("let {name}: Option<String> = None;"),
818 "bytes" => format!("let {name}: Option<Vec<u8>> = None;"),
819 _ => format!("let {name} = None;"),
820 };
821 (vec![none_decl], optional_expr(name))
822 } else if optional {
823 (vec![format!("let {name} = Some({literal});")], optional_expr(name))
824 } else {
825 (vec![format!("let {name} = {literal};")], expr(name))
826 }
827}
828
829fn render_json_object_arg(
837 name: &str,
838 value: &serde_json::Value,
839 optional: bool,
840 owned: bool,
841 element_type: Option<&str>,
842 _module: &str,
843) -> (Vec<String>, String) {
844 let pass_by_ref = !owned;
846
847 if value.is_null() && optional {
848 let expr = if pass_by_ref {
850 format!("&{name}")
851 } else {
852 name.to_string()
853 };
854 return (vec![format!("let {name} = Default::default();")], expr);
855 }
856
857 let normalized = super::normalize_json_keys_to_snake_case(value);
860 let json_literal = json_value_to_macro_literal(&normalized);
862 let mut lines = Vec::new();
863 lines.push(format!("let {name}_json = serde_json::json!({json_literal});"));
864
865 let deser_expr = if let Some(elem) = element_type {
868 format!("serde_json::from_value::<Vec<{elem}>>({name}_json).unwrap()")
869 } else {
870 format!("serde_json::from_value({name}_json).unwrap()")
871 };
872
873 lines.push(format!("let {name} = {deser_expr};"));
876 let expr = if pass_by_ref {
877 format!("&{name}")
878 } else {
879 name.to_string()
880 };
881 (lines, expr)
882}
883
884fn json_value_to_macro_literal(value: &serde_json::Value) -> String {
886 match value {
887 serde_json::Value::Null => "null".to_string(),
888 serde_json::Value::Bool(b) => format!("{b}"),
889 serde_json::Value::Number(n) => n.to_string(),
890 serde_json::Value::String(s) => {
891 let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
892 format!("\"{escaped}\"")
893 }
894 serde_json::Value::Array(arr) => {
895 let items: Vec<String> = arr.iter().map(json_value_to_macro_literal).collect();
896 format!("[{}]", items.join(", "))
897 }
898 serde_json::Value::Object(obj) => {
899 let entries: Vec<String> = obj
900 .iter()
901 .map(|(k, v)| {
902 let escaped_key = k.replace('\\', "\\\\").replace('"', "\\\"");
903 format!("\"{escaped_key}\": {}", json_value_to_macro_literal(v))
904 })
905 .collect();
906 format!("{{{}}}", entries.join(", "))
907 }
908 }
909}
910
911fn json_to_rust_literal(value: &serde_json::Value, arg_type: &str) -> String {
912 match value {
913 serde_json::Value::Null => "None".to_string(),
914 serde_json::Value::Bool(b) => format!("{b}"),
915 serde_json::Value::Number(n) => {
916 if arg_type.contains("float") || arg_type.contains("f64") || arg_type.contains("f32") {
917 if let Some(f) = n.as_f64() {
918 return format!("{f}_f64");
919 }
920 }
921 n.to_string()
922 }
923 serde_json::Value::String(s) => rust_raw_string(s),
924 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
925 let json_str = serde_json::to_string(value).unwrap_or_default();
926 let literal = rust_raw_string(&json_str);
927 format!("serde_json::from_str({literal}).unwrap()")
928 }
929 }
930}
931
932fn render_http_test_function(out: &mut String, fixture: &Fixture, dep_name: &str) {
942 let http = match &fixture.http {
943 Some(h) => h,
944 None => return,
945 };
946
947 let fn_name = sanitize_ident(&fixture.id);
948 let description = &fixture.description;
949
950 let route = &http.handler.route;
951
952 enum RouteRegistration<'a> {
955 Shorthand(&'a str),
957 Explicit(&'a str),
959 }
960 let route_reg = match http.handler.method.to_lowercase().as_str() {
961 "get" => RouteRegistration::Shorthand("get"),
962 "post" => RouteRegistration::Shorthand("post"),
963 "put" => RouteRegistration::Shorthand("put"),
964 "patch" => RouteRegistration::Shorthand("patch"),
965 "delete" => RouteRegistration::Shorthand("delete"),
966 "head" => RouteRegistration::Explicit("Head"),
967 "options" => RouteRegistration::Explicit("Options"),
968 "trace" => RouteRegistration::Explicit("Trace"),
969 _ => RouteRegistration::Shorthand("get"),
970 };
971
972 enum ServerCall<'a> {
975 Shorthand(&'a str),
977 AxumMethod(&'a str),
979 }
980 let server_call = match http.request.method.to_uppercase().as_str() {
981 "GET" => ServerCall::Shorthand("get"),
982 "POST" => ServerCall::Shorthand("post"),
983 "PUT" => ServerCall::Shorthand("put"),
984 "PATCH" => ServerCall::Shorthand("patch"),
985 "DELETE" => ServerCall::Shorthand("delete"),
986 "HEAD" => ServerCall::AxumMethod("HEAD"),
987 "OPTIONS" => ServerCall::AxumMethod("OPTIONS"),
988 "TRACE" => ServerCall::AxumMethod("TRACE"),
989 _ => ServerCall::Shorthand("get"),
990 };
991
992 let req_path = &http.request.path;
993 let status = http.expected_response.status_code;
994
995 let body_str = match &http.expected_response.body {
997 Some(b) => serde_json::to_string(b).unwrap_or_else(|_| "{}".to_string()),
998 None => String::new(),
999 };
1000 let body_literal = rust_raw_string(&body_str);
1001
1002 let req_body_str = match &http.request.body {
1004 Some(b) => serde_json::to_string(b).unwrap_or_else(|_| "{}".to_string()),
1005 None => String::new(),
1006 };
1007 let has_req_body = !req_body_str.is_empty();
1008
1009 let _ = writeln!(out, "#[tokio::test]");
1010 let _ = writeln!(out, "async fn test_{fn_name}() {{");
1011 let _ = writeln!(out, " // {description}");
1012
1013 let _ = writeln!(out, " let expected_body = {body_literal}.to_string();");
1015 let _ = writeln!(out, " let mut app = {dep_name}::App::new();");
1016
1017 match &route_reg {
1019 RouteRegistration::Shorthand(method) => {
1020 let _ = writeln!(
1021 out,
1022 " app.route({dep_name}::{method}({route:?}), move |_ctx: {dep_name}::RequestContext| {{"
1023 );
1024 }
1025 RouteRegistration::Explicit(variant) => {
1026 let _ = writeln!(
1027 out,
1028 " app.route({dep_name}::RouteBuilder::new({dep_name}::Method::{variant}, {route:?}), move |_ctx: {dep_name}::RequestContext| {{"
1029 );
1030 }
1031 }
1032 let _ = writeln!(out, " let body = expected_body.clone();");
1033 let _ = writeln!(out, " async move {{");
1034 let _ = writeln!(out, " Ok(axum::http::Response::builder()");
1035 let _ = writeln!(out, " .status({status}u16)");
1036 let _ = writeln!(out, " .header(\"content-type\", \"application/json\")");
1037 let _ = writeln!(out, " .body(axum::body::Body::from(body))");
1038 let _ = writeln!(out, " .unwrap())");
1039 let _ = writeln!(out, " }}");
1040 let _ = writeln!(out, " }}).unwrap();");
1041
1042 let _ = writeln!(out, " let router = app.into_router().unwrap();");
1044 let _ = writeln!(out, " let server = axum_test::TestServer::new(router);");
1045
1046 match &server_call {
1048 ServerCall::Shorthand(method) => {
1049 let _ = writeln!(out, " let response = server.{method}({req_path:?})");
1050 }
1051 ServerCall::AxumMethod(method) => {
1052 let _ = writeln!(
1053 out,
1054 " let response = server.method(axum::http::Method::{method}, {req_path:?})"
1055 );
1056 }
1057 }
1058
1059 for (name, value) in &http.request.headers {
1061 let n = rust_raw_string(name);
1062 let v = rust_raw_string(value);
1063 let _ = writeln!(out, " .add_header({n}, {v})");
1064 }
1065
1066 if has_req_body {
1068 let req_body_literal = rust_raw_string(&req_body_str);
1069 let _ = writeln!(
1070 out,
1071 " .bytes(bytes::Bytes::copy_from_slice({req_body_literal}.as_bytes()))"
1072 );
1073 }
1074
1075 let _ = writeln!(out, " .await;");
1076
1077 let _ = writeln!(out, " assert_eq!(response.status_code().as_u16(), {status}u16);");
1079
1080 let _ = writeln!(out, "}}");
1081}
1082
1083fn render_mock_server_setup(out: &mut String, fixture: &Fixture, e2e_config: &E2eConfig) {
1093 let mock = match fixture.mock_response.as_ref() {
1094 Some(m) => m,
1095 None => return,
1096 };
1097
1098 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
1100 let path = call_config.path.as_deref().unwrap_or("/");
1101 let method = call_config.method.as_deref().unwrap_or("POST");
1102
1103 let status = mock.status;
1104
1105 let mut header_entries: Vec<(&String, &String)> = mock.headers.iter().collect();
1107 header_entries.sort_by(|a, b| a.0.cmp(b.0));
1108 let render_headers = |out: &mut String| {
1109 let _ = writeln!(out, " headers: vec![");
1110 for (name, value) in &header_entries {
1111 let n = rust_raw_string(name);
1112 let v = rust_raw_string(value);
1113 let _ = writeln!(out, " ({n}.to_string(), {v}.to_string()),");
1114 }
1115 let _ = writeln!(out, " ],");
1116 };
1117
1118 if let Some(chunks) = &mock.stream_chunks {
1119 let _ = writeln!(out, " let mock_route = MockRoute {{");
1121 let _ = writeln!(out, " path: \"{path}\",");
1122 let _ = writeln!(out, " method: \"{method}\",");
1123 let _ = writeln!(out, " status: {status},");
1124 let _ = writeln!(out, " body: String::new(),");
1125 let _ = writeln!(out, " stream_chunks: vec![");
1126 for chunk in chunks {
1127 let chunk_str = match chunk {
1128 serde_json::Value::String(s) => rust_raw_string(s),
1129 other => {
1130 let s = serde_json::to_string(other).unwrap_or_default();
1131 rust_raw_string(&s)
1132 }
1133 };
1134 let _ = writeln!(out, " {chunk_str}.to_string(),");
1135 }
1136 let _ = writeln!(out, " ],");
1137 render_headers(out);
1138 let _ = writeln!(out, " }};");
1139 } else {
1140 let body_str = match &mock.body {
1142 Some(b) => {
1143 let s = serde_json::to_string(b).unwrap_or_default();
1144 rust_raw_string(&s)
1145 }
1146 None => rust_raw_string("{}"),
1147 };
1148 let _ = writeln!(out, " let mock_route = MockRoute {{");
1149 let _ = writeln!(out, " path: \"{path}\",");
1150 let _ = writeln!(out, " method: \"{method}\",");
1151 let _ = writeln!(out, " status: {status},");
1152 let _ = writeln!(out, " body: {body_str}.to_string(),");
1153 let _ = writeln!(out, " stream_chunks: vec![],");
1154 render_headers(out);
1155 let _ = writeln!(out, " }};");
1156 }
1157
1158 let _ = writeln!(out, " let mock_server = MockServer::start(vec![mock_route]).await;");
1159}
1160
1161pub fn render_mock_server_module() -> String {
1163 hash::header(CommentStyle::DoubleSlash)
1166 + r#"//
1167// Minimal axum-based mock HTTP server for e2e tests.
1168
1169use std::net::SocketAddr;
1170use std::sync::Arc;
1171
1172use axum::Router;
1173use axum::body::Body;
1174use axum::extract::State;
1175use axum::http::{Request, StatusCode};
1176use axum::response::{IntoResponse, Response};
1177use tokio::net::TcpListener;
1178
1179/// A single mock route: match by path + method, return a configured response.
1180#[derive(Clone, Debug)]
1181pub struct MockRoute {
1182 /// URL path to match, e.g. `"/v1/chat/completions"`.
1183 pub path: &'static str,
1184 /// HTTP method to match, e.g. `"POST"` or `"GET"`.
1185 pub method: &'static str,
1186 /// HTTP status code to return.
1187 pub status: u16,
1188 /// Response body JSON string (used when `stream_chunks` is empty).
1189 pub body: String,
1190 /// Ordered SSE data payloads for streaming responses.
1191 /// Each entry becomes `data: <chunk>\n\n` in the response.
1192 /// A final `data: [DONE]\n\n` is always appended.
1193 pub stream_chunks: Vec<String>,
1194 /// Response headers to apply (name, value) pairs.
1195 /// Multiple entries with the same name produce multiple header lines.
1196 pub headers: Vec<(String, String)>,
1197}
1198
1199struct ServerState {
1200 routes: Vec<MockRoute>,
1201}
1202
1203pub struct MockServer {
1204 /// Base URL of the mock server, e.g. `"http://127.0.0.1:54321"`.
1205 pub url: String,
1206 handle: tokio::task::JoinHandle<()>,
1207}
1208
1209impl MockServer {
1210 /// Start a mock server with the given routes. Binds to a random port on
1211 /// localhost and returns immediately once the server is listening.
1212 pub async fn start(routes: Vec<MockRoute>) -> Self {
1213 let state = Arc::new(ServerState { routes });
1214
1215 let app = Router::new().fallback(handle_request).with_state(state);
1216
1217 let listener = TcpListener::bind("127.0.0.1:0")
1218 .await
1219 .expect("Failed to bind mock server port");
1220 let addr: SocketAddr = listener.local_addr().expect("Failed to get local addr");
1221 let url = format!("http://{addr}");
1222
1223 let handle = tokio::spawn(async move {
1224 axum::serve(listener, app).await.expect("Mock server failed");
1225 });
1226
1227 MockServer { url, handle }
1228 }
1229
1230 /// Stop the mock server.
1231 pub fn shutdown(self) {
1232 self.handle.abort();
1233 }
1234}
1235
1236impl Drop for MockServer {
1237 fn drop(&mut self) {
1238 self.handle.abort();
1239 }
1240}
1241
1242async fn handle_request(State(state): State<Arc<ServerState>>, req: Request<Body>) -> Response {
1243 let path = req.uri().path().to_owned();
1244 let method = req.method().as_str().to_uppercase();
1245
1246 for route in &state.routes {
1247 if route.path == path && route.method.to_uppercase() == method {
1248 let status =
1249 StatusCode::from_u16(route.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1250
1251 if !route.stream_chunks.is_empty() {
1252 // Build SSE body: data: <chunk>\n\n ... data: [DONE]\n\n
1253 let mut sse = String::new();
1254 for chunk in &route.stream_chunks {
1255 sse.push_str("data: ");
1256 sse.push_str(chunk);
1257 sse.push_str("\n\n");
1258 }
1259 sse.push_str("data: [DONE]\n\n");
1260
1261 let mut builder = Response::builder()
1262 .status(status)
1263 .header("content-type", "text/event-stream")
1264 .header("cache-control", "no-cache");
1265 for (name, value) in &route.headers {
1266 builder = builder.header(name, value);
1267 }
1268 return builder.body(Body::from(sse)).unwrap().into_response();
1269 }
1270
1271 let mut builder =
1272 Response::builder().status(status).header("content-type", "application/json");
1273 for (name, value) in &route.headers {
1274 builder = builder.header(name, value);
1275 }
1276 return builder.body(Body::from(route.body.clone())).unwrap().into_response();
1277 }
1278 }
1279
1280 // No matching route → 404.
1281 Response::builder()
1282 .status(StatusCode::NOT_FOUND)
1283 .body(Body::from(format!("No mock route for {method} {path}")))
1284 .unwrap()
1285 .into_response()
1286}
1287"#
1288}
1289
1290pub fn render_mock_server_binary() -> String {
1302 hash::header(CommentStyle::DoubleSlash)
1303 + r#"//
1304// Standalone mock HTTP server binary for cross-language e2e tests.
1305// Reads fixture JSON files and serves mock responses on /fixtures/{fixture_id}.
1306//
1307// Usage: mock-server [fixtures-dir]
1308// fixtures-dir defaults to "../../fixtures"
1309//
1310// Prints `MOCK_SERVER_URL=http://127.0.0.1:<port>` to stdout once listening,
1311// then blocks until stdin is closed (parent process exit triggers cleanup).
1312
1313use std::collections::HashMap;
1314use std::io::{self, BufRead};
1315use std::net::SocketAddr;
1316use std::path::Path;
1317use std::sync::Arc;
1318
1319use axum::Router;
1320use axum::body::Body;
1321use axum::extract::State;
1322use axum::http::{Request, StatusCode};
1323use axum::response::{IntoResponse, Response};
1324use serde::Deserialize;
1325use tokio::net::TcpListener;
1326
1327// ---------------------------------------------------------------------------
1328// Fixture types (mirrors alef-e2e's fixture.rs for runtime deserialization)
1329// Supports both schemas:
1330// liter-llm: mock_response: { status, body, stream_chunks }
1331// spikard: http.expected_response: { status_code, body, headers }
1332// ---------------------------------------------------------------------------
1333
1334#[derive(Debug, Deserialize)]
1335struct MockResponse {
1336 status: u16,
1337 #[serde(default)]
1338 body: Option<serde_json::Value>,
1339 #[serde(default)]
1340 stream_chunks: Option<Vec<serde_json::Value>>,
1341 #[serde(default)]
1342 headers: HashMap<String, String>,
1343}
1344
1345#[derive(Debug, Deserialize)]
1346struct HttpExpectedResponse {
1347 status_code: u16,
1348 #[serde(default)]
1349 body: Option<serde_json::Value>,
1350 #[serde(default)]
1351 headers: HashMap<String, String>,
1352}
1353
1354#[derive(Debug, Deserialize)]
1355struct HttpFixture {
1356 expected_response: HttpExpectedResponse,
1357}
1358
1359#[derive(Debug, Deserialize)]
1360struct Fixture {
1361 id: String,
1362 #[serde(default)]
1363 mock_response: Option<MockResponse>,
1364 #[serde(default)]
1365 http: Option<HttpFixture>,
1366}
1367
1368impl Fixture {
1369 /// Bridge both schemas into a unified MockResponse.
1370 fn as_mock_response(&self) -> Option<MockResponse> {
1371 if let Some(mock) = &self.mock_response {
1372 return Some(MockResponse {
1373 status: mock.status,
1374 body: mock.body.clone(),
1375 stream_chunks: mock.stream_chunks.clone(),
1376 headers: mock.headers.clone(),
1377 });
1378 }
1379 if let Some(http) = &self.http {
1380 return Some(MockResponse {
1381 status: http.expected_response.status_code,
1382 body: http.expected_response.body.clone(),
1383 stream_chunks: None,
1384 headers: http.expected_response.headers.clone(),
1385 });
1386 }
1387 None
1388 }
1389}
1390
1391// ---------------------------------------------------------------------------
1392// Route table
1393// ---------------------------------------------------------------------------
1394
1395#[derive(Clone, Debug)]
1396struct MockRoute {
1397 status: u16,
1398 body: String,
1399 stream_chunks: Vec<String>,
1400 headers: Vec<(String, String)>,
1401}
1402
1403type RouteTable = Arc<HashMap<String, MockRoute>>;
1404
1405// ---------------------------------------------------------------------------
1406// Axum handler
1407// ---------------------------------------------------------------------------
1408
1409async fn handle_request(State(routes): State<RouteTable>, req: Request<Body>) -> Response {
1410 let path = req.uri().path().to_owned();
1411
1412 // Try exact match first
1413 if let Some(route) = routes.get(&path) {
1414 return serve_route(route);
1415 }
1416
1417 // Try prefix match: find a route that is a prefix of the request path
1418 // This allows /fixtures/basic_chat/v1/chat/completions to match /fixtures/basic_chat
1419 for (route_path, route) in routes.iter() {
1420 if path.starts_with(route_path) && (path.len() == route_path.len() || path.as_bytes()[route_path.len()] == b'/') {
1421 return serve_route(route);
1422 }
1423 }
1424
1425 Response::builder()
1426 .status(StatusCode::NOT_FOUND)
1427 .body(Body::from(format!("No mock route for {path}")))
1428 .unwrap()
1429 .into_response()
1430}
1431
1432fn serve_route(route: &MockRoute) -> Response {
1433 let status = StatusCode::from_u16(route.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
1434
1435 if !route.stream_chunks.is_empty() {
1436 let mut sse = String::new();
1437 for chunk in &route.stream_chunks {
1438 sse.push_str("data: ");
1439 sse.push_str(chunk);
1440 sse.push_str("\n\n");
1441 }
1442 sse.push_str("data: [DONE]\n\n");
1443
1444 let mut builder = Response::builder()
1445 .status(status)
1446 .header("content-type", "text/event-stream")
1447 .header("cache-control", "no-cache");
1448 for (name, value) in &route.headers {
1449 builder = builder.header(name, value);
1450 }
1451 return builder.body(Body::from(sse)).unwrap().into_response();
1452 }
1453
1454 // Only set the default content-type if the fixture does not override it.
1455 // Use application/json when the body looks like JSON (starts with { or [),
1456 // otherwise fall back to text/plain to avoid clients failing JSON-decode.
1457 let has_content_type = route.headers.iter().any(|(k, _)| k.to_lowercase() == "content-type");
1458 let mut builder = Response::builder().status(status);
1459 if !has_content_type {
1460 let trimmed = route.body.trim_start();
1461 let default_ct = if trimmed.starts_with('{') || trimmed.starts_with('[') {
1462 "application/json"
1463 } else {
1464 "text/plain"
1465 };
1466 builder = builder.header("content-type", default_ct);
1467 }
1468 for (name, value) in &route.headers {
1469 // Skip content-encoding headers — the mock server returns uncompressed bodies.
1470 // Sending a content-encoding without actually encoding the body would cause
1471 // clients to fail decompression.
1472 if name.to_lowercase() == "content-encoding" {
1473 continue;
1474 }
1475 // The <<absent>> sentinel means this header must NOT be present in the
1476 // real server response — do not emit it from the mock server either.
1477 if value == "<<absent>>" {
1478 continue;
1479 }
1480 // Replace the <<uuid>> sentinel with a real UUID v4 so clients can
1481 // assert the header value matches the UUID pattern.
1482 if value == "<<uuid>>" {
1483 let uuid = format!(
1484 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
1485 rand_u32(),
1486 rand_u16(),
1487 rand_u16() & 0x0fff,
1488 (rand_u16() & 0x3fff) | 0x8000,
1489 rand_u48(),
1490 );
1491 builder = builder.header(name, uuid);
1492 continue;
1493 }
1494 builder = builder.header(name, value);
1495 }
1496 builder.body(Body::from(route.body.clone())).unwrap().into_response()
1497}
1498
1499/// Generate a pseudo-random u32 using the current time nanoseconds.
1500fn rand_u32() -> u32 {
1501 use std::time::{SystemTime, UNIX_EPOCH};
1502 let ns = SystemTime::now()
1503 .duration_since(UNIX_EPOCH)
1504 .map(|d| d.subsec_nanos())
1505 .unwrap_or(0);
1506 ns ^ (ns.wrapping_shl(13)) ^ (ns.wrapping_shr(17))
1507}
1508
1509fn rand_u16() -> u16 {
1510 (rand_u32() & 0xffff) as u16
1511}
1512
1513fn rand_u48() -> u64 {
1514 ((rand_u32() as u64) << 16) | (rand_u16() as u64)
1515}
1516
1517// ---------------------------------------------------------------------------
1518// Fixture loading
1519// ---------------------------------------------------------------------------
1520
1521fn load_routes(fixtures_dir: &Path) -> HashMap<String, MockRoute> {
1522 let mut routes = HashMap::new();
1523 load_routes_recursive(fixtures_dir, &mut routes);
1524 routes
1525}
1526
1527fn load_routes_recursive(dir: &Path, routes: &mut HashMap<String, MockRoute>) {
1528 let entries = match std::fs::read_dir(dir) {
1529 Ok(e) => e,
1530 Err(err) => {
1531 eprintln!("warning: cannot read directory {}: {err}", dir.display());
1532 return;
1533 }
1534 };
1535
1536 let mut paths: Vec<_> = entries.filter_map(|e| e.ok()).map(|e| e.path()).collect();
1537 paths.sort();
1538
1539 for path in paths {
1540 if path.is_dir() {
1541 load_routes_recursive(&path, routes);
1542 } else if path.extension().is_some_and(|ext| ext == "json") {
1543 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
1544 if filename == "schema.json" || filename.starts_with('_') {
1545 continue;
1546 }
1547 let content = match std::fs::read_to_string(&path) {
1548 Ok(c) => c,
1549 Err(err) => {
1550 eprintln!("warning: cannot read {}: {err}", path.display());
1551 continue;
1552 }
1553 };
1554 let fixtures: Vec<Fixture> = if content.trim_start().starts_with('[') {
1555 match serde_json::from_str(&content) {
1556 Ok(v) => v,
1557 Err(err) => {
1558 eprintln!("warning: cannot parse {}: {err}", path.display());
1559 continue;
1560 }
1561 }
1562 } else {
1563 match serde_json::from_str::<Fixture>(&content) {
1564 Ok(f) => vec![f],
1565 Err(err) => {
1566 eprintln!("warning: cannot parse {}: {err}", path.display());
1567 continue;
1568 }
1569 }
1570 };
1571
1572 for fixture in fixtures {
1573 if let Some(mock) = fixture.as_mock_response() {
1574 let route_path = format!("/fixtures/{}", fixture.id);
1575 let body = mock
1576 .body
1577 .as_ref()
1578 .map(|b| match b {
1579 // Plain strings (e.g. text/plain bodies) are stored as JSON strings in
1580 // fixtures. Return the raw value so clients receive the string itself,
1581 // not its JSON-encoded form with extra surrounding quotes.
1582 serde_json::Value::String(s) => s.clone(),
1583 other => serde_json::to_string(other).unwrap_or_default(),
1584 })
1585 .unwrap_or_default();
1586 let stream_chunks = mock
1587 .stream_chunks
1588 .unwrap_or_default()
1589 .into_iter()
1590 .map(|c| match c {
1591 serde_json::Value::String(s) => s,
1592 other => serde_json::to_string(&other).unwrap_or_default(),
1593 })
1594 .collect();
1595 let mut headers: Vec<(String, String)> =
1596 mock.headers.into_iter().collect();
1597 headers.sort_by(|a, b| a.0.cmp(&b.0));
1598 routes.insert(route_path, MockRoute { status: mock.status, body, stream_chunks, headers });
1599 }
1600 }
1601 }
1602 }
1603}
1604
1605// ---------------------------------------------------------------------------
1606// Entry point
1607// ---------------------------------------------------------------------------
1608
1609#[tokio::main]
1610async fn main() {
1611 let fixtures_dir_arg = std::env::args().nth(1).unwrap_or_else(|| "../../fixtures".to_string());
1612 let fixtures_dir = Path::new(&fixtures_dir_arg);
1613
1614 let routes = load_routes(fixtures_dir);
1615 eprintln!("mock-server: loaded {} routes from {}", routes.len(), fixtures_dir.display());
1616
1617 let route_table: RouteTable = Arc::new(routes);
1618 let app = Router::new().fallback(handle_request).with_state(route_table);
1619
1620 let listener = TcpListener::bind("127.0.0.1:0")
1621 .await
1622 .expect("mock-server: failed to bind port");
1623 let addr: SocketAddr = listener.local_addr().expect("mock-server: failed to get local addr");
1624
1625 // Print the URL so the parent process can read it.
1626 println!("MOCK_SERVER_URL=http://{addr}");
1627 // Flush stdout explicitly so the parent does not block waiting.
1628 use std::io::Write;
1629 std::io::stdout().flush().expect("mock-server: failed to flush stdout");
1630
1631 // Spawn the server in the background.
1632 tokio::spawn(async move {
1633 axum::serve(listener, app).await.expect("mock-server: server error");
1634 });
1635
1636 // Block until stdin is closed — the parent process controls lifetime.
1637 let stdin = io::stdin();
1638 let mut lines = stdin.lock().lines();
1639 while lines.next().is_some() {}
1640}
1641"#
1642}
1643
1644#[allow(clippy::too_many_arguments)]
1649fn render_assertion(
1650 out: &mut String,
1651 assertion: &Assertion,
1652 result_var: &str,
1653 module: &str,
1654 dep_name: &str,
1655 is_error_context: bool,
1656 unwrapped_fields: &[(String, String)], field_resolver: &FieldResolver,
1658 result_is_tree: bool,
1659 result_is_simple: bool,
1660 result_is_vec: bool,
1661 result_is_option: bool,
1662) {
1663 let has_field = assertion.field.as_ref().is_some_and(|f| !f.is_empty());
1668 if result_is_vec && has_field && !is_error_context {
1669 let _ = writeln!(out, " for r in &{result_var} {{");
1670 render_assertion(
1671 out,
1672 assertion,
1673 "r",
1674 module,
1675 dep_name,
1676 is_error_context,
1677 unwrapped_fields,
1678 field_resolver,
1679 result_is_tree,
1680 result_is_simple,
1681 false, result_is_option,
1683 );
1684 let _ = writeln!(out, " }}");
1685 return;
1686 }
1687 if result_is_option && !is_error_context {
1690 let assertion_type = assertion.assertion_type.as_str();
1691 if !has_field && (assertion_type == "is_empty" || assertion_type == "not_empty") {
1692 let check = if assertion_type == "is_empty" {
1693 "is_none"
1694 } else {
1695 "is_some"
1696 };
1697 let _ = writeln!(
1698 out,
1699 " assert!({result_var}.{check}(), \"expected Option to be {check}\");"
1700 );
1701 return;
1702 }
1703 let _ = writeln!(
1707 out,
1708 " let r = {result_var}.as_ref().expect(\"Option<T> should be Some\");"
1709 );
1710 render_assertion(
1711 out,
1712 assertion,
1713 "r",
1714 module,
1715 dep_name,
1716 is_error_context,
1717 unwrapped_fields,
1718 field_resolver,
1719 result_is_tree,
1720 result_is_simple,
1721 result_is_vec,
1722 false, );
1724 return;
1725 }
1726 let _ = dep_name;
1727 if let Some(f) = &assertion.field {
1729 if f == "chunks_have_content" {
1730 match assertion.assertion_type.as_str() {
1731 "is_true" => {
1732 let _ = writeln!(
1733 out,
1734 " assert!({result_var}.chunks.as_ref().is_some_and(|chunks| !chunks.is_empty() && chunks.iter().all(|c| !c.content.is_empty())), \"expected all chunks to have content\");"
1735 );
1736 }
1737 "is_false" => {
1738 let _ = writeln!(
1739 out,
1740 " assert!({result_var}.chunks.as_ref().is_none() || {result_var}.chunks.as_ref().unwrap().iter().any(|c| c.content.is_empty()), \"expected some chunks to be empty\");"
1741 );
1742 }
1743 _ => {
1744 let _ = writeln!(
1745 out,
1746 " // unsupported assertion type on synthetic field chunks_have_content"
1747 );
1748 }
1749 }
1750 return;
1751 }
1752 }
1753
1754 if let Some(f) = &assertion.field {
1756 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1757 let _ = writeln!(out, " // skipped: field '{f}' not available on result type");
1758 return;
1759 }
1760 }
1761
1762 let field_access = match &assertion.field {
1770 Some(f) if !f.is_empty() => {
1771 if let Some((_, local_var)) = unwrapped_fields.iter().find(|(ff, _)| ff == f) {
1772 local_var.clone()
1773 } else if result_is_simple {
1774 result_var.to_string()
1777 } else if f == result_var {
1778 result_var.to_string()
1781 } else if result_is_tree {
1782 tree_field_access_expr(f, result_var, module)
1785 } else {
1786 field_resolver.accessor(f, "rust", result_var)
1787 }
1788 }
1789 _ => result_var.to_string(),
1790 };
1791
1792 let is_unwrapped = assertion
1794 .field
1795 .as_ref()
1796 .is_some_and(|f| unwrapped_fields.iter().any(|(ff, _)| ff == f));
1797
1798 match assertion.assertion_type.as_str() {
1799 "error" => {
1800 let _ = writeln!(out, " assert!({result_var}.is_err(), \"expected call to fail\");");
1801 if let Some(serde_json::Value::String(msg)) = &assertion.value {
1802 let escaped = escape_rust(msg);
1803 let _ = writeln!(
1804 out,
1805 " assert!({result_var}.as_ref().unwrap_err().to_string().contains(\"{escaped}\"), \"error message mismatch\");"
1806 );
1807 }
1808 }
1809 "not_error" => {
1810 }
1812 "equals" => {
1813 if let Some(val) = &assertion.value {
1814 let expected = value_to_rust_string(val);
1815 if is_error_context {
1816 return;
1817 }
1818 if val.is_string() {
1821 let is_opt_str_not_unwrapped = assertion.field.as_ref().is_some_and(|f| {
1826 let resolved = field_resolver.resolve(f);
1827 let is_opt = field_resolver.is_optional(resolved);
1828 let is_arr = field_resolver.is_array(resolved);
1829 is_opt && !is_arr && !is_unwrapped
1830 });
1831 let field_expr = if is_opt_str_not_unwrapped {
1832 format!("{field_access}.as_deref().unwrap_or(\"\").trim()")
1833 } else {
1834 format!("{field_access}.trim()")
1835 };
1836 let _ = writeln!(
1837 out,
1838 " assert_eq!({field_expr}, {expected}, \"equals assertion failed\");"
1839 );
1840 } else if val.is_boolean() {
1841 if val.as_bool() == Some(true) {
1843 let _ = writeln!(out, " assert!({field_access}, \"equals assertion failed\");");
1844 } else {
1845 let _ = writeln!(out, " assert!(!{field_access}, \"equals assertion failed\");");
1846 }
1847 } else {
1848 let is_opt = assertion.field.as_ref().is_some_and(|f| {
1850 let resolved = field_resolver.resolve(f);
1851 field_resolver.is_optional(resolved)
1852 });
1853 if is_opt
1854 && !unwrapped_fields
1855 .iter()
1856 .any(|(ff, _)| assertion.field.as_ref() == Some(ff))
1857 {
1858 let _ = writeln!(
1859 out,
1860 " assert_eq!({field_access}, Some({expected}), \"equals assertion failed\");"
1861 );
1862 } else {
1863 let _ = writeln!(
1864 out,
1865 " assert_eq!({field_access}, {expected}, \"equals assertion failed\");"
1866 );
1867 }
1868 }
1869 }
1870 }
1871 "contains" => {
1872 if let Some(val) = &assertion.value {
1873 let expected = value_to_rust_string(val);
1874 let line = format!(
1875 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
1876 );
1877 let _ = writeln!(out, "{line}");
1878 }
1879 }
1880 "contains_all" => {
1881 if let Some(values) = &assertion.values {
1882 for val in values {
1883 let expected = value_to_rust_string(val);
1884 let line = format!(
1885 " assert!(format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected to contain: {{}}\", {expected});"
1886 );
1887 let _ = writeln!(out, "{line}");
1888 }
1889 }
1890 }
1891 "not_contains" => {
1892 if let Some(val) = &assertion.value {
1893 let expected = value_to_rust_string(val);
1894 let line = format!(
1895 " assert!(!format!(\"{{:?}}\", {field_access}).contains({expected}), \"expected NOT to contain: {{}}\", {expected});"
1896 );
1897 let _ = writeln!(out, "{line}");
1898 }
1899 }
1900 "not_empty" => {
1901 if let Some(f) = &assertion.field {
1902 let resolved = field_resolver.resolve(f);
1903 let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
1904 let is_arr = field_resolver.is_array(resolved);
1905 if is_opt && is_arr {
1906 let accessor = field_resolver.accessor(f, "rust", result_var);
1908 let _ = writeln!(
1909 out,
1910 " assert!({accessor}.as_ref().is_some_and(|v| !v.is_empty()), \"expected {f} to be present and non-empty\");"
1911 );
1912 } else if is_opt {
1913 let accessor = field_resolver.accessor(f, "rust", result_var);
1915 let _ = writeln!(
1916 out,
1917 " assert!({accessor}.is_some(), \"expected {f} to be present\");"
1918 );
1919 } else {
1920 let _ = writeln!(
1921 out,
1922 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
1923 );
1924 }
1925 } else if result_is_option {
1926 let _ = writeln!(
1928 out,
1929 " assert!({field_access}.is_some(), \"expected non-empty value\");"
1930 );
1931 } else {
1932 let _ = writeln!(
1934 out,
1935 " assert!(!{field_access}.is_empty(), \"expected non-empty value\");"
1936 );
1937 }
1938 }
1939 "is_empty" => {
1940 if let Some(f) = &assertion.field {
1941 let resolved = field_resolver.resolve(f);
1942 let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
1943 let is_arr = field_resolver.is_array(resolved);
1944 if is_opt && is_arr {
1945 let accessor = field_resolver.accessor(f, "rust", result_var);
1947 let _ = writeln!(
1948 out,
1949 " assert!({accessor}.as_ref().is_none_or(|v| v.is_empty()), \"expected {f} to be empty or absent\");"
1950 );
1951 } else if is_opt {
1952 let accessor = field_resolver.accessor(f, "rust", result_var);
1953 let _ = writeln!(out, " assert!({accessor}.is_none(), \"expected {f} to be absent\");");
1954 } else {
1955 let _ = writeln!(out, " assert!({field_access}.is_empty(), \"expected empty value\");");
1956 }
1957 } else {
1958 let _ = writeln!(out, " assert!({field_access}.is_none(), \"expected empty value\");");
1959 }
1960 }
1961 "contains_any" => {
1962 if let Some(values) = &assertion.values {
1963 let checks: Vec<String> = values
1964 .iter()
1965 .map(|v| {
1966 let expected = value_to_rust_string(v);
1967 format!("{field_access}.contains({expected})")
1968 })
1969 .collect();
1970 let joined = checks.join(" || ");
1971 let _ = writeln!(
1972 out,
1973 " assert!({joined}, \"expected to contain at least one of the specified values\");"
1974 );
1975 }
1976 }
1977 "greater_than" => {
1978 if let Some(val) = &assertion.value {
1979 if val.as_f64().is_some_and(|n| n < 0.0) {
1981 let _ = writeln!(
1982 out,
1983 " // skipped: greater_than with negative value is always true for unsigned types"
1984 );
1985 } else if val.as_u64() == Some(0) {
1986 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
1988 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected > 0\");");
1989 } else {
1990 let lit = numeric_literal(val);
1991 let _ = writeln!(out, " assert!({field_access} > {lit}, \"expected > {lit}\");");
1992 }
1993 }
1994 }
1995 "less_than" => {
1996 if let Some(val) = &assertion.value {
1997 let lit = numeric_literal(val);
1998 let _ = writeln!(out, " assert!({field_access} < {lit}, \"expected < {lit}\");");
1999 }
2000 }
2001 "greater_than_or_equal" => {
2002 if let Some(val) = &assertion.value {
2003 let lit = numeric_literal(val);
2004 let is_opt_numeric = assertion.field.as_ref().is_some_and(|f| {
2007 let resolved = field_resolver.resolve(f);
2008 let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
2009 let is_arr = field_resolver.is_array(resolved);
2010 is_opt && !is_arr
2011 });
2012 if val.as_u64() == Some(1) && field_access.ends_with(".len()") {
2013 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
2017 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= 1\");");
2018 } else if is_opt_numeric {
2019 let _ = writeln!(
2021 out,
2022 " assert!({field_access}.unwrap_or(0) >= {lit}, \"expected >= {lit}\");"
2023 );
2024 } else {
2025 let _ = writeln!(out, " assert!({field_access} >= {lit}, \"expected >= {lit}\");");
2026 }
2027 }
2028 }
2029 "less_than_or_equal" => {
2030 if let Some(val) = &assertion.value {
2031 let lit = numeric_literal(val);
2032 let _ = writeln!(out, " assert!({field_access} <= {lit}, \"expected <= {lit}\");");
2033 }
2034 }
2035 "starts_with" => {
2036 if let Some(val) = &assertion.value {
2037 let expected = value_to_rust_string(val);
2038 let _ = writeln!(
2039 out,
2040 " assert!({field_access}.starts_with({expected}), \"expected to start with: {{}}\", {expected});"
2041 );
2042 }
2043 }
2044 "ends_with" => {
2045 if let Some(val) = &assertion.value {
2046 let expected = value_to_rust_string(val);
2047 let _ = writeln!(
2048 out,
2049 " assert!({field_access}.ends_with({expected}), \"expected to end with: {{}}\", {expected});"
2050 );
2051 }
2052 }
2053 "min_length" => {
2054 if let Some(val) = &assertion.value {
2055 if let Some(n) = val.as_u64() {
2056 let _ = writeln!(
2057 out,
2058 " assert!({field_access}.len() >= {n}, \"expected length >= {n}, got {{}}\", {field_access}.len());"
2059 );
2060 }
2061 }
2062 }
2063 "max_length" => {
2064 if let Some(val) = &assertion.value {
2065 if let Some(n) = val.as_u64() {
2066 let _ = writeln!(
2067 out,
2068 " assert!({field_access}.len() <= {n}, \"expected length <= {n}, got {{}}\", {field_access}.len());"
2069 );
2070 }
2071 }
2072 }
2073 "count_min" => {
2074 if let Some(val) = &assertion.value {
2075 if let Some(n) = val.as_u64() {
2076 let opt_arr_field = assertion.field.as_ref().is_some_and(|f| {
2077 let resolved = field_resolver.resolve(f);
2078 let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
2079 let is_arr = field_resolver.is_array(resolved);
2080 is_opt && is_arr
2081 });
2082 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
2083 if opt_arr_field {
2084 if n <= 1 {
2086 let _ = writeln!(
2087 out,
2088 " assert!({base}.as_ref().is_some_and(|v| !v.is_empty()), \"expected >= {n}\");"
2089 );
2090 } else {
2091 let _ = writeln!(
2092 out,
2093 " assert!({base}.as_ref().is_some_and(|v| v.len() >= {n}), \"expected at least {n} elements\");"
2094 );
2095 }
2096 } else if n <= 1 {
2097 let _ = writeln!(out, " assert!(!{base}.is_empty(), \"expected >= {n}\");");
2098 } else {
2099 let _ = writeln!(
2100 out,
2101 " assert!({field_access}.len() >= {n}, \"expected at least {n} elements, got {{}}\", {field_access}.len());"
2102 );
2103 }
2104 }
2105 }
2106 }
2107 "count_equals" => {
2108 if let Some(val) = &assertion.value {
2109 if let Some(n) = val.as_u64() {
2110 let opt_arr_field = assertion.field.as_ref().is_some_and(|f| {
2111 let resolved = field_resolver.resolve(f);
2112 let is_opt = !is_unwrapped && field_resolver.is_optional(resolved);
2113 let is_arr = field_resolver.is_array(resolved);
2114 is_opt && is_arr
2115 });
2116 let base = field_access.strip_suffix(".len()").unwrap_or(&field_access);
2117 if opt_arr_field {
2118 let _ = writeln!(
2119 out,
2120 " assert!({base}.as_ref().is_some_and(|v| v.len() == {n}), \"expected exactly {n} elements\");"
2121 );
2122 } else {
2123 let _ = writeln!(
2124 out,
2125 " assert_eq!({field_access}.len(), {n}, \"expected exactly {n} elements, got {{}}\", {field_access}.len());"
2126 );
2127 }
2128 }
2129 }
2130 }
2131 "is_true" => {
2132 let _ = writeln!(out, " assert!({field_access}, \"expected true\");");
2133 }
2134 "is_false" => {
2135 let _ = writeln!(out, " assert!(!{field_access}, \"expected false\");");
2136 }
2137 "method_result" => {
2138 if let Some(method_name) = &assertion.method {
2139 let call_expr = if result_is_tree {
2143 build_tree_call_expr(field_access.as_str(), method_name, assertion.args.as_ref(), module)
2144 } else if let Some(args) = &assertion.args {
2145 let arg_lit = json_to_rust_literal(args, "");
2146 format!("{field_access}.{method_name}({arg_lit})")
2147 } else {
2148 format!("{field_access}.{method_name}()")
2149 };
2150
2151 let returns_numeric = result_is_tree && is_tree_numeric_method(method_name);
2154
2155 let check = assertion.check.as_deref().unwrap_or("is_true");
2156 match check {
2157 "equals" => {
2158 if let Some(val) = &assertion.value {
2159 if val.is_boolean() {
2160 if val.as_bool() == Some(true) {
2161 let _ = writeln!(
2162 out,
2163 " assert!({call_expr}, \"method_result equals assertion failed\");"
2164 );
2165 } else {
2166 let _ = writeln!(
2167 out,
2168 " assert!(!{call_expr}, \"method_result equals assertion failed\");"
2169 );
2170 }
2171 } else {
2172 let expected = value_to_rust_string(val);
2173 let _ = writeln!(
2174 out,
2175 " assert_eq!({call_expr}, {expected}, \"method_result equals assertion failed\");"
2176 );
2177 }
2178 }
2179 }
2180 "is_true" => {
2181 let _ = writeln!(
2182 out,
2183 " assert!({call_expr}, \"method_result is_true assertion failed\");"
2184 );
2185 }
2186 "is_false" => {
2187 let _ = writeln!(
2188 out,
2189 " assert!(!{call_expr}, \"method_result is_false assertion failed\");"
2190 );
2191 }
2192 "greater_than_or_equal" => {
2193 if let Some(val) = &assertion.value {
2194 let lit = numeric_literal(val);
2195 if returns_numeric {
2196 let _ = writeln!(out, " assert!({call_expr} >= {lit}, \"expected >= {lit}\");");
2198 } else if val.as_u64() == Some(1) {
2199 let _ = writeln!(out, " assert!(!{call_expr}.is_empty(), \"expected >= 1\");");
2201 } else {
2202 let _ = writeln!(out, " assert!({call_expr} >= {lit}, \"expected >= {lit}\");");
2203 }
2204 }
2205 }
2206 "count_min" => {
2207 if let Some(val) = &assertion.value {
2208 let n = val.as_u64().unwrap_or(0);
2209 if n <= 1 {
2210 let _ = writeln!(out, " assert!(!{call_expr}.is_empty(), \"expected >= {n}\");");
2211 } else {
2212 let _ = writeln!(
2213 out,
2214 " assert!({call_expr}.len() >= {n}, \"expected at least {n} elements, got {{}}\", {call_expr}.len());"
2215 );
2216 }
2217 }
2218 }
2219 "is_error" => {
2220 let raw_call = call_expr.strip_suffix(".unwrap()").unwrap_or(&call_expr);
2222 let _ = writeln!(
2223 out,
2224 " assert!({raw_call}.is_err(), \"expected method to return error\");"
2225 );
2226 }
2227 "contains" => {
2228 if let Some(val) = &assertion.value {
2229 let expected = value_to_rust_string(val);
2230 let _ = writeln!(
2231 out,
2232 " assert!({call_expr}.contains({expected}), \"expected result to contain {{}}\", {expected});"
2233 );
2234 }
2235 }
2236 "not_empty" => {
2237 let _ = writeln!(
2238 out,
2239 " assert!(!{call_expr}.is_empty(), \"expected non-empty result\");"
2240 );
2241 }
2242 "is_empty" => {
2243 let _ = writeln!(out, " assert!({call_expr}.is_empty(), \"expected empty result\");");
2244 }
2245 other_check => {
2246 panic!("Rust e2e generator: unsupported method_result check type: {other_check}");
2247 }
2248 }
2249 } else {
2250 panic!("Rust e2e generator: method_result assertion missing 'method' field");
2251 }
2252 }
2253 other => {
2254 panic!("Rust e2e generator: unsupported assertion type: {other}");
2255 }
2256 }
2257}
2258
2259fn tree_field_access_expr(field: &str, result_var: &str, module: &str) -> String {
2267 match field {
2268 "root_child_count" => format!("{result_var}.root_node().child_count()"),
2269 "root_node_type" => format!("{result_var}.root_node().kind()"),
2270 "named_children_count" => format!("{result_var}.root_node().named_child_count()"),
2271 "has_error_nodes" => format!("{module}::tree_has_error_nodes(&{result_var})"),
2272 "error_count" | "tree_error_count" => format!("{module}::tree_error_count(&{result_var})"),
2273 "tree_to_sexp" => format!("{module}::tree_to_sexp(&{result_var})"),
2274 other => format!("{result_var}.{other}"),
2277 }
2278}
2279
2280fn build_tree_call_expr(
2287 field_access: &str,
2288 method_name: &str,
2289 args: Option<&serde_json::Value>,
2290 module: &str,
2291) -> String {
2292 match method_name {
2293 "root_child_count" => format!("{field_access}.root_node().child_count()"),
2294 "root_node_type" => format!("{field_access}.root_node().kind()"),
2295 "named_children_count" => format!("{field_access}.root_node().named_child_count()"),
2296 "has_error_nodes" => format!("{module}::tree_has_error_nodes(&{field_access})"),
2297 "error_count" | "tree_error_count" => format!("{module}::tree_error_count(&{field_access})"),
2298 "tree_to_sexp" => format!("{module}::tree_to_sexp(&{field_access})"),
2299 "contains_node_type" => {
2300 let node_type = args
2301 .and_then(|a| a.get("node_type"))
2302 .and_then(|v| v.as_str())
2303 .unwrap_or("");
2304 format!("{module}::tree_contains_node_type(&{field_access}, \"{node_type}\")")
2305 }
2306 "find_nodes_by_type" => {
2307 let node_type = args
2308 .and_then(|a| a.get("node_type"))
2309 .and_then(|v| v.as_str())
2310 .unwrap_or("");
2311 format!("{module}::find_nodes_by_type(&{field_access}, \"{node_type}\")")
2312 }
2313 "run_query" => {
2314 let query_source = args
2315 .and_then(|a| a.get("query_source"))
2316 .and_then(|v| v.as_str())
2317 .unwrap_or("");
2318 let language = args
2319 .and_then(|a| a.get("language"))
2320 .and_then(|v| v.as_str())
2321 .unwrap_or("");
2322 format!(
2325 "{module}::run_query(&{field_access}, \"{language}\", r#\"{query_source}\"#, source.as_bytes()).unwrap()"
2326 )
2327 }
2328 _ => {
2330 if let Some(args) = args {
2331 let arg_lit = json_to_rust_literal(args, "");
2332 format!("{field_access}.{method_name}({arg_lit})")
2333 } else {
2334 format!("{field_access}.{method_name}()")
2335 }
2336 }
2337 }
2338}
2339
2340fn is_tree_numeric_method(method_name: &str) -> bool {
2344 matches!(
2345 method_name,
2346 "root_child_count" | "named_children_count" | "error_count" | "tree_error_count"
2347 )
2348}
2349
2350fn numeric_literal(value: &serde_json::Value) -> String {
2356 if let Some(n) = value.as_f64() {
2357 if n.fract() == 0.0 {
2358 return format!("{}", n as i64);
2361 }
2362 return format!("{n}_f64");
2363 }
2364 value.to_string()
2366}
2367
2368fn value_to_rust_string(value: &serde_json::Value) -> String {
2369 match value {
2370 serde_json::Value::String(s) => rust_raw_string(s),
2371 serde_json::Value::Bool(b) => format!("{b}"),
2372 serde_json::Value::Number(n) => n.to_string(),
2373 other => {
2374 let s = other.to_string();
2375 format!("\"{s}\"")
2376 }
2377 }
2378}
2379
2380fn resolve_visitor_trait(module: &str) -> String {
2386 if module.contains("html_to_markdown") {
2388 "HtmlVisitor".to_string()
2389 } else {
2390 "Visitor".to_string()
2392 }
2393}
2394
2395fn emit_rust_visitor_method(out: &mut String, method_name: &str, action: &CallbackAction) {
2403 let params = match method_name {
2407 "visit_link" => "_: &NodeContext, _: &str, _: &str, _: &str",
2408 "visit_image" => "_: &NodeContext, _: &str, _: &str, _: &str",
2409 "visit_heading" => "_: &NodeContext, _: u8, _: &str, _: Option<&str>",
2410 "visit_code_block" => "_: &NodeContext, _: Option<&str>, _: &str",
2411 "visit_code_inline"
2412 | "visit_strong"
2413 | "visit_emphasis"
2414 | "visit_strikethrough"
2415 | "visit_underline"
2416 | "visit_subscript"
2417 | "visit_superscript"
2418 | "visit_mark"
2419 | "visit_button"
2420 | "visit_summary"
2421 | "visit_figcaption"
2422 | "visit_definition_term"
2423 | "visit_definition_description" => "_: &NodeContext, _: &str",
2424 "visit_text" => "_: &NodeContext, _: &str",
2425 "visit_list_item" => "_: &NodeContext, _: bool, _: &str, _: &str",
2426 "visit_blockquote" => "_: &NodeContext, _: &str, _: u32",
2427 "visit_table_row" => "_: &NodeContext, _: &[String], _: bool",
2428 "visit_custom_element" => "_: &NodeContext, _: &str, _: &str",
2429 "visit_form" => "_: &NodeContext, _: &str, _: &str",
2430 "visit_input" => "_: &NodeContext, _: &str, _: &str, _: &str",
2431 "visit_audio" | "visit_video" | "visit_iframe" => "_: &NodeContext, _: &str",
2432 "visit_details" => "_: &NodeContext, _: bool",
2433 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
2434 "_: &NodeContext, _: &str"
2435 }
2436 "visit_list_start" => "_: &NodeContext, _: bool",
2437 "visit_list_end" => "_: &NodeContext, _: bool, _: &str",
2438 _ => "_: &NodeContext",
2439 };
2440
2441 let _ = writeln!(out, " fn {method_name}(&mut self, {params}) -> VisitResult {{");
2442 match action {
2443 CallbackAction::Skip => {
2444 let _ = writeln!(out, " VisitResult::Skip");
2445 }
2446 CallbackAction::Continue => {
2447 let _ = writeln!(out, " VisitResult::Continue");
2448 }
2449 CallbackAction::PreserveHtml => {
2450 let _ = writeln!(out, " VisitResult::PreserveHtml");
2451 }
2452 CallbackAction::Custom { output } => {
2453 let escaped = escape_rust(output);
2454 let _ = writeln!(out, " VisitResult::Custom(\"{escaped}\".to_string())");
2455 }
2456 CallbackAction::CustomTemplate { template } => {
2457 let escaped = escape_rust(template);
2458 let _ = writeln!(out, " VisitResult::Custom(format!(\"{escaped}\"))");
2459 }
2460 }
2461 let _ = writeln!(out, " }}");
2462}