voa_core/identifiers/
context.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, eof},
12    error::StrContext,
13};
14
15use crate::identifiers::IdentifierString;
16#[cfg(doc)]
17use crate::identifiers::{Os, Purpose};
18
19/// A context within a [`Purpose`] for more fine-grained verifier
20/// assignments.
21///
22/// An example for context is the name of a specific software repository when certificates are
23/// used in the context of the packages purpose (e.g. "core").
24///
25/// If no specific context is required, [`Context::Default`] must be used.
26///
27/// See <https://uapi-group.org/specifications/specs/file_hierarchy_for_the_verification_of_os_artifacts/#context>
28#[derive(Clone, Debug, Default, strum::Display, Eq, IntoStaticStr, PartialEq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize))]
30pub enum Context {
31    /// The default context.
32    #[default]
33    #[strum(to_string = "default")]
34    #[cfg_attr(feature = "serde", serde(rename = "default"))]
35    Default,
36
37    /// Defines a custom [`Context`] for verifiers within an [`Os`] and
38    /// [`Purpose`].
39    #[strum(to_string = "{0}")]
40    Custom(CustomContext),
41}
42
43impl Context {
44    /// Returns the path segment for this context.
45    pub(crate) fn path_segment(&self) -> PathBuf {
46        match self {
47            Self::Default => "default".into(),
48            Self::Custom(custom) => custom.as_ref().into(),
49        }
50    }
51
52    /// Recognizes a [`Context`] in a string slice.
53    ///
54    /// Consumes all of its `input`.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if
59    ///
60    /// - `input` does not contain a variant of [`Context`],
61    /// - or one of the characters in `input` is not covered by [`IdentifierString::valid_chars`].
62    ///
63    /// # Examples
64    ///
65    /// ```
66    /// use voa_core::identifiers::{Context, CustomContext};
67    /// use winnow::Parser;
68    ///
69    /// # fn main() -> Result<(), voa_core::Error> {
70    /// assert_eq!(Context::parser.parse("default")?, Context::Default);
71    /// assert_eq!(
72    ///     Context::parser.parse("test")?,
73    ///     Context::Custom(CustomContext::new("test".parse()?))
74    /// );
75    /// # Ok(())
76    /// # }
77    /// ```
78    pub fn parser(input: &mut &str) -> ModalResult<Self> {
79        alt((
80            ("default", eof).value(Self::Default),
81            CustomContext::parser.map(Self::Custom),
82        ))
83        .parse_next(input)
84    }
85}
86
87impl FromStr for Context {
88    type Err = crate::Error;
89
90    /// Creates a [`Context`] from a string slice.
91    ///
92    /// # Note
93    ///
94    /// Delegates to [`Context::parser`].
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if [`Context::parser`] fails.
99    fn from_str(s: &str) -> Result<Self, Self::Err> {
100        Ok(Self::parser.parse(s)?)
101    }
102}
103
104/// A [`CustomContext`] encodes a value for a [`Context`] that is not [`Context::Default`].
105#[derive(Clone, Debug, Eq, PartialEq)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize))]
107#[cfg_attr(feature = "serde", serde(rename = "kebab-case"))]
108pub struct CustomContext(IdentifierString);
109
110impl CustomContext {
111    /// Creates a new [`CustomContext`] instance.
112    pub fn new(value: IdentifierString) -> Self {
113        Self(value)
114    }
115
116    /// Recognizes a [`CustomContext`] in a string slice.
117    ///
118    /// Consumes all of its `input`.
119    ///
120    /// # Errors
121    ///
122    /// Returns an error if one of the characters in `input` is not covered by
123    /// [`IdentifierString::valid_chars`].
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use voa_core::identifiers::CustomContext;
129    /// use winnow::Parser;
130    ///
131    /// # fn main() -> Result<(), voa_core::Error> {
132    /// assert_eq!(CustomContext::parser.parse("test")?.to_string(), "test");
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub fn parser(input: &mut &str) -> ModalResult<Self> {
137        IdentifierString::parser
138            .map(Self)
139            .context(StrContext::Label("custom context for VOA"))
140            .parse_next(input)
141    }
142}
143
144impl Display for CustomContext {
145    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
146        write!(fmt, "{}", self.0)
147    }
148}
149
150impl AsRef<str> for CustomContext {
151    fn as_ref(&self) -> &str {
152        self.0.as_ref()
153    }
154}
155
156impl FromStr for CustomContext {
157    type Err = crate::Error;
158
159    /// Creates a [`CustomContext`] from a string slice.
160    ///
161    /// # Note
162    ///
163    /// Delegates to [`CustomContext::parser`].
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if [`CustomContext::parser`] fails.
168    fn from_str(s: &str) -> Result<Self, Self::Err> {
169        Ok(Self::parser.parse(s)?)
170    }
171}
172
173impl From<CustomContext> for Context {
174    fn from(val: CustomContext) -> Self {
175        Context::Custom(val)
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use rstest::rstest;
182    use testresult::TestResult;
183
184    use super::*;
185
186    #[rstest]
187    #[case(Context::Default, "default")]
188    #[case(Context::Custom(CustomContext::new("abc".parse()?)), "abc")]
189    fn context_display(#[case] context: Context, #[case] display: &str) -> TestResult {
190        assert_eq!(format!("{context}"), display);
191        Ok(())
192    }
193
194    #[rstest]
195    #[case::default("default", Context::Default)]
196    #[case::custom("test", Context::Custom(CustomContext::new("test".parse()?)))]
197    fn context_from_str_succeeds(#[case] input: &str, #[case] expected: Context) -> TestResult {
198        assert_eq!(Context::from_str(input)?, expected);
199        Ok(())
200    }
201
202    #[rstest]
203    #[case::invalid_character(
204        "test$",
205        "test$\n    ^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
206    )]
207    #[case::all_caps(
208        "TEST",
209        "TEST\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
210    )]
211    #[case::empty_string(
212        "",
213        "\n^\ninvalid VOA identifier string\nexpected lowercase alphanumeric ASCII characters, `_`, `-`, `.`"
214    )]
215    fn context_from_str_fails(#[case] input: &str, #[case] error_msg: &str) -> TestResult {
216        match Context::from_str(input) {
217            Ok(id_string) => {
218                return Err(format!(
219                    "Should have failed to parse {input} but succeeded: {id_string}"
220                )
221                .into());
222            }
223            Err(error) => {
224                assert_eq!(error.to_string(), format!("Parser error:\n{error_msg}"));
225                Ok(())
226            }
227        }
228    }
229}