camel_cli/commands/
new.rs1use crate::template::embedded::EmbeddedTemplate;
2use crate::template::{ProfileLayout, TemplateContext, TemplateProvider};
3
4#[derive(clap::Args)]
5pub struct NewArgs {
6 pub name: String,
8
9 #[arg(long, default_value = "basic")]
11 pub template: String,
12
13 #[arg(long)]
15 pub force: bool,
16
17 #[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}