Skip to main content

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