use std::cmp::Ordering;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum AttributeValue {
String(String),
Int(i64),
Decimal(Decimal),
Bool(bool),
Timestamp(i64),
Array(Vec<AttributeValue>),
}
impl Eq for AttributeValue {}
impl AttributeValue {
#[must_use]
pub fn kind_str(&self) -> &'static str {
match self {
AttributeValue::String(_) => "string",
AttributeValue::Int(_) => "int",
AttributeValue::Decimal(_) => "decimal",
AttributeValue::Bool(_) => "bool",
AttributeValue::Timestamp(_) => "timestamp",
AttributeValue::Array(_) => "array",
}
}
#[must_use]
pub fn is_orderable(&self) -> bool {
matches!(
self,
AttributeValue::Int(_) | AttributeValue::Decimal(_) | AttributeValue::Timestamp(_)
)
}
#[must_use]
pub fn try_cmp(&self, other: &AttributeValue) -> Option<Ordering> {
match (self, other) {
(AttributeValue::Int(a), AttributeValue::Int(b)) => Some(a.cmp(b)),
(AttributeValue::Timestamp(a), AttributeValue::Timestamp(b)) => Some(a.cmp(b)),
(AttributeValue::Decimal(a), AttributeValue::Decimal(b)) => a.partial_cmp(b),
_ => None,
}
}
}
#[must_use]
pub fn kind_str(v: &AttributeValue) -> &'static str {
v.kind_str()
}
impl std::fmt::Display for AttributeValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AttributeValue::String(s) => f.write_str(s),
AttributeValue::Int(n) => write!(f, "{n}"),
AttributeValue::Decimal(d) => write!(f, "{d}"),
AttributeValue::Bool(b) => write!(f, "{b}"),
AttributeValue::Timestamp(n) => write!(f, "{n}ms"),
AttributeValue::Array(_) => match serde_json::to_string(self) {
Ok(s) => f.write_str(&s),
Err(_) => f.write_str("<array>"),
},
}
}
}
impl From<&str> for AttributeValue {
fn from(s: &str) -> Self {
AttributeValue::String(s.to_string())
}
}
impl From<String> for AttributeValue {
fn from(s: String) -> Self {
AttributeValue::String(s)
}
}
impl From<i64> for AttributeValue {
fn from(n: i64) -> Self {
AttributeValue::Int(n)
}
}
impl From<i32> for AttributeValue {
fn from(n: i32) -> Self {
AttributeValue::Int(i64::from(n))
}
}
impl From<bool> for AttributeValue {
fn from(b: bool) -> Self {
AttributeValue::Bool(b)
}
}
impl From<Decimal> for AttributeValue {
fn from(d: Decimal) -> Self {
AttributeValue::Decimal(d)
}
}
impl<T> From<Vec<T>> for AttributeValue
where
T: Into<AttributeValue>,
{
fn from(v: Vec<T>) -> Self {
AttributeValue::Array(v.into_iter().map(Into::into).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::FromStr;
#[test]
fn kind_strings_are_stable() {
assert_eq!(AttributeValue::String("x".into()).kind_str(), "string");
assert_eq!(AttributeValue::Int(0).kind_str(), "int");
assert_eq!(
AttributeValue::Decimal(Decimal::from(1)).kind_str(),
"decimal"
);
assert_eq!(AttributeValue::Bool(true).kind_str(), "bool");
assert_eq!(AttributeValue::Timestamp(0).kind_str(), "timestamp");
assert_eq!(AttributeValue::Array(vec![]).kind_str(), "array");
assert_eq!(kind_str(&AttributeValue::Bool(false)), "bool");
}
#[test]
fn try_cmp_orders_int_decimal_timestamp() {
let a = AttributeValue::Int(1);
let b = AttributeValue::Int(2);
assert_eq!(a.try_cmp(&b), Some(Ordering::Less));
let a = AttributeValue::Timestamp(100);
let b = AttributeValue::Timestamp(50);
assert_eq!(a.try_cmp(&b), Some(Ordering::Greater));
let a = AttributeValue::Decimal(Decimal::from_str("1.5").unwrap());
let b = AttributeValue::Decimal(Decimal::from_str("1.5").unwrap());
assert_eq!(a.try_cmp(&b), Some(Ordering::Equal));
}
#[test]
fn try_cmp_returns_none_on_kind_mismatch() {
let a = AttributeValue::Int(1);
let b = AttributeValue::Timestamp(1);
assert_eq!(a.try_cmp(&b), None);
}
#[test]
fn try_cmp_returns_none_for_string_array_bool() {
let a = AttributeValue::String("x".into());
let b = AttributeValue::String("y".into());
assert_eq!(a.try_cmp(&b), None);
let a = AttributeValue::Bool(false);
let b = AttributeValue::Bool(true);
assert_eq!(a.try_cmp(&b), None);
let a = AttributeValue::Array(vec![]);
let b = AttributeValue::Array(vec![]);
assert_eq!(a.try_cmp(&b), None);
}
#[test]
fn round_trips_through_serde_json() {
for v in [
AttributeValue::String("hello".into()),
AttributeValue::Int(-42),
AttributeValue::Decimal(Decimal::from_str("12345.6789").unwrap()),
AttributeValue::Bool(true),
AttributeValue::Timestamp(1_700_000_000_000),
AttributeValue::Array(vec![
AttributeValue::Int(1),
AttributeValue::String("two".into()),
]),
] {
let s = serde_json::to_string(&v).unwrap();
let back: AttributeValue = serde_json::from_str(&s).unwrap();
assert_eq!(v, back, "round-trip failed for {s}");
}
}
#[test]
fn from_into_conversions_compile() {
let _: AttributeValue = "x".into();
let _: AttributeValue = String::from("x").into();
let _: AttributeValue = 1_i64.into();
let _: AttributeValue = 1_i32.into();
let _: AttributeValue = true.into();
let _: AttributeValue = Decimal::from(1).into();
let v: AttributeValue = vec![1_i64, 2, 3].into();
assert_eq!(v.kind_str(), "array");
let v: AttributeValue = vec!["a", "b"].into();
match v {
AttributeValue::Array(items) => assert_eq!(items.len(), 2),
_ => panic!("expected Array"),
}
}
#[test]
fn is_orderable_matches_try_cmp() {
let cases = [
(AttributeValue::Int(0), true),
(AttributeValue::Decimal(Decimal::from(0)), true),
(AttributeValue::Timestamp(0), true),
(AttributeValue::Bool(true), false),
(AttributeValue::String("x".into()), false),
(AttributeValue::Array(vec![]), false),
];
for (v, want) in cases {
assert_eq!(v.is_orderable(), want, "{:?}", v);
}
}
#[test]
fn display_renders_each_kind() {
assert_eq!(AttributeValue::String("x".into()).to_string(), "x");
assert_eq!(AttributeValue::Int(42).to_string(), "42");
assert_eq!(AttributeValue::Bool(true).to_string(), "true");
assert_eq!(AttributeValue::Timestamp(123).to_string(), "123ms");
assert_eq!(
AttributeValue::Decimal(Decimal::from_str("1.5").unwrap()).to_string(),
"1.5"
);
let arr = AttributeValue::Array(vec![AttributeValue::Int(1)]);
assert!(arr.to_string().contains("\"int\""));
}
}