fast-cache 0.1.0

Embedded-first thread-per-core in-memory cache with optional Redis-compatible server
Documentation
use crate::{FastCacheError, Result};

use super::CommandSpec;

pub(crate) struct CommandArity<C> {
    _command: std::marker::PhantomData<C>,
}

pub(crate) struct StorageInteger<C> {
    _command: std::marker::PhantomData<C>,
}

#[cfg(feature = "server")]
pub(crate) struct AsciiU64;

pub(crate) struct TtlMillis<C> {
    _command: std::marker::PhantomData<C>,
}

impl<C> CommandArity<C>
where
    C: CommandSpec,
{
    pub(crate) fn exact(actual: usize, expected: usize) -> Result<()> {
        match actual == expected {
            true => Ok(()),
            false => Err(FastCacheError::Command(format!(
                "{} expects {expected} arguments, got {actual}",
                C::NAME
            ))),
        }
    }

    pub(crate) fn at_least(actual: usize, min: usize, requirement: &str) -> Result<()> {
        match actual >= min {
            true => Ok(()),
            false => Err(FastCacheError::Command(format!(
                "{} requires at least {requirement}",
                C::NAME
            ))),
        }
    }
}

impl<C> StorageInteger<C>
where
    C: CommandSpec,
{
    pub(crate) fn parse_u64(option: &str, value: &[u8]) -> Result<u64> {
        let value = std::str::from_utf8(value).map_err(|error| {
            FastCacheError::Command(format!(
                "{} {option} expects utf8 integer: {error}",
                C::NAME
            ))
        })?;
        value.parse::<u64>().map_err(|error| {
            FastCacheError::Command(format!(
                "{} {option} expects positive integer: {error}",
                C::NAME
            ))
        })
    }
}

impl<C> TtlMillis<C>
where
    C: CommandSpec,
{
    pub(crate) fn seconds(value: &[u8]) -> Result<u64> {
        StorageInteger::<C>::parse_u64("seconds", value).map(|ttl| ttl.saturating_mul(1_000))
    }

    pub(crate) fn millis(value: &[u8]) -> Result<u64> {
        StorageInteger::<C>::parse_u64("milliseconds", value)
    }
}

#[cfg(feature = "server")]
impl TtlMillis<()> {
    pub(crate) fn ascii_seconds(value: &[u8]) -> Option<u64> {
        AsciiU64::parse(value).map(|ttl| ttl.saturating_mul(1_000))
    }

    pub(crate) fn ascii_millis(value: &[u8]) -> Option<u64> {
        AsciiU64::parse(value)
    }
}

#[cfg(feature = "server")]
impl AsciiU64 {
    pub(crate) fn parse(value: &[u8]) -> Option<u64> {
        match value {
            [] => None,
            value if value.len() > 19 => None,
            value => Self::parse_digits(value),
        }
    }

    fn parse_digits(value: &[u8]) -> Option<u64> {
        value
            .iter()
            .try_fold(0u64, |parsed, digit| match digit.is_ascii_digit() {
                true => parsed.checked_mul(10)?.checked_add((digit - b'0') as u64),
                false => None,
            })
    }
}