voa_core/identifiers/
base.rs1use std::{
4 fmt::Display,
5 ops::Deref,
6 path::{MAIN_SEPARATOR, Path, PathBuf},
7 str::FromStr,
8};
9
10use winnow::{
11 ModalResult,
12 Parser,
13 combinator::{cut_err, eof, repeat},
14 error::StrContext,
15 token::one_of,
16};
17
18#[cfg(doc)]
19use crate::identifiers::{
20 Context,
21 CustomContext,
22 CustomRole,
23 CustomTechnology,
24 Os,
25 Purpose,
26 Technology,
27};
28use crate::{Error, iter_char_context};
29
30#[derive(Debug)]
40pub(crate) struct SegmentPath(PathBuf);
41
42impl SegmentPath {
43 pub fn new(path: PathBuf) -> Result<Self, Error> {
45 if path.is_absolute() {
46 return Err(Error::InvalidSegmentPath {
47 path,
48 context: "it is absolute".to_string(),
49 });
50 }
51 if path.to_string_lossy().contains(MAIN_SEPARATOR) {
52 return Err(Error::InvalidSegmentPath {
53 path,
54 context: format!("it contains the path separator {MAIN_SEPARATOR} character"),
55 });
56 }
57
58 Ok(Self(path))
59 }
60}
61
62impl AsRef<Path> for SegmentPath {
63 fn as_ref(&self) -> &Path {
64 &self.0
65 }
66}
67
68impl TryFrom<String> for SegmentPath {
69 type Error = Error;
70
71 fn try_from(s: String) -> Result<Self, Self::Error> {
72 Self::new(PathBuf::from(s))
73 }
74}
75
76impl FromStr for SegmentPath {
77 type Err = Error;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 Self::new(PathBuf::from(s))
81 }
82}
83
84#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
91#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
92pub struct IdentifierString(String);
93
94impl IdentifierString {
95 pub const SPECIAL_CHARS: &[char; 3] = &['_', '-', '.'];
98
99 pub fn valid_chars(input: &mut &str) -> ModalResult<char> {
110 one_of((
111 |c: char| c.is_ascii_lowercase(),
112 |c: char| c.is_ascii_digit(),
113 Self::SPECIAL_CHARS,
114 ))
115 .context(StrContext::Expected(
116 winnow::error::StrContextValue::Description("lowercase alphanumeric ASCII characters"),
117 ))
118 .context_with(iter_char_context!(Self::SPECIAL_CHARS))
119 .parse_next(input)
120 }
121
122 pub fn parser(input: &mut &str) -> ModalResult<Self> {
149 let id_string = repeat::<_, _, (), _, _>(1.., Self::valid_chars)
150 .take()
151 .context(StrContext::Label("VOA identifier string"))
152 .parse_next(input)?;
153
154 cut_err(eof)
155 .context(StrContext::Label("VOA identifier string"))
156 .context(StrContext::Expected(
157 winnow::error::StrContextValue::Description(
158 "lowercase alphanumeric ASCII characters",
159 ),
160 ))
161 .context_with(iter_char_context!(Self::SPECIAL_CHARS))
162 .parse_next(input)?;
163
164 Ok(Self(id_string.to_string()))
165 }
166
167 pub fn as_str(&self) -> &str {
169 &self.0
170 }
171}
172
173impl AsRef<str> for IdentifierString {
174 fn as_ref(&self) -> &str {
175 &self.0
176 }
177}
178
179impl Deref for IdentifierString {
180 type Target = str;
181 fn deref(&self) -> &Self::Target {
182 self.0.deref()
183 }
184}
185
186impl Display for IdentifierString {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 write!(f, "{}", self.0)
189 }
190}
191
192impl FromStr for IdentifierString {
193 type Err = crate::Error;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
205 Ok(Self::parser.parse(s)?)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211
212 use rstest::rstest;
213 use testresult::TestResult;
214
215 use super::*;
216
217 #[rstest]
218 #[case::absolute_path("/example")]
219 #[case::path_contains_path_separator("example/foo")]
220 fn segment_path_from_str_fails(#[case] input: &str) -> TestResult {
221 match SegmentPath::from_str(input) {
222 Err(Error::InvalidSegmentPath { .. }) => {}
223 Err(error) => panic!(
224 "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
225 ),
226 Ok(path) => panic!(
227 "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
228 ),
229 }
230 match SegmentPath::try_from(input.to_string()) {
231 Err(Error::InvalidSegmentPath { .. }) => {}
232 Err(error) => panic!(
233 "Expected to fail with an Error::InvalidSegmentPath, but failed with a different error instead: {error}"
234 ),
235 Ok(path) => panic!(
236 "Expected to fail with an Error::InvalidSegmentPath, but succeeded instead: {path:?}"
237 ),
238 }
239
240 Ok(())
241 }
242
243 #[test]
244 fn segment_path_from_str_succeeds() -> TestResult {
245 let input = "example";
246 match SegmentPath::from_str(input) {
247 Ok(_) => {}
248 Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
249 }
250 match SegmentPath::try_from(input.to_string()) {
251 Ok(_) => {}
252 Err(error) => panic!("Expected to succeed, but failed instead: {error}"),
253 }
254
255 Ok(())
256 }
257
258 #[rstest]
259 #[case::alpha("foo")]
260 #[case::alpha_numeric("foo123")]
261 #[case::alpha_numeric_special("foo-123")]
262 #[case::alpha_numeric_special("foo_123")]
263 #[case::alpha_numeric_special("foo.123")]
264 #[case::only_special_chars("._-")]
265 fn identifier_string_from_str_valid_chars(#[case] input: &str) -> TestResult {
266 match IdentifierString::from_str(input) {
267 Ok(id_string) => {
268 assert_eq!(id_string, IdentifierString(input.to_string()));
269 Ok(())
270 }
271 Err(error) => {
272 panic!("Should have succeeded to parse {input} but failed: {error}");
273 }
274 }
275 }
276
277 #[rstest]
278 #[case::empty_string("", "\n^")]
279 #[case::all_caps("FOO", "FOO\n^")]
280 #[case::one_caps("foO", "foO\n ^")]
281 #[case::one_caps("foo:", "foo:\n ^")]
282 #[case::one_caps("foö", "foö\n ^")]
283 fn identifier_string_from_str_invalid_chars(
284 #[case] input: &str,
285 #[case] error_msg: &str,
286 ) -> TestResult {
287 match IdentifierString::from_str(input) {
288 Ok(id_string) => {
289 panic!("Should have failed to parse {input} but succeeded: {id_string}");
290 }
291 Err(error) => {
292 assert_eq!(
293 error.to_string(),
294 format!(
295 "Parser error:\n{error_msg}\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
296 )
297 );
298 Ok(())
299 }
300 }
301 }
302}