voa_core/identifiers/
context.rs

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