gesha_rust_types/
identifier.rs1use 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 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}