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(any(feature = "alloc", feature = "toml-boml1"))]
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(all(feature = "alloc", feature = "toml-boml1"))]
16use alloc::format;
17
18#[cfg(feature = "facet-unstable")]
19use facet::Facet;
20
21#[cfg(feature = "toml-boml1")]
22use boml::TomlError;
23
24/// An error that occurred while processing the media type string.
25#[derive(Debug)]
26#[non_exhaustive]
27#[expect(clippy::error_impl_error, reason = "common enough convention")]
28pub enum Error<'data> {
29    /// No prefix specified for the config builder.
30    BuildNoPrefix,
31
32    /// Something went really wrong.
33    Internal(u32),
34
35    /// The media type did not have the specified prefix.
36    NoPrefix(&'data str, &'data str),
37
38    /// The media type did not have the specified suffix.
39    NoSuffix(&'data str, &'data str),
40
41    /// The media type did not have the ".v" part.
42    NoVDot(&'data str),
43
44    #[cfg(feature = "extract-from-table")]
45    /// The hierarchical structure did not contain the specified element.
46    TableNoChild(&'data str),
47
48    #[cfg(feature = "extract-from-table")]
49    /// The hierarchical structure contained something that was not a table.
50    TableNotTable,
51
52    #[cfg(feature = "toml-boml1")]
53    /// Could not parse a TOML document.
54    TomlParse(TomlError<'data>),
55
56    /// The media type's version part did not consist of two dot-separated components.
57    TwoComponentsExpected(&'data str),
58
59    /// The media type contained an invalid version component.
60    UIntExpected(&'data str, &'data str, ParseIntError),
61}
62
63impl Display for Error<'_> {
64    /// Describe the error that occurred.
65    #[inline]
66    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
67    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
68        match *self {
69            Self::BuildNoPrefix => write!(
70                f,
71                "No prefix specified for the media-type-version config builder"
72            ),
73            Self::Internal(code) => write!(f, "media-type-version internal error: code {code}"),
74            Self::NoPrefix(value, prefix) => {
75                write!(
76                    f,
77                    "The '{value}' media type does not have the expected prefix '{prefix}'"
78                )
79            }
80            Self::NoSuffix(value, suffix) => {
81                write!(
82                    f,
83                    "The '{value}' media type does not have the expected suffix '{suffix}'"
84                )
85            }
86            Self::NoVDot(value) => write!(
87                f,
88                "The '{value}' media type does not have the expected '.v' part"
89            ),
90            Self::TwoComponentsExpected(value) => write!(
91                f,
92                "The '{value}' media type does not have two dot-separated version components"
93            ),
94            #[cfg(feature = "extract-from-table")]
95            Self::TableNoChild(comp) => {
96                write!(f, "The parsed structure did not contain the '{comp}' child")
97            }
98            #[cfg(feature = "extract-from-table")]
99            Self::TableNotTable => write!(
100                f,
101                "The parsed structure did not contain an expected table or string"
102            ),
103            #[cfg(feature = "toml-boml1")]
104            Self::TomlParse(ref err) => write!(f, "Could not parse a TOML document: {err}"),
105            Self::UIntExpected(value, comp, _) => write!(
106                f,
107                "The '{value}' media type contains an invalid unsigned integer '{comp}'"
108            ),
109        }
110    }
111}
112
113impl CoreError for Error<'_> {
114    #[inline]
115    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
116        match *self {
117            Self::BuildNoPrefix
118            | Self::Internal(_)
119            | Self::NoPrefix(_, _)
120            | Self::NoSuffix(_, _)
121            | Self::NoVDot(_)
122            | Self::TwoComponentsExpected(_) => None,
123            #[cfg(feature = "extract-from-table")]
124            Self::TableNoChild(_) | Self::TableNotTable => None,
125            #[cfg(feature = "toml-boml1")]
126            Self::TomlParse(_) => None,
127            Self::UIntExpected(_, _, ref err) => Some(err),
128        }
129    }
130}
131
132#[cfg(feature = "alloc")]
133impl Error<'_> {
134    /// Store the error strings into an owned object.
135    #[inline]
136    #[must_use]
137    pub fn into_owned_error(self) -> OwnedError {
138        match self {
139            Self::BuildNoPrefix => OwnedError::BuildNoPrefix,
140            Self::Internal(code) => OwnedError::Internal(code),
141            Self::NoPrefix(value, prefix) => {
142                OwnedError::NoPrefix(value.to_owned(), prefix.to_owned())
143            }
144            Self::NoSuffix(value, suffix) => {
145                OwnedError::NoSuffix(value.to_owned(), suffix.to_owned())
146            }
147            Self::NoVDot(value) => OwnedError::NoVDot(value.to_owned()),
148            #[cfg(feature = "extract-from-table")]
149            Self::TableNoChild(comp) => OwnedError::TableNoChild(comp.to_owned()),
150            #[cfg(feature = "extract-from-table")]
151            Self::TableNotTable => OwnedError::TableNotTable,
152            #[cfg(feature = "toml-boml1")]
153            Self::TomlParse(err) => OwnedError::TomlBoml(format!("{err}")),
154            Self::TwoComponentsExpected(value) => {
155                OwnedError::TwoComponentsExpected(value.to_owned())
156            }
157            Self::UIntExpected(value, comp, err) => {
158                OwnedError::UIntExpected(value.to_owned(), comp.to_owned(), err)
159            }
160        }
161    }
162}
163
164/// An equivalent to [`Error`] that owns the error parameters.
165#[cfg(feature = "alloc")]
166#[derive(Debug)]
167#[non_exhaustive]
168pub enum OwnedError {
169    /// No prefix specified for the config builder.
170    BuildNoPrefix,
171
172    /// Something went really, really wrong...
173    Internal(u32),
174
175    /// The media type did not have the specified prefix.
176    NoPrefix(String, String),
177
178    /// The media type did not have the specified suffix.
179    NoSuffix(String, String),
180
181    /// The media type did not have the ".v" part.
182    NoVDot(String),
183
184    #[cfg(feature = "extract-from-table")]
185    /// The hierarchical structure did not contain the specified element.
186    TableNoChild(String),
187
188    #[cfg(feature = "extract-from-table")]
189    /// The hierarchical structure did not contain the expected table or string.
190    TableNotTable,
191
192    #[cfg(feature = "toml-boml1")]
193    /// Could not parse a TOML document.
194    TomlBoml(String),
195
196    /// The media type's version part did not consist of two dot-separated components.
197    TwoComponentsExpected(String),
198
199    /// The media type contained an invalid version component.
200    UIntExpected(String, String, ParseIntError),
201}
202
203#[cfg(feature = "alloc")]
204impl Display for OwnedError {
205    /// Describe the error that occurred.
206    #[inline]
207    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
208    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
209        match *self {
210            Self::BuildNoPrefix => Error::BuildNoPrefix.fmt(f),
211            Self::Internal(ref code) => Error::Internal(*code).fmt(f),
212            Self::NoPrefix(ref value, ref prefix) => Error::NoPrefix(value, prefix).fmt(f),
213            Self::NoSuffix(ref value, ref suffix) => Error::NoSuffix(value, suffix).fmt(f),
214            Self::NoVDot(ref value) => Error::NoVDot(value).fmt(f),
215            #[cfg(feature = "extract-from-table")]
216            Self::TableNoChild(ref comp) => Error::TableNoChild(comp).fmt(f),
217            #[cfg(feature = "extract-from-table")]
218            Self::TableNotTable => Error::TableNotTable.fmt(f),
219            #[cfg(feature = "toml-boml1")]
220            Self::TomlBoml(ref err) => write!(f, "Could not parse a TOML document: {err}"),
221            Self::TwoComponentsExpected(ref value) => Error::TwoComponentsExpected(value).fmt(f),
222            Self::UIntExpected(ref value, ref comp, ref err) => {
223                Error::UIntExpected(value, comp, (*err).clone()).fmt(f)
224            }
225        }
226    }
227}
228
229#[cfg(feature = "alloc")]
230impl CoreError for OwnedError {
231    #[inline]
232    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
233        match *self {
234            Self::BuildNoPrefix
235            | Self::Internal(_)
236            | Self::NoPrefix(_, _)
237            | Self::NoSuffix(_, _)
238            | Self::NoVDot(_)
239            | Self::TwoComponentsExpected(_) => None,
240            #[cfg(feature = "extract-from-table")]
241            Self::TableNoChild(_) | Self::TableNotTable => None,
242            #[cfg(feature = "toml-boml1")]
243            Self::TomlBoml(_) => None,
244            Self::UIntExpected(_, _, ref err) => Some(err),
245        }
246    }
247}
248
249/// The extracted format version.
250#[cfg_attr(feature = "facet-unstable", derive(Facet))]
251pub struct Version {
252    /// The major version number.
253    major: u32,
254
255    /// The minor version number.
256    minor: u32,
257}
258
259impl Version {
260    /// The major version number.
261    #[inline]
262    #[must_use]
263    pub const fn major(&self) -> u32 {
264        self.major
265    }
266
267    /// The minor version number.
268    #[inline]
269    #[must_use]
270    pub const fn minor(&self) -> u32 {
271        self.minor
272    }
273
274    /// Return a (major, minor) tuple.
275    #[inline]
276    #[must_use]
277    pub const fn as_tuple(&self) -> (u32, u32) {
278        (self.major, self.minor)
279    }
280}
281
282impl From<(u32, u32)> for Version {
283    /// Build a [`Version`] object from the major and minor version numbers.
284    #[inline]
285    fn from(value: (u32, u32)) -> Self {
286        Self {
287            major: value.0,
288            minor: value.1,
289        }
290    }
291}
292
293impl From<Version> for (u32, u32) {
294    /// Break a [`Version`] object down into the major and minor version numbers.
295    #[inline]
296    fn from(value: Version) -> Self {
297        value.as_tuple()
298    }
299}
300
301/// Runtime configuration for the media-type-version library.
302#[cfg_attr(feature = "facet-unstable", derive(Facet))]
303pub struct Config<'data> {
304    /// The prefix to strip from the media type string.
305    prefix: &'data str,
306
307    /// The suffix (possibly empty) to strip from the media type string.
308    suffix: &'data str,
309}
310
311impl<'data> Config<'data> {
312    /// The prefix to strip from the media type string.
313    #[inline]
314    #[must_use]
315    pub const fn prefix(&self) -> &str {
316        self.prefix
317    }
318
319    /// The suffix (possibly empty) to strip from the media type string.
320    #[inline]
321    #[must_use]
322    pub const fn suffix(&self) -> &str {
323        self.suffix
324    }
325
326    /// Start building a configuration object.
327    #[inline]
328    #[must_use]
329    pub fn builder() -> ConfigBuilder<'data> {
330        ConfigBuilder::default()
331    }
332
333    /// For test porpoises only, build something out of things.
334    #[cfg(test)]
335    #[inline]
336    #[must_use]
337    pub const fn from_parts(prefix: &'data str, suffix: &'data str) -> Self {
338        Self { prefix, suffix }
339    }
340}
341
342/// Build the runtime configuration.
343#[derive(Default)]
344pub struct ConfigBuilder<'data> {
345    /// The prefix to strip from the media type string.
346    prefix: Option<&'data str>,
347
348    /// The suffix (possibly empty) to strip from the media type string.
349    suffix: Option<&'data str>,
350}
351
352impl<'data> ConfigBuilder<'data> {
353    /// Set the prefix to strip from the media type string.
354    #[inline]
355    #[must_use]
356    pub const fn prefix(self, value: &'data str) -> Self {
357        Self {
358            prefix: Some(value),
359            ..self
360        }
361    }
362
363    /// Set the suffix (possibly empty) to strip from the media type string.
364    #[inline]
365    #[must_use]
366    pub const fn suffix(self, value: &'data str) -> Self {
367        Self {
368            suffix: Some(value),
369            ..self
370        }
371    }
372
373    /// Build a [`Config`] object with the specified settings.
374    ///
375    /// # Errors
376    ///
377    /// [`Error::BuildNoPrefix`] if [`ConfigBuilder::prefix`] was not called.
378    #[inline]
379    pub fn build(self) -> Result<Config<'data>, Error<'data>> {
380        Ok(Config {
381            prefix: self.prefix.ok_or(Error::BuildNoPrefix)?,
382            suffix: self.suffix.unwrap_or_default(),
383        })
384    }
385}
386
387#[cfg(test)]
388#[expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
389#[expect(clippy::unwrap_used, reason = "this is a test suite")]
390mod tests {
391    extern crate alloc;
392
393    use alloc::format;
394    use alloc::string::String;
395
396    #[cfg(feature = "facet-unstable")]
397    use alloc::string::ToString as _;
398
399    #[cfg(feature = "alloc")]
400    use core::str::FromStr as _;
401
402    use eyre::{Result, WrapErr as _};
403    use facet_testhelpers::test;
404    use log::{info, trace};
405
406    #[cfg(feature = "facet-unstable")]
407    use facet_pretty::FacetPretty as _;
408
409    use super::Config;
410
411    #[cfg(feature = "alloc")]
412    use super::Error;
413
414    #[cfg(feature = "facet-unstable")]
415    use super::Version;
416
417    #[cfg(feature = "facet-unstable")]
418    fn pretty_cfg(cfg: &Config<'_>) -> String {
419        format!("{cfg}", cfg = cfg.pretty())
420    }
421
422    #[cfg(not(feature = "facet-unstable"))]
423    fn pretty_cfg(cfg: &Config<'_>) -> String {
424        format!(
425            "Config {{ prefix = {prefix:?}, suffix = {suffix:?} }}",
426            prefix = cfg.prefix(),
427            suffix = cfg.suffix()
428        )
429    }
430
431    /// Make sure the builder, well, builds a [`Config`] object.
432    #[test]
433    fn builder() -> Result<()> {
434        info!("Building a config builder");
435        let cfg = Config::builder()
436            .prefix("hello")
437            .suffix("goodbye")
438            .build()
439            .context("build")?;
440        trace!("{cfg}", cfg = pretty_cfg(&cfg));
441        assert_eq!(cfg.prefix(), "hello");
442        assert_eq!(cfg.suffix(), "goodbye");
443        Ok(())
444    }
445
446    /// Make sure the error message does not change.
447    #[cfg(feature = "alloc")]
448    #[test]
449    fn error_to_owned() {
450        let check_to_owned_msg = |err: Error<'_>| {
451            let msg = format!("{err}");
452            trace!("{msg}");
453            let owned = err.into_owned_error();
454            let owned_msg = format!("{owned}");
455            trace!("{owned_msg}");
456            assert_eq!(msg, owned_msg);
457        };
458
459        check_to_owned_msg(Error::BuildNoPrefix);
460        check_to_owned_msg(Error::NoPrefix("some value", "some prefix"));
461        check_to_owned_msg(Error::NoSuffix("some value", "some suffix"));
462        check_to_owned_msg(Error::NoVDot("stuff"));
463        check_to_owned_msg(Error::TwoComponentsExpected("some kind of thing"));
464        check_to_owned_msg(Error::UIntExpected(
465            "something",
466            "something else",
467            u32::from_str("?").unwrap_err(),
468        ));
469    }
470
471    /// Make sure the [`Facet`] trait for [`Version`] works.
472    #[cfg(feature = "facet-unstable")]
473    #[test]
474    fn facet_pretty_contains_things() {
475        let major = 42;
476        let minor = 616;
477        let ver = Version::from((major, minor));
478        let repr = format!("{ver}", ver = ver.pretty());
479        assert!(
480            repr.contains("/// The major version number"),
481            "no docstring in the pretty representation: {repr:?}"
482        );
483        assert!(
484            repr.contains(&major.to_string()),
485            "no '{major}' in the pretty representation: {repr:?}"
486        );
487        assert!(
488            repr.contains(&minor.to_string()),
489            "no '{minor}' in the pretty representation: {repr:?}"
490        );
491    }
492}