media-type-version 0.2.0

Extract the format version from a media type string
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
// SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
// SPDX-License-Identifier: BSD-2-Clause
//! Common definitions for the media-type-version library.

#[cfg(any(feature = "alloc", feature = "toml-boml1"))]
extern crate alloc;

use core::error::Error as CoreError;
use core::fmt::{Display, Error as FmtError, Formatter};
use core::num::ParseIntError;

#[cfg(feature = "alloc")]
use alloc::{borrow::ToOwned as _, string::String};

#[cfg(all(feature = "alloc", feature = "toml-boml1"))]
use alloc::format;

#[cfg(feature = "facet-unstable")]
use facet::Facet;

#[cfg(feature = "toml-boml1")]
use boml::TomlError;

/// An error that occurred while processing the media type string.
#[derive(Debug)]
#[non_exhaustive]
#[expect(clippy::error_impl_error, reason = "common enough convention")]
pub enum Error<'data> {
    /// No prefix specified for the config builder.
    BuildNoPrefix,

    /// Something went really wrong.
    Internal(u32),

    /// The media type did not have the specified prefix.
    NoPrefix(&'data str, &'data str),

    /// The media type did not have the specified suffix.
    NoSuffix(&'data str, &'data str),

    /// The media type did not have the ".v" part.
    NoVDot(&'data str),

    #[cfg(feature = "extract-from-table")]
    /// The hierarchical structure did not contain the specified element.
    TableNoChild(&'data str),

    #[cfg(feature = "extract-from-table")]
    /// The hierarchical structure contained something that was not a table.
    TableNotTable,

    #[cfg(feature = "toml-boml1")]
    /// Could not parse a TOML document.
    TomlParse(TomlError<'data>),

    /// The media type's version part did not consist of two dot-separated components.
    TwoComponentsExpected(&'data str),

    /// The media type contained an invalid version component.
    UIntExpected(&'data str, &'data str, ParseIntError),
}

impl Display for Error<'_> {
    /// Describe the error that occurred.
    #[inline]
    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
        match *self {
            Self::BuildNoPrefix => write!(
                f,
                "No prefix specified for the media-type-version config builder"
            ),
            Self::Internal(code) => write!(f, "media-type-version internal error: code {code}"),
            Self::NoPrefix(value, prefix) => {
                write!(
                    f,
                    "The '{value}' media type does not have the expected prefix '{prefix}'"
                )
            }
            Self::NoSuffix(value, suffix) => {
                write!(
                    f,
                    "The '{value}' media type does not have the expected suffix '{suffix}'"
                )
            }
            Self::NoVDot(value) => write!(
                f,
                "The '{value}' media type does not have the expected '.v' part"
            ),
            Self::TwoComponentsExpected(value) => write!(
                f,
                "The '{value}' media type does not have two dot-separated version components"
            ),
            #[cfg(feature = "extract-from-table")]
            Self::TableNoChild(comp) => {
                write!(f, "The parsed structure did not contain the '{comp}' child")
            }
            #[cfg(feature = "extract-from-table")]
            Self::TableNotTable => write!(
                f,
                "The parsed structure did not contain an expected table or string"
            ),
            #[cfg(feature = "toml-boml1")]
            Self::TomlParse(ref err) => write!(f, "Could not parse a TOML document: {err}"),
            Self::UIntExpected(value, comp, _) => write!(
                f,
                "The '{value}' media type contains an invalid unsigned integer '{comp}'"
            ),
        }
    }
}

impl CoreError for Error<'_> {
    #[inline]
    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
        match *self {
            Self::BuildNoPrefix
            | Self::Internal(_)
            | Self::NoPrefix(_, _)
            | Self::NoSuffix(_, _)
            | Self::NoVDot(_)
            | Self::TwoComponentsExpected(_) => None,
            #[cfg(feature = "extract-from-table")]
            Self::TableNoChild(_) | Self::TableNotTable => None,
            #[cfg(feature = "toml-boml1")]
            Self::TomlParse(_) => None,
            Self::UIntExpected(_, _, ref err) => Some(err),
        }
    }
}

#[cfg(feature = "alloc")]
impl Error<'_> {
    /// Store the error strings into an owned object.
    #[inline]
    #[must_use]
    pub fn into_owned_error(self) -> OwnedError {
        match self {
            Self::BuildNoPrefix => OwnedError::BuildNoPrefix,
            Self::Internal(code) => OwnedError::Internal(code),
            Self::NoPrefix(value, prefix) => {
                OwnedError::NoPrefix(value.to_owned(), prefix.to_owned())
            }
            Self::NoSuffix(value, suffix) => {
                OwnedError::NoSuffix(value.to_owned(), suffix.to_owned())
            }
            Self::NoVDot(value) => OwnedError::NoVDot(value.to_owned()),
            #[cfg(feature = "extract-from-table")]
            Self::TableNoChild(comp) => OwnedError::TableNoChild(comp.to_owned()),
            #[cfg(feature = "extract-from-table")]
            Self::TableNotTable => OwnedError::TableNotTable,
            #[cfg(feature = "toml-boml1")]
            Self::TomlParse(err) => OwnedError::TomlBoml(format!("{err}")),
            Self::TwoComponentsExpected(value) => {
                OwnedError::TwoComponentsExpected(value.to_owned())
            }
            Self::UIntExpected(value, comp, err) => {
                OwnedError::UIntExpected(value.to_owned(), comp.to_owned(), err)
            }
        }
    }
}

/// An equivalent to [`Error`] that owns the error parameters.
#[cfg(feature = "alloc")]
#[derive(Debug)]
#[non_exhaustive]
pub enum OwnedError {
    /// No prefix specified for the config builder.
    BuildNoPrefix,

    /// Something went really, really wrong...
    Internal(u32),

    /// The media type did not have the specified prefix.
    NoPrefix(String, String),

    /// The media type did not have the specified suffix.
    NoSuffix(String, String),

    /// The media type did not have the ".v" part.
    NoVDot(String),

    #[cfg(feature = "extract-from-table")]
    /// The hierarchical structure did not contain the specified element.
    TableNoChild(String),

    #[cfg(feature = "extract-from-table")]
    /// The hierarchical structure did not contain the expected table or string.
    TableNotTable,

    #[cfg(feature = "toml-boml1")]
    /// Could not parse a TOML document.
    TomlBoml(String),

    /// The media type's version part did not consist of two dot-separated components.
    TwoComponentsExpected(String),

    /// The media type contained an invalid version component.
    UIntExpected(String, String, ParseIntError),
}

#[cfg(feature = "alloc")]
impl Display for OwnedError {
    /// Describe the error that occurred.
    #[inline]
    #[expect(clippy::min_ident_chars, reason = "this is the way it is defined")]
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
        match *self {
            Self::BuildNoPrefix => Error::BuildNoPrefix.fmt(f),
            Self::Internal(ref code) => Error::Internal(*code).fmt(f),
            Self::NoPrefix(ref value, ref prefix) => Error::NoPrefix(value, prefix).fmt(f),
            Self::NoSuffix(ref value, ref suffix) => Error::NoSuffix(value, suffix).fmt(f),
            Self::NoVDot(ref value) => Error::NoVDot(value).fmt(f),
            #[cfg(feature = "extract-from-table")]
            Self::TableNoChild(ref comp) => Error::TableNoChild(comp).fmt(f),
            #[cfg(feature = "extract-from-table")]
            Self::TableNotTable => Error::TableNotTable.fmt(f),
            #[cfg(feature = "toml-boml1")]
            Self::TomlBoml(ref err) => write!(f, "Could not parse a TOML document: {err}"),
            Self::TwoComponentsExpected(ref value) => Error::TwoComponentsExpected(value).fmt(f),
            Self::UIntExpected(ref value, ref comp, ref err) => {
                Error::UIntExpected(value, comp, (*err).clone()).fmt(f)
            }
        }
    }
}

#[cfg(feature = "alloc")]
impl CoreError for OwnedError {
    #[inline]
    fn source(&self) -> Option<&(dyn CoreError + 'static)> {
        match *self {
            Self::BuildNoPrefix
            | Self::Internal(_)
            | Self::NoPrefix(_, _)
            | Self::NoSuffix(_, _)
            | Self::NoVDot(_)
            | Self::TwoComponentsExpected(_) => None,
            #[cfg(feature = "extract-from-table")]
            Self::TableNoChild(_) | Self::TableNotTable => None,
            #[cfg(feature = "toml-boml1")]
            Self::TomlBoml(_) => None,
            Self::UIntExpected(_, _, ref err) => Some(err),
        }
    }
}

/// The extracted format version.
#[cfg_attr(feature = "facet-unstable", derive(Facet))]
pub struct Version {
    /// The major version number.
    major: u32,

    /// The minor version number.
    minor: u32,
}

impl Version {
    /// The major version number.
    #[inline]
    #[must_use]
    pub const fn major(&self) -> u32 {
        self.major
    }

    /// The minor version number.
    #[inline]
    #[must_use]
    pub const fn minor(&self) -> u32 {
        self.minor
    }

    /// Return a (major, minor) tuple.
    #[inline]
    #[must_use]
    pub const fn as_tuple(&self) -> (u32, u32) {
        (self.major, self.minor)
    }
}

impl From<(u32, u32)> for Version {
    /// Build a [`Version`] object from the major and minor version numbers.
    #[inline]
    fn from(value: (u32, u32)) -> Self {
        Self {
            major: value.0,
            minor: value.1,
        }
    }
}

impl From<Version> for (u32, u32) {
    /// Break a [`Version`] object down into the major and minor version numbers.
    #[inline]
    fn from(value: Version) -> Self {
        value.as_tuple()
    }
}

/// Runtime configuration for the media-type-version library.
#[cfg_attr(feature = "facet-unstable", derive(Facet))]
pub struct Config<'data> {
    /// The prefix to strip from the media type string.
    prefix: &'data str,

    /// The suffix (possibly empty) to strip from the media type string.
    suffix: &'data str,
}

impl<'data> Config<'data> {
    /// The prefix to strip from the media type string.
    #[inline]
    #[must_use]
    pub const fn prefix(&self) -> &str {
        self.prefix
    }

    /// The suffix (possibly empty) to strip from the media type string.
    #[inline]
    #[must_use]
    pub const fn suffix(&self) -> &str {
        self.suffix
    }

    /// Start building a configuration object.
    #[inline]
    #[must_use]
    pub fn builder() -> ConfigBuilder<'data> {
        ConfigBuilder::default()
    }

    /// For test porpoises only, build something out of things.
    #[cfg(test)]
    #[inline]
    #[must_use]
    pub const fn from_parts(prefix: &'data str, suffix: &'data str) -> Self {
        Self { prefix, suffix }
    }
}

/// Build the runtime configuration.
#[derive(Default)]
pub struct ConfigBuilder<'data> {
    /// The prefix to strip from the media type string.
    prefix: Option<&'data str>,

    /// The suffix (possibly empty) to strip from the media type string.
    suffix: Option<&'data str>,
}

impl<'data> ConfigBuilder<'data> {
    /// Set the prefix to strip from the media type string.
    #[inline]
    #[must_use]
    pub const fn prefix(self, value: &'data str) -> Self {
        Self {
            prefix: Some(value),
            ..self
        }
    }

    /// Set the suffix (possibly empty) to strip from the media type string.
    #[inline]
    #[must_use]
    pub const fn suffix(self, value: &'data str) -> Self {
        Self {
            suffix: Some(value),
            ..self
        }
    }

    /// Build a [`Config`] object with the specified settings.
    ///
    /// # Errors
    ///
    /// [`Error::BuildNoPrefix`] if [`ConfigBuilder::prefix`] was not called.
    #[inline]
    pub fn build(self) -> Result<Config<'data>, Error<'data>> {
        Ok(Config {
            prefix: self.prefix.ok_or(Error::BuildNoPrefix)?,
            suffix: self.suffix.unwrap_or_default(),
        })
    }
}

#[cfg(test)]
#[expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
#[expect(clippy::unwrap_used, reason = "this is a test suite")]
mod tests {
    extern crate alloc;

    use alloc::format;
    use alloc::string::String;

    #[cfg(feature = "facet-unstable")]
    use alloc::string::ToString as _;

    #[cfg(feature = "alloc")]
    use core::str::FromStr as _;

    use eyre::{Result, WrapErr as _};
    use facet_testhelpers::test;
    use log::{info, trace};

    #[cfg(feature = "facet-unstable")]
    use facet_pretty::FacetPretty as _;

    use super::Config;

    #[cfg(feature = "alloc")]
    use super::Error;

    #[cfg(feature = "facet-unstable")]
    use super::Version;

    #[cfg(feature = "facet-unstable")]
    fn pretty_cfg(cfg: &Config<'_>) -> String {
        format!("{cfg}", cfg = cfg.pretty())
    }

    #[cfg(not(feature = "facet-unstable"))]
    fn pretty_cfg(cfg: &Config<'_>) -> String {
        format!(
            "Config {{ prefix = {prefix:?}, suffix = {suffix:?} }}",
            prefix = cfg.prefix(),
            suffix = cfg.suffix()
        )
    }

    /// Make sure the builder, well, builds a [`Config`] object.
    #[test]
    fn builder() -> Result<()> {
        info!("Building a config builder");
        let cfg = Config::builder()
            .prefix("hello")
            .suffix("goodbye")
            .build()
            .context("build")?;
        trace!("{cfg}", cfg = pretty_cfg(&cfg));
        assert_eq!(cfg.prefix(), "hello");
        assert_eq!(cfg.suffix(), "goodbye");
        Ok(())
    }

    /// Make sure the error message does not change.
    #[cfg(feature = "alloc")]
    #[test]
    fn error_to_owned() {
        let check_to_owned_msg = |err: Error<'_>| {
            let msg = format!("{err}");
            trace!("{msg}");
            let owned = err.into_owned_error();
            let owned_msg = format!("{owned}");
            trace!("{owned_msg}");
            assert_eq!(msg, owned_msg);
        };

        check_to_owned_msg(Error::BuildNoPrefix);
        check_to_owned_msg(Error::NoPrefix("some value", "some prefix"));
        check_to_owned_msg(Error::NoSuffix("some value", "some suffix"));
        check_to_owned_msg(Error::NoVDot("stuff"));
        check_to_owned_msg(Error::TwoComponentsExpected("some kind of thing"));
        check_to_owned_msg(Error::UIntExpected(
            "something",
            "something else",
            u32::from_str("?").unwrap_err(),
        ));
    }

    /// Make sure the [`Facet`] trait for [`Version`] works.
    #[cfg(feature = "facet-unstable")]
    #[test]
    fn facet_pretty_contains_things() {
        let major = 42;
        let minor = 616;
        let ver = Version::from((major, minor));
        let repr = format!("{ver}", ver = ver.pretty());
        assert!(
            repr.contains("/// The major version number"),
            "no docstring in the pretty representation: {repr:?}"
        );
        assert!(
            repr.contains(&major.to_string()),
            "no '{major}' in the pretty representation: {repr:?}"
        );
        assert!(
            repr.contains(&minor.to_string()),
            "no '{minor}' in the pretty representation: {repr:?}"
        );
    }
}