1use std::fs;
4use std::io::ErrorKind;
5use std::path::Path;
6use std::process::ExitCode;
7
8use nifty_syntax::project::ENTRY_FILE;
9
10use crate::CommandResult;
11use crate::error::CliError;
12
13const TEMPLATE: &str = include_str!("../templates/main.nf");
14const TITLE_PLACEHOLDER: &str = "{{title}}";
15
16pub(crate) fn run(name: &str) -> CommandResult {
17 validate_name(name)?;
18 let dir = Path::new(name);
19 fs::create_dir(dir).map_err(|error| match error.kind() {
20 ErrorKind::AlreadyExists => CliError::new(format!("{name} already exists")),
21 _ => CliError::new(format!("can't create {name}: {error}")),
22 })?;
23
24 let entry = dir.join(ENTRY_FILE);
25 fs::write(&entry, starter_source(name))
26 .map_err(|error| CliError::new(format!("can't write {}: {error}", entry.display())))?;
27
28 println!("Created {name}/{ENTRY_FILE}\n\nRun it with:\n nf run {name}");
29 Ok(ExitCode::SUCCESS)
30}
31
32fn validate_name(name: &str) -> Result<(), CliError> {
34 let mut chars = name.chars();
35 let starts_with_letter = chars.next().is_some_and(|c| c.is_ascii_alphabetic());
36 let rest_is_valid = chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
37 if starts_with_letter && rest_is_valid {
38 Ok(())
39 } else {
40 Err(CliError::new(format!(
41 "`{name}` isn't a valid app name; start with a letter and use only letters, digits, `-` and `_`"
42 )))
43 }
44}
45
46fn starter_source(name: &str) -> String {
47 TEMPLATE.replace(TITLE_PLACEHOLDER, name)
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 #[test]
55 fn accepts_simple_names() {
56 for name in ["todo", "Todo2", "my-app", "my_app"] {
57 assert!(validate_name(name).is_ok(), "{name} should be valid");
58 }
59 }
60
61 #[test]
62 fn rejects_names_that_are_not_portable_folder_names() {
63 for name in ["", "2do", "-app", "my app", "a/b", "../x", "app.nf", "café"] {
64 assert!(validate_name(name).is_err(), "{name:?} should be rejected");
65 }
66 }
67
68 #[test]
69 fn starter_source_is_a_valid_program_titled_after_the_app() {
70 let source = starter_source("todo");
71 assert!(source.contains(r#"window("todo","#));
72 assert_eq!(
73 nifty_syntax::parse(&source, nifty_syntax::FileId::default()).diagnostics,
74 []
75 );
76 }
77}