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