1#![deny(missing_docs)]
2#![deny(clippy::missing_docs_in_private_items)]
3#![no_std]
4#![doc(html_root_url = "https://docs.rs/media-type-version/0.2.0")]
52#![expect(clippy::pub_use, reason = "re-export common symbols")]
53
54use core::str::FromStr as _;
55
56use log::debug;
57
58#[cfg(feature = "toml-boml1")]
59use boml::prelude::TomlGetError as TomlBomlGetError;
60
61#[cfg(feature = "toml-boml1")]
62use boml::table::TomlTable as TomlBomlTable;
63
64#[cfg(feature = "json-serialzero-unstable")]
65use serialzero::JsonValue;
66
67mod defs;
68
69pub use defs::{Config, ConfigBuilder, Error, Version};
70
71#[cfg(feature = "alloc")]
72pub use defs::OwnedError;
73
74const FEATURES_COUNT_BASE: usize = 2;
76
77#[cfg(not(feature = "extract-from-table"))]
78const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 0;
80
81#[cfg(feature = "extract-from-table")]
82const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 1;
84
85const FEATURES_COUNT: usize = FEATURES_COUNT_BASE + FEATURES_COUNT_EXTRACT_FROM_TABLE;
87
88pub const FEATURES: [(&str, &str); FEATURES_COUNT] = [
90 ("media-type-version", env!("CARGO_PKG_VERSION")),
91 ("extract", "0.1"),
92 #[cfg(feature = "extract-from-table")]
93 ("extract-from-table", "0.1"),
94];
95
96#[inline]
108pub fn extract<'data>(
109 cfg: &'data Config<'data>,
110 value: &'data str,
111) -> Result<Version, Error<'data>> {
112 debug!(
113 "Parsing a media type string '{value}', expecting prefix '{prefix}' and suffix '{suffix}'",
114 prefix = cfg.prefix(),
115 suffix = cfg.suffix()
116 );
117 let no_prefix = value
118 .strip_prefix(cfg.prefix())
119 .ok_or_else(|| Error::NoPrefix(value, cfg.prefix()))?;
120 let no_suffix = no_prefix
121 .strip_suffix(cfg.suffix())
122 .ok_or_else(|| Error::NoSuffix(value, cfg.suffix()))?;
123 let no_vdot = no_suffix
124 .strip_prefix(".v")
125 .ok_or(Error::NoVDot(no_suffix))?;
126 let (first, second) = {
127 let mut parts_it = no_vdot.split('.');
128 let first = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
129 let second = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
130 if parts_it.next().is_some() {
131 return Err(Error::TwoComponentsExpected(value));
132 }
133 (first, second)
134 };
135 let major = u32::from_str(first).map_err(|err| Error::UIntExpected(value, first, err))?;
136 let minor = u32::from_str(second).map_err(|err| Error::UIntExpected(value, second, err))?;
137 Ok(Version::from((major, minor)))
138}
139
140#[cfg(feature = "extract-from-table")]
141pub trait Table<'data> {
143 fn is_table(&'data self) -> bool;
145
146 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>>;
152
153 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>>;
159}
160
161#[cfg(feature = "extract-from-table")]
162#[inline]
171pub fn extract_from_table<'data, T>(
172 cfg: &'data Config<'data>,
173 mut value: &'data T,
174 path: &'data [&'data str],
175) -> Result<Version, Error<'data>>
176where
177 T: Table<'data>,
178{
179 if !value.is_table() {
180 return Err(Error::TableNotTable);
181 }
182 value = path
183 .iter()
184 .try_fold(value, |current_value, comp| -> Result<&T, Error<'data>> {
185 current_value.get_child_table(comp)
186 })?;
187 let media_type = value.get_child_string("mediaType")?;
188 extract(cfg, media_type)
189}
190
191#[cfg(feature = "json-serialzero-unstable")]
192impl<'data> Table<'data> for JsonValue {
193 #[inline]
194 fn is_table(&self) -> bool {
195 matches!(*self, Self::Object(_))
196 }
197
198 #[inline]
199 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
200 let Self::Object(ref map) = *self else {
201 return Err(Error::TableNotTable);
202 };
203 map.get(name).ok_or(Error::TableNoChild(name))
204 }
205
206 #[inline]
207 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
208 let Self::Object(ref map) = *self else {
209 return Err(Error::TableNotTable);
210 };
211 let child = map.get(name).ok_or(Error::TableNoChild(name))?;
212 let Self::String(ref value) = *child else {
213 return Err(Error::TableNotTable);
214 };
215 Ok(value)
216 }
217}
218
219#[cfg(feature = "toml-boml1")]
220impl<'data> Table<'data> for TomlBomlTable<'data> {
221 #[inline]
222 fn is_table(&self) -> bool {
223 true
224 }
225
226 #[inline]
227 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
228 self.get_table(name).map_err(|err| match err {
229 TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
230 TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
231 })
232 }
233
234 #[inline]
235 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
236 self.get_string(name).map_err(|err| match err {
237 TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
238 TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 extern crate alloc;
246
247 use alloc::format;
248 use alloc::string::String;
249
250 use eyre::{Result, WrapErr as _};
251 use facet_testhelpers::test;
252
253 #[cfg(feature = "facet-unstable")]
254 use facet_pretty::FacetPretty as _;
255
256 use crate::{Config, Error, Version};
257
258 static CFG: Config<'_> = Config::from_parts("this/and", "+that");
260
261 #[cfg(feature = "facet-unstable")]
262 fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
263 match *res {
264 Ok(ref ver) => format!("OK: {ver}", ver = ver.pretty()),
265 Err(ref err) => format!("Error: {err}"),
266 }
267 }
268
269 #[cfg(not(feature = "facet-unstable"))]
270 fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
271 match *res {
272 Ok(ref ver) => format!(
273 "OK: Version {{ major: {major}, minor: {minor} }}",
274 major = ver.major(),
275 minor = ver.minor(),
276 ),
277 Err(ref err) => format!("Error: {err}"),
278 }
279 }
280
281 #[test]
283 fn extract_fail_no_prefix() {
284 let res = crate::extract(&CFG, "nothing");
285 assert!(
286 matches!(res, Err(Error::NoPrefix(_, _))),
287 "expected Error::NoPrefix, got {res}",
288 res = pretty_res(&res)
289 );
290 }
291
292 #[test]
294 fn extract_fail_no_suffix() {
295 let res = crate::extract(&CFG, "this/andnothing");
296 assert!(
297 matches!(res, Err(Error::NoSuffix(_, _))),
298 "expected Error::NoSuffix, got {res}",
299 res = pretty_res(&res)
300 );
301 }
302
303 #[test]
305 fn extract_fail_no_vdot() {
306 let res = crate::extract(&CFG, "this/andnothing+that");
307 assert!(
308 matches!(res, Err(Error::NoVDot(_))),
309 "expected Error::NoVDot, got {res}",
310 res = pretty_res(&res)
311 );
312 }
313
314 #[test]
316 fn extract_fail_two_expected() {
317 let res = crate::extract(&CFG, "this/and.vnothing+that");
318 assert!(
319 matches!(res, Err(Error::TwoComponentsExpected(_))),
320 "expected Error::TwoComponentsExpected, got {res}",
321 res = pretty_res(&res)
322 );
323 }
324
325 #[test]
327 fn extract_fail_uint_expected() {
328 let res_first = crate::extract(&CFG, "this/and.va.42+that");
329 assert!(
330 matches!(res_first, Err(Error::UIntExpected(_, _, _))),
331 "expected Error::UIntExpected, got {res_first}",
332 res_first = pretty_res(&res_first)
333 );
334
335 let res_second = crate::extract(&CFG, "this/and.v42.+that");
336 assert!(
337 matches!(res_second, Err(Error::UIntExpected(_, _, _))),
338 "expected Error::UIntExpected, got {res_second}",
339 res_second = pretty_res(&res_second)
340 );
341 }
342
343 #[test]
345 fn extract_ok() -> Result<()> {
346 let ver = crate::extract(&CFG, "this/and.v616.42+that").context("extract")?;
347 assert_eq!(ver.as_tuple(), (616, 42));
348 Ok(())
349 }
350}