autocache 0.5.0

automatic cache management
Documentation
use std::fmt::Debug;

#[cfg(feature = "serialize")]
use bytes::Buf;
use chrono::Utc;
#[cfg(feature = "serialize")]
use serde::{de::DeserializeOwned, Deserialize, Serialize};

#[cfg(feature = "serialize")]
use crate::{codec::Codec, Error, Result, SerializationOperation};

#[derive(Debug, Clone)]
pub struct Entry<K, V> {
    pub key: K,
    pub value: Option<V>,
    pub expire_at_ms: Option<i64>,
}

impl<K, V> Entry<K, V> {
    pub fn is_expired(&self) -> bool {
        self.expire_at_ms
            .is_some_and(|expire_at_ms| expire_at_ms < Utc::now().timestamp_millis())
    }
}

#[cfg(feature = "serialize")]
impl<K, V> Entry<K, V>
where
    K: Serialize + DeserializeOwned + Clone,
    V: Codec,
{
    pub(crate) fn decode(data: bytes::Bytes) -> Result<Self> {
        let eni: EntryInner<K> = serde_json::from_reader(data.reader()).map_err(|error| {
            Error::serialization(SerializationOperation::Decode, "entry-json", error)
        })?;

        let value = eni
            .value_data
            .map(|data| {
                V::decode(data.into()).map_err(|error| {
                    Error::serialization(SerializationOperation::Decode, V::name(), error)
                })
            })
            .transpose()?;

        Ok(Self {
            key: eni.key,
            value,
            expire_at_ms: eni.expire_at_ms,
        })
    }

    pub(crate) fn encode(&self) -> Result<bytes::Bytes> {
        let value_data = self
            .value
            .as_ref()
            .map(|value| {
                value.encode().map(|data| data.to_vec()).map_err(|error| {
                    Error::serialization(SerializationOperation::Encode, V::name(), error)
                })
            })
            .transpose()?;

        let eni = EntryInner {
            key: self.key.clone(),
            value_data,
            expire_at_ms: self.expire_at_ms,
        };

        Ok(serde_json::to_vec(&eni)
            .map_err(|error| {
                Error::serialization(SerializationOperation::Encode, "entry-json", error)
            })?
            .into())
    }
}

#[cfg(feature = "serialize")]
#[derive(Serialize, Deserialize)]
struct EntryInner<K> {
    key: K,
    value_data: Option<Vec<u8>>,
    expire_at_ms: Option<i64>,
}

#[cfg(all(test, feature = "serialize"))]
mod tests {
    use bytes::Bytes;
    use serde::{Deserialize, Serialize};

    use super::Entry;
    use crate::{Codec, Error, SerializationOperation};

    #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
    struct EmptyValue;

    impl Codec for EmptyValue {
        fn decode(data: Bytes) -> anyhow::Result<Self> {
            anyhow::ensure!(data.is_empty(), "expected an empty payload");
            Ok(Self)
        }

        fn encode(&self) -> anyhow::Result<Bytes> {
            Ok(Bytes::new())
        }
    }

    #[test]
    fn some_empty_payload_round_trips_as_some() {
        let entry = Entry {
            key: "test-key".to_string(),
            value: Some(EmptyValue),
            expire_at_ms: Some(42),
        };

        let decoded = Entry::<String, EmptyValue>::decode(entry.encode().unwrap()).unwrap();

        assert_eq!(decoded.key, entry.key);
        assert_eq!(decoded.value, entry.value);
        assert_eq!(decoded.expire_at_ms, entry.expire_at_ms);
    }

    #[test]
    fn none_round_trips_as_none() {
        let entry = Entry::<String, EmptyValue> {
            key: "test-key".to_string(),
            value: None,
            expire_at_ms: Some(42),
        };

        let decoded = Entry::<String, EmptyValue>::decode(entry.encode().unwrap()).unwrap();

        assert_eq!(decoded.key, entry.key);
        assert_eq!(decoded.value, entry.value);
        assert_eq!(decoded.expire_at_ms, entry.expire_at_ms);
    }

    #[test]
    fn invalid_entry_payload_returns_typed_decode_error() {
        let error =
            Entry::<String, EmptyValue>::decode(Bytes::from_static(b"not-json")).unwrap_err();

        assert!(matches!(
            error,
            Error::Serialization {
                operation: SerializationOperation::Decode,
                codec: "entry-json",
                ..
            }
        ));
    }
}