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, PartialEq)]
24#[cfg_attr(feature = "serde", derive(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 return Err(
165 format!("Should have succeeded to parse {input} but failed: {error}").into(),
166 );
167 }
168 }
169 }
170
171 #[rstest]
172 #[case::empty_string("", "\n^")]
173 #[case::all_caps("FOO", "FOO\n^")]
174 #[case::one_caps("foO", "foO\n ^")]
175 #[case::one_caps("foo:", "foo:\n ^")]
176 #[case::one_caps("foö", "foö\n ^")]
177 fn identifier_string_from_str_invalid_chars(
178 #[case] input: &str,
179 #[case] error_msg: &str,
180 ) -> TestResult {
181 match IdentifierString::from_str(input) {
182 Ok(id_string) => {
183 return Err(format!(
184 "Should have failed to parse {input} but succeeded: {id_string}"
185 )
186 .into());
187 }
188 Err(error) => {
189 assert_eq!(
190 error.to_string(),
191 format!(
192 "Parser error:\n{error_msg}\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
193 )
194 );
195 Ok(())
196 }
197 }
198 }
199}