1#[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#[derive(Debug)]
26#[non_exhaustive]
27#[expect(clippy::error_impl_error, reason = "common enough convention")]
28pub enum Error<'data> {
29 BuildNoPrefix,
31
32 Internal(u32),
34
35 NoPrefix(&'data str, &'data str),
37
38 NoSuffix(&'data str, &'data str),
40
41 NoVDot(&'data str),
43
44 #[cfg(feature = "extract-from-table")]
45 TableNoChild(&'data str),
47
48 #[cfg(feature = "extract-from-table")]
49 TableNotTable,
51
52 #[cfg(feature = "toml-boml1")]
53 TomlParse(TomlError<'data>),
55
56 TwoComponentsExpected(&'data str),
58
59 UIntExpected(&'data str, &'data str, ParseIntError),
61}
62
63impl Display for Error<'_> {
64 #[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 #[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#[cfg(feature = "alloc")]
166#[derive(Debug)]
167#[non_exhaustive]
168pub enum OwnedError {
169 BuildNoPrefix,
171
172 Internal(u32),
174
175 NoPrefix(String, String),
177
178 NoSuffix(String, String),
180
181 NoVDot(String),
183
184 #[cfg(feature = "extract-from-table")]
185 TableNoChild(String),
187
188 #[cfg(feature = "extract-from-table")]
189 TableNotTable,
191
192 #[cfg(feature = "toml-boml1")]
193 TomlBoml(String),
195
196 TwoComponentsExpected(String),
198
199 UIntExpected(String, String, ParseIntError),
201}
202
203#[cfg(feature = "alloc")]
204impl Display for OwnedError {
205 #[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#[cfg_attr(feature = "facet-unstable", derive(Facet))]
251pub struct Version {
252 major: u32,
254
255 minor: u32,
257}
258
259impl Version {
260 #[inline]
262 #[must_use]
263 pub const fn major(&self) -> u32 {
264 self.major
265 }
266
267 #[inline]
269 #[must_use]
270 pub const fn minor(&self) -> u32 {
271 self.minor
272 }
273
274 #[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 #[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 #[inline]
296 fn from(value: Version) -> Self {
297 value.as_tuple()
298 }
299}
300
301#[cfg_attr(feature = "facet-unstable", derive(Facet))]
303pub struct Config<'data> {
304 prefix: &'data str,
306
307 suffix: &'data str,
309}
310
311impl<'data> Config<'data> {
312 #[inline]
314 #[must_use]
315 pub const fn prefix(&self) -> &str {
316 self.prefix
317 }
318
319 #[inline]
321 #[must_use]
322 pub const fn suffix(&self) -> &str {
323 self.suffix
324 }
325
326 #[inline]
328 #[must_use]
329 pub fn builder() -> ConfigBuilder<'data> {
330 ConfigBuilder::default()
331 }
332
333 #[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#[derive(Default)]
344pub struct ConfigBuilder<'data> {
345 prefix: Option<&'data str>,
347
348 suffix: Option<&'data str>,
350}
351
352impl<'data> ConfigBuilder<'data> {
353 #[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 #[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 #[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 #[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 #[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 #[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}