1use anyhow::Context;
2use askama::Template;
3use cargo_metadata::DependencyKind::{Build, Development, Normal};
4use convert_case::{Case, Casing};
5use std::{
6 env,
7 ffi::OsStr,
8 fs::{self, File},
9 path::{Path, PathBuf},
10 process::{Command, ExitStatus},
11};
12
13const SAILS_VERSION: &str = env!("CARGO_PKG_VERSION");
14
15#[derive(Template)]
16#[template(path = "app/build.askama")]
17struct AppBuild;
18
19#[derive(Template)]
20#[template(path = "app/src/lib.askama")]
21struct AppLib {
22 service_name: String,
23 service_name_snake: String,
24 program_struct_name: String,
25}
26
27#[derive(Template)]
28#[template(path = "client/build.askama")]
29struct ClientBuild {
30 program_crate_name: String,
31 program_struct_name: String,
32}
33
34#[derive(Template)]
35#[template(path = "client/src/lib.askama")]
36struct ClientLib {
37 client_file_name: String,
38}
39
40#[derive(Template)]
41#[template(path = "tests/gtest.askama")]
42struct TestsGtest {
43 program_crate_name: String,
44 client_crate_name: String,
45 client_program_name: String,
46 service_name: String,
47}
48
49pub struct ProgramGenerator {
50 path: PathBuf,
51 package_name: String,
52 sails_path: Option<PathBuf>,
53 app: bool,
54 offline: bool,
55}
56
57impl ProgramGenerator {
58 pub fn new(
59 path: PathBuf,
60 name: Option<String>,
61 sails_path: Option<PathBuf>,
62 app: bool,
63 offline: bool,
64 ) -> Self {
65 let package_name = name.map_or_else(
66 || {
67 path.file_name()
68 .expect("Invalid Path")
69 .to_str()
70 .expect("Invalid UTF-8 Path")
71 .to_case(Case::Kebab)
72 },
73 |name| name.to_case(Case::Kebab),
74 );
75 Self {
76 path,
77 package_name,
78 sails_path,
79 app,
80 offline,
81 }
82 }
83
84 fn app_build(&self) -> AppBuild {
85 AppBuild
86 }
87
88 fn app_lib(&self) -> AppLib {
89 AppLib {
90 service_name: "Service".to_string(),
91 service_name_snake: "service".to_string(),
92 program_struct_name: "Program".to_string(),
93 }
94 }
95
96 fn client_build(&self) -> ClientBuild {
97 ClientBuild {
98 program_crate_name: self.app_name().to_case(Case::Snake),
99 program_struct_name: "Program".to_string(),
100 }
101 }
102
103 fn client_lib(&self) -> ClientLib {
104 ClientLib {
105 client_file_name: format!("{}_client", self.package_name.to_case(Case::Snake)),
106 }
107 }
108
109 fn tests_gtest(&self) -> TestsGtest {
110 TestsGtest {
111 program_crate_name: self.app_name().to_case(Case::Snake),
112 client_crate_name: self.client_name().to_case(Case::Snake),
113 client_program_name: self.client_name().to_case(Case::Pascal),
114 service_name: "Service".to_string(),
115 }
116 }
117
118 fn app_path(&self) -> PathBuf {
119 if self.app {
120 self.path.clone()
121 } else {
122 self.path.join("app")
123 }
124 }
125
126 fn app_name(&self) -> String {
127 if self.app {
128 self.package_name.clone()
129 } else {
130 format!("{}-app", self.package_name)
131 }
132 }
133
134 fn client_path(&self) -> PathBuf {
135 self.path.join("client")
136 }
137
138 fn client_name(&self) -> String {
139 format!("{}-client", self.package_name)
140 }
141
142 fn cargo_add_sails_rs<P: AsRef<Path>>(
143 &self,
144 manifest_path: P,
145 dependency: cargo_metadata::DependencyKind,
146 features: Option<&str>,
147 ) -> anyhow::Result<ExitStatus> {
148 if let Some(sails_path) = self.sails_path.as_ref() {
149 cargo_add_by_path(
150 manifest_path,
151 sails_path,
152 dependency,
153 features,
154 self.offline,
155 )
156 } else {
157 let sails_package = &[format!("sails-rs@{SAILS_VERSION}")];
158 cargo_add(
159 manifest_path,
160 sails_package,
161 dependency,
162 features,
163 self.offline,
164 )
165 }
166 }
167
168 pub fn generate(self) -> anyhow::Result<()> {
169 if self.app {
170 self.generate_app()?;
171 } else {
172 self.generate_root()?;
173 self.generate_app()?;
174 self.generate_client()?;
175 self.generate_tests()?;
176 }
177 self.fmt()?;
178 Ok(())
179 }
180
181 fn generate_app(&self) -> anyhow::Result<()> {
182 let path = &self.app_path();
183 cargo_new(path, &self.app_name(), self.offline)?;
184 let manifest_path = &manifest_path(path);
185
186 self.cargo_add_sails_rs(manifest_path, Normal, None)?;
188 self.cargo_add_sails_rs(manifest_path, Build, Some("wasm-builder"))?;
189
190 let mut build_rs = File::create(build_rs_path(path))?;
191 self.app_build().write_into(&mut build_rs)?;
192
193 let mut lib_rs = File::create(lib_rs_path(path))?;
194 self.app_lib().write_into(&mut lib_rs)?;
195
196 Ok(())
197 }
198
199 fn generate_root(&self) -> anyhow::Result<()> {
200 let path = &self.path;
201 cargo_new(path, &self.package_name, self.offline)?;
202
203 cargo_toml_create_workspace(manifest_path(path))?;
204
205 Ok(())
206 }
207
208 fn generate_client(&self) -> anyhow::Result<()> {
209 let path = &self.client_path();
210 cargo_new(path, &self.client_name(), self.offline)?;
211
212 let manifest_path = &manifest_path(path);
213 self.cargo_add_sails_rs(manifest_path, Normal, None)?;
215 self.cargo_add_sails_rs(manifest_path, Build, Some("build"))?;
216
217 cargo_add_by_path(manifest_path, self.app_path(), Build, None, self.offline)?;
219
220 let mut build_rs = File::create(build_rs_path(path))?;
221 self.client_build().write_into(&mut build_rs)?;
222
223 let mut lib_rs = File::create(lib_rs_path(path))?;
224 self.client_lib().write_into(&mut lib_rs)?;
225
226 Ok(())
227 }
228
229 fn generate_tests(&self) -> anyhow::Result<()> {
230 let path = &self.path;
231 let manifest_path = &manifest_path(path);
232 self.cargo_add_sails_rs(manifest_path, Development, Some("gtest,gclient"))?;
234
235 cargo_add(
237 manifest_path,
238 ["tokio"],
239 Development,
240 Some("rt,macros"),
241 self.offline,
242 )?;
243
244 cargo_add_by_path(
246 manifest_path,
247 self.app_path(),
248 Development,
249 None,
250 self.offline,
251 )?;
252 cargo_add_by_path(
254 manifest_path,
255 self.client_path(),
256 Development,
257 None,
258 self.offline,
259 )?;
260
261 fs::remove_dir_all(src_path(path))?;
263
264 let test_path = &tests_path(path);
266 fs::create_dir_all(test_path.parent().context("Parent should exists")?)?;
267 let mut gtest_rs = File::create(test_path)?;
268 self.tests_gtest().write_into(&mut gtest_rs)?;
269
270 Ok(())
271 }
272
273 fn fmt(&self) -> anyhow::Result<ExitStatus> {
274 let manifest_path = &manifest_path(&self.path);
275 cargo_fmt(manifest_path)
276 }
277}
278
279fn cargo_new<P: AsRef<Path>>(
280 target_dir: P,
281 name: &str,
282 offline: bool,
283) -> anyhow::Result<ExitStatus> {
284 let cargo_command = cargo_command();
285 let target_dir = target_dir.as_ref();
286 let cargo_new_or_init = if target_dir.exists() { "init" } else { "new" };
287 let mut cmd = Command::new(cargo_command);
288 cmd.stdout(std::process::Stdio::null()) .arg(cargo_new_or_init)
290 .arg(target_dir)
291 .arg("--name")
292 .arg(name)
293 .arg("--lib")
294 .arg("--quiet");
295
296 if offline {
297 cmd.arg("--offline");
298 }
299
300 cmd.status()
301 .context("failed to execute `cargo new` command")
302}
303
304fn cargo_add<P, I, S>(
305 manifest_path: P,
306 packages: I,
307 dependency: cargo_metadata::DependencyKind,
308 features: Option<&str>,
309 offline: bool,
310) -> anyhow::Result<ExitStatus>
311where
312 P: AsRef<Path>,
313 I: IntoIterator<Item = S>,
314 S: AsRef<OsStr>,
315{
316 let cargo_command = cargo_command();
317
318 let mut cmd = Command::new(cargo_command);
319 cmd.stdout(std::process::Stdio::null()) .arg("add")
321 .args(packages)
322 .arg("--manifest-path")
323 .arg(manifest_path.as_ref())
324 .arg("--quiet");
325
326 match dependency {
327 Development => {
328 cmd.arg("--dev");
329 }
330 Build => {
331 cmd.arg("--build");
332 }
333 _ => (),
334 };
335
336 if let Some(features) = features {
337 cmd.arg("--features").arg(features);
338 }
339
340 if offline {
341 cmd.arg("--offline");
342 }
343
344 cmd.status()
345 .context("failed to execute `cargo add` command")
346}
347
348fn cargo_fmt<P: AsRef<Path>>(manifest_path: P) -> anyhow::Result<ExitStatus> {
349 let cargo_command = cargo_command();
350
351 let mut cmd = Command::new(cargo_command);
352 cmd.stdout(std::process::Stdio::null()) .arg("fmt")
354 .arg("--manifest-path")
355 .arg(manifest_path.as_ref())
356 .arg("--quiet");
357
358 cmd.status()
359 .context("failed to execute `cargo fmt` command")
360}
361
362fn cargo_add_by_path<P1: AsRef<Path>, P2: AsRef<Path>>(
363 manifest_path: P1,
364 crate_path: P2,
365 dependency: cargo_metadata::DependencyKind,
366 features: Option<&str>,
367 offline: bool,
368) -> anyhow::Result<ExitStatus> {
369 let crate_path = crate_path.as_ref().to_str().context("Invalid UTF-8 Path")?;
370 let package = &["--path", crate_path];
371 cargo_add(manifest_path, package, dependency, features, offline)
372}
373
374fn cargo_toml_create_workspace<P: AsRef<Path>>(manifest_path: P) -> anyhow::Result<()> {
375 let manifest_path = manifest_path.as_ref();
376 let cargo_toml = fs::read_to_string(manifest_path)?;
377 let mut document: toml_edit::DocumentMut = cargo_toml.parse()?;
378
379 let workspace = document
380 .entry("workspace")
381 .or_insert_with(toml_edit::table)
382 .as_table_mut()
383 .context("workspace was not a table in Cargo.toml")?;
384 _ = workspace
385 .entry("resolver")
386 .or_insert_with(|| toml_edit::value("3"));
387 _ = workspace
388 .entry("members")
389 .or_insert_with(|| toml_edit::value(toml_edit::Array::new()));
390
391 let workspace_package = workspace
392 .entry("package")
393 .or_insert_with(toml_edit::table)
394 .as_table_mut()
395 .context("workspace.package was not a table in Cargo.toml")?;
396 _ = workspace_package
397 .entry("version")
398 .or_insert_with(|| toml_edit::value("0.1.0"));
399 _ = workspace_package
400 .entry("edition")
401 .or_insert_with(|| toml_edit::value("2024"));
402
403 fs::write(manifest_path, document.to_string())?;
404
405 Ok(())
406}
407
408fn manifest_path<P: AsRef<Path>>(path: P) -> PathBuf {
409 path.as_ref().join("Cargo.toml")
410}
411
412fn build_rs_path<P: AsRef<Path>>(path: P) -> PathBuf {
413 path.as_ref().join("build.rs")
414}
415
416fn src_path<P: AsRef<Path>>(path: P) -> PathBuf {
417 path.as_ref().join("src")
418}
419
420fn lib_rs_path<P: AsRef<Path>>(path: P) -> PathBuf {
421 path.as_ref().join("src/lib.rs")
422}
423
424fn tests_path<P: AsRef<Path>>(path: P) -> PathBuf {
425 path.as_ref().join("tests/gtest.rs")
426}
427
428fn cargo_command() -> String {
429 std::env::var("CARGO").unwrap_or("cargo".into())
430}