toon_format/encode/
writer.rs

1use crate::{
2    types::{
3        Delimiter,
4        EncodeOptions,
5        ToonResult,
6    },
7    utils::{
8        string::{
9            is_valid_unquoted_key,
10            needs_quoting,
11            quote_string,
12        },
13        QuotingContext,
14    },
15};
16
17/// Writer that builds TOON output string from JSON values.
18pub struct Writer {
19    buffer: String,
20    pub(crate) options: EncodeOptions,
21    active_delimiters: Vec<Delimiter>,
22}
23
24impl Writer {
25    /// Create a new writer with the given options.
26    pub fn new(options: EncodeOptions) -> Self {
27        Self {
28            buffer: String::new(),
29            active_delimiters: vec![options.delimiter],
30            options,
31        }
32    }
33
34    /// Finish writing and return the complete TOON string.
35    pub fn finish(self) -> String {
36        self.buffer
37    }
38
39    pub fn write_str(&mut self, s: &str) -> ToonResult<()> {
40        self.buffer.push_str(s);
41        Ok(())
42    }
43
44    pub fn write_char(&mut self, ch: char) -> ToonResult<()> {
45        self.buffer.push(ch);
46        Ok(())
47    }
48
49    pub fn write_newline(&mut self) -> ToonResult<()> {
50        self.buffer.push('\n');
51        Ok(())
52    }
53
54    pub fn write_indent(&mut self, depth: usize) -> ToonResult<()> {
55        let indent_string = self.options.indent.get_string(depth);
56        if !indent_string.is_empty() {
57            self.buffer.push_str(&indent_string);
58        }
59        Ok(())
60    }
61
62    pub fn write_delimiter(&mut self) -> ToonResult<()> {
63        self.buffer.push(self.options.delimiter.as_char());
64        Ok(())
65    }
66
67    pub fn write_key(&mut self, key: &str) -> ToonResult<()> {
68        if is_valid_unquoted_key(key) {
69            self.write_str(key)
70        } else {
71            self.write_quoted_string(key)
72        }
73    }
74
75    /// Write an array header with key, length, and optional field list.
76    pub fn write_array_header(
77        &mut self,
78        key: Option<&str>,
79        length: usize,
80        fields: Option<&[String]>,
81        depth: usize,
82    ) -> ToonResult<()> {
83        if let Some(k) = key {
84            if depth > 0 {
85                self.write_indent(depth)?;
86            }
87            self.write_key(k)?;
88        }
89
90        self.write_char('[')?;
91
92        let length_str = self.options.format_length(length);
93        self.write_str(&length_str)?;
94
95        if self.options.delimiter != Delimiter::Comma {
96            self.write_delimiter()?;
97        }
98
99        self.write_char(']')?;
100
101        if let Some(field_list) = fields {
102            self.write_char('{')?;
103            for (i, field) in field_list.iter().enumerate() {
104                if i > 0 {
105                    self.write_delimiter()?;
106                }
107                self.write_key(field)?;
108            }
109            self.write_char('}')?;
110        }
111
112        self.write_char(':')
113    }
114
115    /// Write an empty array header.
116    pub fn write_empty_array_with_key(&mut self, key: Option<&str>) -> ToonResult<()> {
117        if let Some(k) = key {
118            self.write_key(k)?;
119        }
120        self.write_char('[')?;
121
122        let length_str = self.options.format_length(0);
123        self.write_str(&length_str)?;
124
125        if self.options.delimiter != Delimiter::Comma {
126            self.write_delimiter()?;
127        }
128
129        self.write_char(']')?;
130        self.write_char(':')
131    }
132
133    pub fn needs_quoting(&self, s: &str, context: QuotingContext) -> bool {
134        let delim_char = match context {
135            QuotingContext::ObjectValue => self.get_document_delimiter_char(),
136            QuotingContext::ArrayValue => self.get_active_delimiter_char(),
137        };
138        needs_quoting(s, delim_char)
139    }
140
141    pub fn write_quoted_string(&mut self, s: &str) -> ToonResult<()> {
142        self.write_str(&quote_string(s))
143    }
144
145    pub fn write_value(&mut self, s: &str, context: QuotingContext) -> ToonResult<()> {
146        if self.needs_quoting(s, context) {
147            self.write_quoted_string(s)
148        } else {
149            self.write_str(s)
150        }
151    }
152
153    pub fn push_active_delimiter(&mut self, delim: Delimiter) {
154        self.active_delimiters.push(delim);
155    }
156    pub fn pop_active_delimiter(&mut self) {
157        if self.active_delimiters.len() > 1 {
158            self.active_delimiters.pop();
159        }
160    }
161    fn get_active_delimiter_char(&self) -> char {
162        self.active_delimiters
163            .last()
164            .unwrap_or(&self.options.delimiter)
165            .as_char()
166    }
167
168    fn get_document_delimiter_char(&self) -> char {
169        self.options.delimiter.as_char()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_writer_basic() {
179        let opts = EncodeOptions::default();
180        let mut writer = Writer::new(opts);
181
182        writer.write_str("hello").unwrap();
183        writer.write_str(" ").unwrap();
184        writer.write_str("world").unwrap();
185
186        assert_eq!(writer.finish(), "hello world");
187    }
188
189    #[test]
190    fn test_write_delimiter() {
191        let mut opts = EncodeOptions::default();
192        let mut writer = Writer::new(opts.clone());
193
194        writer.write_str("a").unwrap();
195        writer.write_delimiter().unwrap();
196        writer.write_str("b").unwrap();
197
198        assert_eq!(writer.finish(), "a,b");
199
200        opts = opts.with_delimiter(Delimiter::Pipe);
201        let mut writer = Writer::new(opts);
202
203        writer.write_str("a").unwrap();
204        writer.write_delimiter().unwrap();
205        writer.write_str("b").unwrap();
206
207        assert_eq!(writer.finish(), "a|b");
208    }
209
210    #[test]
211    fn test_write_indent() {
212        let opts = EncodeOptions::default();
213        let mut writer = Writer::new(opts);
214
215        writer.write_indent(0).unwrap();
216        writer.write_str("a").unwrap();
217        writer.write_newline().unwrap();
218
219        writer.write_indent(1).unwrap();
220        writer.write_str("b").unwrap();
221        writer.write_newline().unwrap();
222
223        writer.write_indent(2).unwrap();
224        writer.write_str("c").unwrap();
225
226        assert_eq!(writer.finish(), "a\n  b\n    c");
227    }
228
229    #[test]
230    fn test_write_array_header() {
231        let opts = EncodeOptions::default();
232        let mut writer = Writer::new(opts);
233
234        writer
235            .write_array_header(Some("items"), 3, None, 0)
236            .unwrap();
237        assert_eq!(writer.finish(), "items[3]:");
238
239        let opts = EncodeOptions::default();
240        let mut writer = Writer::new(opts);
241        let fields = vec!["id".to_string(), "name".to_string()];
242
243        writer
244            .write_array_header(Some("users"), 2, Some(&fields), 0)
245            .unwrap();
246        assert_eq!(writer.finish(), "users[2]{id,name}:");
247    }
248
249    #[test]
250    fn test_write_array_header_with_length_marker() {
251        let opts = EncodeOptions::new().with_length_marker('#');
252        let mut writer = Writer::new(opts);
253
254        writer
255            .write_array_header(Some("items"), 3, None, 0)
256            .unwrap();
257        assert_eq!(writer.finish(), "items[#3]:");
258    }
259
260    #[test]
261    fn test_write_array_header_with_pipe_delimiter() {
262        let opts = EncodeOptions::new().with_delimiter(Delimiter::Pipe);
263        let mut writer = Writer::new(opts);
264
265        writer
266            .write_array_header(Some("items"), 3, None, 0)
267            .unwrap();
268        assert_eq!(writer.finish(), "items[3|]:");
269
270        let opts = EncodeOptions::new().with_delimiter(Delimiter::Pipe);
271        let mut writer = Writer::new(opts);
272        let fields = vec!["id".to_string(), "name".to_string()];
273
274        writer
275            .write_array_header(Some("users"), 2, Some(&fields), 0)
276            .unwrap();
277        assert_eq!(writer.finish(), "users[2|]{id|name}:");
278    }
279
280    #[test]
281    fn test_write_key_with_special_chars() {
282        let opts = EncodeOptions::default();
283        let mut writer = Writer::new(opts);
284
285        writer.write_key("normal_key").unwrap();
286        assert_eq!(writer.finish(), "normal_key");
287
288        let opts = EncodeOptions::default();
289        let mut writer = Writer::new(opts);
290
291        writer.write_key("key:with:colons").unwrap();
292        assert_eq!(writer.finish(), "\"key:with:colons\"");
293    }
294
295    #[test]
296    fn test_write_quoted_string() {
297        let opts = EncodeOptions::default();
298        let mut writer = Writer::new(opts);
299
300        writer.write_quoted_string("hello world").unwrap();
301        assert_eq!(writer.finish(), "\"hello world\"");
302
303        let opts = EncodeOptions::default();
304        let mut writer = Writer::new(opts);
305
306        writer.write_quoted_string("say \"hi\"").unwrap();
307        assert_eq!(writer.finish(), r#""say \"hi\"""#);
308    }
309
310    #[test]
311    fn test_needs_quoting() {
312        let opts = EncodeOptions::default();
313        let writer = Writer::new(opts);
314        let ctx = QuotingContext::ObjectValue;
315
316        assert!(!writer.needs_quoting("hello", ctx));
317        assert!(writer.needs_quoting("hello,world", ctx));
318        assert!(writer.needs_quoting("true", ctx));
319        assert!(writer.needs_quoting("false", ctx));
320        assert!(writer.needs_quoting("null", ctx));
321        assert!(writer.needs_quoting("123", ctx));
322        assert!(writer.needs_quoting("", ctx));
323        assert!(writer.needs_quoting("hello:world", ctx));
324    }
325
326    #[test]
327    fn test_write_empty_array() {
328        let opts = EncodeOptions::default();
329        let mut writer = Writer::new(opts);
330
331        writer.write_empty_array_with_key(Some("items")).unwrap();
332        assert_eq!(writer.finish(), "items[0]:");
333    }
334}