sails_cli/
program_new.rs

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        // add sails-rs refs
187        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        // add sails-rs refs
214        self.cargo_add_sails_rs(manifest_path, Normal, None)?;
215        self.cargo_add_sails_rs(manifest_path, Build, Some("build"))?;
216
217        // add app ref
218        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        // add sails-rs refs
233        self.cargo_add_sails_rs(manifest_path, Development, Some("gtest,gclient"))?;
234
235        // add tokio
236        cargo_add(
237            manifest_path,
238            ["tokio"],
239            Development,
240            Some("rt,macros"),
241            self.offline,
242        )?;
243
244        // add app ref
245        cargo_add_by_path(
246            manifest_path,
247            self.app_path(),
248            Development,
249            None,
250            self.offline,
251        )?;
252        // add client ref
253        cargo_add_by_path(
254            manifest_path,
255            self.client_path(),
256            Development,
257            None,
258            self.offline,
259        )?;
260
261        // delete ./src folder
262        fs::remove_dir_all(src_path(path))?;
263
264        // add tests
265        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()) // Don't pollute output
289        .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()) // Don't pollute output
320        .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()) // Don't pollute output
353        .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}