Skip to main content

myrmic_common/cells/
names.rs

1use alloc::{borrow::ToOwned, string::String};
2use serde::{Deserialize, Serialize};
3
4/// Represents a validated command identifier
5#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
6pub struct Command(String);
7
8/// Represents a validated event identifier
9#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
10pub struct Event(String);
11
12// generates the convenience impls for the name types
13macro_rules! impl_name_type {
14    ($ty: ident) => {
15        impl $ty {
16            #[doc = concat!("Validates `name` and wraps it as a `", stringify!($ty), "`.")]
17            pub fn new(name: String) -> Result<Self, &'static str> {
18                validate_function_name_component(&name)?;
19                Ok(Self(name))
20            }
21        }
22
23        impl AsRef<str> for $ty {
24            fn as_ref(&self) -> &str {
25                &self.0
26            }
27        }
28
29        impl TryFrom<&str> for $ty {
30            type Error = &'static str;
31
32            fn try_from(value: &str) -> Result<Self, Self::Error> {
33                $ty::new(value.to_owned())
34            }
35        }
36
37        impl TryFrom<String> for $ty {
38            type Error = &'static str;
39
40            fn try_from(value: String) -> Result<Self, Self::Error> {
41                $ty::new(value)
42            }
43        }
44
45        // Also implement FromStr for ergonomic parsing
46        impl alloc::str::FromStr for $ty {
47            type Err = &'static str;
48
49            fn from_str(s: &str) -> Result<Self, Self::Err> {
50                $ty::new(s.to_owned())
51            }
52        }
53    };
54}
55
56// generate the impls shared by the name types
57impl_name_type!(Command);
58impl_name_type!(Event);
59
60/// Validates that a string can be used as a component of a function name (we need this for the command and event names).
61/// Since function names will be prefixed (e.g., "command_" or "event_"),
62/// the component can start with a number.
63///
64/// Returns `Ok(())` if valid, `Err(&'static str)` with an error message if invalid.
65fn validate_function_name_component(input: &str) -> Result<(), &'static str> {
66    // Check for empty string
67    if input.is_empty() {
68        return Err("name cannot be empty");
69    }
70
71    if input.contains(char::is_whitespace) {
72        return Err("name cannot contain whitespace");
73    }
74
75    // Check that all characters are ASCII alphanumeric or underscore
76    if !input.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
77        return Err("name can only contain ASCII alphanumeric characters and underscores");
78    }
79
80    Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85
86    // Note to run tests in this crate, we need to run them with `cargo test --target x86_64-unknown-linux-gnu -Z build-std=`
87
88    use super::validate_function_name_component;
89    use claims::{assert_err, assert_ok};
90
91    #[test]
92    fn validate_function_name_component_valid_cases() {
93        // Valid: simple alphanumeric
94        assert_ok!(validate_function_name_component("my_event"));
95        assert_ok!(validate_function_name_component("MyEvent"));
96        assert_ok!(validate_function_name_component("event123"));
97        assert_ok!(validate_function_name_component("_event"));
98        assert_ok!(validate_function_name_component("event_name_123"));
99
100        // Valid: can start with number (since we use prefix)
101        assert_ok!(validate_function_name_component("123event"));
102        assert_ok!(validate_function_name_component("0"));
103
104        // Valid: underscores
105        assert_ok!(validate_function_name_component("_"));
106        assert_ok!(validate_function_name_component("__"));
107        assert_ok!(validate_function_name_component(
108            "event_name_with_underscores"
109        ));
110    }
111
112    #[test]
113    fn validate_function_name_component_empty_string() {
114        assert_err!(validate_function_name_component(""));
115    }
116
117    #[test]
118    fn validate_function_name_component_whitespace_leading() {
119        assert_err!(validate_function_name_component(" event"));
120        assert_err!(validate_function_name_component("  event"));
121        assert_err!(validate_function_name_component("\tevent"));
122        assert_err!(validate_function_name_component("\nevent"));
123        assert_err!(validate_function_name_component("\revent"));
124    }
125
126    #[test]
127    fn validate_function_name_component_whitespace_trailing() {
128        assert_err!(validate_function_name_component("event "));
129        assert_err!(validate_function_name_component("event  "));
130        assert_err!(validate_function_name_component("event\t"));
131        assert_err!(validate_function_name_component("event\n"));
132        assert_err!(validate_function_name_component("event\r"));
133    }
134
135    #[test]
136    fn validate_function_name_component_whitespace_middle() {
137        assert_err!(validate_function_name_component("my event"));
138        assert_err!(validate_function_name_component("my  event"));
139        assert_err!(validate_function_name_component("my\tevent"));
140        assert_err!(validate_function_name_component("my\nevent"));
141        assert_err!(validate_function_name_component("my\revent"));
142        assert_err!(validate_function_name_component("my event name"));
143    }
144
145    #[test]
146    fn validate_function_name_component_special_characters() {
147        // Common special characters that can't be in function names
148        assert_err!(validate_function_name_component("event!"));
149        assert_err!(validate_function_name_component("event@"));
150        assert_err!(validate_function_name_component("event#"));
151        assert_err!(validate_function_name_component("event$"));
152        assert_err!(validate_function_name_component("event%"));
153        assert_err!(validate_function_name_component("event^"));
154        assert_err!(validate_function_name_component("event&"));
155        assert_err!(validate_function_name_component("event*"));
156        assert_err!(validate_function_name_component("event("));
157        assert_err!(validate_function_name_component("event)"));
158        assert_err!(validate_function_name_component("event-"));
159        assert_err!(validate_function_name_component("event+"));
160        assert_err!(validate_function_name_component("event="));
161        assert_err!(validate_function_name_component("event["));
162        assert_err!(validate_function_name_component("event]"));
163        assert_err!(validate_function_name_component("event{"));
164        assert_err!(validate_function_name_component("event}"));
165        assert_err!(validate_function_name_component("event|"));
166        assert_err!(validate_function_name_component("event\\"));
167        assert_err!(validate_function_name_component("event:"));
168        assert_err!(validate_function_name_component("event;"));
169        assert_err!(validate_function_name_component("event\""));
170        assert_err!(validate_function_name_component("event'"));
171        assert_err!(validate_function_name_component("event<"));
172        assert_err!(validate_function_name_component("event>"));
173        assert_err!(validate_function_name_component("event,"));
174        assert_err!(validate_function_name_component("event."));
175        assert_err!(validate_function_name_component("event?"));
176        assert_err!(validate_function_name_component("event/"));
177        assert_err!(validate_function_name_component("event~"));
178        assert_err!(validate_function_name_component("event`"));
179    }
180
181    #[test]
182    fn validate_function_name_component_unicode_characters() {
183        // Unicode characters that aren't valid in function names
184        assert_err!(validate_function_name_component("évént"));
185        assert_err!(validate_function_name_component("event中文"));
186        assert_err!(validate_function_name_component("event🚀"));
187        assert_err!(validate_function_name_component("eventα"));
188        assert_err!(validate_function_name_component("event→"));
189    }
190
191    #[test]
192    fn validate_function_name_component_mixed_invalid() {
193        // Multiple issues
194        assert_err!(validate_function_name_component(" my event! "));
195        assert_err!(validate_function_name_component("event@123"));
196        assert_err!(validate_function_name_component("event-name"));
197        assert_err!(validate_function_name_component("event.name"));
198    }
199
200    #[test]
201    fn validate_function_name_component_edge_cases() {
202        // Just whitespace
203        assert_err!(validate_function_name_component(" "));
204        assert_err!(validate_function_name_component("  "));
205        assert_err!(validate_function_name_component("\t"));
206        assert_err!(validate_function_name_component("\n"));
207
208        // Only special characters
209        assert_err!(validate_function_name_component("!@#$"));
210
211        // Valid but edge cases
212        assert_ok!(validate_function_name_component("a")); // single letter
213        assert_ok!(validate_function_name_component("A")); // single uppercase letter
214        assert_ok!(validate_function_name_component("1")); // single digit (OK with prefix)
215        assert_ok!(validate_function_name_component("_")); // single underscore
216    }
217}