Skip to main content

camel_cli/commands/
new.rs

1use crate::template::embedded::EmbeddedTemplate;
2use crate::template::{ProfileLayout, TemplateContext, TemplateProvider};
3
4#[derive(clap::Args)]
5pub struct NewArgs {
6    /// Project name and directory to create
7    pub name: String,
8
9    /// Template to use (available: basic)
10    #[arg(long, default_value = "basic")]
11    pub template: String,
12
13    /// Overwrite files if the directory already exists
14    #[arg(long)]
15    pub force: bool,
16
17    /// Profile layout: simple ([default] only) or env ([default], [development], [production])
18    #[arg(long, value_name = "LAYOUT", default_value = "env")]
19    pub profile_layout: ProfileLayout,
20}
21
22fn resolve_template(name: &str) -> Result<Box<dyn TemplateProvider>, Box<dyn std::error::Error>> {
23    match name {
24        "basic" => Ok(Box::new(EmbeddedTemplate::basic())),
25        other => Err(format!("Unknown template: '{other}'. Available templates: basic").into()),
26    }
27}
28
29pub fn run_new(args: NewArgs) {
30    let NewArgs {
31        name,
32        template,
33        force,
34        profile_layout,
35    } = args;
36
37    if name.trim().is_empty() {
38        eprintln!("Error: project name must not be empty or whitespace-only");
39        std::process::exit(1);
40    }
41
42    if name.contains("..") || name.contains('\\') {
43        eprintln!("Error: project name must not contain '..' or backslashes");
44        std::process::exit(1);
45    }
46
47    let ctx = TemplateContext {
48        project_name: name.clone(),
49        profile_layout,
50    };
51
52    let provider = resolve_template(&template).unwrap_or_else(|e| {
53        eprintln!("Error: {e}");
54        std::process::exit(1);
55    });
56
57    let files = provider.files(&ctx).unwrap_or_else(|e| {
58        eprintln!("Error generating project: {e}");
59        std::process::exit(1);
60    });
61
62    let target = std::path::Path::new(&name);
63    if target.exists() && !force {
64        let is_non_empty = target.read_dir().is_ok_and(|mut d| d.next().is_some());
65        if is_non_empty {
66            eprintln!(
67                "Directory '{}' already exists and is not empty. Use --force to overwrite.",
68                name
69            );
70            std::process::exit(1);
71        }
72    }
73
74    std::fs::create_dir_all(target).unwrap_or_else(|e| {
75        eprintln!("Failed to create directory '{}': {}", name, e);
76        std::process::exit(1);
77    });
78
79    for file in &files {
80        let file_path = target.join(&file.path);
81        if let Some(parent) = file_path.parent() {
82            std::fs::create_dir_all(parent).unwrap_or_else(|e| {
83                eprintln!("Failed to create directory '{}': {}", parent.display(), e);
84                std::process::exit(1);
85            });
86        }
87        std::fs::write(&file_path, &file.content).unwrap_or_else(|e| {
88            eprintln!("Failed to write '{}': {}", file_path.display(), e);
89            std::process::exit(1);
90        });
91    }
92
93    let display_name = std::path::Path::new(&name)
94        .file_name()
95        .and_then(|n| n.to_str())
96        .unwrap_or(&name);
97    println!("Created camel project: {}\n", display_name);
98    println!("Next steps:");
99    println!("  cd {}", name);
100    println!("  camel run");
101    println!("  camel run --watch");
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn resolve_template_basic_returns_ok() {
110        let result = resolve_template("basic");
111        assert!(result.is_ok());
112        assert_eq!(result.unwrap().name(), "basic");
113    }
114
115    #[test]
116    fn resolve_template_unknown_returns_error() {
117        let result = resolve_template("nonexistent");
118        match result {
119            Ok(_) => panic!("expected error, got Ok"),
120            Err(err) => {
121                let msg = err.to_string();
122                assert!(msg.contains("Unknown template"), "got: {msg}");
123                assert!(msg.contains("nonexistent"), "got: {msg}");
124            }
125        }
126    }
127
128    #[test]
129    fn test_run_new_empty_name_exits() {
130        let name = "";
131        assert!(name.trim().is_empty());
132    }
133
134    #[test]
135    fn test_run_new_whitespace_name_detected() {
136        let name = "   ";
137        assert!(name.trim().is_empty());
138    }
139}