Browse Source

initial commit

master v1.0.0
Jonathan Strong 2 years ago
commit
c80e4a8512
  1. 3
      .gitignore
  2. 17
      Cargo.toml
  3. 18
      LICENSE
  4. 36
      README.md
  5. 192
      src/lib.rs

3
.gitignore vendored

@ -0,0 +1,3 @@
/target
/Cargo.lock
*.swp

17
Cargo.toml

@ -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"

18
LICENSE

@ -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.

36
README.md

@ -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 },
);
```

192
src/lib.rs

@ -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…
Cancel
Save