config-types 0.1.0

A library which provides ergononic types for configuration files
Documentation
use regex::Regex;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default)]
pub struct ByteSizeConf(pub u64);

impl ByteSizeConf {
    pub fn as_bytes(&self) -> u64 {
        self.0
    }

    pub fn as_kilobytes(&self) -> u64 {
        self.0 / 1024
    }

    pub fn as_megabytes(&self) -> u64 {
        self.0 / 1024 / 1024
    }

    pub fn as_gigabytes(&self) -> u64 {
        self.0 / 1024 / 1024 / 1024
    }

    fn to_string(&self) -> String {
        let size = self.0;
        if size < 1024 {
            format!("{} bytes", size)
        } else if size < 2u64.pow(20) {
            format!("{} KiB", size / 1024)
        } else if size < 2u64.pow(30) {
            format!("{} MiB", size / 2u64.pow(20))
        } else {
            format!("{} GiB", size / 2u64.pow(30))
        }
    }
}

impl<'de> Deserialize<'de> for ByteSizeConf {
    fn deserialize<D>(deserializer: D) -> Result<ByteSizeConf, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let size = parse_byte_size(&s).map_err(serde::de::Error::custom)?;
        Ok(ByteSizeConf(size))
    }
}

impl Serialize for ByteSizeConf {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        self.to_string().serialize(serializer)
    }
}

impl std::fmt::Display for ByteSizeConf {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.to_string())
    }
}

fn parse_byte_size(s: &str) -> Result<u64, String> {
    let re = Regex::new(r"^(\d+)\s*(b|kb|ki|mb|mi|gb|gi|tb|ti)$").map_err(|_| "Invalid regex")?;
    let s = s.to_lowercase();
    let caps = re
        .captures(s.as_str())
        .ok_or_else(|| format!("Invalid byte size: {}", s))?;
    let size = caps
        .get(1)
        .ok_or_else(|| "Invalid byte size".to_string())?
        .as_str()
        .parse()
        .map_err(|_| "Invalid byte size".to_string())?;
    let unit = caps
        .get(2)
        .ok_or_else(|| "Invalid byte size".to_string())?
        .as_str()
        .to_lowercase();
    let unit = unit.as_str();

    match unit {
        "b" => Ok(size),
        "kb" => Ok(size * 1000),
        "ki" => Ok(size * 2u64.pow(10)),
        "mb" => Ok(size * 10u64.pow(6)),
        "mi" => Ok(size * 2u64.pow(20)),
        "gb" => Ok(size * 10u64.pow(9)),
        "gi" => Ok(size * 2u64.pow(30)),
        "tb" => Ok(size * 10u64.pow(12)),
        "ti" => Ok(size * 2u64.pow(40)),
        _ => Err("Invalid byte size".to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_byte_size() {
        assert_eq!(parse_byte_size("1b").unwrap(), 1);
        assert_eq!(parse_byte_size("1kb").unwrap(), 1000);
        assert_eq!(parse_byte_size("1ki").unwrap(), 1024);
        assert_eq!(parse_byte_size("1mb").unwrap(), 1000 * 1000);
        assert_eq!(parse_byte_size("1mi").unwrap(), 1024 * 1024);
        assert_eq!(parse_byte_size("1gb").unwrap(), 1000 * 1000 * 1000);
        assert_eq!(parse_byte_size("1gi").unwrap(), 1024 * 1024 * 1024);
        assert_eq!(parse_byte_size("1tb").unwrap(), 1000 * 1000 * 1000 * 1000);
        assert_eq!(parse_byte_size("1ti").unwrap(), 1024 * 1024 * 1024 * 1024);
    }

    #[test]
    fn test_byte_size_display() {
        assert_eq!(ByteSizeConf(1).to_string(), "1 bytes");
        assert_eq!(ByteSizeConf(1024).to_string(), "1 KiB");
        assert_eq!(ByteSizeConf(1024 * 1024).to_string(), "1 MiB");
        assert_eq!(ByteSizeConf(1024 * 1024 * 1024).to_string(), "1 GiB");
    }
}