use super::store::std_duration_to_chrono_duration;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration as StdDuration;
mod datetime {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serializer};
pub(super) fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&date.to_rfc3339())
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
DateTime::parse_from_rfc3339(&s)
.map(|dt| dt.with_timezone(&Utc))
.map_err(serde::de::Error::custom)
}
}
mod datetime_option {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer, Serializer};
#[allow(clippy::ref_option)]
pub(super) fn serialize<S>(
date: &Option<DateTime<Utc>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(dt) => serializer.serialize_some(&dt.to_rfc3339()),
None => serializer.serialize_none(),
}
}
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
let opt: Option<String> = Option::deserialize(deserializer)?;
match opt {
Some(s) => DateTime::parse_from_rfc3339(&s)
.map(|dt| Some(dt.with_timezone(&Utc)))
.map_err(serde::de::Error::custom),
None => Ok(None),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CedarType {
String,
Long,
Bool,
Set,
Record,
Entity,
Ip,
Decimal,
DateTime,
Duration,
}
#[derive(PartialEq, PartialOrd)]
pub(crate) enum UnitRank {
Start,
Days,
Hours,
Minutes,
Seconds,
Millis,
}
impl CedarType {
#[must_use]
pub fn from_value(value: &Value) -> Self {
match value {
Value::String(s) => Self::from_string_value(s),
Value::Number(n) => {
if n.is_i64() || n.is_u64() {
Self::Long
} else {
Self::Decimal
}
},
Value::Bool(_) => Self::Bool,
Value::Array(_) => Self::Set,
Value::Object(obj) => {
if let Some(extn) = obj.get("__extn")
&& let Some(extn_obj) = extn.as_object()
&& let Some(fn_name) = extn_obj.get("fn").and_then(|v| v.as_str())
{
return match fn_name {
"ip" | "ipaddr" => Self::Ip,
"decimal" => Self::Decimal,
"datetime" => Self::DateTime,
"duration" => Self::Duration,
_ => Self::Record,
};
}
if obj.len() == 2
&& obj.contains_key("type")
&& obj.contains_key("id")
&& obj.get("type").is_some_and(serde_json::Value::is_string)
&& obj.get("id").is_some_and(serde_json::Value::is_string)
{
Self::Entity
} else {
Self::Record
}
},
Value::Null => {
Self::String
},
}
}
fn from_string_value(s: &str) -> Self {
use crate::context_data_api::mapper::CedarValueMapper;
use crate::context_data_api::mapper::ExtensionValue;
match CedarValueMapper::detect_extension(s) {
Some(ExtensionValue::IpAddr(_)) => Self::Ip,
Some(ExtensionValue::DateTime(_)) => Self::DateTime,
Some(ExtensionValue::Duration(_)) => Self::Duration,
Some(ExtensionValue::Decimal(_)) => Self::Decimal,
None => Self::String,
}
}
pub(crate) fn is_datetime_format(value: &str) -> bool {
DateTime::parse_from_rfc3339(value).is_ok()
|| NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok()
}
pub(crate) fn is_duration_format(value: &str) -> bool {
if value.is_empty() {
return false;
}
let bytes = value.as_bytes();
let mut i = 0;
if bytes[i] == b'-' {
i += 1;
if i == bytes.len() {
return false;
}
}
let mut last_rank = UnitRank::Start;
while i < bytes.len() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if start == i {
return false;
}
let (current_rank, consumed) = match bytes.get(i) {
Some(b'd') if last_rank < UnitRank::Days => (UnitRank::Days, 1),
Some(b'h') if last_rank < UnitRank::Hours => (UnitRank::Hours, 1),
Some(b's') if last_rank < UnitRank::Seconds => (UnitRank::Seconds, 1),
Some(b'm') => {
if i + 1 < bytes.len() && bytes[i + 1] == b's' {
if last_rank < UnitRank::Millis {
(UnitRank::Millis, 2)
} else {
return false;
}
} else if last_rank < UnitRank::Minutes {
(UnitRank::Minutes, 1)
} else {
return false;
}
},
_ => return false,
};
last_rank = current_rank;
i += consumed;
}
true
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataEntry {
pub key: String,
pub value: Value,
pub data_type: CedarType,
#[serde(with = "datetime")]
pub created_at: DateTime<Utc>,
#[serde(with = "datetime_option")]
pub expires_at: Option<DateTime<Utc>>,
pub access_count: u64,
}
impl DataEntry {
#[must_use]
pub fn new(key: String, value: Value, ttl: Option<StdDuration>) -> Self {
let created_at = Utc::now();
let expires_at = ttl.map(|duration| {
let chrono_duration = std_duration_to_chrono_duration(duration);
created_at + chrono_duration
});
Self {
key,
data_type: CedarType::from_value(&value),
value,
created_at,
expires_at,
access_count: 0,
}
}
pub fn increment_access(&mut self) {
self.access_count = self.access_count.saturating_add(1);
}
#[must_use]
pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
if let Some(expires_at) = self.expires_at {
now > expires_at
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_cedar_type_from_value() {
assert_eq!(CedarType::from_value(&json!("test")), CedarType::String);
assert_eq!(CedarType::from_value(&json!(42)), CedarType::Long);
assert_eq!(CedarType::from_value(&json!(true)), CedarType::Bool);
assert_eq!(CedarType::from_value(&json!([1, 2, 3])), CedarType::Set);
assert_eq!(CedarType::from_value(&json!({"a": 1})), CedarType::Record);
assert_eq!(
CedarType::from_value(&json!({"type": "User", "id": "123"})),
CedarType::Entity
);
}
#[test]
fn test_data_entry_new() {
use test_utils::assert_eq;
let entry = DataEntry::new("key1".to_string(), json!("value1"), None);
assert_eq!(entry.key, "key1");
assert_eq!(&entry.value, &json!("value1"));
assert_eq!(entry.data_type, CedarType::String);
assert_eq!(entry.access_count, 0);
assert!(entry.expires_at.is_none());
}
#[test]
fn test_data_entry_with_ttl() {
let entry = DataEntry::new(
"key1".to_string(),
json!("value1"),
Some(StdDuration::from_secs(60)),
);
assert!(entry.expires_at.is_some());
assert!(entry.expires_at.unwrap() > entry.created_at);
}
#[test]
fn test_increment_access() {
let mut entry = DataEntry::new("key1".to_string(), json!("value1"), None);
assert_eq!(entry.access_count, 0);
entry.increment_access();
assert_eq!(entry.access_count, 1);
entry.increment_access();
assert_eq!(entry.access_count, 2);
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_is_expired() {
let entry = DataEntry::new(
"key1".to_string(),
json!("value1"),
Some(StdDuration::from_millis(100)),
);
let now = Utc::now();
assert!(!entry.is_expired(now));
std::thread::sleep(StdDuration::from_millis(150));
let now_after = Utc::now();
assert!(entry.is_expired(now_after));
}
#[test]
fn test_serialization() {
let entry = DataEntry::new(
"key1".to_string(),
json!("value1"),
Some(StdDuration::from_secs(3600)),
);
let serialized = serde_json::to_string(&entry).expect("should serialize");
let deserialized: DataEntry =
serde_json::from_str(&serialized).expect("should deserialize");
assert_eq!(entry.key, deserialized.key);
assert_eq!(entry.value, deserialized.value);
assert_eq!(entry.data_type, deserialized.data_type);
assert_eq!(entry.access_count, deserialized.access_count);
assert_eq!(
entry.created_at.to_rfc3339(),
deserialized.created_at.to_rfc3339(),
"created_at should survive serde_json round-trip"
);
assert_eq!(
entry.expires_at.map(|dt| dt.to_rfc3339()),
deserialized.expires_at.map(|dt| dt.to_rfc3339()),
"expires_at should survive serde_json round-trip"
);
}
}