Skip to main content

pulith_serde_backend/
lib.rs

1//! Serialization backend contract crate.
2
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use thiserror::Error;
6
7pub type Result<T> = std::result::Result<T, CodecError>;
8
9#[derive(Debug, Error)]
10pub enum CodecError {
11    #[error("json codec error: {0}")]
12    Json(#[from] serde_json::Error),
13    #[error("invalid utf-8 payload: {0}")]
14    InvalidUtf8(String),
15}
16
17/// Text-oriented structured codec boundary.
18pub trait TextCodec {
19    fn encode_pretty<T: Serialize>(&self, value: &T) -> Result<String>;
20    fn decode_str<T: DeserializeOwned>(&self, input: &str) -> Result<T>;
21}
22
23pub fn encode_pretty_vec<C: TextCodec, T: Serialize>(codec: &C, value: &T) -> Result<Vec<u8>> {
24    Ok(codec.encode_pretty(value)?.into_bytes())
25}
26
27pub fn decode_slice<C: TextCodec, T: DeserializeOwned>(codec: &C, input: &[u8]) -> Result<T> {
28    let text =
29        std::str::from_utf8(input).map_err(|error| CodecError::InvalidUtf8(error.to_string()))?;
30    codec.decode_str(text)
31}
32
33/// JSON baseline adapter for structured persistence.
34#[derive(Debug, Clone, Copy, Default)]
35pub struct JsonTextCodec;
36
37impl TextCodec for JsonTextCodec {
38    fn encode_pretty<T: Serialize>(&self, value: &T) -> Result<String> {
39        Ok(serde_json::to_string_pretty(value)?)
40    }
41
42    fn decode_str<T: DeserializeOwned>(&self, input: &str) -> Result<T> {
43        Ok(serde_json::from_str(input)?)
44    }
45}
46
47/// Compact JSON adapter used for parity/compatibility testing.
48#[derive(Debug, Clone, Copy, Default)]
49pub struct CompactJsonTextCodec;
50
51impl TextCodec for CompactJsonTextCodec {
52    fn encode_pretty<T: Serialize>(&self, value: &T) -> Result<String> {
53        Ok(serde_json::to_string(value)?)
54    }
55
56    fn decode_str<T: DeserializeOwned>(&self, input: &str) -> Result<T> {
57        Ok(serde_json::from_str(input)?)
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use serde::{Deserialize, Serialize};
65    use std::collections::BTreeMap;
66
67    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68    struct Example {
69        schema_version: u32,
70        entries: BTreeMap<String, String>,
71    }
72
73    #[test]
74    fn json_text_codec_round_trip_preserves_data() {
75        let mut entries = BTreeMap::new();
76        entries.insert("alpha".to_string(), "1".to_string());
77        entries.insert("zeta".to_string(), "2".to_string());
78        let value = Example {
79            schema_version: 1,
80            entries,
81        };
82
83        let codec = JsonTextCodec;
84        let encoded = codec.encode_pretty(&value).unwrap();
85        let decoded: Example = codec.decode_str(&encoded).unwrap();
86
87        assert_eq!(decoded, value);
88    }
89
90    #[test]
91    fn json_text_codec_preserves_btreemap_ordering_in_output() {
92        let mut entries = BTreeMap::new();
93        entries.insert("zeta".to_string(), "2".to_string());
94        entries.insert("alpha".to_string(), "1".to_string());
95        let value = Example {
96            schema_version: 1,
97            entries,
98        };
99
100        let codec = JsonTextCodec;
101        let encoded = codec.encode_pretty(&value).unwrap();
102        let alpha = encoded.find("alpha").unwrap();
103        let zeta = encoded.find("zeta").unwrap();
104        assert!(alpha < zeta);
105    }
106
107    #[test]
108    fn helpers_encode_and_decode_bytes() {
109        let mut entries = BTreeMap::new();
110        entries.insert("alpha".to_string(), "1".to_string());
111        let value = Example {
112            schema_version: 1,
113            entries,
114        };
115
116        let codec = JsonTextCodec;
117        let encoded = encode_pretty_vec(&codec, &value).unwrap();
118        let decoded: Example = decode_slice(&codec, &encoded).unwrap();
119
120        assert_eq!(decoded, value);
121    }
122
123    #[test]
124    fn codecs_preserve_semantic_parity() {
125        let mut entries = BTreeMap::new();
126        entries.insert("zeta".to_string(), "2".to_string());
127        entries.insert("alpha".to_string(), "1".to_string());
128        let value = Example {
129            schema_version: 1,
130            entries,
131        };
132
133        let pretty = JsonTextCodec.encode_pretty(&value).unwrap();
134        let compact = CompactJsonTextCodec.encode_pretty(&value).unwrap();
135
136        let pretty_decoded: Example = JsonTextCodec.decode_str(&pretty).unwrap();
137        let compact_decoded: Example = CompactJsonTextCodec.decode_str(&compact).unwrap();
138        let cross_decoded: Example = JsonTextCodec.decode_str(&compact).unwrap();
139
140        assert_eq!(pretty_decoded, compact_decoded);
141        assert_eq!(pretty_decoded, cross_decoded);
142    }
143}