polyvalid/
lib.rs

1//! polyvalid is written to provide a single source of truth for validating
2//! package names, usernames, namespace names, app names.
3
4//! The library can then be used from python and JS.
5mod bindings;
6use anyhow::{bail, Result};
7use regex::Regex;
8
9// Rules:
10// 1. Start with an alphabet character
11// 2. Have one or more alphanumeric characters, or `_` or `-`
12// 3. End with an alphanumeric character
13const APP_PATTERN: &str = r"^[a-zA-Z][-_a-zA-Z0-9]+[a-zA-Z0-9]$";
14
15fn matches_pattern(input_string: &str, pattern_var: &str) -> bool {
16    let pattern_regex = Regex::new(pattern_var).unwrap();
17    pattern_regex.is_match(input_string)
18}
19
20/// Checks if `name` is a valid identifier for username, package name, namespaces, and app names.
21///
22/// # Examples
23///
24/// ```rust
25/// use polyvalid::is_app_name_valid;
26///
27///
28/// let name = "ayush";
29/// let result = is_app_name_valid(name.to_string());
30///
31/// assert!(result.is_ok());
32///
33/// ```
34pub fn is_app_name_valid(name: String) -> Result<()> {
35    if name.len() < 3 {
36        bail!(format!(
37            "App name `{name}` needs to be atleast 3 characters long."
38        ))
39    }
40    let matches: bool = matches_pattern(&name, APP_PATTERN);
41    if !matches {
42        bail!(format!(
43            r#"App name `{name}` does not conform to wasmer's app-name pattern.
44The name should start with an alphabetical character,
45followed by one or more alphanumeric characters or hyphen/underscore characters,
46and end with an alphanumeric character.
47Please modify the name to match the pattern."#
48        ))
49    }
50    // Instead of making the regex more complicated,
51    // lets handle the additional rules here
52    // (there is only one at the moment)
53
54    // we check if string contains `--` because the urls are delemeted with this.
55    // So if a name has -- in it, that will break the url rules.
56    if name.contains("--") {
57        bail!(format!("The app name `{name}` cannot contain `--`. Please modify your app name to match the pattern."))
58    }
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use rstest::rstest;
66
67    #[rstest(
68        // valid cases
69        case::valid_start_with_uppercase("ValidName1", true),
70        case::valid_all_small_letters("helloworld", true),
71        case::valid_all_capital_letters("HELLOWORLD", true),
72        case::valid_start_and_end_with_capital_letter("HelloworlD", true),
73        case::valid_start_with_letter_all_nums("d981273", true),
74        case::valid_capitalized_letters_seperated_by_dash("Valid-Name", true),
75        case::name_with_dash_in_middle("hello-world", true),
76        case::invalid_double_underscore_in_middle("hel__lo", true),
77        // invalid cases
78        case::invalid_starts_with_number("1InvalidName", false),
79        case::invalid_name_with_at_symbol("Inva@lidName", false),
80        case::invalid_name_with_exclamation_mark("InvalidName!", false),
81        case::invalid_no_whitespace(" hello ", false),
82        case::invalid_no_dashes_at_start("-hello", false),
83        case::invalid_no_dashes_at_end("hello-", false),
84        case::invalid_no_dashes_at_start_and_end("-hello-", false),
85        case::invalid_no_underscorees_at_start("_hello", false),
86        case::invalid_no_underscorees_at_end("hello_", false),
87        case::invalid_double_dash_at_start("--hello", false),
88        case::invalid_double_dash_at_end("hello--", false),
89        case::invalid_double_dash_in_middle("hel--lo", false),
90        case::invalid_double_dash_at_start("__hello", false),
91        case::invalid_double_dash_at_end("hello__", false),
92        case::invalid_no_underscorees_at_start_and_end("_hello_", false)
93    )]
94    fn validation_test(#[case] input_string: &str, #[case] should_match: bool) {
95        assert_eq!(
96            is_app_name_valid(input_string.to_string()).is_ok(),
97            should_match
98        );
99    }
100}