use std::fmt;
use std::sync::Arc;
use serde_json::Value;
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum SerializationError {
#[error("encode failed: {0}")]
Encode(String),
#[error("decode failed: {0}")]
Decode(String),
}
pub trait AttributeCoder: Send + Sync {
fn dump(&self, value: &Value) -> Result<String, SerializationError>;
fn load(&self, raw: &str) -> Result<Value, SerializationError>;
}
#[derive(Debug, Default)]
pub struct JsonCoder;
impl AttributeCoder for JsonCoder {
fn dump(&self, value: &Value) -> Result<String, SerializationError> {
serde_json::to_string(value).map_err(|error| SerializationError::Encode(error.to_string()))
}
fn load(&self, raw: &str) -> Result<Value, SerializationError> {
serde_json::from_str(raw).map_err(|error| SerializationError::Decode(error.to_string()))
}
}
#[derive(Clone)]
pub struct FunctionCoder {
dump_fn: fn(&Value) -> Result<String, SerializationError>,
load_fn: fn(&str) -> Result<Value, SerializationError>,
}
impl fmt::Debug for FunctionCoder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FunctionCoder(<functions>)")
}
}
impl FunctionCoder {
#[must_use]
pub fn new(
dump_fn: fn(&Value) -> Result<String, SerializationError>,
load_fn: fn(&str) -> Result<Value, SerializationError>,
) -> Self {
Self { dump_fn, load_fn }
}
}
impl AttributeCoder for FunctionCoder {
fn dump(&self, value: &Value) -> Result<String, SerializationError> {
(self.dump_fn)(value)
}
fn load(&self, raw: &str) -> Result<Value, SerializationError> {
(self.load_fn)(raw)
}
}
#[derive(Clone)]
pub struct SerializedFieldConfig {
pub field: String,
coder: Arc<dyn AttributeCoder>,
}
impl fmt::Debug for SerializedFieldConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SerializedFieldConfig")
.field("field", &self.field)
.field("coder", &"<coder>")
.finish()
}
}
impl SerializedFieldConfig {
#[must_use]
pub fn new(field: &str, coder: impl AttributeCoder + 'static) -> Self {
Self {
field: field.to_owned(),
coder: Arc::new(coder),
}
}
pub fn dump(&self, value: &Value) -> Result<String, SerializationError> {
self.coder.dump(value)
}
pub fn load(&self, raw: &str) -> Result<Value, SerializationError> {
self.coder.load(raw)
}
}
#[must_use]
pub fn serialize_attribute(
field: &str,
coder: impl AttributeCoder + 'static,
) -> SerializedFieldConfig {
SerializedFieldConfig::new(field, coder)
}
pub trait SerializedAttribute {
fn serialized_attributes() -> &'static [SerializedFieldConfig] {
&[]
}
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use serde_json::json;
use super::{
FunctionCoder, JsonCoder, SerializationError, SerializedAttribute, SerializedFieldConfig,
serialize_attribute,
};
struct ProfileRecord;
impl SerializedAttribute for ProfileRecord {
fn serialized_attributes() -> &'static [SerializedFieldConfig] {
static CONFIGS: LazyLock<Vec<SerializedFieldConfig>> =
LazyLock::new(|| vec![serialize_attribute("settings", JsonCoder)]);
CONFIGS.as_slice()
}
}
fn reverse_dump(value: &serde_json::Value) -> Result<String, SerializationError> {
let text = value
.as_str()
.ok_or_else(|| SerializationError::Encode("expected string".to_owned()))?;
Ok(text.chars().rev().collect())
}
fn reverse_load(raw: &str) -> Result<serde_json::Value, SerializationError> {
if raw.is_empty() {
return Err(SerializationError::Decode("empty payload".to_owned()));
}
Ok(json!(raw.chars().rev().collect::<String>()))
}
#[test]
fn json_coder_round_trips_json_values() {
let config = serialize_attribute("settings", JsonCoder);
let dumped = config
.dump(&json!({"theme": "dark"}))
.expect("dump should succeed");
let loaded = config.load(&dumped).expect("load should succeed");
assert_eq!(loaded, json!({"theme": "dark"}));
}
#[test]
fn function_coder_supports_custom_serialization() {
let config =
serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
let dumped = config
.dump(&json!("stressed"))
.expect("dump should succeed");
let loaded = config.load(&dumped).expect("load should succeed");
assert_eq!(dumped, "desserts");
assert_eq!(loaded, json!("stressed"));
}
#[test]
fn function_coder_surfaces_encode_errors() {
let config =
serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
assert_eq!(
config.dump(&json!(1)).map_err(|error| error.to_string()),
Err("encode failed: expected string".to_owned())
);
}
#[test]
fn function_coder_surfaces_decode_errors() {
let config =
serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
assert_eq!(
config.load("").map_err(|error| error.to_string()),
Err("decode failed: empty payload".to_owned())
);
}
#[test]
fn trait_exposes_declared_serialized_attributes() {
assert_eq!(ProfileRecord::serialized_attributes().len(), 1);
assert_eq!(ProfileRecord::serialized_attributes()[0].field, "settings");
}
}