pamoja_codec/
transcode.rs1use alloc::format;
17use alloc::string::ToString;
18use alloc::vec::Vec;
19
20use pamoja_core::{Error, Result};
21
22use crate::cbor::{read_error, write_error};
23
24pub fn json_to_cbor(json: &[u8]) -> Result<Vec<u8>> {
49 let value: serde_json::Value =
50 serde_json::from_slice(json).map_err(|error| Error::Codec(error.to_string()))?;
51 let mut buffer = Vec::new();
52 ciborium::into_writer(&value, &mut buffer).map_err(write_error)?;
53 Ok(buffer)
54}
55
56pub fn cbor_to_json(cbor: &[u8]) -> Result<Vec<u8>> {
81 let value: ciborium::Value = ciborium::from_reader(cbor).map_err(read_error)?;
82 let value: serde_json::Value =
83 value
84 .deserialized()
85 .map_err(|ciborium::value::Error::Custom(message)| {
86 Error::Codec(format!("the CBOR has no JSON form: {message}"))
87 })?;
88 serde_json::to_vec(&value).map_err(|error| Error::Codec(error.to_string()))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn object_keys_come_back_sorted() {
97 let cbor = json_to_cbor(br#"{"c":21.5,"a":1}"#).expect("to cbor");
100 assert_eq!(
101 cbor_to_json(&cbor).expect("to json"),
102 br#"{"a":1,"c":21.5}"#
103 );
104 }
105
106 #[test]
107 fn round_trips_a_document() {
108 let json = br#"{"battery":88,"id":"probe-1","reading":21.5}"#;
109 let cbor = json_to_cbor(json).expect("to cbor");
110 assert_eq!(cbor_to_json(&cbor).expect("to json"), json);
111 }
112
113 #[test]
114 fn cbor_is_smaller_than_the_json_it_came_from() {
115 let json = br#"{"a":1,"b":2,"c":3,"d":4,"e":5}"#;
116 let cbor = json_to_cbor(json).expect("to cbor");
117 assert!(cbor.len() < json.len());
118 }
119
120 #[test]
121 fn round_trips_nested_and_empty_containers() {
122 let json = br#"{"empty":{},"list":[1,[2,3],{"deep":true}],"none":null}"#;
123 let cbor = json_to_cbor(json).expect("to cbor");
124 assert_eq!(cbor_to_json(&cbor).expect("to json"), json);
125 }
126
127 #[test]
128 fn invalid_json_is_a_codec_error() {
129 assert!(matches!(json_to_cbor(b"not json"), Err(Error::Codec(_))));
130 }
131
132 #[test]
133 fn invalid_cbor_is_a_codec_error() {
134 assert!(matches!(cbor_to_json(&[0xff, 0xff]), Err(Error::Codec(_))));
135 }
136
137 #[test]
138 fn a_document_cut_short_says_so_in_words() {
139 let cbor = json_to_cbor(br#"{"c":21.5}"#).expect("to cbor");
140 match cbor_to_json(&cbor[..cbor.len() - 1]) {
141 Err(Error::Codec(reason)) => {
142 assert_eq!(reason, "the CBOR ends part-way through a value");
143 }
144 other => panic!("a document cut short decoded: {other:?}"),
145 }
146 }
147
148 #[test]
149 fn a_non_string_map_key_has_no_json_form() {
150 let mut cbor = Vec::new();
153 let value = ciborium::Value::Map(vec![(
154 ciborium::Value::Integer(1.into()),
155 ciborium::Value::Bool(true),
156 )]);
157 ciborium::into_writer(&value, &mut cbor).expect("write cbor");
158 match cbor_to_json(&cbor) {
159 Err(Error::Codec(reason)) => {
160 assert!(
161 reason.starts_with("the CBOR has no JSON form: "),
162 "{reason}"
163 );
164 }
165 other => panic!("an integer key was given a JSON form: {other:?}"),
166 }
167 }
168
169 #[test]
170 fn a_document_transcodes_to_the_bytes_rfc_8949_fixes() {
171 let json = br#"{"c":21.5,"ok":true}"#;
175 let cbor = json_to_cbor(json).expect("a valid document");
176 assert_eq!(
177 cbor,
178 [0xA2, 0x61, 0x63, 0xF9, 0x4D, 0x60, 0x62, 0x6F, 0x6B, 0xF5]
179 );
180 assert_eq!(cbor_to_json(&cbor).expect("a valid document"), json);
181 }
182}