basyx_rs/
id_short_from_string.rs

1// SPDX-FileCopyrightText: 2023 Jan Hecht
2//
3// SPDX-License-Identifier: MIT
4
5use regex::{Error, Regex};
6
7#[derive(Debug, PartialEq)]
8pub enum IdShortErrorReason {
9    TooShort,
10    TooLong,
11    NoMatch(String),
12    RegexError(regex::Error),
13}
14
15#[derive(Debug, PartialEq)]
16pub struct IdShortError {
17    reason: IdShortErrorReason,
18}
19
20impl IdShortError {
21    pub fn new(reason: IdShortErrorReason) -> IdShortError {
22        IdShortError { reason }
23    }
24}
25
26impl From<regex::Error> for IdShortError {
27    fn from(value: Error) -> Self {
28        IdShortError::new(IdShortErrorReason::RegexError(value))
29    }
30}
31
32/// id_short from &str
33///
34/// This function checks length and regex bounds, returning a Result.
35pub fn id_short_from_str(hopefully_id_short: &str) -> Result<String, IdShortError> {
36    id_short_from_string(hopefully_id_short.to_string())
37}
38
39/// id_short from String
40///
41/// This function checks length and regex bounds, returning a Result.
42pub fn id_short_from_string(hopefully_id_short: String) -> Result<String, IdShortError> {
43    if hopefully_id_short.is_empty() {
44        Err(IdShortError::new(IdShortErrorReason::TooShort))
45    } else if hopefully_id_short.len() > 128 {
46        Err(IdShortError::new(IdShortErrorReason::TooLong))
47    } else {
48        let re = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*$")?;
49        let re2 = Regex::new(
50            r"^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$",
51        )?;
52
53        if re.is_match(hopefully_id_short.as_str()) && re2.is_match(hopefully_id_short.as_str()) {
54            Ok(hopefully_id_short)
55        } else {
56            Err(IdShortError::new(IdShortErrorReason::NoMatch(
57                hopefully_id_short,
58            )))
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn given_empty_string_then_test_fails() {
69        let result = id_short_from_str("");
70        assert_eq!(result, Err(IdShortError::new(IdShortErrorReason::TooShort)));
71    }
72
73    #[test]
74    fn given_string_too_long_then_test_fails() {
75        let result = id_short_from_str(
76            "a123456789\
77            0123456789\
78            0123456789\
79            0123456789\
80            0123456789\
81            0123456789\
82            0123456789\
83            0123456789\
84            0123456789\
85            0123456789\
86            0123456789\
87            0123456789\
88            012345678",
89        ); // 12 * 10 + 9 = 129 (>128)
90        assert_eq!(result, Err(IdShortError::new(IdShortErrorReason::TooLong)));
91    }
92
93    #[test]
94    fn given_length_max_then_test_passes() {
95        // 12 * 10 + 8 = 128
96        let input = "a123456789\
97            0123456789\
98            0123456789\
99            0123456789\
100            0123456789\
101            0123456789\
102            0123456789\
103            0123456789\
104            0123456789\
105            0123456789\
106            0123456789\
107            0123456789\
108            01234567"
109            .to_string();
110
111        let result = id_short_from_string(input.clone());
112        assert_eq!(result, Ok(input));
113    }
114
115    #[test]
116    fn given_first_character_when_question_mark_then_test_fails() {
117        let input = "?Hello";
118        let result = id_short_from_str(input);
119        assert_eq!(
120            result,
121            Err(IdShortError::new(IdShortErrorReason::NoMatch(
122                input.to_string()
123            )))
124        );
125    }
126
127    #[test]
128    fn given_first_character_when_underscore_then_test_fails() {
129        let input = "_Hello";
130        let result = id_short_from_str(input);
131        assert_eq!(
132            result,
133            Err(IdShortError::new(IdShortErrorReason::NoMatch(
134                input.to_string()
135            )))
136        );
137    }
138
139    #[test]
140    fn given_valid_str_then_passes() {
141        let result = id_short_from_str("my_new_id_short");
142        assert_eq!(result, Ok(String::from("my_new_id_short")));
143    }
144
145    #[test]
146    fn given_single_digit_then_test_fails() {
147        let input = "1";
148        let result = id_short_from_str(input);
149        assert_eq!(
150            result,
151            Err(IdShortError::new(IdShortErrorReason::NoMatch(
152                input.to_string()
153            )))
154        );
155    }
156
157    #[test]
158    fn given_string_len_1_lowercase_test_passes() {
159        let result = id_short_from_str("a");
160        assert_eq!(result, Ok("a".to_string()));
161    }
162
163    #[test]
164    fn given_string_len_1_uppercase_test_passes() {
165        let result = id_short_from_str("A");
166        assert_eq!(result, Ok("A".to_string()));
167    }
168
169    // spaces
170    #[test]
171    fn given_space_then_test_fails() {
172        let input = " ";
173        let result = id_short_from_str(input);
174        assert_eq!(
175            result,
176            Err(IdShortError::new(IdShortErrorReason::NoMatch(
177                input.to_string()
178            )))
179        );
180    }
181
182    #[test]
183    fn given_leading_space_then_test_fails() {
184        let input = " hello";
185        let result = id_short_from_str(input);
186        assert_eq!(
187            result,
188            Err(IdShortError::new(IdShortErrorReason::NoMatch(
189                input.to_string()
190            )))
191        );
192    }
193
194    #[test]
195    fn given_trailing_space_then_test_fails() {
196        let input = "hello ";
197        let result = id_short_from_str(input);
198        assert_eq!(
199            result,
200            Err(IdShortError::new(IdShortErrorReason::NoMatch(
201                input.to_string()
202            )))
203        );
204    }
205
206    #[test]
207    fn given_space_in_middle_then_test_fails() {
208        let input = "hello world";
209        let result = id_short_from_str(input);
210        assert_eq!(
211            result,
212            Err(IdShortError::new(IdShortErrorReason::NoMatch(
213                input.to_string()
214            )))
215        );
216    }
217
218    #[test]
219    fn given_invalid_character_then_test_fails() {
220        let input = "hello?world";
221        let result = id_short_from_str(input);
222        assert_eq!(
223            result,
224            Err(IdShortError::new(IdShortErrorReason::NoMatch(
225                input.to_string()
226            )))
227        );
228    }
229}