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        self.write_str(&length.to_string())?;
92
93        // Only write delimiter in header if it's not comma (comma is default/implied)
94        if self.options.delimiter != Delimiter::Comma {
95            self.write_delimiter()?;
96        }
97
98        self.write_char(']')?;
99
100        // Write field list for tabular arrays: {field1,field2}
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(
117        &mut self,
118        key: Option<&str>,
119        depth: usize,
120    ) -> ToonResult<()> {
121        if let Some(k) = key {
122            if depth > 0 {
123                self.write_indent(depth)?;
124            }
125            self.write_key(k)?;
126        }
127        self.write_char('[')?;
128        self.write_str("0")?;
129
130        if self.options.delimiter != Delimiter::Comma {
131            self.write_delimiter()?;
132        }
133
134        self.write_char(']')?;
135        self.write_char(':')
136    }
137
138    pub fn needs_quoting(&self, s: &str, context: QuotingContext) -> bool {
139        // Use active delimiter for array values, document delimiter for object values
140        let delim_char = match context {
141            QuotingContext::ObjectValue => self.get_document_delimiter_char(),
142            QuotingContext::ArrayValue => self.get_active_delimiter_char(),
143        };
144        needs_quoting(s, delim_char)
145    }
146
147    pub fn write_quoted_string(&mut self, s: &str) -> ToonResult<()> {
148        self.write_str(&quote_string(s))
149    }
150
151    pub fn write_value(&mut self, s: &str, context: QuotingContext) -> ToonResult<()> {
152        if self.needs_quoting(s, context) {
153            self.write_quoted_string(s)
154        } else {
155            self.write_str(s)
156        }
157    }
158
159    /// Push a new delimiter onto the stack (for nested arrays with different
160    /// delimiters).
161    pub fn push_active_delimiter(&mut self, delim: Delimiter) {
162        self.active_delimiters.push(delim);
163    }
164    /// Pop the active delimiter, keeping at least one (the document default).
165    pub fn pop_active_delimiter(&mut self) {
166        if self.active_delimiters.len() > 1 {
167            self.active_delimiters.pop();
168        }
169    }
170    fn get_active_delimiter_char(&self) -> char {
171        self.active_delimiters
172            .last()
173            .unwrap_or(&self.options.delimiter)
174            .as_char()
175    }
176
177    fn get_document_delimiter_char(&self) -> char {
178        self.options.delimiter.as_char()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_writer_basic() {
188        let opts = EncodeOptions::default();
189        let mut writer = Writer::new(opts);
190
191        writer.write_str("hello").unwrap();
192        writer.write_str(" ").unwrap();
193        writer.write_str("world").unwrap();
194
195        assert_eq!(writer.finish(), "hello world");
196    }
197
198    #[test]
199    fn test_write_delimiter() {
200        let mut opts = EncodeOptions::default();
201        let mut writer = Writer::new(opts.clone());
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        opts = opts.with_delimiter(Delimiter::Pipe);
210        let mut writer = Writer::new(opts);
211
212        writer.write_str("a").unwrap();
213        writer.write_delimiter().unwrap();
214        writer.write_str("b").unwrap();
215
216        assert_eq!(writer.finish(), "a|b");
217    }
218
219    #[test]
220    fn test_write_indent() {
221        let opts = EncodeOptions::default();
222        let mut writer = Writer::new(opts);
223
224        writer.write_indent(0).unwrap();
225        writer.write_str("a").unwrap();
226        writer.write_newline().unwrap();
227
228        writer.write_indent(1).unwrap();
229        writer.write_str("b").unwrap();
230        writer.write_newline().unwrap();
231
232        writer.write_indent(2).unwrap();
233        writer.write_str("c").unwrap();
234
235        assert_eq!(writer.finish(), "a\n  b\n    c");
236    }
237
238    #[test]
239    fn test_write_array_header() {
240        let opts = EncodeOptions::default();
241        let mut writer = Writer::new(opts);
242
243        writer
244            .write_array_header(Some("items"), 3, None, 0)
245            .unwrap();
246        assert_eq!(writer.finish(), "items[3]:");
247
248        let opts = EncodeOptions::default();
249        let mut writer = Writer::new(opts);
250        let fields = vec!["id".to_string(), "name".to_string()];
251
252        writer
253            .write_array_header(Some("users"), 2, Some(&fields), 0)
254            .unwrap();
255        assert_eq!(writer.finish(), "users[2]{id,name}:");
256    }
257
258    #[test]
259    fn test_write_array_header_with_pipe_delimiter() {
260        let opts = EncodeOptions::new().with_delimiter(Delimiter::Pipe);
261        let mut writer = Writer::new(opts);
262
263        writer
264            .write_array_header(Some("items"), 3, None, 0)
265            .unwrap();
266        assert_eq!(writer.finish(), "items[3|]:");
267
268        let opts = EncodeOptions::new().with_delimiter(Delimiter::Pipe);
269        let mut writer = Writer::new(opts);
270        let fields = vec!["id".to_string(), "name".to_string()];
271
272        writer
273            .write_array_header(Some("users"), 2, Some(&fields), 0)
274            .unwrap();
275        assert_eq!(writer.finish(), "users[2|]{id|name}:");
276    }
277
278    #[test]
279    fn test_write_key_with_special_chars() {
280        let opts = EncodeOptions::default();
281        let mut writer = Writer::new(opts);
282
283        writer.write_key("normal_key").unwrap();
284        assert_eq!(writer.finish(), "normal_key");
285
286        let opts = EncodeOptions::default();
287        let mut writer = Writer::new(opts);
288
289        writer.write_key("key:with:colons").unwrap();
290        assert_eq!(writer.finish(), "\"key:with:colons\"");
291    }
292
293    #[test]
294    fn test_write_quoted_string() {
295        let opts = EncodeOptions::default();
296        let mut writer = Writer::new(opts);
297
298        writer.write_quoted_string("hello world").unwrap();
299        assert_eq!(writer.finish(), "\"hello world\"");
300
301        let opts = EncodeOptions::default();
302        let mut writer = Writer::new(opts);
303
304        writer.write_quoted_string("say \"hi\"").unwrap();
305        assert_eq!(writer.finish(), r#""say \"hi\"""#);
306    }
307
308    #[test]
309    fn test_needs_quoting() {
310        let opts = EncodeOptions::default();
311        let writer = Writer::new(opts);
312        let ctx = QuotingContext::ObjectValue;
313
314        assert!(!writer.needs_quoting("hello", ctx));
315        assert!(writer.needs_quoting("hello,world", ctx));
316        assert!(writer.needs_quoting("true", ctx));
317        assert!(writer.needs_quoting("false", ctx));
318        assert!(writer.needs_quoting("null", ctx));
319        assert!(writer.needs_quoting("123", ctx));
320        assert!(writer.needs_quoting("", ctx));
321        assert!(writer.needs_quoting("hello:world", ctx));
322    }
323
324    #[test]
325    fn test_write_empty_array() {
326        let opts = EncodeOptions::default();
327        let mut writer = Writer::new(opts);
328
329        writer.write_empty_array_with_key(Some("items"), 0).unwrap();
330        assert_eq!(writer.finish(), "items[0]:");
331    }
332}