unity_yaml_rust/
emitter.rs

1use std::convert::From;
2use std::error::Error;
3use std::fmt::{self, Display};
4use crate::yaml::{Hash, Yaml};
5
6#[derive(Copy, Clone, Debug)]
7pub enum EmitError {
8    FmtError(fmt::Error),
9    BadHashmapKey,
10}
11
12impl Error for EmitError {
13    fn cause(&self) -> Option<&dyn Error> {
14        None
15    }
16}
17
18impl Display for EmitError {
19    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
20        match *self {
21            EmitError::FmtError(ref err) => Display::fmt(err, formatter),
22            EmitError::BadHashmapKey => formatter.write_str("bad hashmap key"),
23        }
24    }
25}
26
27impl From<fmt::Error> for EmitError {
28    fn from(f: fmt::Error) -> Self {
29        EmitError::FmtError(f)
30    }
31}
32
33pub struct YamlEmitter<'a> {
34    writer: &'a mut dyn fmt::Write,
35    best_indent: usize,
36    compact: bool,
37    bad_value: Option<&'a str>,
38    level: isize,
39    written: bool,
40}
41
42pub type EmitResult = Result<(), EmitError>;
43
44// from serialize::json
45fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
46    wr.write_str("\"")?;
47
48    let mut start = 0;
49
50    for (i, byte) in v.bytes().enumerate() {
51        let escaped = match byte {
52            b'"' => "\\\"",
53            b'\\' => "\\\\",
54            b'\x00' => "\\u0000",
55            b'\x01' => "\\u0001",
56            b'\x02' => "\\u0002",
57            b'\x03' => "\\u0003",
58            b'\x04' => "\\u0004",
59            b'\x05' => "\\u0005",
60            b'\x06' => "\\u0006",
61            b'\x07' => "\\u0007",
62            b'\x08' => "\\b",
63            b'\t' => "\\t",
64            b'\n' => "\\n",
65            b'\x0b' => "\\u000b",
66            b'\x0c' => "\\f",
67            b'\r' => "\\r",
68            b'\x0e' => "\\u000e",
69            b'\x0f' => "\\u000f",
70            b'\x10' => "\\u0010",
71            b'\x11' => "\\u0011",
72            b'\x12' => "\\u0012",
73            b'\x13' => "\\u0013",
74            b'\x14' => "\\u0014",
75            b'\x15' => "\\u0015",
76            b'\x16' => "\\u0016",
77            b'\x17' => "\\u0017",
78            b'\x18' => "\\u0018",
79            b'\x19' => "\\u0019",
80            b'\x1a' => "\\u001a",
81            b'\x1b' => "\\u001b",
82            b'\x1c' => "\\u001c",
83            b'\x1d' => "\\u001d",
84            b'\x1e' => "\\u001e",
85            b'\x1f' => "\\u001f",
86            b'\x7f' => "\\u007f",
87            _ => continue,
88        };
89
90        if start < i {
91            wr.write_str(&v[start..i])?;
92        }
93
94        wr.write_str(escaped)?;
95
96        start = i + 1;
97    }
98
99    if start != v.len() {
100        wr.write_str(&v[start..])?;
101    }
102
103    wr.write_str("\"")?;
104    Ok(())
105}
106
107impl<'a> YamlEmitter<'a> {
108    pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter {
109        YamlEmitter {
110            writer,
111            best_indent: 2,
112            compact: true,
113            level: -1,
114            written: false,
115            bad_value: None,
116        }
117    }
118
119    /// Set 'compact inline notation' on or off, as described for block
120    /// [sequences](http://www.yaml.org/spec/1.2/spec.html#id2797382)
121    /// and
122    /// [mappings](http://www.yaml.org/spec/1.2/spec.html#id2798057).
123    ///
124    /// In this form, blocks cannot have any properties (such as anchors
125    /// or tags), which should be OK, because this emitter doesn't
126    /// (currently) emit those anyways.
127    pub fn compact(&mut self, compact: bool) {
128        self.compact = compact;
129    }
130
131    /// Determine if this emitter is using 'compact inline notation'.
132    pub fn is_compact(&self) -> bool {
133        self.compact
134    }
135
136    /// Set description for Null and BadValue
137    pub fn bad_value(&mut self, value: &'a str) {
138        self.bad_value = Some(value);
139    }
140
141    pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
142        if self.written {
143            writeln!(self.writer)?;
144        }
145        self.level = -1;
146        self.emit_node(doc)?;
147        self.written = true;
148        Ok(())
149    }
150
151    fn write_indent(&mut self) -> EmitResult {
152        if self.level <= 0 {
153            return Ok(());
154        }
155        for _ in 0..self.level {
156            for _ in 0..self.best_indent {
157                write!(self.writer, " ")?;
158            }
159        }
160        Ok(())
161    }
162
163    fn emit_node(&mut self, node: &Yaml) -> EmitResult {
164        match *node {
165            Yaml::Array(ref v) => self.emit_array(v),
166            Yaml::Hash(ref h) => self.emit_hash(h),
167            Yaml::String(ref v) => {
168                if need_quotes(v) {
169                    escape_str(self.writer, v)?;
170                } else {
171                    write!(self.writer, "{}", v)?;
172                }
173                Ok(())
174            }
175            Yaml::Boolean(v) => {
176                if v {
177                    self.writer.write_str("true")?;
178                } else {
179                    self.writer.write_str("false")?;
180                }
181                Ok(())
182            }
183            Yaml::Integer(v) => {
184                write!(self.writer, "{}", v)?;
185                Ok(())
186            }
187            Yaml::Real(ref v) => {
188                write!(self.writer, "{}", v)?;
189                Ok(())
190            }
191            Yaml::Null | Yaml::BadValue => {
192                write!(self.writer, "{}", self.bad_value.unwrap_or(""))?;
193                Ok(())
194            }
195            Yaml::Original(ref v) => {
196                write!(self.writer, "{}", v)?;
197                Ok(())
198            }
199            // XXX(chenyh) Alias
200            _ => Ok(()),
201        }
202    }
203
204    fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
205        if v.is_empty() {
206            write!(self.writer, "[]")?;
207        } else {
208            self.level += 1;
209            for (cnt, x) in v.iter().enumerate() {
210                if cnt > 0 {
211                    writeln!(self.writer)?;
212                    self.write_indent()?;
213                }
214                write!(self.writer, "-")?;
215                self.emit_val(true, x)?;
216            }
217            self.level -= 1;
218        }
219        Ok(())
220    }
221
222    fn emit_hash(&mut self, h: &Hash) -> EmitResult {
223        if h.is_empty() {
224            self.writer.write_str("{}")?;
225        } else {
226            self.level += 1;
227            if !h.block {
228                write!(self.writer, "{{")?;
229            }
230            for (cnt, (k, v)) in h.iter().enumerate() {
231                let complex_key = matches!(*k, Yaml::Hash(_) | Yaml::Array(_));
232                if cnt > 0 {
233                    if h.block {
234                        writeln!(self.writer)?;
235                        self.write_indent()?;
236                    } else {
237                        write!(self.writer, ", ")?;
238                    } 
239                }
240                if complex_key {
241                    write!(self.writer, "?")?;
242                    self.emit_val(true, k)?;
243                    writeln!(self.writer)?;
244                    self.write_indent()?;
245                    write!(self.writer, ":")?;
246                    self.emit_val(true, v)?;
247                } else {
248                    self.emit_node(k)?;
249                    write!(self.writer, ":")?;
250                    self.emit_val(false, v)?;
251                }
252            }
253            if !h.block {
254                write!(self.writer, "}}")?;
255            }
256            self.level -= 1;
257        }
258        Ok(())
259    }
260
261    /// Emit a yaml as a hash or array value: i.e., which should appear
262    /// following a ":" or "-", either after a space, or on a new line.
263    /// If `inline` is true, then the preceding characters are distinct
264    /// and short enough to respect the compact flag.
265    fn emit_val(&mut self, mut inline: bool, val: &Yaml) -> EmitResult {
266        match *val {
267            Yaml::Array(ref v) => {
268                if (inline && self.compact) || v.is_empty() {
269                    write!(self.writer, " ")?;
270                } else {
271                    writeln!(self.writer)?;
272                    self.level += 1;
273                    self.write_indent()?;
274                    self.level -= 1;
275                }
276                self.emit_array(v)
277            }
278            Yaml::Hash(ref h) => {
279                inline = inline || !h.block;
280                if (inline && self.compact) || h.is_empty() {
281                    write!(self.writer, " ")?;
282                } else {
283                    writeln!(self.writer)?;
284                    self.level += 1;
285                    self.write_indent()?;
286                    self.level -= 1;
287                }
288                self.emit_hash(h)
289            }
290            _ => {
291                write!(self.writer, " ")?;
292                self.emit_node(val)
293            }
294        }
295    }
296}
297
298/// Check if the string requires quoting.
299/// Strings starting with any of the following characters must be quoted.
300/// :, &, *, ?, |, -, <, >, =, !, %, @
301/// Strings containing any of the following characters must be quoted.
302/// {, }, [, ], ,, #, `
303///
304/// If the string contains any of the following control characters, it must be escaped with double quotes:
305/// \0, \x01, \x02, \x03, \x04, \x05, \x06, \a, \b, \t, \n, \v, \f, \r, \x0e, \x0f, \x10, \x11, \x12, \x13, \x14, \x15, \x16, \x17, \x18, \x19, \x1a, \e, \x1c, \x1d, \x1e, \x1f, \N, \_, \L, \P
306///
307/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
308/// * When the string is true or false (otherwise, it would be treated as a boolean value);
309/// * When the string is null or ~ (otherwise, it would be considered as a null value);
310/// * When the string looks like a number, such as integers (e.g. 2, 14, etc.), floats (e.g. 2.6, 14.9) and exponential numbers (e.g. 12e7, etc.) (otherwise, it would be treated as a numeric value);
311/// * When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).
312fn need_quotes(string: &str) -> bool {
313    fn need_quotes_spaces(string: &str) -> bool {
314        string.starts_with(' ') || string.ends_with(' ')
315    }
316
317    string.is_empty()
318        || need_quotes_spaces(string)
319        || string.starts_with(|character: char| matches!(character, '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'))
320        || string.contains(|character: char| matches!(character, ':'
321            | '{'
322            | '}'
323            | '['
324            | ']'
325            | ','
326            | '#'
327            | '`'
328            | '\"'
329            | '\''
330            | '\\'
331            | '\0'..='\x06'
332            | '\t'
333            | '\n'
334            | '\r'
335            | '\x0e'..='\x1a'
336            | '\x1c'..='\x1f'))
337        || [
338            // http://yaml.org/type/bool.html
339            // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse
340            // them as string, not booleans, although it is violating the YAML 1.1 specification.
341            // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088.
342            "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE",
343            "false", "on", "On", "ON", "off", "Off", "OFF",
344            // http://yaml.org/type/null.html
345            "null", "Null", "NULL", "~",
346        ]
347        .contains(&string)
348        || string.starts_with('.')
349        || string.starts_with("0x")
350        || string.parse::<i64>().is_ok()
351        || string.parse::<f64>().is_ok()
352}
353
354#[cfg(test)]
355mod test {
356    use super::*;
357    use crate::YamlLoader;
358
359    #[test]
360    fn test_emit_simple() {
361        let s = "
362# comment
363a0 bb: val
364a1:
365    b1: 4
366    b2: d
367a2: 4 # i'm comment
368a3: [1, 2, 3]
369a4:
370    - [a1, a2]
371    - 2
372";
373
374        let docs = YamlLoader::load_from_str(s).unwrap();
375        let doc = &docs[0];
376        let mut writer = String::new();
377        {
378            let mut emitter = YamlEmitter::new(&mut writer);
379            emitter.dump(doc).unwrap();
380        }
381        println!("original:\n{}", s);
382        println!("emitted:\n{}", writer);
383        let docs_new = match YamlLoader::load_from_str(&writer) {
384            Ok(y) => y,
385            Err(e) => panic!("{}", e),
386        };
387        let doc_new = &docs_new[0];
388
389        assert_eq!(doc, doc_new);
390    }
391
392    #[test]
393    fn test_emit_complex() {
394        let s = r#"
395cataloge:
396  product: &coffee   { name: Coffee,    price: 2.5  ,  unit: 1l  }
397  product: &cookies  { name: Cookies!,  price: 3.40 ,  unit: 400g}
398
399products:
400  *coffee:
401    amount: 4
402  *cookies:
403    amount: 4
404  [1,2,3,4]:
405    array key
406  2.4:
407    real key
408  true:
409    bool key
410  {}:
411    empty hash key
412            "#;
413        let docs = YamlLoader::load_from_str(s).unwrap();
414        let doc = &docs[0];
415        let mut writer = String::new();
416        {
417            let mut emitter = YamlEmitter::new(&mut writer);
418            emitter.dump(doc).unwrap();
419        }
420        let docs_new = match YamlLoader::load_from_str(&writer) {
421            Ok(y) => y,
422            Err(e) => panic!("{}", e),
423        };
424        let doc_new = &docs_new[0];
425        assert_eq!(doc, doc_new);
426    }
427
428    #[test]
429    fn test_emit_avoid_quotes() {
430        let s = r#"---
431a7: 你好
432boolean: "true"
433boolean2: "false"
434date: 2014-12-31
435empty_string: ""
436empty_string1: " "
437empty_string2: "    a"
438empty_string3: "    a "
439exp: "12e7"
440field: ":"
441field2: "{"
442field3: "\\"
443field4: "\n"
444field5: "can't avoid quote"
445float: "2.6"
446int: "4"
447nullable: "null"
448nullable2: "~"
449products:
450  "*coffee":
451    amount: 4
452  "*cookies":
453    amount: 4
454  ".milk":
455    amount: 1
456  "2.4": real key
457  "[1,2,3,4]": array key
458  "true": bool key
459  "{}": empty hash key
460x: test
461y: avoid quoting here
462z: string with spaces"#;
463
464        let docs = YamlLoader::load_from_str(s).unwrap();
465        let doc = &docs[0];
466        let mut writer = String::new();
467        {
468            let mut emitter = YamlEmitter::new(&mut writer);
469            emitter.dump(doc).unwrap();
470        }
471
472        assert_eq!(s, writer, "actual:\n\n{}\n", writer);
473    }
474
475    #[test]
476    fn emit_quoted_bools() {
477        let input = r#"---
478string0: yes
479string1: no
480string2: "true"
481string3: "false"
482string4: "~"
483null0: ~
484[true, false]: real_bools
485[True, TRUE, False, FALSE, y,Y,yes,Yes,YES,n,N,no,No,NO,on,On,ON,off,Off,OFF]: false_bools
486bool0: true
487bool1: false"#;
488        let expected = r#"---
489string0: "yes"
490string1: "no"
491string2: "true"
492string3: "false"
493string4: "~"
494null0: ~
495? - true
496  - false
497: real_bools
498? - "True"
499  - "TRUE"
500  - "False"
501  - "FALSE"
502  - y
503  - Y
504  - "yes"
505  - "Yes"
506  - "YES"
507  - n
508  - N
509  - "no"
510  - "No"
511  - "NO"
512  - "on"
513  - "On"
514  - "ON"
515  - "off"
516  - "Off"
517  - "OFF"
518: false_bools
519bool0: true
520bool1: false"#;
521
522        let docs = YamlLoader::load_from_str(input).unwrap();
523        let doc = &docs[0];
524        let mut writer = String::new();
525        {
526            let mut emitter = YamlEmitter::new(&mut writer);
527            emitter.dump(doc).unwrap();
528        }
529
530        assert_eq!(
531            expected, writer,
532            "expected:\n{}\nactual:\n{}\n",
533            expected, writer
534        );
535    }
536
537    #[test]
538    fn test_empty_and_nested() {
539        test_empty_and_nested_flag(false)
540    }
541
542    #[test]
543    fn test_empty_and_nested_compact() {
544        test_empty_and_nested_flag(true)
545    }
546
547    fn test_empty_and_nested_flag(compact: bool) {
548        let s = if compact {
549            r#"---
550a:
551  b:
552    c: hello
553  d: {}
554e:
555  - f
556  - g
557  - h: []"#
558        } else {
559            r#"---
560a:
561  b:
562    c: hello
563  d: {}
564e:
565  - f
566  - g
567  -
568    h: []"#
569        };
570
571        let docs = YamlLoader::load_from_str(s).unwrap();
572        let doc = &docs[0];
573        let mut writer = String::new();
574        {
575            let mut emitter = YamlEmitter::new(&mut writer);
576            emitter.compact(compact);
577            emitter.dump(doc).unwrap();
578        }
579
580        assert_eq!(s, writer);
581    }
582
583    #[test]
584    fn test_nested_arrays() {
585        let s = r#"---
586a:
587  - b
588  - - c
589    - d
590    - - e
591      - f"#;
592
593        let docs = YamlLoader::load_from_str(s).unwrap();
594        let doc = &docs[0];
595        let mut writer = String::new();
596        {
597            let mut emitter = YamlEmitter::new(&mut writer);
598            emitter.dump(doc).unwrap();
599        }
600        println!("original:\n{}", s);
601        println!("emitted:\n{}", writer);
602
603        assert_eq!(s, writer);
604    }
605
606    #[test]
607    fn test_deeply_nested_arrays() {
608        let s = r#"---
609a:
610  - b
611  - - c
612    - d
613    - - e
614      - - f
615      - - e"#;
616
617        let docs = YamlLoader::load_from_str(s).unwrap();
618        let doc = &docs[0];
619        let mut writer = String::new();
620        {
621            let mut emitter = YamlEmitter::new(&mut writer);
622            emitter.dump(doc).unwrap();
623        }
624        println!("original:\n{}", s);
625        println!("emitted:\n{}", writer);
626
627        assert_eq!(s, writer);
628    }
629
630    #[test]
631    fn test_nested_hashes() {
632        let s = r#"---
633a:
634  b:
635    c:
636      d:
637        e: f"#;
638
639        let docs = YamlLoader::load_from_str(s).unwrap();
640        let doc = &docs[0];
641        let mut writer = String::new();
642        {
643            let mut emitter = YamlEmitter::new(&mut writer);
644            emitter.dump(doc).unwrap();
645        }
646        println!("original:\n{}", s);
647        println!("emitted:\n{}", writer);
648
649        assert_eq!(s, writer);
650    }
651
652}