voa_core/identifiers/
technology.rs

1use 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/// The name of a technology backend.
18///
19/// Technology-specific backends implement the logic for each supported verification technology
20/// in VOA.
21///
22/// See <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#technology>
23#[derive(Clone, Debug, strum::Display, Eq, IntoStaticStr, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25pub enum Technology {
26    /// The [OpenPGP] technology.
27    ///
28    /// [OpenPGP]: https://www.openpgp.org/
29    #[strum(to_string = "openpgp")]
30    #[cfg_attr(feature = "serde", serde(rename = "openpgp"))]
31    Openpgp,
32
33    /// The [SSH] technology.
34    ///
35    /// [SSH]: https://www.openssh.com/
36    #[strum(to_string = "ssh")]
37    #[cfg_attr(feature = "serde", serde(rename = "ssh"))]
38    SSH,
39
40    /// Defines a custom [`Technology`] name.
41    #[strum(to_string = "{0}")]
42    Custom(CustomTechnology),
43}
44
45impl Technology {
46    /// Returns the path segment for this technology.
47    pub(crate) fn path_segment(&self) -> PathBuf {
48        format!("{self}").into()
49    }
50
51    /// Recognizes a [`Technology`] in a string slice.
52    ///
53    /// Consumes all of its `input`.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if
58    ///
59    /// - `input` does not contain a variant of [`Technology`],
60    /// - or one of the characters in `input` is not covered by [`IdentifierString::valid_chars`].
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use voa_core::identifiers::Technology;
66    /// use winnow::Parser;
67    ///
68    /// # fn main() -> Result<(), voa_core::Error> {
69    /// assert_eq!(Technology::parser.parse("openpgp")?, Technology::Openpgp);
70    /// assert_eq!(Technology::parser.parse("ssh")?, Technology::SSH);
71    /// assert_eq!(Technology::parser.parse("test")?.to_string(), "test");
72    /// # Ok(())
73    /// # }
74    /// ```
75    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    /// Creates a [`Technology`] from a string slice.
93    ///
94    /// # Note
95    ///
96    /// Delegates to [`Technology::parser`].
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if [`Technology::parser`] fails.
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        Ok(Self::parser.parse(s)?)
103    }
104}
105
106/// A [`CustomTechnology`] defines a technology name that is not covered by the variants defined in
107/// [`Technology`].
108#[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    /// Creates a new [`CustomTechnology`] instance.
115    pub fn new(value: IdentifierString) -> Self {
116        Self(value)
117    }
118
119    /// Recognizes a [`CustomTechnology`] in a string slice.
120    ///
121    /// Consumes all of its `input`.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if one of the characters in `input` is not covered by
126    /// [`IdentifierString::valid_chars`].
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// use voa_core::identifiers::CustomTechnology;
132    /// use winnow::Parser;
133    ///
134    /// # fn main() -> Result<(), voa_core::Error> {
135    /// assert_eq!(CustomTechnology::parser.parse("test")?.to_string(), "test");
136    /// # Ok(())
137    /// # }
138    /// ```
139    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    /// Creates a [`CustomTechnology`] from a string slice.
163    ///
164    /// # Note
165    ///
166    /// Delegates to [`CustomTechnology::parser`].
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if [`CustomTechnology::parser`] fails.
171    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}