Skip to main content

frame_cli/scaffold/
names.rs

1//! Application-name validation: one name must satisfy both the Rust crate
2//! identifier rule and the Gleam project-name rule.
3
4use super::NewError;
5
6/// Validates an application name against both language rules.
7///
8/// # Errors
9///
10/// Returns the specific rule (or both) the name violates, with the exact
11/// grammar that would pass.
12pub fn validate_name(name: &str) -> Result<(), NewError> {
13    let crate_error = crate_name_error(name);
14    let gleam_error = gleam_name_error(name);
15    match (crate_error, gleam_error) {
16        (None, None) => Ok(()),
17        (Some(crate_reason), None) => Err(NewError::InvalidCrateName {
18            name: name.to_owned(),
19            reason: crate_reason,
20        }),
21        (None, Some(gleam_reason)) => Err(NewError::InvalidGleamName {
22            name: name.to_owned(),
23            reason: gleam_reason,
24        }),
25        (Some(crate_reason), Some(gleam_reason)) => Err(NewError::InvalidBothNames {
26            name: name.to_owned(),
27            crate_reason,
28            gleam_reason,
29        }),
30    }
31}
32
33fn crate_name_error(name: &str) -> Option<&'static str> {
34    const KEYWORDS: &[&str] = &[
35        "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
36        "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if",
37        "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv",
38        "pub", "ref", "return", "self", "static", "struct", "super", "trait", "true", "try",
39        "type", "typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while",
40        "yield",
41    ];
42    let mut chars = name.chars();
43    if name == "_"
44        || !chars
45            .next()
46            .is_some_and(|first| first == '_' || first.is_ascii_alphabetic())
47        || !chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
48    {
49        return Some("it must be an ASCII Rust identifier");
50    }
51    KEYWORDS
52        .contains(&name)
53        .then_some("Rust keywords are not crate identifiers")
54}
55
56fn gleam_name_error(name: &str) -> Option<&'static str> {
57    let mut chars = name.chars();
58    if !chars.next().is_some_and(|first| first.is_ascii_lowercase())
59        || !chars.all(|character| {
60            character == '_' || character.is_ascii_lowercase() || character.is_ascii_digit()
61        })
62    {
63        Some(
64            "it must start with a lowercase ASCII letter and contain only lowercase letters, digits, or underscores",
65        )
66    } else {
67        None
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::validate_name;
74
75    #[test]
76    fn invalid_names_report_crate_gleam_and_both_rules() {
77        let crate_error = validate_name("type").err().map(|error| error.to_string());
78        assert!(crate_error.is_some_and(|message| {
79            message.contains("Rust crate identifier") && message.contains("[a-z][a-z0-9_]*")
80        }));
81
82        let gleam_error = validate_name("Hello").err().map(|error| error.to_string());
83        assert!(gleam_error.is_some_and(|message| {
84            message.contains("Gleam project name") && message.contains("[a-z][a-z0-9_]*")
85        }));
86
87        let both_error = validate_name("bad-name")
88            .err()
89            .map(|error| error.to_string());
90        assert!(both_error.is_some_and(|message| {
91            message.contains("fails both rules")
92                && message.contains("Rust crate identifier")
93                && message.contains("Gleam project name")
94        }));
95    }
96}