Skip to main content

rustrails_record/
serialization_ext.rs

1use std::fmt;
2use std::sync::Arc;
3
4use serde_json::Value;
5
6/// Errors returned while serializing or deserializing attributes.
7#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
8pub enum SerializationError {
9    /// Encoding failed.
10    #[error("encode failed: {0}")]
11    Encode(String),
12    /// Decoding failed.
13    #[error("decode failed: {0}")]
14    Decode(String),
15}
16
17/// Serializer contract for JSON-backed custom coders.
18pub trait AttributeCoder: Send + Sync {
19    /// Serializes a JSON value into a database column string.
20    fn dump(&self, value: &Value) -> Result<String, SerializationError>;
21    /// Deserializes a database column string into JSON.
22    fn load(&self, raw: &str) -> Result<Value, SerializationError>;
23}
24
25/// Default coder that round-trips JSON with `serde_json`.
26#[derive(Debug, Default)]
27pub struct JsonCoder;
28
29impl AttributeCoder for JsonCoder {
30    fn dump(&self, value: &Value) -> Result<String, SerializationError> {
31        serde_json::to_string(value).map_err(|error| SerializationError::Encode(error.to_string()))
32    }
33
34    fn load(&self, raw: &str) -> Result<Value, SerializationError> {
35        serde_json::from_str(raw).map_err(|error| SerializationError::Decode(error.to_string()))
36    }
37}
38
39/// Function-pointer based coder for custom serialization behavior.
40#[derive(Clone)]
41pub struct FunctionCoder {
42    dump_fn: fn(&Value) -> Result<String, SerializationError>,
43    load_fn: fn(&str) -> Result<Value, SerializationError>,
44}
45
46impl fmt::Debug for FunctionCoder {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        f.write_str("FunctionCoder(<functions>)")
49    }
50}
51
52impl FunctionCoder {
53    /// Creates a custom coder from `dump_fn` and `load_fn`.
54    #[must_use]
55    pub fn new(
56        dump_fn: fn(&Value) -> Result<String, SerializationError>,
57        load_fn: fn(&str) -> Result<Value, SerializationError>,
58    ) -> Self {
59        Self { dump_fn, load_fn }
60    }
61}
62
63impl AttributeCoder for FunctionCoder {
64    fn dump(&self, value: &Value) -> Result<String, SerializationError> {
65        (self.dump_fn)(value)
66    }
67
68    fn load(&self, raw: &str) -> Result<Value, SerializationError> {
69        (self.load_fn)(raw)
70    }
71}
72
73/// Metadata describing a serialized attribute and its coder.
74#[derive(Clone)]
75pub struct SerializedFieldConfig {
76    /// The attribute name.
77    pub field: String,
78    coder: Arc<dyn AttributeCoder>,
79}
80
81impl fmt::Debug for SerializedFieldConfig {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.debug_struct("SerializedFieldConfig")
84            .field("field", &self.field)
85            .field("coder", &"<coder>")
86            .finish()
87    }
88}
89
90impl SerializedFieldConfig {
91    /// Creates serialization metadata for `field` using `coder`.
92    #[must_use]
93    pub fn new(field: &str, coder: impl AttributeCoder + 'static) -> Self {
94        Self {
95            field: field.to_owned(),
96            coder: Arc::new(coder),
97        }
98    }
99
100    /// Serializes `value` with the configured coder.
101    pub fn dump(&self, value: &Value) -> Result<String, SerializationError> {
102        self.coder.dump(value)
103    }
104
105    /// Deserializes `raw` with the configured coder.
106    pub fn load(&self, raw: &str) -> Result<Value, SerializationError> {
107        self.coder.load(raw)
108    }
109}
110
111/// Declares a serialized attribute configuration.
112#[must_use]
113pub fn serialize_attribute(
114    field: &str,
115    coder: impl AttributeCoder + 'static,
116) -> SerializedFieldConfig {
117    SerializedFieldConfig::new(field, coder)
118}
119
120/// Trait implemented by records that declare serialized attributes.
121pub trait SerializedAttribute {
122    /// Returns serialized attribute metadata for the record type.
123    fn serialized_attributes() -> &'static [SerializedFieldConfig] {
124        &[]
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use std::sync::LazyLock;
131
132    use serde_json::json;
133
134    use super::{
135        FunctionCoder, JsonCoder, SerializationError, SerializedAttribute, SerializedFieldConfig,
136        serialize_attribute,
137    };
138
139    struct ProfileRecord;
140
141    impl SerializedAttribute for ProfileRecord {
142        fn serialized_attributes() -> &'static [SerializedFieldConfig] {
143            static CONFIGS: LazyLock<Vec<SerializedFieldConfig>> =
144                LazyLock::new(|| vec![serialize_attribute("settings", JsonCoder)]);
145            CONFIGS.as_slice()
146        }
147    }
148
149    fn reverse_dump(value: &serde_json::Value) -> Result<String, SerializationError> {
150        let text = value
151            .as_str()
152            .ok_or_else(|| SerializationError::Encode("expected string".to_owned()))?;
153        Ok(text.chars().rev().collect())
154    }
155
156    fn reverse_load(raw: &str) -> Result<serde_json::Value, SerializationError> {
157        if raw.is_empty() {
158            return Err(SerializationError::Decode("empty payload".to_owned()));
159        }
160        Ok(json!(raw.chars().rev().collect::<String>()))
161    }
162
163    #[test]
164    fn json_coder_round_trips_json_values() {
165        let config = serialize_attribute("settings", JsonCoder);
166        let dumped = config
167            .dump(&json!({"theme": "dark"}))
168            .expect("dump should succeed");
169        let loaded = config.load(&dumped).expect("load should succeed");
170        assert_eq!(loaded, json!({"theme": "dark"}));
171    }
172
173    #[test]
174    fn function_coder_supports_custom_serialization() {
175        let config =
176            serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
177        let dumped = config
178            .dump(&json!("stressed"))
179            .expect("dump should succeed");
180        let loaded = config.load(&dumped).expect("load should succeed");
181
182        assert_eq!(dumped, "desserts");
183        assert_eq!(loaded, json!("stressed"));
184    }
185
186    #[test]
187    fn function_coder_surfaces_encode_errors() {
188        let config =
189            serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
190
191        assert_eq!(
192            config.dump(&json!(1)).map_err(|error| error.to_string()),
193            Err("encode failed: expected string".to_owned())
194        );
195    }
196
197    #[test]
198    fn function_coder_surfaces_decode_errors() {
199        let config =
200            serialize_attribute("nickname", FunctionCoder::new(reverse_dump, reverse_load));
201
202        assert_eq!(
203            config.load("").map_err(|error| error.to_string()),
204            Err("decode failed: empty payload".to_owned())
205        );
206    }
207
208    #[test]
209    fn trait_exposes_declared_serialized_attributes() {
210        assert_eq!(ProfileRecord::serialized_attributes().len(), 1);
211        assert_eq!(ProfileRecord::serialized_attributes()[0].field, "settings");
212    }
213}