voa_core/identifiers/
technology.rs1use std::{
2 fmt::{Display, Formatter},
3 path::PathBuf,
4 str::FromStr,
5};
6
7use strum::IntoStaticStr;
8use winnow::{
9 ModalResult,
10 Parser,
11 combinator::{alt, cut_err, eof},
12 error::{StrContext, StrContextValue},
13};
14
15use crate::identifiers::IdentifierString;
16
17#[derive(Clone, Debug, strum::Display, Eq, IntoStaticStr, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25pub enum Technology {
26 #[strum(to_string = "openpgp")]
30 #[cfg_attr(feature = "serde", serde(rename = "openpgp"))]
31 Openpgp,
32
33 #[strum(to_string = "ssh")]
37 #[cfg_attr(feature = "serde", serde(rename = "ssh"))]
38 SSH,
39
40 #[strum(to_string = "{0}")]
42 Custom(CustomTechnology),
43}
44
45impl Technology {
46 pub(crate) fn path_segment(&self) -> PathBuf {
48 format!("{self}").into()
49 }
50
51 pub fn parser(input: &mut &str) -> ModalResult<Self> {
76 cut_err(alt((
77 ("openpgp", eof).value(Self::Openpgp),
78 ("ssh", eof).value(Self::SSH),
79 CustomTechnology::parser.map(Self::Custom),
80 )))
81 .context(StrContext::Label("a valid VOA technology"))
82 .context(StrContext::Expected(StrContextValue::Description(
83 "'opengpg', 'ssh', or a custom value",
84 )))
85 .parse_next(input)
86 }
87}
88
89impl FromStr for Technology {
90 type Err = crate::Error;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 Ok(Self::parser.parse(s)?)
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize))]
110#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
111pub struct CustomTechnology(IdentifierString);
112
113impl CustomTechnology {
114 pub fn new(value: IdentifierString) -> Self {
116 Self(value)
117 }
118
119 pub fn parser(input: &mut &str) -> ModalResult<Self> {
140 IdentifierString::parser
141 .map(Self)
142 .context(StrContext::Label("custom technology for VOA"))
143 .parse_next(input)
144 }
145}
146
147impl Display for CustomTechnology {
148 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
149 write!(f, "{}", self.0)
150 }
151}
152
153impl AsRef<str> for CustomTechnology {
154 fn as_ref(&self) -> &str {
155 self.0.as_ref()
156 }
157}
158
159impl FromStr for CustomTechnology {
160 type Err = crate::Error;
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
172 Ok(Self::parser.parse(s)?)
173 }
174}
175impl From<CustomTechnology> for Technology {
176 fn from(val: CustomTechnology) -> Self {
177 Technology::Custom(val)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use rstest::rstest;
184 use testresult::TestResult;
185
186 use super::*;
187
188 #[rstest]
189 #[case(Technology::Openpgp, "openpgp")]
190 #[case(Technology::Custom(CustomTechnology::new("foo".parse()?)), "foo")]
191 fn technology_display(
192 #[case] technology: Technology,
193 #[case] display: &str,
194 ) -> testresult::TestResult {
195 assert_eq!(format!("{technology}",), display);
196
197 Ok(())
198 }
199
200 #[test]
201 fn custom_as_ref() -> TestResult {
202 let custom = CustomTechnology::new("foo".parse()?);
203 assert_eq!(custom.as_ref(), "foo");
204
205 Ok(())
206 }
207
208 #[rstest]
209 #[case::default("openpgp", Technology::Openpgp)]
210 #[case::default("ssh", Technology::SSH)]
211 #[case::custom("test", Technology::Custom(CustomTechnology::new("test".parse()?)))]
212 fn technology_from_str_succeeds(
213 #[case] input: &str,
214 #[case] expected: Technology,
215 ) -> TestResult {
216 assert_eq!(Technology::from_str(input)?, expected);
217 Ok(())
218 }
219
220 #[rstest]
221 #[case::invalid_character(
222 "test$",
223 "test$\n ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
224 )]
225 #[case::all_caps(
226 "TEST",
227 "TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
228 )]
229 #[case::empty_string(
230 "",
231 "\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
232 )]
233 fn technology_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
234 match Technology::from_str(input) {
235 Ok(id_string) => {
236 return Err(format!(
237 "Should have failed to parse {input} but succeeded: {id_string}"
238 )
239 .into());
240 }
241 Err(error) => {
242 assert_eq!(error.to_string(), format!("Parser error:\n{error_msg}"));
243 Ok(())
244 }
245 }
246 }
247}