Jonathan Strong
2 years ago
commit
c80e4a8512
5 changed files with 266 additions and 0 deletions
@ -0,0 +1,17 @@
|
||||
[package] |
||||
name = "deserialize-file-size" |
||||
version = "1.0.0" |
||||
edition = "2021" |
||||
authors = ["Jonathan Strong <jstrong@shipyard.rs>"] |
||||
description = "A serde helper function for deserializing file size input flexibly and robustly." |
||||
repository = "https://git.shipyard.rs/jstrong/deserialize-file-size" |
||||
keywords = ["serde", "deserialize", "human", "value formatting"] |
||||
readme = "README.md" |
||||
license = "MIT" |
||||
|
||||
[dependencies] |
||||
serde = { version = "1", features = ["derive"] } |
||||
byte-unit = "4.0.12" |
||||
|
||||
[dev-dependencies] |
||||
serde_json = "1" |
@ -0,0 +1,18 @@
|
||||
MIT License |
||||
|
||||
Copyright (c) 2022 Jonathan Strong |
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,36 @@
|
||||
# deserialize-file-size |
||||
|
||||
A serde helper function for deserializing file size input |
||||
flexibly and robustly. |
||||
|
||||
Accepts either: |
||||
|
||||
1) a "human" size string, e.g. "1k", "5mb", "12GiB", etc. |
||||
2) an integer number of bytes |
||||
|
||||
## Example |
||||
|
||||
```rust |
||||
use serde::Deserialize; |
||||
use deserialize_file_size::deserialize_file_size; |
||||
|
||||
#[derive(Deserialize, Debug, PartialEq)] |
||||
struct FileSize { |
||||
#[serde(deserialize_with = "deserialize_file_size")] |
||||
sz: usize, |
||||
} |
||||
|
||||
let size_str = r#"{"sz": "42mb"}"#; |
||||
assert_eq!( |
||||
serde_json::from_str::<FileSize>(size_str).unwrap(), |
||||
FileSize { sz: 1024 * 1024 * 42 }, |
||||
); |
||||
|
||||
let int_bytes = r#"{"sz": 4096}"#; |
||||
assert_eq!( |
||||
serde_json::from_str::<FileSize>(int_bytes).unwrap(), |
||||
FileSize { sz: 4096 }, |
||||
); |
||||
``` |
||||
|
||||
|
@ -0,0 +1,192 @@
|
||||
//! A serde helper function for deserializing file size input
|
||||
//! flexibly and robustly.
|
||||
//!
|
||||
//! Accepts either:
|
||||
//!
|
||||
//! 1) a "human" size string, e.g. "1k", "5mb", "12GiB", etc.
|
||||
//! 2) an integer number of bytes
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use serde::Deserialize;
|
||||
//! use deserialize_file_size::deserialize_file_size;
|
||||
//!
|
||||
//! #[derive(Deserialize, Debug, PartialEq)]
|
||||
//! struct FileSize {
|
||||
//! #[serde(deserialize_with = "deserialize_file_size")]
|
||||
//! sz: usize,
|
||||
//! }
|
||||
//!
|
||||
//! let size_str = r#"{"sz": "42mb"}"#;
|
||||
//! assert_eq!(
|
||||
//! serde_json::from_str::<FileSize>(size_str).unwrap(),
|
||||
//! FileSize { sz: 1024 * 1024 * 42 },
|
||||
//! );
|
||||
//!
|
||||
//! let int_bytes = r#"{"sz": 4096}"#;
|
||||
//! assert_eq!(
|
||||
//! serde_json::from_str::<FileSize>(int_bytes).unwrap(),
|
||||
//! FileSize { sz: 4096 },
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
#![allow(clippy::single_char_pattern)] // annoying, who the f cares "a" vs 'a'
|
||||
|
||||
use std::fmt; |
||||
use serde::de::{self, Visitor}; |
||||
|
||||
/// returns size in bytes if parsing is successful. note: units "k"/"kb", "m"/"mb",
|
||||
/// and "g"/"gb" are converted to KiB, MiB, and GiB respectively. there are no 1000-based
|
||||
/// file sizes as far as this implementation is concerned, 1024 is file size god.
|
||||
pub fn parse_file_size(s: &str) -> Option<usize> { |
||||
let mut s = s.trim().to_string(); |
||||
s[..].make_ascii_lowercase(); |
||||
let s = if s.contains("ib") { |
||||
s |
||||
} else { |
||||
s.replace("kb", "k") // first truncate kb/mb/gb -> k/m/g
|
||||
.replace("mb", "m") |
||||
.replace("gb", "g") |
||||
.replace("k", "kib") // then transform k/m/g -> kib/mib/gib
|
||||
.replace("m", "mib") |
||||
.replace("g", "gib") |
||||
}; |
||||
|
||||
let bytes: u128 = byte_unit::Byte::from_str(&s) |
||||
.ok()? |
||||
.get_bytes(); |
||||
|
||||
usize::try_from(bytes).ok() |
||||
} |
||||
|
||||
/// A serde "deserialize_with" helper function for parsing a `usize` field
|
||||
/// flexibly and robustly.
|
||||
///
|
||||
/// Accepts input that is either:
|
||||
///
|
||||
/// 1) a "human" size string, e.g. "10k", "42mb", "7GiB", etc.
|
||||
/// 2) an integer number of bytes
|
||||
///
|
||||
/// To make the point explicit, either `String`/`&str` or integer
|
||||
/// (`visit_u64`-compatible) input is accepted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use serde::Deserialize;
|
||||
/// use deserialize_file_size::deserialize_file_size;
|
||||
///
|
||||
/// #[derive(Deserialize, Debug, PartialEq)]
|
||||
/// struct FileSize {
|
||||
/// #[serde(deserialize_with = "deserialize_file_size")]
|
||||
/// sz: usize,
|
||||
/// }
|
||||
///
|
||||
/// let size_str = r#"{"sz": "42mb"}"#;
|
||||
/// assert_eq!(
|
||||
/// serde_json::from_str::<FileSize>(size_str).unwrap(),
|
||||
/// FileSize { sz: 1024 * 1024 * 42 },
|
||||
/// );
|
||||
///
|
||||
/// let int_bytes = r#"{"sz": 4096}"#;
|
||||
/// assert_eq!(
|
||||
/// serde_json::from_str::<FileSize>(int_bytes).unwrap(),
|
||||
/// FileSize { sz: 4096 },
|
||||
/// );
|
||||
/// ```
|
||||
pub fn deserialize_file_size<'de, D>(deserializer: D) -> Result<usize, D::Error> |
||||
where D: serde::de::Deserializer<'de> |
||||
{ |
||||
struct SizeStringOrUsize; |
||||
|
||||
impl<'de> Visitor<'de> for SizeStringOrUsize { |
||||
type Value = usize; |
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { |
||||
formatter.write_str("file size string or integer number of bytes") |
||||
} |
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> |
||||
where |
||||
E: de::Error, |
||||
{ |
||||
match parse_file_size(value) { |
||||
Some(size) => Ok(size), |
||||
None => { |
||||
Err(serde::de::Error::custom(format!( |
||||
"parsing file size input '{}' failed; expected number followed \ |
||||
by human size abbreviation (e.g. 10k/5mb/15GiB) or integer number \ |
||||
of bytes", |
||||
value, |
||||
))) |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> |
||||
where E: de::Error, |
||||
{ |
||||
Ok(value as usize) |
||||
} |
||||
} |
||||
|
||||
deserializer.deserialize_any(SizeStringOrUsize) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn check_file_size_parser_against_several_inputs() { |
||||
assert_eq!(parse_file_size("1k") , Some(1024)); |
||||
assert_eq!(parse_file_size("1kib") , Some(1024)); |
||||
assert_eq!(parse_file_size("1K") , Some(1024)); |
||||
assert_eq!(parse_file_size("1Kb") , Some(1024)); |
||||
assert_eq!(parse_file_size("1kb") , Some(1024)); |
||||
assert_eq!(parse_file_size("1 kb") , Some(1024)); |
||||
assert_eq!(parse_file_size("1 k") , Some(1024)); |
||||
assert_eq!(parse_file_size("1 kib") , Some(1024)); |
||||
|
||||
assert_eq!(parse_file_size("1m") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1mib") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1M") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1Mb") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1MB") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1mb") , Some(1024 * 1024)); |
||||
assert_eq!(parse_file_size("1 MB") , Some(1024 * 1024)); |
||||
|
||||
assert_eq!(parse_file_size("1g") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1gib") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1G") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1Gb") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1gb") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1 gb") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1 g") , Some(1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("1 Gib") , Some(1024 * 1024 * 1024)); |
||||
|
||||
assert_eq!(parse_file_size("48G") , Some( 48 * 1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("96G") , Some( 96 * 1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("2G") , Some( 2 * 1024 * 1024 * 1024)); |
||||
assert_eq!(parse_file_size("128G"), Some(128 * 1024 * 1024 * 1024)); |
||||
} |
||||
|
||||
#[test] |
||||
fn check_deserialize_file_size() { |
||||
#[derive(serde::Deserialize)] |
||||
struct A { |
||||
#[serde(deserialize_with = "deserialize_file_size")] |
||||
sz: usize, |
||||
} |
||||
|
||||
assert_eq!( |
||||
serde_json::from_str::<A>(r#"{"sz":"1mb"}"#).unwrap().sz, |
||||
1024 * 1024, |
||||
); |
||||
assert_eq!( |
||||
serde_json::from_str::<A>(r#"{"sz":4096}"#).unwrap().sz, |
||||
4096, |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue