Skip to main content

structfs_serde_store/
codec.rs

1//! Versioned, bounded value codecs.
2use crate::{limits::Failure, Limits};
3use bytes::Bytes;
4use structfs_core_store::{Codec, CodecErrorKind, CodecOperation, Error, Format, Value};
5
6/// Explicitly selected StructFS v1 codec contract.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Profile {
9    ValueJson,
10    Json,
11    Cbor,
12    Flexbuffers,
13}
14impl Profile {
15    pub fn identifier(self) -> &'static str {
16        match self {
17            Self::ValueJson => "structfs-value-json/1",
18            Self::Json => "structfs-json/1",
19            Self::Cbor => "structfs-cbor/1",
20            Self::Flexbuffers => "structfs-flexbuffers/1",
21        }
22    }
23    pub fn format(self) -> Format {
24        match self {
25            Self::ValueJson => Format::VALUE_JSON,
26            Self::Json => Format::JSON,
27            Self::Cbor => Format::CBOR,
28            Self::Flexbuffers => Format::FLEXBUFFERS,
29        }
30    }
31}
32/// A codec with caller-configured finite limits. Canonical validation applies only
33/// to tagged JSON and compares the complete supplied document, including whitespace.
34#[derive(Debug, Clone)]
35pub struct ValueCodec {
36    pub profile: Profile,
37    pub limits: Limits,
38    pub require_canonical: bool,
39}
40impl ValueCodec {
41    pub fn new(profile: Profile) -> Self {
42        Self {
43            profile,
44            limits: Limits::default(),
45            require_canonical: false,
46        }
47    }
48    pub fn with_limits(mut self, limits: Limits) -> Self {
49        self.limits = limits;
50        self
51    }
52    pub fn canonical(mut self) -> Self {
53        self.require_canonical = true;
54        self
55    }
56}
57
58/// Validate and transcode a document, even when both profiles are the same.
59/// Raw byte forwarding is deliberately a different operation.
60pub fn transcode(bytes: &Bytes, source: &ValueCodec, target: &ValueCodec) -> Result<Bytes, Error> {
61    let value = source.decode(bytes, &source.profile.format())?;
62    target.encode(&value, &target.profile.format())
63}
64impl Codec for ValueCodec {
65    fn supports(&self, format: &Format) -> bool {
66        *format == self.profile.format()
67    }
68    fn decode(&self, bytes: &Bytes, format: &Format) -> Result<Value, Error> {
69        if !self.supports(format) {
70            return Err(Error::UnsupportedFormat(format.clone()));
71        }
72        let result = (|| {
73            if self.require_canonical && self.profile != Profile::ValueJson {
74                return Err(Failure(CodecErrorKind::UnsupportedProfile));
75            }
76            let v = match self.profile {
77                Profile::ValueJson => crate::json_profile::decode(bytes, &self.limits, true),
78                Profile::Json => crate::json_profile::decode(bytes, &self.limits, false),
79                Profile::Cbor => crate::cbor_profile::decode(bytes, &self.limits),
80                Profile::Flexbuffers => crate::flex_profile::decode(bytes, &self.limits),
81            }?;
82            if self.require_canonical
83                && crate::json_profile::encode(&v, &self.limits, true)?.as_slice() != bytes.as_ref()
84            {
85                return Err(Failure(CodecErrorKind::Noncanonical));
86            }
87            Ok(v)
88        })();
89        result.map_err(|e: Failure| e.core(format, CodecOperation::Decode, &self.limits))
90    }
91    fn encode(&self, value: &Value, format: &Format) -> Result<Bytes, Error> {
92        if !self.supports(format) {
93            return Err(Error::UnsupportedFormat(format.clone()));
94        }
95        let result = match self.profile {
96            Profile::ValueJson => crate::json_profile::encode(value, &self.limits, true),
97            Profile::Json => crate::json_profile::encode(value, &self.limits, false),
98            Profile::Cbor => crate::cbor_profile::encode(value, &self.limits),
99            Profile::Flexbuffers => crate::flex_profile::encode(value, &self.limits),
100        };
101        result
102            .map(Bytes::from)
103            .map_err(|e| e.core(format, CodecOperation::Encode, &self.limits))
104    }
105}
106macro_rules! default_codec {
107    ($name:ident,$profile:ident,$doc:literal) => {
108        #[doc=$doc]
109        #[derive(Debug, Clone, Copy, Default)]
110        pub struct $name;
111        impl Codec for $name {
112            fn supports(&self, f: &Format) -> bool {
113                ValueCodec::new(Profile::$profile).supports(f)
114            }
115            fn decode(&self, b: &Bytes, f: &Format) -> Result<Value, Error> {
116                ValueCodec::new(Profile::$profile).decode(b, f)
117            }
118            fn encode(&self, v: &Value, f: &Format) -> Result<Bytes, Error> {
119                ValueCodec::new(Profile::$profile).encode(v, f)
120            }
121        }
122    };
123}
124default_codec!(
125    JsonCodec,
126    Json,
127    "Strict plain JSON with default limits; bytes and non-finite floats fail."
128);
129default_codec!(
130    ValueJsonCodec,
131    ValueJson,
132    "Lossless canonical StructFS Value JSON v1 with default limits."
133);
134default_codec!(CborCodec, Cbor, "StructFS CBOR v1 with default limits.");
135default_codec!(
136    FlexbuffersCodec,
137    Flexbuffers,
138    "StructFS FlexBuffers v1 with default limits; NUL map keys fail."
139);
140
141/// A codec that combines multiple codecs.
142///
143/// Routes encode/decode to the appropriate codec based on format.
144pub struct MultiCodec {
145    codecs: Vec<Box<dyn Codec>>,
146}
147
148impl MultiCodec {
149    /// Create an empty multi-codec.
150    pub fn new() -> Self {
151        Self { codecs: Vec::new() }
152    }
153
154    /// Add a codec.
155    pub fn add(&mut self, codec: impl Codec + 'static) {
156        self.codecs.push(Box::new(codec));
157    }
158
159    /// Create a multi-codec with the JSON codec included.
160    pub fn with_json() -> Self {
161        let mut mc = Self::new();
162        mc.add(JsonCodec);
163        mc
164    }
165
166    /// The v1 transports, routed by explicit format. Each has its documented subset.
167    pub fn standard() -> Self {
168        let mut mc = Self::new();
169        mc.add(JsonCodec);
170        mc.add(CborCodec);
171        mc.add(FlexbuffersCodec);
172        mc.add(ValueJsonCodec);
173        mc
174    }
175}
176
177impl Default for MultiCodec {
178    fn default() -> Self {
179        Self::standard()
180    }
181}
182
183impl Codec for MultiCodec {
184    fn decode(&self, bytes: &Bytes, format: &Format) -> Result<Value, Error> {
185        for codec in &self.codecs {
186            if codec.supports(format) {
187                return codec.decode(bytes, format);
188            }
189        }
190        Err(Error::UnsupportedFormat(format.clone()))
191    }
192
193    fn encode(&self, value: &Value, format: &Format) -> Result<Bytes, Error> {
194        for codec in &self.codecs {
195            if codec.supports(format) {
196                return codec.encode(value, format);
197            }
198        }
199        Err(Error::UnsupportedFormat(format.clone()))
200    }
201
202    fn supports(&self, format: &Format) -> bool {
203        self.codecs.iter().any(|c| c.supports(format))
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn json_codec_roundtrip() {
213        let codec = JsonCodec;
214
215        let original = Value::Map(
216            [
217                ("name".to_string(), Value::String("Alice".to_string())),
218                ("age".to_string(), Value::Integer(30)),
219            ]
220            .into_iter()
221            .collect(),
222        );
223
224        let bytes = codec.encode(&original, &Format::JSON).unwrap();
225        let decoded = codec.decode(&bytes, &Format::JSON).unwrap();
226
227        assert_eq!(original, decoded);
228    }
229
230    #[test]
231    fn json_codec_rejects_other_formats() {
232        let codec = JsonCodec;
233
234        let bytes = Bytes::from_static(b"hello");
235        let result = codec.decode(&bytes, &Format::PROTOBUF);
236
237        assert!(matches!(result, Err(Error::UnsupportedFormat(_))));
238    }
239
240    #[test]
241    fn multi_codec_routes_correctly() {
242        let codec = MultiCodec::with_json();
243
244        assert!(codec.supports(&Format::JSON));
245        assert!(!codec.supports(&Format::PROTOBUF));
246
247        let value = Value::from("hello");
248        let bytes = codec.encode(&value, &Format::JSON).unwrap();
249        let decoded = codec.decode(&bytes, &Format::JSON).unwrap();
250
251        assert_eq!(value, decoded);
252    }
253
254    #[test]
255    fn json_codec_encode_unsupported_format() {
256        let codec = JsonCodec;
257        let value = Value::from("test");
258        let result = codec.encode(&value, &Format::PROTOBUF);
259        assert!(matches!(result, Err(Error::UnsupportedFormat(_))));
260    }
261
262    #[test]
263    fn json_codec_decode_invalid_json() {
264        let codec = JsonCodec;
265        let bytes = Bytes::from_static(b"not valid json {{{");
266        let result = codec.decode(&bytes, &Format::JSON);
267        assert!(matches!(
268            result,
269            Err(Error::Codec {
270                operation: structfs_core_store::CodecOperation::Decode,
271                ..
272            })
273        ));
274    }
275
276    #[test]
277    fn multi_codec_decode_unsupported() {
278        let codec = MultiCodec::new(); // Empty, no codecs
279        let bytes = Bytes::from_static(b"hello");
280        let result = codec.decode(&bytes, &Format::JSON);
281        assert!(matches!(result, Err(Error::UnsupportedFormat(_))));
282    }
283
284    #[test]
285    fn multi_codec_encode_unsupported() {
286        let codec = MultiCodec::new(); // Empty, no codecs
287        let value = Value::from("test");
288        let result = codec.encode(&value, &Format::JSON);
289        assert!(matches!(result, Err(Error::UnsupportedFormat(_))));
290    }
291
292    #[test]
293    fn multi_codec_default() {
294        let codec = MultiCodec::default();
295        // Default includes JSON
296        assert!(codec.supports(&Format::JSON));
297    }
298
299    #[test]
300    fn multi_codec_add_custom() {
301        use structfs_core_store::Codec as CoreCodec;
302
303        struct CustomCodec;
304
305        impl CoreCodec for CustomCodec {
306            fn decode(&self, bytes: &Bytes, _format: &Format) -> Result<Value, Error> {
307                Ok(Value::Bytes(bytes.to_vec()))
308            }
309
310            fn encode(&self, _value: &Value, _format: &Format) -> Result<Bytes, Error> {
311                Ok(Bytes::from_static(b"custom"))
312            }
313
314            fn supports(&self, format: &Format) -> bool {
315                format == &Format::OCTET_STREAM
316            }
317        }
318
319        let mut codec = MultiCodec::new();
320        codec.add(CustomCodec);
321
322        assert!(codec.supports(&Format::OCTET_STREAM));
323        assert!(!codec.supports(&Format::JSON));
324
325        let decoded = codec
326            .decode(&Bytes::from_static(b"data"), &Format::OCTET_STREAM)
327            .unwrap();
328        assert_eq!(decoded, Value::Bytes(b"data".to_vec()));
329
330        let encoded = codec.encode(&Value::Null, &Format::OCTET_STREAM).unwrap();
331        assert_eq!(encoded.as_ref(), b"custom");
332    }
333
334    #[test]
335    fn json_codec_supports() {
336        let codec = JsonCodec;
337        assert!(codec.supports(&Format::JSON));
338        assert!(!codec.supports(&Format::PROTOBUF));
339        assert!(!codec.supports(&Format::OCTET_STREAM));
340    }
341
342    #[test]
343    fn json_codec_default() {
344        let codec: JsonCodec = Default::default();
345        assert!(codec.supports(&Format::JSON));
346    }
347
348    #[test]
349    fn json_codec_copy() {
350        let codec1 = JsonCodec;
351        let codec2 = codec1; // Copy
352                             // Both should work the same
353        let bytes = codec1.encode(&Value::from("test"), &Format::JSON).unwrap();
354        let decoded = codec2.decode(&bytes, &Format::JSON).unwrap();
355        assert_eq!(decoded, Value::from("test"));
356    }
357
358    #[test]
359    fn json_codec_debug() {
360        let codec = JsonCodec;
361        let debug = format!("{:?}", codec);
362        assert!(debug.contains("JsonCodec"));
363    }
364
365    fn sample() -> Value {
366        Value::Map(
367            [
368                ("name".to_string(), Value::from("Alice")),
369                ("age".to_string(), Value::Integer(30)),
370                ("score".to_string(), Value::Float(0.5)),
371                ("active".to_string(), Value::Bool(true)),
372                ("note".to_string(), Value::Null),
373                (
374                    "tags".to_string(),
375                    Value::Array(vec![Value::from("a"), Value::Integer(-7)]),
376                ),
377            ]
378            .into_iter()
379            .collect(),
380        )
381    }
382
383    #[test]
384    fn cbor_codec_roundtrip() {
385        let codec = CborCodec;
386        let bytes = codec.encode(&sample(), &Format::CBOR).unwrap();
387        assert_eq!(codec.decode(&bytes, &Format::CBOR).unwrap(), sample());
388    }
389
390    #[test]
391    fn flexbuffers_codec_roundtrip() {
392        let codec = FlexbuffersCodec;
393        let bytes = codec.encode(&sample(), &Format::FLEXBUFFERS).unwrap();
394        assert_eq!(
395            codec.decode(&bytes, &Format::FLEXBUFFERS).unwrap(),
396            sample()
397        );
398    }
399
400    #[test]
401    fn bytes_survive_the_binary_transports() {
402        // The property JSON lacks: Value::Bytes round-trips as bytes,
403        // not as an array of numbers.
404        let value = Value::Map(
405            [("payload".to_string(), Value::Bytes(vec![0, 159, 146, 150]))]
406                .into_iter()
407                .collect(),
408        );
409        for (codec, format) in [
410            (&CborCodec as &dyn Codec, Format::CBOR),
411            (&FlexbuffersCodec as &dyn Codec, Format::FLEXBUFFERS),
412        ] {
413            let bytes = codec.encode(&value, &format).unwrap();
414            assert_eq!(
415                codec.decode(&bytes, &format).unwrap(),
416                value,
417                "bytes mangled by {format}"
418            );
419        }
420    }
421
422    #[test]
423    fn binary_codecs_reject_other_formats() {
424        assert!(matches!(
425            CborCodec.encode(&Value::Null, &Format::JSON),
426            Err(Error::UnsupportedFormat(_))
427        ));
428        assert!(matches!(
429            FlexbuffersCodec.decode(&Bytes::from_static(b"x"), &Format::CBOR),
430            Err(Error::UnsupportedFormat(_))
431        ));
432    }
433
434    #[test]
435    fn binary_codecs_report_decode_errors() {
436        let garbage = Bytes::from_static(&[0xff, 0xfe, 0xfd]);
437        assert!(matches!(
438            CborCodec.decode(&garbage, &Format::CBOR),
439            Err(Error::Codec { .. })
440        ));
441        assert!(matches!(
442            FlexbuffersCodec.decode(&Bytes::from_static(&[]), &Format::FLEXBUFFERS),
443            Err(Error::Codec { .. })
444        ));
445    }
446
447    #[test]
448    fn standard_multi_codec_routes_every_transport() {
449        let codec = MultiCodec::standard();
450        for format in [Format::JSON, Format::CBOR, Format::FLEXBUFFERS] {
451            assert!(codec.supports(&format), "missing transport: {format}");
452            let bytes = codec.encode(&sample(), &format).unwrap();
453            assert_eq!(codec.decode(&bytes, &format).unwrap(), sample());
454        }
455        assert!(!codec.supports(&Format::PROTOBUF));
456    }
457
458    #[test]
459    fn multi_codec_supports_empty() {
460        let codec = MultiCodec::new();
461        assert!(!codec.supports(&Format::JSON));
462        assert!(!codec.supports(&Format::PROTOBUF));
463    }
464}