#![deny(missing_docs)]
#![deny(clippy::missing_docs_in_private_items)]
#![no_std]
#![doc(html_root_url = "https://docs.rs/media-type-version/0.2.1")]
#![expect(clippy::pub_use, reason = "re-export common symbols")]
use core::str::FromStr as _;
use log::debug;
#[cfg(feature = "toml-boml1")]
use boml::prelude::TomlGetError as TomlBomlGetError;
#[cfg(feature = "toml-boml1")]
use boml::table::TomlTable as TomlBomlTable;
#[cfg(all(feature = "facet030-unstable", not(feature = "facet032-unstable")))]
extern crate facet030 as facet;
#[cfg(feature = "facet032-unstable")]
extern crate facet032 as facet;
#[cfg(feature = "json-serialzero0-unstable")]
use serialzero0::JsonValue;
mod defs;
pub use defs::{Config, ConfigBuilder, Error, Version};
#[cfg(feature = "alloc")]
pub use defs::OwnedError;
const FEATURES_COUNT_BASE: usize = 2;
#[cfg(not(feature = "extract-from-table"))]
const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 0;
#[cfg(feature = "extract-from-table")]
const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 1;
const FEATURES_COUNT: usize = FEATURES_COUNT_BASE + FEATURES_COUNT_EXTRACT_FROM_TABLE;
pub const FEATURES: [(&str, &str); FEATURES_COUNT] = [
("media-type-version", env!("CARGO_PKG_VERSION")),
("extract", "0.1"),
#[cfg(feature = "extract-from-table")]
("extract-from-table", "0.1"),
];
#[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 (first, second) = {
let mut parts_it = no_vdot.split('.');
let first = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
let second = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
if parts_it.next().is_some() {
return Err(Error::TwoComponentsExpected(value));
}
(first, second)
};
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)))
}
#[cfg(feature = "extract-from-table")]
pub trait Table<'data> {
fn is_table(&'data self) -> bool;
fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>>;
fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>>;
}
#[cfg(feature = "extract-from-table")]
#[inline]
pub fn extract_from_table<'data, T>(
cfg: &'data Config<'data>,
mut value: &'data T,
path: &'data [&'data str],
) -> Result<Version, Error<'data>>
where
T: Table<'data>,
{
if !value.is_table() {
return Err(Error::TableNotTable);
}
value = path
.iter()
.try_fold(value, |current_value, comp| -> Result<&T, Error<'data>> {
current_value.get_child_table(comp)
})?;
let media_type = value.get_child_string("mediaType")?;
extract(cfg, media_type)
}
#[cfg(feature = "json-serialzero0-unstable")]
impl<'data> Table<'data> for JsonValue {
#[inline]
fn is_table(&self) -> bool {
matches!(*self, Self::Object(_))
}
#[inline]
fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
let Self::Object(ref map) = *self else {
return Err(Error::TableNotTable);
};
map.get(name).ok_or(Error::TableNoChild(name))
}
#[inline]
fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
let Self::Object(ref map) = *self else {
return Err(Error::TableNotTable);
};
let child = map.get(name).ok_or(Error::TableNoChild(name))?;
let Self::String(ref value) = *child else {
return Err(Error::TableNotTable);
};
Ok(value)
}
}
#[cfg(feature = "toml-boml1")]
impl<'data> Table<'data> for TomlBomlTable<'data> {
#[inline]
fn is_table(&self) -> bool {
true
}
#[inline]
fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
self.get_table(name).map_err(|err| match err {
TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
})
}
#[inline]
fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
self.get_string(name).map_err(|err| match err {
TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
})
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::format;
use alloc::string::String;
use eyre::{Result, WrapErr as _};
use facet_testhelpers::test;
#[cfg(feature = "facet030-unstable")]
use facet_pretty030::FacetPretty as _;
#[cfg(feature = "facet032-unstable")]
use facet_pretty032::FacetPretty as _;
use crate::{Config, Error, Version};
static CFG: Config<'_> = Config::from_parts("this/and", "+that");
#[cfg(any(feature = "facet030-unstable", feature = "facet032-unstable"))]
fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
match *res {
Ok(ref ver) => format!("OK: {ver}", ver = ver.pretty()),
Err(ref err) => format!("Error: {err}"),
}
}
#[cfg(not(any(feature = "facet030-unstable", feature = "facet032-unstable")))]
fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
match *res {
Ok(ref ver) => format!(
"OK: Version {{ major: {major}, minor: {minor} }}",
major = ver.major(),
minor = ver.minor(),
),
Err(ref err) => format!("Error: {err}"),
}
}
#[test]
fn extract_fail_no_prefix() {
let res = crate::extract(&CFG, "nothing");
assert!(
matches!(res, Err(Error::NoPrefix(_, _))),
"expected Error::NoPrefix, got {res}",
res = pretty_res(&res)
);
}
#[test]
fn extract_fail_no_suffix() {
let res = crate::extract(&CFG, "this/andnothing");
assert!(
matches!(res, Err(Error::NoSuffix(_, _))),
"expected Error::NoSuffix, got {res}",
res = pretty_res(&res)
);
}
#[test]
fn extract_fail_no_vdot() {
let res = crate::extract(&CFG, "this/andnothing+that");
assert!(
matches!(res, Err(Error::NoVDot(_))),
"expected Error::NoVDot, got {res}",
res = pretty_res(&res)
);
}
#[test]
fn extract_fail_two_expected() {
let res = crate::extract(&CFG, "this/and.vnothing+that");
assert!(
matches!(res, Err(Error::TwoComponentsExpected(_))),
"expected Error::TwoComponentsExpected, got {res}",
res = pretty_res(&res)
);
}
#[test]
fn extract_fail_uint_expected() {
let res_first = crate::extract(&CFG, "this/and.va.42+that");
assert!(
matches!(res_first, Err(Error::UIntExpected(_, _, _))),
"expected Error::UIntExpected, got {res_first}",
res_first = pretty_res(&res_first)
);
let res_second = crate::extract(&CFG, "this/and.v42.+that");
assert!(
matches!(res_second, Err(Error::UIntExpected(_, _, _))),
"expected Error::UIntExpected, got {res_second}",
res_second = pretty_res(&res_second)
);
}
#[test]
fn extract_ok() -> Result<()> {
let ver = crate::extract(&CFG, "this/and.v616.42+that").context("extract")?;
assert_eq!(ver.as_tuple(), (616, 42));
Ok(())
}
}