voa_core/identifiers/
base.rs1use std::{fmt::Display, ops::Deref, str::FromStr};
4
5use winnow::{
6 ModalResult,
7 Parser,
8 combinator::{cut_err, eof, repeat},
9 error::StrContext,
10 token::one_of,
11};
12
13#[cfg(doc)]
14use crate::identifiers::{CustomContext, CustomRole, CustomTechnology, Os};
15use crate::iter_char_context;
16
17#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
24#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
25pub struct IdentifierString(String);
26
27impl IdentifierString {
28 pub const SPECIAL_CHARS: &[char; 3] = &['_', '-', '.'];
31
32 pub fn valid_chars(input: &mut &str) -> ModalResult<char> {
43 one_of((
44 |c: char| c.is_ascii_lowercase(),
45 |c: char| c.is_ascii_digit(),
46 Self::SPECIAL_CHARS,
47 ))
48 .context(StrContext::Expected(
49 winnow::error::StrContextValue::Description("lowercase alphanumeric ASCII characters"),
50 ))
51 .context_with(iter_char_context!(Self::SPECIAL_CHARS))
52 .parse_next(input)
53 }
54
55 pub fn parser(input: &mut &str) -> ModalResult<Self> {
82 let id_string = repeat::<_, _, (), _, _>(1.., Self::valid_chars)
83 .take()
84 .context(StrContext::Label("VOA identifier string"))
85 .parse_next(input)?;
86
87 cut_err(eof)
88 .context(StrContext::Label("VOA identifier string"))
89 .context(StrContext::Expected(
90 winnow::error::StrContextValue::Description(
91 "lowercase alphanumeric ASCII characters",
92 ),
93 ))
94 .context_with(iter_char_context!(Self::SPECIAL_CHARS))
95 .parse_next(input)?;
96
97 Ok(Self(id_string.to_string()))
98 }
99
100 pub fn as_str(&self) -> &str {
102 &self.0
103 }
104}
105
106impl AsRef<str> for IdentifierString {
107 fn as_ref(&self) -> &str {
108 &self.0
109 }
110}
111
112impl Deref for IdentifierString {
113 type Target = str;
114 fn deref(&self) -> &Self::Target {
115 self.0.deref()
116 }
117}
118
119impl Display for IdentifierString {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(f, "{}", self.0)
122 }
123}
124
125impl FromStr for IdentifierString {
126 type Err = crate::Error;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 Ok(Self::parser.parse(s)?)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144
145 use rstest::rstest;
146 use testresult::TestResult;
147
148 use super::*;
149
150 #[rstest]
151 #[case::alpha("foo")]
152 #[case::alpha_numeric("foo123")]
153 #[case::alpha_numeric_special("foo-123")]
154 #[case::alpha_numeric_special("foo_123")]
155 #[case::alpha_numeric_special("foo.123")]
156 #[case::only_special_chars("._-")]
157 fn identifier_string_from_str_valid_chars(#[case] input: &str) -> TestResult {
158 match IdentifierString::from_str(input) {
159 Ok(id_string) => {
160 assert_eq!(id_string, IdentifierString(input.to_string()));
161 Ok(())
162 }
163 Err(error) => {
164 panic!("Should have succeeded to parse {input} but failed: {error}");
165 }
166 }
167 }
168
169 #[rstest]
170 #[case::empty_string("", "\n^")]
171 #[case::all_caps("FOO", "FOO\n^")]
172 #[case::one_caps("foO", "foO\n ^")]
173 #[case::one_caps("foo:", "foo:\n ^")]
174 #[case::one_caps("foö", "foö\n ^")]
175 fn identifier_string_from_str_invalid_chars(
176 #[case] input: &str,
177 #[case] error_msg: &str,
178 ) -> TestResult {
179 match IdentifierString::from_str(input) {
180 Ok(id_string) => {
181 panic!("Should have failed to parse {input} but succeeded: {id_string}");
182 }
183 Err(error) => {
184 assert_eq!(
185 error.to_string(),
186 format!(
187 "Parser error:\n{error_msg}\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
188 )
189 );
190 Ok(())
191 }
192 }
193 }
194}