rustrails_record/
serialization_ext.rs1use std::fmt;
2use std::sync::Arc;
3
4use serde_json::Value;
5
6#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
8pub enum SerializationError {
9 #[error("encode failed: {0}")]
11 Encode(String),
12 #[error("decode failed: {0}")]
14 Decode(String),
15}
16
17pub trait AttributeCoder: Send + Sync {
19 fn dump(&self, value: &Value) -> Result<String, SerializationError>;
21 fn load(&self, raw: &str) -> Result<Value, SerializationError>;
23}
24
25#[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#[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 #[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#[derive(Clone)]
75pub struct SerializedFieldConfig {
76 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 #[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 pub fn dump(&self, value: &Value) -> Result<String, SerializationError> {
102 self.coder.dump(value)
103 }
104
105 pub fn load(&self, raw: &str) -> Result<Value, SerializationError> {
107 self.coder.load(raw)
108 }
109}
110
111#[must_use]
113pub fn serialize_attribute(
114 field: &str,
115 coder: impl AttributeCoder + 'static,
116) -> SerializedFieldConfig {
117 SerializedFieldConfig::new(field, coder)
118}
119
120pub trait SerializedAttribute {
122 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}