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 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}