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.1")]
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(all(feature = "facet030-unstable", not(feature = "facet032-unstable")))]
65extern crate facet030 as facet;
66
67#[cfg(feature = "facet032-unstable")]
68extern crate facet032 as facet;
69
70#[cfg(feature = "json-serialzero0-unstable")]
71use serialzero0::JsonValue;
72
73mod defs;
74
75pub use defs::{Config, ConfigBuilder, Error, Version};
76
77#[cfg(feature = "alloc")]
78pub use defs::OwnedError;
79
80const FEATURES_COUNT_BASE: usize = 2;
82
83#[cfg(not(feature = "extract-from-table"))]
84const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 0;
86
87#[cfg(feature = "extract-from-table")]
88const FEATURES_COUNT_EXTRACT_FROM_TABLE: usize = 1;
90
91const FEATURES_COUNT: usize = FEATURES_COUNT_BASE + FEATURES_COUNT_EXTRACT_FROM_TABLE;
93
94pub const FEATURES: [(&str, &str); FEATURES_COUNT] = [
96 ("media-type-version", env!("CARGO_PKG_VERSION")),
97 ("extract", "0.1"),
98 #[cfg(feature = "extract-from-table")]
99 ("extract-from-table", "0.1"),
100];
101
102#[inline]
114pub fn extract<'data>(
115 cfg: &'data Config<'data>,
116 value: &'data str,
117) -> Result<Version, Error<'data>> {
118 debug!(
119 "Parsing a media type string '{value}', expecting prefix '{prefix}' and suffix '{suffix}'",
120 prefix = cfg.prefix(),
121 suffix = cfg.suffix()
122 );
123 let no_prefix = value
124 .strip_prefix(cfg.prefix())
125 .ok_or_else(|| Error::NoPrefix(value, cfg.prefix()))?;
126 let no_suffix = no_prefix
127 .strip_suffix(cfg.suffix())
128 .ok_or_else(|| Error::NoSuffix(value, cfg.suffix()))?;
129 let no_vdot = no_suffix
130 .strip_prefix(".v")
131 .ok_or(Error::NoVDot(no_suffix))?;
132 let (first, second) = {
133 let mut parts_it = no_vdot.split('.');
134 let first = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
135 let second = parts_it.next().ok_or(Error::TwoComponentsExpected(value))?;
136 if parts_it.next().is_some() {
137 return Err(Error::TwoComponentsExpected(value));
138 }
139 (first, second)
140 };
141 let major = u32::from_str(first).map_err(|err| Error::UIntExpected(value, first, err))?;
142 let minor = u32::from_str(second).map_err(|err| Error::UIntExpected(value, second, err))?;
143 Ok(Version::from((major, minor)))
144}
145
146#[cfg(feature = "extract-from-table")]
147pub trait Table<'data> {
149 fn is_table(&'data self) -> bool;
151
152 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>>;
158
159 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>>;
165}
166
167#[cfg(feature = "extract-from-table")]
168#[inline]
177pub fn extract_from_table<'data, T>(
178 cfg: &'data Config<'data>,
179 mut value: &'data T,
180 path: &'data [&'data str],
181) -> Result<Version, Error<'data>>
182where
183 T: Table<'data>,
184{
185 if !value.is_table() {
186 return Err(Error::TableNotTable);
187 }
188 value = path
189 .iter()
190 .try_fold(value, |current_value, comp| -> Result<&T, Error<'data>> {
191 current_value.get_child_table(comp)
192 })?;
193 let media_type = value.get_child_string("mediaType")?;
194 extract(cfg, media_type)
195}
196
197#[cfg(feature = "json-serialzero0-unstable")]
198impl<'data> Table<'data> for JsonValue {
199 #[inline]
200 fn is_table(&self) -> bool {
201 matches!(*self, Self::Object(_))
202 }
203
204 #[inline]
205 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
206 let Self::Object(ref map) = *self else {
207 return Err(Error::TableNotTable);
208 };
209 map.get(name).ok_or(Error::TableNoChild(name))
210 }
211
212 #[inline]
213 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
214 let Self::Object(ref map) = *self else {
215 return Err(Error::TableNotTable);
216 };
217 let child = map.get(name).ok_or(Error::TableNoChild(name))?;
218 let Self::String(ref value) = *child else {
219 return Err(Error::TableNotTable);
220 };
221 Ok(value)
222 }
223}
224
225#[cfg(feature = "toml-boml1")]
226impl<'data> Table<'data> for TomlBomlTable<'data> {
227 #[inline]
228 fn is_table(&self) -> bool {
229 true
230 }
231
232 #[inline]
233 fn get_child_table(&'data self, name: &'data str) -> Result<&'data Self, Error<'data>> {
234 self.get_table(name).map_err(|err| match err {
235 TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
236 TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
237 })
238 }
239
240 #[inline]
241 fn get_child_string(&'data self, name: &'data str) -> Result<&'data str, Error<'data>> {
242 self.get_string(name).map_err(|err| match err {
243 TomlBomlGetError::InvalidKey => Error::TableNoChild(name),
244 TomlBomlGetError::TypeMismatch(_, _) => Error::TableNotTable,
245 })
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 extern crate alloc;
252
253 use alloc::format;
254 use alloc::string::String;
255
256 use eyre::{Result, WrapErr as _};
257 use facet_testhelpers::test;
258
259 #[cfg(feature = "facet030-unstable")]
260 use facet_pretty030::FacetPretty as _;
261
262 #[cfg(feature = "facet032-unstable")]
263 use facet_pretty032::FacetPretty as _;
264
265 use crate::{Config, Error, Version};
266
267 static CFG: Config<'_> = Config::from_parts("this/and", "+that");
269
270 #[cfg(any(feature = "facet030-unstable", feature = "facet032-unstable"))]
271 fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
272 match *res {
273 Ok(ref ver) => format!("OK: {ver}", ver = ver.pretty()),
274 Err(ref err) => format!("Error: {err}"),
275 }
276 }
277
278 #[cfg(not(any(feature = "facet030-unstable", feature = "facet032-unstable")))]
279 fn pretty_res(res: &Result<Version, Error<'_>>) -> String {
280 match *res {
281 Ok(ref ver) => format!(
282 "OK: Version {{ major: {major}, minor: {minor} }}",
283 major = ver.major(),
284 minor = ver.minor(),
285 ),
286 Err(ref err) => format!("Error: {err}"),
287 }
288 }
289
290 #[test]
292 fn extract_fail_no_prefix() {
293 let res = crate::extract(&CFG, "nothing");
294 assert!(
295 matches!(res, Err(Error::NoPrefix(_, _))),
296 "expected Error::NoPrefix, got {res}",
297 res = pretty_res(&res)
298 );
299 }
300
301 #[test]
303 fn extract_fail_no_suffix() {
304 let res = crate::extract(&CFG, "this/andnothing");
305 assert!(
306 matches!(res, Err(Error::NoSuffix(_, _))),
307 "expected Error::NoSuffix, got {res}",
308 res = pretty_res(&res)
309 );
310 }
311
312 #[test]
314 fn extract_fail_no_vdot() {
315 let res = crate::extract(&CFG, "this/andnothing+that");
316 assert!(
317 matches!(res, Err(Error::NoVDot(_))),
318 "expected Error::NoVDot, got {res}",
319 res = pretty_res(&res)
320 );
321 }
322
323 #[test]
325 fn extract_fail_two_expected() {
326 let res = crate::extract(&CFG, "this/and.vnothing+that");
327 assert!(
328 matches!(res, Err(Error::TwoComponentsExpected(_))),
329 "expected Error::TwoComponentsExpected, got {res}",
330 res = pretty_res(&res)
331 );
332 }
333
334 #[test]
336 fn extract_fail_uint_expected() {
337 let res_first = crate::extract(&CFG, "this/and.va.42+that");
338 assert!(
339 matches!(res_first, Err(Error::UIntExpected(_, _, _))),
340 "expected Error::UIntExpected, got {res_first}",
341 res_first = pretty_res(&res_first)
342 );
343
344 let res_second = crate::extract(&CFG, "this/and.v42.+that");
345 assert!(
346 matches!(res_second, Err(Error::UIntExpected(_, _, _))),
347 "expected Error::UIntExpected, got {res_second}",
348 res_second = pretty_res(&res_second)
349 );
350 }
351
352 #[test]
354 fn extract_ok() -> Result<()> {
355 let ver = crate::extract(&CFG, "this/and.v616.42+that").context("extract")?;
356 assert_eq!(ver.as_tuple(), (616, 42));
357 Ok(())
358 }
359}