alef_e2e/codegen/rust/
mod.rs1pub mod assertions;
7pub mod cargo_toml;
8pub mod http;
9pub mod mock_server;
10pub mod test_file;
11
12mod args;
13mod assertion_helpers;
14mod assertion_synthetic;
15
16pub use cargo_toml::render_cargo_toml;
17pub use mock_server::{render_common_module, render_mock_server_binary, render_mock_server_module};
18
19use alef_core::backend::GeneratedFile;
20use alef_core::config::ResolvedCrateConfig;
21use anyhow::Result;
22use std::path::PathBuf;
23
24use crate::config::E2eConfig;
25use crate::escape::sanitize_filename;
26use crate::fixture::{Fixture, FixtureGroup};
27
28use super::E2eCodegen;
29use test_file::{is_skipped, render_test_file};
30
31pub struct RustE2eCodegen;
33
34impl E2eCodegen for RustE2eCodegen {
35 fn generate(
36 &self,
37 groups: &[FixtureGroup],
38 e2e_config: &E2eConfig,
39 config: &ResolvedCrateConfig,
40 _type_defs: &[alef_core::ir::TypeDef],
41 _enums: &[alef_core::ir::EnumDef],
42 ) -> Result<Vec<GeneratedFile>> {
43 let mut files = Vec::new();
44 let output_base = PathBuf::from(e2e_config.effective_output()).join("rust");
45
46 let crate_name = resolve_crate_name(e2e_config, config);
48 let crate_path = resolve_crate_path(e2e_config, &crate_name);
49 let dep_name = crate_name.replace('-', "_");
50
51 let all_call_configs = std::iter::once(&e2e_config.call).chain(e2e_config.calls.values());
54 let needs_serde_json = all_call_configs
55 .flat_map(|c| c.args.iter())
56 .any(|a| a.arg_type == "json_object" || a.arg_type == "handle");
57
58 let needs_mock_server = groups
61 .iter()
62 .flat_map(|g| g.fixtures.iter())
63 .any(|f| !is_skipped(f, "rust") && f.needs_mock_server());
64
65 let needs_http_tests = groups
67 .iter()
68 .flat_map(|g| g.fixtures.iter())
69 .any(|f| !is_skipped(f, "rust") && f.http.is_some());
70
71 let needs_tower_http = groups
73 .iter()
74 .flat_map(|g| g.fixtures.iter())
75 .filter(|f| !is_skipped(f, "rust"))
76 .filter_map(|f| f.http.as_ref())
77 .filter_map(|h| h.handler.middleware.as_ref())
78 .any(|m| m.cors.is_some() || m.static_files.is_some());
79
80 let any_async_call = std::iter::once(&e2e_config.call)
82 .chain(e2e_config.calls.values())
83 .any(|c| c.r#async);
84 let needs_tokio = needs_mock_server || needs_http_tests || any_async_call;
85
86 let crate_version = resolve_crate_version(e2e_config).or_else(|| config.resolved_version());
87 files.push(GeneratedFile {
88 path: output_base.join("Cargo.toml"),
89 content: render_cargo_toml(
90 &crate_name,
91 &dep_name,
92 &crate_path,
93 needs_serde_json,
94 needs_mock_server,
95 needs_http_tests,
96 needs_tokio,
97 needs_tower_http,
98 e2e_config.dep_mode,
99 crate_version.as_deref(),
100 &config.features,
101 ),
102 generated_header: true,
103 });
104
105 if needs_mock_server {
107 files.push(GeneratedFile {
108 path: output_base.join("tests").join("mock_server.rs"),
109 content: render_mock_server_module(),
110 generated_header: true,
111 });
112 files.push(GeneratedFile {
114 path: output_base.join("tests").join("common.rs"),
115 content: render_common_module(),
116 generated_header: true,
117 });
118 }
119 if needs_mock_server || needs_http_tests {
122 files.push(GeneratedFile {
123 path: output_base.join("src").join("main.rs"),
124 content: render_mock_server_binary(),
125 generated_header: true,
126 });
127 }
128
129 for group in groups {
131 let fixtures: Vec<&Fixture> = group.fixtures.iter().filter(|f| !is_skipped(f, "rust")).collect();
132
133 if fixtures.is_empty() {
134 continue;
135 }
136
137 let filename = format!("{}_test.rs", sanitize_filename(&group.category));
138 let content = render_test_file(&group.category, &fixtures, e2e_config, &dep_name, needs_mock_server);
139
140 files.push(GeneratedFile {
141 path: output_base.join("tests").join(filename),
142 content,
143 generated_header: true,
144 });
145 }
146
147 Ok(files)
148 }
149
150 fn language_name(&self) -> &'static str {
151 "rust"
152 }
153}
154
155fn resolve_crate_name(_e2e_config: &E2eConfig, config: &ResolvedCrateConfig) -> String {
160 config.name.clone()
164}
165
166fn resolve_crate_path(e2e_config: &E2eConfig, crate_name: &str) -> String {
167 e2e_config
168 .resolve_package("rust")
169 .and_then(|p| p.path.clone())
170 .unwrap_or_else(|| format!("../../crates/{crate_name}"))
171}
172
173fn resolve_crate_version(e2e_config: &E2eConfig) -> Option<String> {
174 e2e_config.resolve_package("rust").and_then(|p| p.version.clone())
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn resolve_crate_name_uses_config_name() {
183 use alef_core::config::NewAlefConfig;
184 let cfg: NewAlefConfig = toml::from_str(
185 r#"
186[workspace]
187languages = ["rust"]
188
189[[crates]]
190name = "my-lib"
191sources = ["src/lib.rs"]
192
193[crates.e2e]
194fixtures = "fixtures"
195output = "e2e"
196[crates.e2e.call]
197function = "process"
198module = "my_lib"
199result_var = "result"
200"#,
201 )
202 .unwrap();
203 let e2e = cfg.crates[0].e2e.clone().unwrap();
204 let resolved = cfg.resolve().unwrap().remove(0);
205 let name = resolve_crate_name(&e2e, &resolved);
206 assert_eq!(name, "my-lib");
207 }
208}