gesha_rust_types/
identifier.rs

1use crate::ModuleName;
2use gesha_core::conversions::Error::InvalidToken;
3use gesha_core::conversions::Result;
4use heck::{ToSnakeCase, ToUpperCamelCase};
5use std::fmt::{Display, Formatter};
6use syn::Ident;
7use syn::parse_str;
8
9#[derive(Clone, Debug, Hash, Eq, PartialEq)]
10pub struct TypeIdentifier(String);
11
12impl TypeIdentifier {
13    pub fn parse<A: AsRef<str>>(a: A) -> Result<Self> {
14        let a = a.as_ref();
15        let converted = a.to_upper_camel_case();
16        let result = parse_str::<Ident>(&converted);
17        if result.is_ok() {
18            return Ok(Self(converted));
19        }
20        let init: Vec<String> = vec!["".to_string()];
21        let mut converted = a
22            .chars()
23            .fold(init, replace_symbol_with_name)
24            .join("_")
25            .to_upper_camel_case();
26
27        if converted.starts_with(char::is_numeric) {
28            converted = "_".to_string() + &converted;
29        }
30        if converted.is_empty() || !converted.is_ascii() {
31            return Err(InvalidToken {
32                target: a.to_string(),
33            });
34        }
35        Ok(Self(converted))
36    }
37
38    pub fn to_mod_name(&self) -> ModuleName {
39        ModuleName::new(self.0.to_snake_case())
40    }
41}
42
43fn replace_symbol_with_name(mut acc: Vec<String>, c: char) -> Vec<String> {
44    match ascii_symbol_to_name(c) {
45        Some(converted) => {
46            acc.push(converted.into());
47            acc.push("".to_string());
48        }
49        _ => {
50            let last = acc.len() - 1;
51            acc[last].push(c);
52        }
53    };
54    acc
55}
56
57impl Display for TypeIdentifier {
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        Display::fmt(&self.0, f)
60    }
61}
62
63impl AsRef<str> for TypeIdentifier {
64    fn as_ref(&self) -> &str {
65        &self.0
66    }
67}
68
69impl From<TypeIdentifier> for String {
70    fn from(this: TypeIdentifier) -> Self {
71        this.0
72    }
73}
74
75impl PartialEq<&str> for TypeIdentifier {
76    fn eq(&self, other: &&str) -> bool {
77        self.0 == *other
78    }
79}
80
81fn ascii_symbol_to_name(c: char) -> Option<&'static str> {
82    let str = match c {
83        ' ' => "space",
84        '!' => "exclamation",
85        '"' => "double_quote",
86        '#' => "hash",
87        '$' => "dollar",
88        '%' => "percent",
89        '&' => "ampersand",
90        '\'' => "apostrophe",
91        '(' => "left_parenthesis",
92        ')' => "right_parenthesis",
93        '*' => "asterisk",
94        '+' => "plus",
95        ',' => "comma",
96        '-' => "minus",
97        '.' => "period",
98        '/' => "slash",
99        ':' => "colon",
100        ';' => "semicolon",
101        '<' => "less_than",
102        '=' => "equals",
103        '>' => "greater_than",
104        '?' => "question",
105        '@' => "at",
106        '[' => "left_bracket",
107        '\\' => "backslash",
108        ']' => "right_bracket",
109        '^' => "caret",
110        '_' => "underscore",
111        '`' => "backtick",
112        '{' => "left_brace",
113        '|' => "pipe",
114        '}' => "right_brace",
115        '~' => "tilde",
116        _ => {
117            // non-ascii character
118            return None;
119        }
120    };
121    Some(str)
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use rstest::rstest;
128
129    #[rstest]
130    #[case::as_it_is("hello_world", "HelloWorld")]
131    #[case::only_symbol("*+-/", "AsteriskPlusMinusSlash")]
132    #[case::only_symbol("123foo", "_123foo")]
133    #[case::only_symbol("1+foo=345%bar", "_1PlusFooEquals345PercentBar")]
134    #[case::with_minus("-42", "Minus42")]
135    #[case::with_numeric_and_symbol("_42", "Underscore42")]
136    #[case::with_symbol_and_numeric("%_42", "PercentUnderscore42")]
137    fn ok(#[case] input: &str, #[case] expected: &str) {
138        let actual = TypeIdentifier::parse(input).unwrap();
139        assert_eq!(actual, expected);
140    }
141
142    #[rstest]
143    #[case::empty_string("")]
144    #[case::non_ascii("🔥🔥🔥")]
145    fn ng(#[case] input: &str) {
146        let actual = match TypeIdentifier::parse(input) {
147            Err(InvalidToken { target }) => target,
148            other => panic!("expected error not returned but got: {other:?}"),
149        };
150        let expected = input.to_string();
151        assert_eq!(actual, expected);
152    }
153}