Skip to main content

dealer/
scaffold.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::cli::InitKind;
5use crate::error::{DealerError, DealerResult};
6
7pub(crate) fn init_project(kind: InitKind, path: &Path) -> DealerResult<PathBuf> {
8    fs::create_dir_all(path).map_err(|error| DealerError::io(path, error))?;
9
10    let app = path.join("app.x");
11    let package = path.join("package.x");
12    if app.exists() || package.exists() {
13        return Err(DealerError::Backend(format!(
14            "project root '{}' already contains app.x or package.x",
15            path.display()
16        )));
17    }
18
19    let name = project_name(path);
20    let (root_file, content) = match kind {
21        InitKind::App => (
22            app,
23            format!("app {name}\n\tterminal.log\n\t\tmessage: \"Hello from Xtazy\"\n"),
24        ),
25        InitKind::Package => (package, format!("package {name}\n\tdeliver\n")),
26    };
27
28    fs::write(&root_file, content).map_err(|error| DealerError::io(&root_file, error))?;
29    Ok(root_file)
30}
31
32fn project_name(path: &Path) -> String {
33    path.file_name()
34        .and_then(|name| name.to_str())
35        .filter(|name| !name.is_empty() && *name != ".")
36        .map(sanitize_xtazy_ident)
37        .unwrap_or_else(|| "NewProject".to_string())
38}
39
40fn sanitize_xtazy_ident(value: &str) -> String {
41    let mut ident = String::new();
42    let mut upper_next = true;
43    for ch in value.chars() {
44        if ch.is_ascii_alphanumeric() {
45            if upper_next {
46                ident.push(ch.to_ascii_uppercase());
47                upper_next = false;
48            } else {
49                ident.push(ch);
50            }
51        } else {
52            upper_next = true;
53        }
54    }
55    if ident.is_empty() || ident.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
56        format!("Xtazy{ident}")
57    } else {
58        ident
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::test_support::TempProject;
66
67    #[test]
68    fn init_app_creates_app_root_only() {
69        let temp = TempProject::new("init-app");
70
71        let root = init_project(InitKind::App, temp.path()).expect("app init should pass");
72
73        assert_eq!(root, temp.path().join("app.x"));
74        assert!(temp.path().join("app.x").is_file());
75        assert!(!temp.path().join("package.x").exists());
76    }
77
78    #[test]
79    fn init_refuses_existing_root() {
80        let temp = TempProject::new("init-existing");
81        fs::write(temp.path().join("app.x"), "app Existing\n").expect("app.x should be written");
82
83        let error =
84            init_project(InitKind::Package, temp.path()).expect_err("existing root should fail");
85
86        assert!(
87            error
88                .to_string()
89                .contains("already contains app.x or package.x")
90        );
91    }
92}