voa_core/identifiers/
technology.rs

1use std::{
2    fmt::{Display, Formatter},
3    str::FromStr,
4};
5
6use strum::IntoStaticStr;
7use winnow::{
8    ModalResult,
9    Parser,
10    combinator::{alt, cut_err, eof},
11    error::{StrContext, StrContextValue},
12};
13
14use crate::{
15    Error,
16    identifiers::{IdentifierString, SegmentPath},
17};
18
19/// The name of a technology backend.
20///
21/// Technology-specific backends implement the logic for each supported verification technology
22/// in VOA.
23///
24/// See <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#technology>
25#[derive(Clone, Debug, strum::Display, Eq, Hash, IntoStaticStr, PartialEq)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize))]
27pub enum Technology {
28    /// The [OpenPGP] technology.
29    ///
30    /// [OpenPGP]: https://www.openpgp.org/
31    #[strum(to_string = "openpgp")]
32    #[cfg_attr(feature = "serde", serde(rename = "openpgp"))]
33    Openpgp,
34
35    /// The [SSH] technology.
36    ///
37    /// [SSH]: https://www.openssh.com/
38    #[strum(to_string = "ssh")]
39    #[cfg_attr(feature = "serde", serde(rename = "ssh"))]
40    SSH,
41
42    /// Defines a custom [`Technology`] name.
43    #[strum(to_string = "{0}")]
44    Custom(CustomTechnology),
45}
46
47impl Technology {
48    /// Returns the path segment for this technology.
49    pub(crate) fn path_segment(&self) -> Result<SegmentPath, Error> {
50        format!("{self}").try_into()
51    }
52
53    /// Recognizes a [`Technology`] in a string slice.
54    ///
55    /// Consumes all of its `input`.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if
60    ///
61    /// - `input` does not contain a variant of [`Technology`],
62    /// - or one of the characters in `input` is not covered by [`IdentifierString::valid_chars`].
63    ///
64    /// # Examples
65    ///
66    /// ```
67    /// use voa_core::identifiers::Technology;
68    /// use winnow::Parser;
69    ///
70    /// # fn main() -> Result<(), voa_core::Error> {
71    /// assert_eq!(Technology::parser.parse("openpgp")?, Technology::Openpgp);
72    /// assert_eq!(Technology::parser.parse("ssh")?, Technology::SSH);
73    /// assert_eq!(Technology::parser.parse("test")?.to_string(), "test");
74    /// # Ok(())
75    /// # }
76    /// ```
77    pub fn parser(input: &mut &str) -> ModalResult<Self> {
78        cut_err(alt((
79            ("openpgp", eof).value(Self::Openpgp),
80            ("ssh", eof).value(Self::SSH),
81            CustomTechnology::parser.map(Self::Custom),
82        )))
83        .context(StrContext::Label("a valid VOA technology"))
84        .context(StrContext::Expected(StrContextValue::Description(
85            "'opengpg', 'ssh', or a custom value",
86        )))
87        .parse_next(input)
88    }
89}
90
91impl FromStr for Technology {
92    type Err = crate::Error;
93
94    /// Creates a [`Technology`] from a string slice.
95    ///
96    /// # Note
97    ///
98    /// Delegates to [`Technology::parser`].
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if [`Technology::parser`] fails.
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        Ok(Self::parser.parse(s)?)
105    }
106}
107
108/// A [`CustomTechnology`] defines a technology name that is not covered by the variants defined in
109/// [`Technology`].
110#[derive(Clone, Debug, Eq, Hash, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize))]
112#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
113pub struct CustomTechnology(IdentifierString);
114
115impl CustomTechnology {
116    /// Creates a new [`CustomTechnology`] instance.
117    pub fn new(value: IdentifierString) -> Self {
118        Self(value)
119    }
120
121    /// Recognizes a [`CustomTechnology`] in a string slice.
122    ///
123    /// Consumes all of its `input`.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if one of the characters in `input` is not covered by
128    /// [`IdentifierString::valid_chars`].
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use voa_core::identifiers::CustomTechnology;
134    /// use winnow::Parser;
135    ///
136    /// # fn main() -> Result<(), voa_core::Error> {
137    /// assert_eq!(CustomTechnology::parser.parse("test")?.to_string(), "test");
138    /// # Ok(())
139    /// # }
140    /// ```
141    pub fn parser(input: &mut &str) -> ModalResult<Self> {
142        IdentifierString::parser
143            .map(Self)
144            .context(StrContext::Label("custom technology for VOA"))
145            .parse_next(input)
146    }
147}
148
149impl Display for CustomTechnology {
150    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}", self.0)
152    }
153}
154
155impl AsRef<str> for CustomTechnology {
156    fn as_ref(&self) -> &str {
157        self.0.as_ref()
158    }
159}
160
161impl FromStr for CustomTechnology {
162    type Err = crate::Error;
163
164    /// Creates a [`CustomTechnology`] from a string slice.
165    ///
166    /// # Note
167    ///
168    /// Delegates to [`CustomTechnology::parser`].
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if [`CustomTechnology::parser`] fails.
173    fn from_str(s: &str) -> Result<Self, Self::Err> {
174        Ok(Self::parser.parse(s)?)
175    }
176}
177impl From<CustomTechnology> for Technology {
178    fn from(val: CustomTechnology) -> Self {
179        Technology::Custom(val)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use rstest::rstest;
186    use testresult::TestResult;
187
188    use super::*;
189
190    #[rstest]
191    #[case(Technology::Openpgp, "openpgp")]
192    #[case(Technology::Custom(CustomTechnology::new("foo".parse()?)), "foo")]
193    fn technology_display(
194        #[case] technology: Technology,
195        #[case] display: &str,
196    ) -> testresult::TestResult {
197        assert_eq!(format!("{technology}",), display);
198
199        Ok(())
200    }
201
202    #[test]
203    fn custom_as_ref() -> TestResult {
204        let custom = CustomTechnology::new("foo".parse()?);
205        assert_eq!(custom.as_ref(), "foo");
206
207        Ok(())
208    }
209
210    #[rstest]
211    #[case::default("openpgp", Technology::Openpgp)]
212    #[case::default("ssh", Technology::SSH)]
213    #[case::custom("test", Technology::Custom(CustomTechnology::new("test".parse()?)))]
214    fn technology_from_str_succeeds(
215        #[case] input: &str,
216        #[case] expected: Technology,
217    ) -> TestResult {
218        assert_eq!(Technology::from_str(input)?, expected);
219        Ok(())
220    }
221
222    #[rstest]
223    #[case::invalid_character(
224        "test$",
225        "test$\n    ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
226    )]
227    #[case::all_caps(
228        "TEST",
229        "TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
230    )]
231    #[case::empty_string(
232        "",
233        "\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`, 'opengpg', 'ssh', or a custom value"
234    )]
235    fn technology_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
236        match Technology::from_str(input) {
237            Ok(id_string) => {
238                panic!("Should have failed to parse {input} but succeeded: {id_string}");
239            }
240            Err(error) => {
241                assert_eq!(error.to_string(), format!("Parser error:\n{error_msg}"));
242                Ok(())
243            }
244        }
245    }
246}