use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CodecError {
#[error("codec encode failed: {0}")]
Encode(String),
#[error("codec decode failed: {0}")]
Decode(String),
}
pub trait Codec<T>: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> u32;
fn encode(&self, value: &T) -> Result<Vec<u8>, CodecError>;
fn decode(&self, bytes: &[u8]) -> Result<T, CodecError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct JsonCodec;
impl<T> Codec<T> for JsonCodec
where
T: Serialize + DeserializeOwned + Send + Sync,
{
fn name(&self) -> &'static str {
"json"
}
fn version(&self) -> u32 {
1
}
fn encode(&self, value: &T) -> Result<Vec<u8>, CodecError> {
let v = serde_json::to_value(value).map_err(|e| CodecError::Encode(e.to_string()))?;
serde_json::to_vec(&v).map_err(|e| CodecError::Encode(e.to_string()))
}
fn decode(&self, bytes: &[u8]) -> Result<T, CodecError> {
serde_json::from_slice(bytes).map_err(|e| CodecError::Decode(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Counter {
zebra: u32,
apple: u32,
monkey: u32,
}
#[test]
fn json_codec_round_trip() {
let codec = JsonCodec;
let v = Counter {
zebra: 1,
apple: 2,
monkey: 3,
};
let bytes = <JsonCodec as Codec<Counter>>::encode(&codec, &v).unwrap();
let back: Counter = <JsonCodec as Codec<Counter>>::decode(&codec, &bytes).unwrap();
assert_eq!(v, back);
}
#[test]
fn json_codec_canonical_sorts_keys() {
let codec = JsonCodec;
let v = Counter {
zebra: 1,
apple: 2,
monkey: 3,
};
let bytes = <JsonCodec as Codec<Counter>>::encode(&codec, &v).unwrap();
let s = std::str::from_utf8(&bytes).unwrap();
assert_eq!(s, r#"{"apple":2,"monkey":3,"zebra":1}"#);
}
#[test]
fn json_codec_name_and_version() {
let codec = JsonCodec;
assert_eq!(<JsonCodec as Codec<Counter>>::name(&codec), "json");
assert_eq!(<JsonCodec as Codec<Counter>>::version(&codec), 1);
}
#[test]
fn json_codec_decode_rejects_invalid_bytes() {
let codec = JsonCodec;
let result: Result<Counter, _> = <JsonCodec as Codec<Counter>>::decode(&codec, b"not json");
assert!(matches!(result, Err(CodecError::Decode(_))));
}
}