pulith_serde_backend/
lib.rs1use 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
17pub 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#[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#[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}