Skip to main content

neco_json/
encode.rs

1use alloc::string::String;
2use alloc::vec::Vec;
3use core::fmt::Write;
4
5use crate::{EncodeError, JsonValue};
6
7/// Encodes a `JsonValue` into its minimal JSON byte representation.
8pub fn encode(value: &JsonValue) -> Result<Vec<u8>, EncodeError> {
9    let mut output = Vec::new();
10    encode_value(value, &mut output)?;
11    Ok(output)
12}
13
14fn encode_value(value: &JsonValue, output: &mut Vec<u8>) -> Result<(), EncodeError> {
15    match value {
16        JsonValue::Null => output.extend_from_slice(b"null"),
17        JsonValue::Bool(true) => output.extend_from_slice(b"true"),
18        JsonValue::Bool(false) => output.extend_from_slice(b"false"),
19        JsonValue::Number(number) => encode_number(*number, output)?,
20        JsonValue::String(text) => encode_string(text, output),
21        JsonValue::Array(items) => {
22            output.push(b'[');
23            for (index, item) in items.iter().enumerate() {
24                if index > 0 {
25                    output.push(b',');
26                }
27                encode_value(item, output)?;
28            }
29            output.push(b']');
30        }
31        JsonValue::Object(fields) => {
32            output.push(b'{');
33            for (index, (key, value)) in fields.iter().enumerate() {
34                if index > 0 {
35                    output.push(b',');
36                }
37                encode_string(key, output);
38                output.push(b':');
39                encode_value(value, output)?;
40            }
41            output.push(b'}');
42        }
43    }
44    Ok(())
45}
46
47fn encode_number(number: f64, output: &mut Vec<u8>) -> Result<(), EncodeError> {
48    if !number.is_finite() {
49        return Err(EncodeError::NonFiniteNumber);
50    }
51
52    let mut buffer = String::new();
53    write!(&mut buffer, "{number}").expect("writing to String should not fail");
54    output.extend_from_slice(buffer.as_bytes());
55    Ok(())
56}
57
58fn encode_string(text: &str, output: &mut Vec<u8>) {
59    output.push(b'"');
60    for ch in text.chars() {
61        match ch {
62            '"' => output.extend_from_slice(br#"\""#),
63            '\\' => output.extend_from_slice(br#"\\"#),
64            '\u{0008}' => output.extend_from_slice(br#"\b"#),
65            '\u{000C}' => output.extend_from_slice(br#"\f"#),
66            '\n' => output.extend_from_slice(br#"\n"#),
67            '\r' => output.extend_from_slice(br#"\r"#),
68            '\t' => output.extend_from_slice(br#"\t"#),
69            '\u{0000}'..='\u{001F}' => encode_control_escape(ch as u32, output),
70            _ => {
71                let mut buffer = [0_u8; 4];
72                let encoded = ch.encode_utf8(&mut buffer);
73                output.extend_from_slice(encoded.as_bytes());
74            }
75        }
76    }
77    output.push(b'"');
78}
79
80fn encode_control_escape(code: u32, output: &mut Vec<u8>) {
81    output.extend_from_slice(br#"\u00"#);
82    output.push(hex_digit(((code >> 4) & 0x0F) as u8));
83    output.push(hex_digit((code & 0x0F) as u8));
84}
85
86fn hex_digit(value: u8) -> u8 {
87    match value {
88        0..=9 => b'0' + value,
89        10..=15 => b'A' + (value - 10),
90        _ => unreachable!("hex digit is always in range"),
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use alloc::vec;
97
98    use super::encode;
99    use crate::{parse, EncodeError, JsonValue};
100
101    #[test]
102    fn encodes_basic_variants() {
103        assert_eq!(encode(&JsonValue::Null).unwrap(), b"null");
104        assert_eq!(encode(&JsonValue::Bool(true)).unwrap(), b"true");
105        assert_eq!(encode(&JsonValue::Bool(false)).unwrap(), b"false");
106        assert_eq!(encode(&JsonValue::Number(42.5)).unwrap(), b"42.5");
107        assert_eq!(
108            encode(&JsonValue::String("neco".into())).unwrap(),
109            b"\"neco\""
110        );
111        assert_eq!(encode(&JsonValue::Array(vec![])).unwrap(), b"[]");
112        assert_eq!(encode(&JsonValue::Object(vec![])).unwrap(), b"{}");
113    }
114
115    #[test]
116    fn rejects_non_finite_numbers() {
117        assert_eq!(
118            encode(&JsonValue::Number(f64::NAN)),
119            Err(EncodeError::NonFiniteNumber)
120        );
121        assert_eq!(
122            encode(&JsonValue::Number(f64::INFINITY)),
123            Err(EncodeError::NonFiniteNumber)
124        );
125        assert_eq!(
126            encode(&JsonValue::Number(f64::NEG_INFINITY)),
127            Err(EncodeError::NonFiniteNumber)
128        );
129    }
130
131    #[test]
132    fn escapes_strings() {
133        let value = JsonValue::String("quote:\" slash:\\\n\r\t".into());
134        assert_eq!(encode(&value).unwrap(), br#""quote:\" slash:\\\n\r\t""#);
135
136        let control = JsonValue::String("\u{0000}\u{0008}\u{000C}\u{001f}".into());
137        assert_eq!(encode(&control).unwrap(), br#""\u0000\b\f\u001F""#);
138    }
139
140    #[test]
141    fn encodes_integer_valued_f64_using_current_representation() {
142        assert_eq!(encode(&JsonValue::Number(42.0)).unwrap(), b"42");
143        assert_eq!(encode(&JsonValue::Number(-0.0)).unwrap(), b"-0");
144    }
145
146    #[test]
147    fn escapes_null_character_in_string() {
148        let value = JsonValue::String("a\u{0000}b".into());
149        assert_eq!(encode(&value).unwrap(), br#""a\u0000b""#);
150    }
151
152    #[test]
153    fn encodes_nested_structures() {
154        let value = JsonValue::Object(vec![
155            ("name".into(), JsonValue::String("neco".into())),
156            (
157                "items".into(),
158                JsonValue::Array(vec![
159                    JsonValue::Null,
160                    JsonValue::Bool(true),
161                    JsonValue::Object(vec![("x".into(), JsonValue::Number(1.0))]),
162                ]),
163            ),
164        ]);
165
166        assert_eq!(
167            encode(&value).unwrap(),
168            br#"{"name":"neco","items":[null,true,{"x":1}]}"#
169        );
170    }
171
172    #[test]
173    fn encode_parse_roundtrip_for_finite_values() {
174        let value = JsonValue::Object(vec![
175            ("null".into(), JsonValue::Null),
176            ("bool".into(), JsonValue::Bool(true)),
177            ("int".into(), JsonValue::Number(7.0)),
178            ("float".into(), JsonValue::Number(-3.25)),
179            ("text".into(), JsonValue::String("ねこ\njson".into())),
180            (
181                "array".into(),
182                JsonValue::Array(vec![
183                    JsonValue::Number(0.5),
184                    JsonValue::String("\"quoted\"".into()),
185                ]),
186            ),
187            (
188                "object".into(),
189                JsonValue::Object(vec![("nested".into(), JsonValue::Bool(false))]),
190            ),
191        ]);
192
193        let encoded = encode(&value).unwrap();
194        let decoded = parse(&encoded).expect("encoded bytes should parse");
195        assert_eq!(decoded, value);
196    }
197
198    #[test]
199    fn encode_parse_roundtrip_for_complex_nested_structure() {
200        let value = JsonValue::Object(vec![
201            (
202                "meta".into(),
203                JsonValue::Object(vec![
204                    ("version".into(), JsonValue::Number(1.0)),
205                    (
206                        "tags".into(),
207                        JsonValue::Array(vec![
208                            JsonValue::String("alpha".into()),
209                            JsonValue::String("beta".into()),
210                            JsonValue::Null,
211                        ]),
212                    ),
213                ]),
214            ),
215            (
216                "items".into(),
217                JsonValue::Array(vec![
218                    JsonValue::Object(vec![
219                        ("id".into(), JsonValue::String("one".into())),
220                        ("active".into(), JsonValue::Bool(true)),
221                        ("score".into(), JsonValue::Number(1.5e2)),
222                    ]),
223                    JsonValue::Array(vec![
224                        JsonValue::String("nested".into()),
225                        JsonValue::Object(vec![(
226                            "control".into(),
227                            JsonValue::String("x\u{0000}y".into()),
228                        )]),
229                    ]),
230                ]),
231            ),
232            ("empty".into(), JsonValue::Object(vec![])),
233        ]);
234
235        let encoded = encode(&value).unwrap();
236        let decoded = parse(&encoded).expect("encoded bytes should parse");
237        assert_eq!(decoded, value);
238    }
239}