use std::collections::{btree_map, BTreeMap};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum PayloadValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
Text(String),
Bytes(Vec<u8>),
Array(Vec<PayloadValue>),
Object(BTreeMap<String, PayloadValue>),
}
impl PayloadValue {
#[inline]
#[must_use]
pub const fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
#[inline]
#[must_use]
pub const fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
#[inline]
#[must_use]
pub const fn is_int(&self) -> bool {
matches!(self, Self::Int(_))
}
#[inline]
#[must_use]
pub const fn is_float(&self) -> bool {
matches!(self, Self::Float(_))
}
#[inline]
#[must_use]
pub const fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
#[inline]
#[must_use]
pub const fn is_bytes(&self) -> bool {
matches!(self, Self::Bytes(_))
}
#[inline]
#[must_use]
pub const fn is_array(&self) -> bool {
matches!(self, Self::Array(_))
}
#[inline]
#[must_use]
pub const fn is_object(&self) -> bool {
matches!(self, Self::Object(_))
}
#[inline]
#[must_use]
pub const fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn as_int(&self) -> Option<i64> {
match self {
Self::Int(n) => Some(*n),
_ => None,
}
}
#[inline]
#[must_use]
pub const fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
_ => None,
}
}
#[inline]
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s.as_str()),
_ => None,
}
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::Bytes(b) => Some(b.as_slice()),
_ => None,
}
}
}
impl From<bool> for PayloadValue {
#[inline]
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<i64> for PayloadValue {
#[inline]
fn from(value: i64) -> Self {
Self::Int(value)
}
}
impl From<i32> for PayloadValue {
#[inline]
fn from(value: i32) -> Self {
Self::Int(i64::from(value))
}
}
impl From<f64> for PayloadValue {
#[inline]
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl From<f32> for PayloadValue {
#[inline]
fn from(value: f32) -> Self {
Self::Float(f64::from(value))
}
}
impl From<String> for PayloadValue {
#[inline]
fn from(value: String) -> Self {
Self::Text(value)
}
}
impl From<&str> for PayloadValue {
#[inline]
fn from(value: &str) -> Self {
Self::Text(value.to_owned())
}
}
impl From<Vec<u8>> for PayloadValue {
#[inline]
fn from(value: Vec<u8>) -> Self {
Self::Bytes(value)
}
}
impl From<Vec<PayloadValue>> for PayloadValue {
#[inline]
fn from(value: Vec<PayloadValue>) -> Self {
Self::Array(value)
}
}
impl From<BTreeMap<String, PayloadValue>> for PayloadValue {
#[inline]
fn from(value: BTreeMap<String, PayloadValue>) -> Self {
Self::Object(value)
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Payload {
fields: BTreeMap<String, PayloadValue>,
}
impl Payload {
#[inline]
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
key: impl Into<String>,
value: impl Into<PayloadValue>,
) -> Option<PayloadValue> {
self.fields.insert(key.into(), value.into())
}
#[inline]
#[must_use]
pub fn get(&self, key: &str) -> Option<&PayloadValue> {
self.fields.get(key)
}
pub fn remove(&mut self, key: &str) -> Option<PayloadValue> {
self.fields.remove(key)
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.fields.len()
}
#[inline]
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
self.fields.contains_key(key)
}
#[inline]
#[must_use]
pub fn as_map(&self) -> &BTreeMap<String, PayloadValue> {
&self.fields
}
#[inline]
pub fn iter(&self) -> btree_map::Iter<'_, String, PayloadValue> {
self.fields.iter()
}
}
impl<'a> IntoIterator for &'a Payload {
type Item = (&'a String, &'a PayloadValue);
type IntoIter = btree_map::Iter<'a, String, PayloadValue>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.fields.iter()
}
}
impl IntoIterator for Payload {
type Item = (String, PayloadValue);
type IntoIter = btree_map::IntoIter<String, PayloadValue>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.fields.into_iter()
}
}
impl FromIterator<(String, PayloadValue)> for Payload {
fn from_iter<I: IntoIterator<Item = (String, PayloadValue)>>(iter: I) -> Self {
Self {
fields: iter.into_iter().collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_is_empty() {
let p = Payload::new();
assert!(p.is_empty());
assert_eq!(p.len(), 0);
}
#[test]
fn insert_then_get_round_trip() {
let mut p = Payload::new();
let prior = p.insert("title", "Rust");
assert!(prior.is_none());
let value = p.get("title").unwrap();
assert_eq!(value.as_text(), Some("Rust"));
}
#[test]
fn insert_replacing_returns_prior() {
let mut p = Payload::new();
assert!(p.insert("k", "a").is_none());
let prior = p.insert("k", "b").unwrap();
assert_eq!(prior.as_text(), Some("a"));
assert_eq!(p.get("k").and_then(PayloadValue::as_text), Some("b"));
}
#[test]
fn remove_returns_prior_value() {
let mut p = Payload::new();
p.insert("k", "v");
let removed = p.remove("k").unwrap();
assert_eq!(removed.as_text(), Some("v"));
assert!(p.is_empty());
}
#[test]
fn remove_absent_returns_none() {
let mut p = Payload::new();
assert!(p.remove("missing").is_none());
}
#[test]
fn contains_key_reflects_presence() {
let mut p = Payload::new();
p.insert("k", true);
assert!(p.contains_key("k"));
assert!(!p.contains_key("missing"));
}
#[test]
fn iter_is_ordered_by_key() {
let mut p = Payload::new();
p.insert("c", 3_i64);
p.insert("a", 1_i64);
p.insert("b", 2_i64);
let order: Vec<&str> = p.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(order, vec!["a", "b", "c"]);
}
#[test]
fn typed_accessors_return_none_for_wrong_variant() {
let pv = PayloadValue::Text("hi".to_string());
assert!(pv.as_bool().is_none());
assert!(pv.as_int().is_none());
assert!(pv.as_float().is_none());
assert!(pv.as_bytes().is_none());
}
#[test]
fn from_iter_collects_into_payload() {
let p: Payload = [
("a".to_string(), PayloadValue::from(1_i64)),
("b".to_string(), PayloadValue::from(2_i64)),
]
.into_iter()
.collect();
assert_eq!(p.len(), 2);
assert_eq!(p.get("a").and_then(PayloadValue::as_int), Some(1));
}
#[test]
fn from_conversions_cover_primitive_types() {
assert!(matches!(PayloadValue::from(true), PayloadValue::Bool(true)));
assert!(matches!(PayloadValue::from(42_i64), PayloadValue::Int(42)));
assert!(matches!(PayloadValue::from(42_i32), PayloadValue::Int(42)));
assert!(matches!(
PayloadValue::from(1.5_f64),
PayloadValue::Float(v) if (v - 1.5).abs() < 1e-9
));
assert!(matches!(
PayloadValue::from(1.5_f32),
PayloadValue::Float(v) if (v - 1.5).abs() < 1e-6
));
assert!(matches!(
PayloadValue::from("hi"),
PayloadValue::Text(s) if s == "hi"
));
assert!(matches!(
PayloadValue::from("hi".to_string()),
PayloadValue::Text(s) if s == "hi"
));
assert!(matches!(
PayloadValue::from(vec![1_u8, 2, 3]),
PayloadValue::Bytes(b) if b == vec![1, 2, 3]
));
}
#[test]
fn type_predicates_match_active_variant() {
assert!(PayloadValue::Null.is_null());
assert!(PayloadValue::Bool(true).is_bool());
assert!(PayloadValue::Int(1).is_int());
assert!(PayloadValue::Float(1.0).is_float());
assert!(PayloadValue::Text("a".to_string()).is_text());
assert!(PayloadValue::Bytes(vec![0]).is_bytes());
assert!(PayloadValue::Array(Vec::new()).is_array());
assert!(PayloadValue::Object(BTreeMap::new()).is_object());
}
#[test]
fn into_iter_by_ref_yields_field_pairs() {
let mut p = Payload::new();
p.insert("k", 1_i64);
for (key, value) in &p {
assert_eq!(key.as_str(), "k");
assert_eq!(value.as_int(), Some(1));
}
}
#[test]
fn into_iter_by_value_consumes_payload() {
let mut p = Payload::new();
p.insert("k", 1_i64);
let collected: Vec<(String, PayloadValue)> = p.into_iter().collect();
assert_eq!(collected.len(), 1);
}
}