1#[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#[derive(Debug)]
29#[non_exhaustive]
30#[expect(clippy::error_impl_error, reason = "common enough convention")]
31pub enum Error<'data> {
32 BuildNoPrefix,
34
35 Internal(u32),
37
38 NoPrefix(&'data str, &'data str),
40
41 NoSuffix(&'data str, &'data str),
43
44 NoVDot(&'data str),
46
47 #[cfg(feature = "extract-from-table")]
48 TableNoChild(&'data str),
50
51 #[cfg(feature = "extract-from-table")]
52 TableNotTable,
54
55 #[cfg(feature = "toml-boml1")]
56 TomlParse(TomlError<'data>),
58
59 TwoComponentsExpected(&'data str),
61
62 UIntExpected(&'data str, &'data str, ParseIntError),
64}
65
66impl Display for Error<'_> {
67 #[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 #[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#[cfg(feature = "alloc")]
168#[derive(Debug)]
169#[non_exhaustive]
170pub enum OwnedError {
171 BuildNoPrefix,
173
174 Internal(u32),
176
177 NoPrefix(String, String),
179
180 NoSuffix(String, String),
182
183 NoVDot(String),
185
186 #[cfg(feature = "extract-from-table")]
187 TableNoChild(String),
189
190 #[cfg(feature = "extract-from-table")]
191 TableNotTable,
193
194 #[cfg(feature = "toml-boml1")]
195 TomlBoml(String),
197
198 TwoComponentsExpected(String),
200
201 UIntExpected(String, String, ParseIntError),
203}
204
205#[cfg(feature = "alloc")]
206impl Display for OwnedError {
207 #[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#[derive(Debug)]
252pub struct Version {
253 major: u32,
255
256 minor: u32,
258}
259
260impl Version {
261 #[inline]
263 #[must_use]
264 pub const fn major(&self) -> u32 {
265 self.major
266 }
267
268 #[inline]
270 #[must_use]
271 pub const fn minor(&self) -> u32 {
272 self.minor
273 }
274
275 #[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 #[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 #[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
332pub struct Config<'data> {
334 prefix: &'data str,
336
337 suffix: &'data str,
339}
340
341impl<'data> Config<'data> {
342 #[inline]
344 #[must_use]
345 pub const fn prefix(&self) -> &str {
346 self.prefix
347 }
348
349 #[inline]
351 #[must_use]
352 pub const fn suffix(&self) -> &str {
353 self.suffix
354 }
355
356 #[inline]
358 #[must_use]
359 pub fn builder() -> ConfigBuilder<'data> {
360 ConfigBuilder::default()
361 }
362
363 #[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#[derive(Default)]
373pub struct ConfigBuilder<'data> {
374 prefix: Option<&'data str>,
376
377 suffix: Option<&'data str>,
379}
380
381impl<'data> ConfigBuilder<'data> {
382 #[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 #[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 #[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 #[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 #[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}