media_type_version/
defs.rs

1// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
2// SPDX-License-Identifier: BSD-2-Clause
3//! Common definitions for the media-type-version library.
4
5#[cfg(feature = "alloc")]
6extern crate alloc;
7
8use core::error::Error as CoreError;
9use core::fmt::{Display, Error as FmtError, Formatter};
10use core::num::ParseIntError;
11
12#[cfg(feature = "alloc")]
13use alloc::{borrow::ToOwned as _, string::String};
14
15#[cfg(feature = "facet-unstable")]
16use facet::Facet;
17
18/// An error that occurred while processing the media type string.
19#[derive(Debug)]
20#[non_exhaustive]
21#[expect(clippy::error_impl_error, reason = "common enough convention")]
22pub enum Error<'data> {
23    /// No prefix specified for the config builder.
24    BuildNoPrefix,
25
26    /// The media type did not have the specified prefix.
27    NoPrefix(&'data str, &'data str),
28
29    /// The media type did not have the specified suffix.
30    NoSuffix(&'data str, &'data str),
31
32    /// The media type did not have the ".v" part.
33    NoVDot(&'data str),
34
35    /// The media type's version part did not consist of two dot-separated components.
36    TwoComponentsExpected(&'data str),
37
38    /// The media type contained an invalid version component.
39    UIntExpected(&'data str, &'data str, ParseIntError),
40}
41
42impl Display for Error<'_> {
43    /// Describe the error that occurred.
44    #[inline]
45    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
46    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
47        match *self {
48            Self::BuildNoPrefix => write!(
49                f,
50                "No prefix specified for the media-type-version config builder"
51            ),
52            Self::NoPrefix(value, prefix) => {
53                write!(
54                    f,
55                    "The '{value}' media type does not have the expected prefix '{prefix}'"
56                )
57            }
58            Self::NoSuffix(value, suffix) => {
59                write!(
60                    f,
61                    "The '{value}' media type does not have the expected suffix '{suffix}'"
62                )
63            }
64            Self::NoVDot(value) => write!(
65                f,
66                "The '{value}' media type does not have the expected '.v' part"
67            ),
68            Self::TwoComponentsExpected(value) => write!(
69                f,
70                "The '{value}' media type does not have two dot-separated version components"
71            ),
72            Self::UIntExpected(value, comp, ref err) => write!(
73                f,
74                "The '{value}' media type contains an invalid unsigned integer '{comp}': {err}"
75            ),
76        }
77    }
78}
79
80impl CoreError for Error<'_> {}
81
82#[cfg(feature = "alloc")]
83impl Error<'_> {
84    /// Store the error strings into an owned object.
85    #[inline]
86    #[must_use]
87    pub fn into_owned_error(self) -> OwnedError {
88        match self {
89            Self::BuildNoPrefix => OwnedError::BuildNoPrefix,
90            Self::NoPrefix(value, prefix) => {
91                OwnedError::NoPrefix(value.to_owned(), prefix.to_owned())
92            }
93            Self::NoSuffix(value, suffix) => {
94                OwnedError::NoSuffix(value.to_owned(), suffix.to_owned())
95            }
96            Self::NoVDot(value) => OwnedError::NoVDot(value.to_owned()),
97            Self::TwoComponentsExpected(value) => {
98                OwnedError::TwoComponentsExpected(value.to_owned())
99            }
100            Self::UIntExpected(value, comp, err) => {
101                OwnedError::UIntExpected(value.to_owned(), comp.to_owned(), err)
102            }
103        }
104    }
105}
106
107/// An equivalent to [`Error`] that owns the error parameters.
108#[cfg(feature = "alloc")]
109#[derive(Debug)]
110#[non_exhaustive]
111pub enum OwnedError {
112    /// No prefix specified for the config builder.
113    BuildNoPrefix,
114
115    /// The media type did not have the specified prefix.
116    NoPrefix(String, String),
117
118    /// The media type did not have the specified suffix.
119    NoSuffix(String, String),
120
121    /// The media type did not have the ".v" part.
122    NoVDot(String),
123
124    /// The media type's version part did not consist of two dot-separated components.
125    TwoComponentsExpected(String),
126
127    /// The media type contained an invalid version component.
128    UIntExpected(String, String, ParseIntError),
129}
130
131#[cfg(feature = "alloc")]
132impl Display for OwnedError {
133    /// Describe the error that occurred.
134    #[inline]
135    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
136    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
137        match *self {
138            Self::BuildNoPrefix => Error::BuildNoPrefix.fmt(f),
139            Self::NoPrefix(ref value, ref prefix) => Error::NoPrefix(value, prefix).fmt(f),
140            Self::NoSuffix(ref value, ref suffix) => Error::NoSuffix(value, suffix).fmt(f),
141            Self::NoVDot(ref value) => Error::NoVDot(value).fmt(f),
142            Self::TwoComponentsExpected(ref value) => Error::TwoComponentsExpected(value).fmt(f),
143            Self::UIntExpected(ref value, ref comp, ref err) => {
144                Error::UIntExpected(value, comp, (*err).clone()).fmt(f)
145            }
146        }
147    }
148}
149
150#[cfg(feature = "alloc")]
151impl CoreError for OwnedError {}
152
153/// The extracted format version.
154#[cfg_attr(feature = "facet-unstable", derive(Facet))]
155pub struct Version {
156    /// The major version number.
157    major: u32,
158
159    /// The minor version number.
160    minor: u32,
161}
162
163impl Version {
164    /// The major version number.
165    #[inline]
166    #[must_use]
167    pub const fn major(&self) -> u32 {
168        self.major
169    }
170
171    /// The minor version number.
172    #[inline]
173    #[must_use]
174    pub const fn minor(&self) -> u32 {
175        self.minor
176    }
177
178    /// Return a (major, minor) tuple.
179    #[inline]
180    #[must_use]
181    pub const fn as_tuple(&self) -> (u32, u32) {
182        (self.major, self.minor)
183    }
184}
185
186impl From<(u32, u32)> for Version {
187    /// Build a [`Version`] object from the major and minor version numbers.
188    #[inline]
189    fn from(value: (u32, u32)) -> Self {
190        Self {
191            major: value.0,
192            minor: value.1,
193        }
194    }
195}
196
197impl From<Version> for (u32, u32) {
198    /// Break a [`Version`] object down into the major and minor version numbers.
199    #[inline]
200    fn from(value: Version) -> Self {
201        value.as_tuple()
202    }
203}
204
205/// Runtime configuration for the media-type-version library.
206#[cfg_attr(feature = "facet-unstable", derive(Facet))]
207pub struct Config<'data> {
208    /// The prefix to strip from the media type string.
209    prefix: &'data str,
210
211    /// The suffix (possibly empty) to strip from the media type string.
212    suffix: &'data str,
213}
214
215impl<'data> Config<'data> {
216    /// The prefix to strip from the media type string.
217    #[inline]
218    #[must_use]
219    pub const fn prefix(&self) -> &str {
220        self.prefix
221    }
222
223    /// The suffix (possibly empty) to strip from the media type string.
224    #[inline]
225    #[must_use]
226    pub const fn suffix(&self) -> &str {
227        self.suffix
228    }
229
230    /// Start building a configuration object.
231    #[inline]
232    #[must_use]
233    pub fn builder() -> ConfigBuilder<'data> {
234        ConfigBuilder::default()
235    }
236
237    /// For test porpoises only, build something out of things.
238    #[cfg(test)]
239    #[inline]
240    #[must_use]
241    pub const fn from_parts(prefix: &'data str, suffix: &'data str) -> Self {
242        Self { prefix, suffix }
243    }
244}
245
246/// Build the runtime configuration.
247#[derive(Default)]
248pub struct ConfigBuilder<'data> {
249    /// The prefix to strip from the media type string.
250    prefix: Option<&'data str>,
251
252    /// The suffix (possibly empty) to strip from the media type string.
253    suffix: Option<&'data str>,
254}
255
256impl<'data> ConfigBuilder<'data> {
257    /// Set the prefix to strip from the media type string.
258    #[inline]
259    #[must_use]
260    pub const fn prefix(self, value: &'data str) -> Self {
261        Self {
262            prefix: Some(value),
263            ..self
264        }
265    }
266
267    /// Set the suffix (possibly empty) to strip from the media type string.
268    #[inline]
269    #[must_use]
270    pub const fn suffix(self, value: &'data str) -> Self {
271        Self {
272            suffix: Some(value),
273            ..self
274        }
275    }
276
277    /// Build a [`Config`] object with the specified settings.
278    ///
279    /// # Errors
280    ///
281    /// [`Error::BuildNoPrefix`] if [`ConfigBuilder::prefix`] was not called.
282    #[inline]
283    pub fn build(self) -> Result<Config<'data>, Error<'data>> {
284        Ok(Config {
285            prefix: self.prefix.ok_or(Error::BuildNoPrefix)?,
286            suffix: self.suffix.unwrap_or_default(),
287        })
288    }
289}
290
291#[cfg(test)]
292#[expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
293#[expect(clippy::unwrap_used, reason = "this is a test suite")]
294mod tests {
295    extern crate alloc;
296
297    use alloc::format;
298    use alloc::string::String;
299
300    #[cfg(feature = "alloc")]
301    use core::str::FromStr as _;
302
303    use eyre::WrapErr as _;
304    use facet_testhelpers::test;
305    use log::{info, trace};
306
307    #[cfg(feature = "facet-unstable")]
308    use facet_pretty::FacetPretty as _;
309
310    use super::Config;
311
312    #[cfg(feature = "alloc")]
313    use super::Error;
314
315    #[cfg(feature = "facet-unstable")]
316    fn pretty_cfg(cfg: &Config<'_>) -> String {
317        format!("{cfg}", cfg = cfg.pretty())
318    }
319
320    #[cfg(not(feature = "facet-unstable"))]
321    fn pretty_cfg(cfg: &Config<'_>) -> String {
322        format!(
323            "Config {{ prefix = {prefix:?}, suffix = {suffix:?} }}",
324            prefix = cfg.prefix(),
325            suffix = cfg.suffix()
326        )
327    }
328
329    /// Make sure the builder, well, builds a [`Config`] object.
330    #[test]
331    fn builder() {
332        info!("Building a config builder");
333        let cfg = Config::builder()
334            .prefix("hello")
335            .suffix("goodbye")
336            .build()
337            .context("build")?;
338        trace!("{cfg}", cfg = pretty_cfg(&cfg));
339        assert_eq!(cfg.prefix(), "hello");
340        assert_eq!(cfg.suffix(), "goodbye");
341    }
342
343    /// Make sure the error message does not change.
344    #[cfg(feature = "alloc")]
345    #[test]
346    fn error_to_owned() {
347        let check_to_owned_msg = |err: Error<'_>| {
348            let msg = format!("{err}");
349            trace!("{msg}");
350            let owned = err.into_owned_error();
351            let owned_msg = format!("{owned}");
352            trace!("{owned_msg}");
353            assert_eq!(msg, owned_msg);
354        };
355
356        check_to_owned_msg(Error::BuildNoPrefix);
357        check_to_owned_msg(Error::NoPrefix("some value", "some prefix"));
358        check_to_owned_msg(Error::NoSuffix("some value", "some suffix"));
359        check_to_owned_msg(Error::NoVDot("stuff"));
360        check_to_owned_msg(Error::TwoComponentsExpected("some kind of thing"));
361        check_to_owned_msg(Error::UIntExpected(
362            "something",
363            "something else",
364            u32::from_str("?").unwrap_err(),
365        ));
366    }
367}