Skip to main content

cratestack_codec_cbor/
lib.rs

1use cratestack_core::{CoolCodec, CoolError};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Default)]
5pub struct CborCodec;
6
7impl CoolCodec for CborCodec {
8    const CONTENT_TYPE: &'static str = "application/cbor";
9
10    fn encode<T: Serialize + ?Sized>(&self, value: &T) -> Result<Vec<u8>, CoolError> {
11        // `minicbor-serde` reports `is_human_readable() = true`, which keeps
12        // wire compatibility for types whose serde impl branches on that
13        // hint (uuid, chrono::DateTime). The macro-emitted projection
14        // strips `Value::Null` map entries before reaching this codec, so
15        // the non-RFC-8949 "Null = empty array" quirk of this backend
16        // never lands on the wire — see `project_*_model_value` in
17        // cratestack-macros.
18        minicbor_serde::to_vec(value)
19            .map_err(|error| CoolError::Codec(format!("failed to encode CBOR body: {error}")))
20    }
21
22    fn decode<T: for<'de> Deserialize<'de>>(&self, bytes: &[u8]) -> Result<T, CoolError> {
23        minicbor_serde::from_slice(bytes)
24            .map_err(|error| CoolError::Codec(format!("failed to decode CBOR body: {error}")))
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use cratestack_core::CoolCodec;
31
32    use super::CborCodec;
33
34    #[test]
35    fn round_trips_value() {
36        let codec = CborCodec;
37        let bytes = codec
38            .encode(&vec!["cool", "stack"])
39            .expect("encode should succeed");
40        let value: Vec<String> = codec.decode(&bytes).expect("decode should succeed");
41
42        assert_eq!(value, vec!["cool".to_owned(), "stack".to_owned()]);
43    }
44
45    #[test]
46    fn optional_none_round_trips_as_cbor_null() {
47        // minicbor-serde encodes `Option::<T>::None` as the CBOR null
48        // marker (`0xf6`, RFC 8949 §3.3 simple-value 22) — which is what
49        // we want. `serde_json::Value::Null` would mis-encode here as the
50        // CBOR empty-array marker (`0x80`); the macro-emitted projection
51        // strips `Value::Null` map entries *before* they reach this codec
52        // so the bug can't land on the wire.
53        let codec = CborCodec;
54        let bytes = codec.encode(&Option::<String>::None).expect("encode none");
55        assert_eq!(bytes, vec![0xf6]);
56        let decoded: Option<String> = codec.decode(&bytes).expect("decode none");
57        assert!(decoded.is_none());
58    }
59}