#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
#![no_std]
#![expect(clippy::pub_use, reason = "re-export common symbols")]
use core::str::FromStr as _;
use log::debug;
pub mod defs;
pub use defs::{Config, Error, Version};
#[inline]
pub fn extract<'data>(
cfg: &'data Config<'data>,
value: &'data str,
) -> Result<Version, Error<'data>> {
debug!(
"Parsing a media type string '{value}', expecting prefix '{prefix}' and suffix '{suffix}'",
prefix = cfg.prefix(),
suffix = cfg.suffix()
);
let no_prefix = value
.strip_prefix(cfg.prefix())
.ok_or_else(|| Error::NoPrefix(value, cfg.prefix()))?;
let no_suffix = no_prefix
.strip_suffix(cfg.suffix())
.ok_or_else(|| Error::NoSuffix(value, cfg.suffix()))?;
let no_vdot = no_suffix
.strip_prefix(".v")
.ok_or(Error::NoVDot(no_suffix))?;
let mut parts_it = no_vdot.split('.');
if let Some(first) = parts_it.next() {
if let Some(second) = parts_it.next() {
if parts_it.next().is_some() {
Err(Error::TwoComponentsExpected(value))
} else {
let major =
u32::from_str(first).map_err(|err| Error::UIntExpected(value, first, err))?;
let minor =
u32::from_str(second).map_err(|err| Error::UIntExpected(value, second, err))?;
Ok(Version::from((major, minor)))
}
} else {
Err(Error::TwoComponentsExpected(value))
}
} else {
Err(Error::TwoComponentsExpected(value))
}
}
#[cfg(test)]
#[expect(clippy::panic_in_result_fn, reason = "this is a test suite")]
mod tests {
extern crate alloc;
use alloc::format;
use alloc::string::String;
use anyhow::{Context as _, Result, bail};
use test_log::test;
#[cfg(feature = "facet-unstable")]
use facet_pretty::FacetPretty as _;
use crate::{Config, Error, Version};
static CFG: Config<'_> = Config::from_parts("this/and", "+that");
#[cfg(feature = "facet-unstable")]
fn pretty(ver: &Version) -> String {
format!("{ver}", ver = ver.pretty())
}
#[cfg(not(feature = "facet-unstable"))]
fn pretty(ver: &Version) -> String {
format!(
"Version {{ major: {major}, minor: {minor} }}",
major = ver.major(),
minor = ver.minor(),
)
}
#[test]
fn extract_fail_no_prefix() -> Result<()> {
match crate::extract(&CFG, "nothing") {
Err(Error::NoPrefix(_, _)) => Ok(()),
Err(other) => Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
}
#[test]
fn extract_fail_no_suffix() -> Result<()> {
match crate::extract(&CFG, "this/andnothing") {
Err(Error::NoSuffix(_, _)) => Ok(()),
Err(other) => Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
}
#[test]
fn extract_fail_no_vdot() -> Result<()> {
match crate::extract(&CFG, "this/andnothing+that") {
Err(Error::NoVDot(_)) => Ok(()),
Err(other) => Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
}
#[test]
fn extract_fail_two_expected() -> Result<()> {
match crate::extract(&CFG, "this/and.vnothing+that") {
Err(Error::TwoComponentsExpected(_)) => Ok(()),
Err(other) => Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
}
#[test]
fn extract_fail_uint_expected() -> Result<()> {
match crate::extract(&CFG, "this/and.va.42+that") {
Err(Error::UIntExpected(_, _, _)) => (),
Err(other) => return Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
match crate::extract(&CFG, "this/and.v42.+that") {
Err(Error::UIntExpected(_, _, _)) => (),
Err(other) => return Err(other).context("unexpected error"),
Ok(res) => bail!("Unexpected extract() success: {res}", res = pretty(&res)),
}
Ok(())
}
#[test]
fn extract_ok() -> Result<()> {
assert_eq!(
crate::extract(&CFG, "this/and.v616.42+that",)?.as_tuple(),
(616, 42)
);
Ok(())
}
}