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    let header = hash::header(CommentStyle::Hash);
138    format!(
139        r#"{header}
140[workspace]
141
142[package]
143name = "{e2e_name}"
144version = "0.1.0"
145edition = "2021"
146license = "MIT"
147publish = false
148{bin_section}
149[dependencies]
150{dep_spec}{serde_line}{mock_lines}{tokio_line}
151{machete_section}"#
152    )
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::config::DependencyMode;
159
160    #[test]
161    fn render_cargo_toml_local_no_features_produces_path_dep() {
162        // When crate_name ("my-crate") differs from dep_name ("my_crate") a
163        // `package = …` key is required to tell Cargo the actual crate name.
164        let out = render_cargo_toml(
165            "my-crate",
166            "my_crate",
167            "../../crates/my-crate",
168            false,
169            false,
170            false,
171            false,
172            false,
173            DependencyMode::Local,
174            None,
175            &[],
176        );
177        assert!(
178            out.contains("my_crate = { package = \"my-crate\", path = \"../../crates/my-crate\" }"),
179            "got:\n{out}"
180        );
181        assert!(out.contains("edition = \"2021\""));
182    }
183
184    #[test]
185    fn render_cargo_toml_local_same_name_produces_simple_path_dep() {
186        // When crate_name and dep_name are identical no `package` key is needed.
187        let out = render_cargo_toml(
188            "my_crate",
189            "my_crate",
190            "../../crates/my_crate",
191            false,
192            false,
193            false,
194            false,
195            false,
196            DependencyMode::Local,
197            None,
198            &[],
199        );
200        assert!(
201            out.contains("my_crate = { path = \"../../crates/my_crate\" }"),
202            "got:\n{out}"
203        );
204    }
205}