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