Skip to main content

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