Skip to main content

toon/encode/
primitives.rs

1use std::fmt::Write;
2
3use crate::JsonPrimitive;
4use crate::StringOrNumberOrBoolOrNull;
5use crate::shared::constants::{DEFAULT_DELIMITER, DOUBLE_QUOTE};
6use crate::shared::string_utils::escape_string;
7use crate::shared::validation::{is_safe_unquoted, is_valid_unquoted_key};
8
9#[must_use]
10pub fn encode_primitive(value: &JsonPrimitive, delimiter: char) -> String {
11    match value {
12        StringOrNumberOrBoolOrNull::Null => "null".to_string(),
13        StringOrNumberOrBoolOrNull::Bool(value) => value.to_string(),
14        StringOrNumberOrBoolOrNull::Number(value) => format_number(*value),
15        StringOrNumberOrBoolOrNull::String(value) => encode_string_literal(value, delimiter),
16    }
17}
18
19#[must_use]
20pub fn encode_string_literal(value: &str, delimiter: char) -> String {
21    if is_safe_unquoted(value, delimiter) {
22        return value.to_string();
23    }
24    format!("{DOUBLE_QUOTE}{}{DOUBLE_QUOTE}", escape_string(value))
25}
26
27#[must_use]
28pub fn encode_key(key: &str) -> String {
29    if is_valid_unquoted_key(key) {
30        return key.to_string();
31    }
32    format!("{DOUBLE_QUOTE}{}{DOUBLE_QUOTE}", escape_string(key))
33}
34
35#[must_use]
36pub fn encode_and_join_primitives(values: &[JsonPrimitive], delimiter: char) -> String {
37    if values.is_empty() {
38        return String::new();
39    }
40    // Estimate: average 10 chars per primitive + delimiter
41    let mut out = String::with_capacity(values.len() * 11);
42    for (idx, value) in values.iter().enumerate() {
43        if idx > 0 {
44            out.push(delimiter);
45        }
46        out.push_str(&encode_primitive(value, delimiter));
47    }
48    out
49}
50
51#[must_use]
52pub fn format_header(
53    length: usize,
54    key: Option<&str>,
55    fields: Option<&[String]>,
56    delimiter: char,
57) -> String {
58    let mut header = String::new();
59
60    if let Some(key) = key {
61        header.push_str(&encode_key(key));
62    }
63
64    if delimiter == DEFAULT_DELIMITER {
65        let _ = write!(header, "[{length}]");
66    } else {
67        let _ = write!(header, "[{length}{delimiter}]");
68    }
69
70    if let Some(fields) = fields {
71        header.push('{');
72        for (idx, field) in fields.iter().enumerate() {
73            if idx > 0 {
74                header.push(delimiter);
75            }
76            header.push_str(&encode_key(field));
77        }
78        header.push('}');
79    }
80
81    header.push(':');
82    header
83}
84
85fn format_number(value: f64) -> String {
86    if value == 0.0 {
87        return "0".to_string();
88    }
89    if value.is_nan() || !value.is_finite() {
90        return "null".to_string();
91    }
92    value.to_string()
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn encode_null_primitive() {
101        assert_eq!(
102            encode_primitive(&StringOrNumberOrBoolOrNull::Null, ','),
103            "null"
104        );
105    }
106
107    #[test]
108    fn encode_bool_primitive() {
109        assert_eq!(
110            encode_primitive(&StringOrNumberOrBoolOrNull::Bool(true), ','),
111            "true"
112        );
113        assert_eq!(
114            encode_primitive(&StringOrNumberOrBoolOrNull::Bool(false), ','),
115            "false"
116        );
117    }
118
119    #[test]
120    fn encode_number_zero_is_bare_zero() {
121        assert_eq!(
122            encode_primitive(&StringOrNumberOrBoolOrNull::Number(0.0), ','),
123            "0"
124        );
125    }
126
127    #[test]
128    fn encode_number_nan_is_null() {
129        assert_eq!(
130            encode_primitive(&StringOrNumberOrBoolOrNull::Number(f64::NAN), ','),
131            "null"
132        );
133    }
134
135    #[test]
136    fn encode_number_infinity_is_null() {
137        assert_eq!(
138            encode_primitive(&StringOrNumberOrBoolOrNull::Number(f64::INFINITY), ','),
139            "null"
140        );
141    }
142
143    #[test]
144    fn encode_simple_string_is_unquoted() {
145        assert_eq!(
146            encode_primitive(&StringOrNumberOrBoolOrNull::String("hello".into()), ','),
147            "hello"
148        );
149    }
150
151    #[test]
152    fn encode_string_with_comma_is_quoted_when_delimiter_is_comma() {
153        let out = encode_primitive(&StringOrNumberOrBoolOrNull::String("a,b".into()), ',');
154        assert!(out.starts_with('"'));
155        assert!(out.ends_with('"'));
156        assert!(out.contains("a,b"));
157    }
158
159    #[test]
160    fn encode_string_with_newline_is_escaped_and_quoted() {
161        let out = encode_string_literal("line\nfeed", ',');
162        assert_eq!(out, "\"line\\nfeed\"");
163    }
164
165    #[test]
166    fn encode_string_that_looks_like_bool_is_quoted() {
167        let out = encode_string_literal("true", ',');
168        assert_eq!(out, "\"true\"");
169    }
170
171    #[test]
172    fn encode_key_valid_is_unquoted() {
173        assert_eq!(encode_key("valid_key"), "valid_key");
174    }
175
176    #[test]
177    fn encode_key_with_space_is_quoted() {
178        let out = encode_key("has space");
179        assert!(out.starts_with('"'));
180        assert!(out.contains("has space"));
181    }
182
183    #[test]
184    fn encode_key_with_quotes_is_escaped() {
185        let out = encode_key("a\"b");
186        assert_eq!(out, "\"a\\\"b\"");
187    }
188
189    #[test]
190    fn encode_and_join_primitives_empty_is_empty() {
191        assert_eq!(encode_and_join_primitives(&[], ','), "");
192    }
193
194    #[test]
195    fn encode_and_join_primitives_joins_with_delimiter() {
196        let vals = vec![
197            StringOrNumberOrBoolOrNull::Number(1.0),
198            StringOrNumberOrBoolOrNull::String("two".into()),
199            StringOrNumberOrBoolOrNull::Bool(true),
200        ];
201        assert_eq!(encode_and_join_primitives(&vals, ','), "1,two,true");
202    }
203
204    #[test]
205    fn encode_and_join_primitives_different_delimiters() {
206        let vals = vec![
207            StringOrNumberOrBoolOrNull::Number(1.0),
208            StringOrNumberOrBoolOrNull::Number(2.0),
209        ];
210        assert_eq!(encode_and_join_primitives(&vals, '|'), "1|2");
211        assert_eq!(encode_and_join_primitives(&vals, '\t'), "1\t2");
212    }
213
214    #[test]
215    fn format_header_length_only_default_delimiter() {
216        assert_eq!(format_header(3, None, None, ','), "[3]:");
217    }
218
219    #[test]
220    fn format_header_length_only_custom_delimiter() {
221        assert_eq!(format_header(3, None, None, '|'), "[3|]:");
222    }
223
224    #[test]
225    fn format_header_with_key_and_fields() {
226        let fields = vec!["id".to_string(), "name".to_string()];
227        assert_eq!(
228            format_header(2, Some("users"), Some(&fields), ','),
229            "users[2]{id,name}:"
230        );
231    }
232
233    #[test]
234    fn format_header_with_quoted_field_name() {
235        let fields = vec!["weird name".to_string()];
236        let out = format_header(1, Some("data"), Some(&fields), ',');
237        assert!(out.contains("{\"weird name\"}"));
238    }
239
240    #[test]
241    fn format_header_zero_length() {
242        assert_eq!(format_header(0, Some("items"), None, ','), "items[0]:");
243    }
244}