Skip to main content

alef_e2e/codegen/rust/
cargo_toml.rs

1//! Cargo.toml generation for Rust e2e test crates.
2
3use alef_core::hash::{self, CommentStyle};
4use alef_core::template_versions as tv;
5
6/// Render a `Cargo.toml` for the Rust e2e test crate.
7///
8/// Generates all dependency lines based on which test features are needed
9/// (mock server, HTTP tests, tokio, etc.).
10#[allow(clippy::too_many_arguments)]
11pub fn render_cargo_toml(
12    crate_name: &str,
13    dep_name: &str,
14    crate_path: &str,
15    needs_serde_json: bool,
16    needs_mock_server: bool,
17    needs_http_tests: bool,
18    needs_tokio: bool,
19    needs_tower_http: bool,
20    dep_mode: crate::config::DependencyMode,
21    version: Option<&str>,
22    features: &[String],
23) -> String {
24    let e2e_name = format!("{dep_name}-e2e-rust");
25    // Use only the features explicitly configured in alef.toml.
26    // Do NOT auto-add "serde" — the target crate may not have that feature.
27    // serde_json is added as a separate dependency when needed.
28    let effective_features: Vec<&str> = features.iter().map(|s| s.as_str()).collect();
29    let features_str = if effective_features.is_empty() {
30        String::new()
31    } else {
32        format!(", default-features = false, features = {:?}", effective_features)
33    };
34    let dep_spec = match dep_mode {
35        crate::config::DependencyMode::Registry => {
36            let ver = version.unwrap_or("0.1.0");
37            if crate_name != dep_name {
38                format!("{dep_name} = {{ package = \"{crate_name}\", version = \"{ver}\"{features_str} }}")
39            } else if effective_features.is_empty() {
40                format!("{dep_name} = \"{ver}\"")
41            } else {
42                format!("{dep_name} = {{ version = \"{ver}\"{features_str} }}")
43            }
44        }
45        crate::config::DependencyMode::Local => {
46            if crate_name != dep_name {
47                format!("{dep_name} = {{ package = \"{crate_name}\", path = \"{crate_path}\"{features_str} }}")
48            } else if effective_features.is_empty() {
49                format!("{dep_name} = {{ path = \"{crate_path}\" }}")
50            } else {
51                format!("{dep_name} = {{ path = \"{crate_path}\"{features_str} }}")
52            }
53        }
54    };
55    // serde_json is needed either when args use json_object/handle, or when the
56    // mock server binary is present (it uses serde_json::Value for fixture bodies),
57    // or when http integration tests are generated (they serialize fixture bodies).
58    let effective_needs_serde_json = needs_serde_json || needs_mock_server || needs_http_tests;
59    let serde_line = if effective_needs_serde_json {
60        "\nserde_json = \"1\""
61    } else {
62        ""
63    };
64    // An empty `[workspace]` table makes the e2e crate its own workspace root, so
65    // it never gets pulled into a parent crate's workspace. This means consumers
66    // don't have to remember to add `e2e/rust` to `workspace.exclude`, and
67    // `cargo fmt`/`cargo build` work the same whether the parent has a
68    // workspace or not.
69    // Mock server requires axum (HTTP router) and tokio-stream (SSE streaming).
70    // The standalone binary additionally needs serde (derive) and walkdir.
71    // Http integration tests require axum-test for the test server.
72    let needs_axum = needs_mock_server || needs_http_tests;
73    let mock_lines = if needs_axum {
74        let mut lines = format!(
75            "\naxum = \"{axum}\"\nserde = {{ version = \"1\", features = [\"derive\"] }}\nwalkdir = \"{walkdir}\"",
76            axum = tv::cargo::AXUM,
77            walkdir = tv::cargo::WALKDIR,
78        );
79        if needs_mock_server {
80            lines.push_str(&format!(
81                "\ntokio-stream = \"{tokio_stream}\"",
82                tokio_stream = tv::cargo::TOKIO_STREAM
83            ));
84        }
85        if needs_http_tests {
86            lines.push_str("\naxum-test = \"20\"\nbytes = \"1\"");
87        }
88        if needs_tower_http {
89            lines.push_str(&format!(
90                "\ntower-http = {{ version = \"{tower_http}\", features = [\"cors\", \"fs\"] }}\ntempfile = \"{tempfile}\"",
91                tower_http = tv::cargo::TOWER_HTTP,
92                tempfile = tv::cargo::TEMPFILE,
93            ));
94        }
95        lines
96    } else {
97        String::new()
98    };
99    let mut machete_ignored: Vec<&str> = Vec::new();
100    if effective_needs_serde_json {
101        machete_ignored.push("\"serde_json\"");
102    }
103    if needs_axum {
104        machete_ignored.push("\"axum\"");
105        machete_ignored.push("\"serde\"");
106        machete_ignored.push("\"walkdir\"");
107    }
108    if needs_mock_server {
109        machete_ignored.push("\"tokio-stream\"");
110    }
111    if needs_http_tests {
112        machete_ignored.push("\"axum-test\"");
113        machete_ignored.push("\"bytes\"");
114    }
115    if needs_tower_http {
116        machete_ignored.push("\"tower-http\"");
117        machete_ignored.push("\"tempfile\"");
118    }
119    let machete_section = if machete_ignored.is_empty() {
120        String::new()
121    } else {
122        format!(
123            "\n[package.metadata.cargo-machete]\nignored = [{}]\n",
124            machete_ignored.join(", ")
125        )
126    };
127    let tokio_line = if needs_tokio {
128        "\ntokio = { version = \"1\", features = [\"full\"] }"
129    } else {
130        ""
131    };
132    let bin_section = if needs_mock_server || needs_http_tests {
133        "\n[[bin]]\nname = \"mock-server\"\npath = \"src/main.rs\"\n"
134    } else {
135        ""
136    };
137    // E2e package version tracks the consumer crate version so `alef sync-versions`
138    // doesn't rewrite the generated file on every prek run. Fall back to "0.1.0" only
139    // when no consumer version is known (test fixtures, etc.).
140    let pkg_version = version.unwrap_or("0.1.0");
141    let header = hash::header(CommentStyle::Hash);
142    format!(
143        r#"{header}
144[workspace]
145
146[package]
147name = "{e2e_name}"
148version = "{pkg_version}"
149edition = "2021"
150license = "MIT"
151publish = false
152{bin_section}
153[dependencies]
154{dep_spec}{serde_line}{mock_lines}{tokio_line}
155{machete_section}"#
156    )
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::config::DependencyMode;
163
164    #[test]
165    fn render_cargo_toml_local_no_features_produces_path_dep() {
166        // When crate_name ("my-crate") differs from dep_name ("my_crate") a
167        // `package = …` key is required to tell Cargo the actual crate name.
168        let out = render_cargo_toml(
169            "my-crate",
170            "my_crate",
171            "../../crates/my-crate",
172            false,
173            false,
174            false,
175            false,
176            false,
177            DependencyMode::Local,
178            None,
179            &[],
180        );
181        assert!(
182            out.contains("my_crate = { package = \"my-crate\", path = \"../../crates/my-crate\" }"),
183            "got:\n{out}"
184        );
185        assert!(out.contains("edition = \"2021\""));
186    }
187
188    #[test]
189    fn render_cargo_toml_local_same_name_produces_simple_path_dep() {
190        // When crate_name and dep_name are identical no `package` key is needed.
191        let out = render_cargo_toml(
192            "my_crate",
193            "my_crate",
194            "../../crates/my_crate",
195            false,
196            false,
197            false,
198            false,
199            false,
200            DependencyMode::Local,
201            None,
202            &[],
203        );
204        assert!(
205            out.contains("my_crate = { path = \"../../crates/my_crate\" }"),
206            "got:\n{out}"
207        );
208    }
209}