519e2812e4
For compatibility with error_chain
611 lines
16 KiB
Rust
611 lines
16 KiB
Rust
use std::fmt::{self, Display};
|
|
use std::convert::From;
|
|
use std::error::Error;
|
|
use yaml::{Hash, Yaml};
|
|
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum EmitError {
|
|
FmtError(fmt::Error),
|
|
BadHashmapKey,
|
|
}
|
|
|
|
impl Error for EmitError {
|
|
fn description(&self) -> &str {
|
|
match *self {
|
|
EmitError::FmtError(ref err) => err.description(),
|
|
EmitError::BadHashmapKey => "bad hashmap key",
|
|
}
|
|
}
|
|
|
|
fn cause(&self) -> Option<&Error> {
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Display for EmitError {
|
|
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
EmitError::FmtError(ref err) => Display::fmt(err, formatter),
|
|
EmitError::BadHashmapKey => formatter.write_str("bad hashmap key"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<fmt::Error> for EmitError {
|
|
fn from(f: fmt::Error) -> Self {
|
|
EmitError::FmtError(f)
|
|
}
|
|
}
|
|
|
|
pub struct YamlEmitter<'a> {
|
|
writer: &'a mut fmt::Write,
|
|
best_indent: usize,
|
|
compact: bool,
|
|
|
|
level: isize,
|
|
}
|
|
|
|
pub type EmitResult = Result<(), EmitError>;
|
|
|
|
// from serialize::json
|
|
fn escape_str(wr: &mut fmt::Write, v: &str) -> Result<(), fmt::Error> {
|
|
try!(wr.write_str("\""));
|
|
|
|
let mut start = 0;
|
|
|
|
for (i, byte) in v.bytes().enumerate() {
|
|
let escaped = match byte {
|
|
b'"' => "\\\"",
|
|
b'\\' => "\\\\",
|
|
b'\x00' => "\\u0000",
|
|
b'\x01' => "\\u0001",
|
|
b'\x02' => "\\u0002",
|
|
b'\x03' => "\\u0003",
|
|
b'\x04' => "\\u0004",
|
|
b'\x05' => "\\u0005",
|
|
b'\x06' => "\\u0006",
|
|
b'\x07' => "\\u0007",
|
|
b'\x08' => "\\b",
|
|
b'\t' => "\\t",
|
|
b'\n' => "\\n",
|
|
b'\x0b' => "\\u000b",
|
|
b'\x0c' => "\\f",
|
|
b'\r' => "\\r",
|
|
b'\x0e' => "\\u000e",
|
|
b'\x0f' => "\\u000f",
|
|
b'\x10' => "\\u0010",
|
|
b'\x11' => "\\u0011",
|
|
b'\x12' => "\\u0012",
|
|
b'\x13' => "\\u0013",
|
|
b'\x14' => "\\u0014",
|
|
b'\x15' => "\\u0015",
|
|
b'\x16' => "\\u0016",
|
|
b'\x17' => "\\u0017",
|
|
b'\x18' => "\\u0018",
|
|
b'\x19' => "\\u0019",
|
|
b'\x1a' => "\\u001a",
|
|
b'\x1b' => "\\u001b",
|
|
b'\x1c' => "\\u001c",
|
|
b'\x1d' => "\\u001d",
|
|
b'\x1e' => "\\u001e",
|
|
b'\x1f' => "\\u001f",
|
|
b'\x7f' => "\\u007f",
|
|
_ => { continue; }
|
|
};
|
|
|
|
if start < i {
|
|
try!(wr.write_str(&v[start..i]));
|
|
}
|
|
|
|
try!(wr.write_str(escaped));
|
|
|
|
start = i + 1;
|
|
}
|
|
|
|
if start != v.len() {
|
|
try!(wr.write_str(&v[start..]));
|
|
}
|
|
|
|
try!(wr.write_str("\""));
|
|
Ok(())
|
|
}
|
|
|
|
impl<'a> YamlEmitter<'a> {
|
|
pub fn new(writer: &'a mut fmt::Write) -> YamlEmitter {
|
|
YamlEmitter {
|
|
writer: writer,
|
|
best_indent: 2,
|
|
compact: true,
|
|
|
|
level: -1
|
|
}
|
|
}
|
|
|
|
/// Set 'compact inline notation' on or off, as described for block
|
|
/// [sequences](http://www.yaml.org/spec/1.2/spec.html#id2797382)
|
|
/// and
|
|
/// [mappings](http://www.yaml.org/spec/1.2/spec.html#id2798057).
|
|
///
|
|
/// In this form, blocks cannot have any properties (such as anchors
|
|
/// or tags), which should be OK, because this emitter doesn't
|
|
/// (currently) emit those anyways.
|
|
pub fn compact(&mut self, compact: bool) {
|
|
self.compact = compact;
|
|
}
|
|
|
|
/// Determine if this emitter is using 'compact inline notation'.
|
|
pub fn is_compact(&self) -> bool {
|
|
self.compact
|
|
}
|
|
|
|
pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
|
|
// write DocumentStart
|
|
try!(write!(self.writer, "---\n"));
|
|
self.level = -1;
|
|
self.emit_node(doc)
|
|
}
|
|
|
|
fn write_indent(&mut self) -> EmitResult {
|
|
if self.level <= 0 { return Ok(()); }
|
|
for _ in 0..self.level {
|
|
for _ in 0..self.best_indent {
|
|
try!(write!(self.writer, " "));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_node(&mut self, node: &Yaml) -> EmitResult {
|
|
match *node {
|
|
Yaml::Array(ref v) => self.emit_array(v),
|
|
Yaml::Hash(ref h) => self.emit_hash(h),
|
|
Yaml::String(ref v) => {
|
|
if need_quotes(v) {
|
|
try!(escape_str(self.writer, v));
|
|
}
|
|
else {
|
|
try!(write!(self.writer, "{}", v));
|
|
}
|
|
Ok(())
|
|
},
|
|
Yaml::Boolean(v) => {
|
|
if v {
|
|
try!(self.writer.write_str("true"));
|
|
} else {
|
|
try!(self.writer.write_str("false"));
|
|
}
|
|
Ok(())
|
|
},
|
|
Yaml::Integer(v) => {
|
|
try!(write!(self.writer, "{}", v));
|
|
Ok(())
|
|
},
|
|
Yaml::Real(ref v) => {
|
|
try!(write!(self.writer, "{}", v));
|
|
Ok(())
|
|
},
|
|
Yaml::Null | Yaml::BadValue => {
|
|
try!(write!(self.writer, "~"));
|
|
Ok(())
|
|
},
|
|
// XXX(chenyh) Alias
|
|
_ => { Ok(()) }
|
|
}
|
|
}
|
|
|
|
fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
|
|
if v.is_empty() {
|
|
try!(write!(self.writer, "[]"));
|
|
} else {
|
|
self.level += 1;
|
|
for (cnt, x) in v.iter().enumerate() {
|
|
if cnt > 0 {
|
|
try!(write!(self.writer, "\n"));
|
|
try!(self.write_indent());
|
|
}
|
|
try!(write!(self.writer, "-"));
|
|
try!(self.emit_val(true, x));
|
|
}
|
|
self.level -= 1;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_hash(&mut self, h: &Hash) -> EmitResult {
|
|
if h.is_empty() {
|
|
try!(self.writer.write_str("{}"));
|
|
} else {
|
|
self.level += 1;
|
|
for (cnt, (k, v)) in h.iter().enumerate() {
|
|
let complex_key = match *k {
|
|
Yaml::Hash(_) | Yaml::Array(_) => true,
|
|
_ => false,
|
|
};
|
|
if cnt > 0 {
|
|
try!(write!(self.writer, "\n"));
|
|
try!(self.write_indent());
|
|
}
|
|
if complex_key {
|
|
try!(write!(self.writer, "?"));
|
|
try!(self.emit_val(true, k));
|
|
try!(write!(self.writer, "\n"));
|
|
try!(self.write_indent());
|
|
try!(write!(self.writer, ":"));
|
|
try!(self.emit_val(true, v));
|
|
} else {
|
|
try!(self.emit_node(k));
|
|
try!(write!(self.writer, ":"));
|
|
try!(self.emit_val(false, v));
|
|
}
|
|
}
|
|
self.level -= 1;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Emit a yaml as a hash or array value: i.e., which should appear
|
|
/// following a ":" or "-", either after a space, or on a new line.
|
|
/// If `inline` is true, then the preceeding characters are distinct
|
|
/// and short enough to respect the compact flag.
|
|
fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
|
|
match *val {
|
|
Yaml::Array(ref v) => {
|
|
if (inline && self.compact) || v.is_empty() {
|
|
try!(write!(self.writer, " "));
|
|
} else {
|
|
try!(write!(self.writer, "\n"));
|
|
self.level += 1;
|
|
try!(self.write_indent());
|
|
self.level -= 1;
|
|
}
|
|
self.emit_array(v)
|
|
},
|
|
Yaml::Hash(ref h) => {
|
|
if (inline && self.compact) || h.is_empty() {
|
|
try!(write!(self.writer, " "));
|
|
} else {
|
|
try!(write!(self.writer, "\n"));
|
|
self.level += 1;
|
|
try!(self.write_indent());
|
|
self.level -= 1;
|
|
}
|
|
self.emit_hash(h)
|
|
},
|
|
_ => {
|
|
try!(write!(self.writer, " "));
|
|
self.emit_node(val)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if the string requires quoting.
|
|
/// Strings containing any of the following characters must be quoted.
|
|
/// :, {, }, [, ], ,, &, *, #, ?, |, -, <, >, =, !, %, @, `
|
|
///
|
|
/// If the string contains any of the following control characters, it must be escaped with double quotes:
|
|
/// \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
|
|
///
|
|
/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
|
|
/// * When the string is true or false (otherwise, it would be treated as a boolean value);
|
|
/// * When the string is null or ~ (otherwise, it would be considered as a null value);
|
|
/// * 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);
|
|
/// * When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).
|
|
fn need_quotes(string: &str) -> bool {
|
|
fn need_quotes_spaces(string: &str) -> bool {
|
|
string.starts_with(' ')
|
|
|| string.ends_with(' ')
|
|
}
|
|
|
|
string == ""
|
|
|| need_quotes_spaces(string)
|
|
|| string.contains(|character: char| {
|
|
match character {
|
|
':' | '{' | '}' | '[' | ']' | ',' | '&' | '*' | '#' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@' | '`' | '\"' | '\'' | '\\' | '\0' ... '\x06' | '\t' | '\n' | '\r' | '\x0e' ... '\x1a' | '\x1c' ... '\x1f' => true,
|
|
_ => false,
|
|
}
|
|
})
|
|
|| [// http://yaml.org/type/bool.html
|
|
"y","Y","yes","Yes","YES","n","N","no","No","NO",
|
|
"True", "TRUE", "true", "False", "FALSE", "false",
|
|
"on","On","ON","off","Off","OFF",
|
|
// http://yaml.org/type/null.html
|
|
"null","Null","NULL", "~"
|
|
].contains(&string)
|
|
|| string.starts_with('.')
|
|
|| string.parse::<i64>().is_ok()
|
|
|| string.parse::<f64>().is_ok()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
use YamlLoader;
|
|
|
|
#[test]
|
|
fn test_emit_simple() {
|
|
let s = "
|
|
# comment
|
|
a0 bb: val
|
|
a1:
|
|
b1: 4
|
|
b2: d
|
|
a2: 4 # i'm comment
|
|
a3: [1, 2, 3]
|
|
a4:
|
|
- [a1, a2]
|
|
- 2
|
|
";
|
|
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
println!("original:\n{}", s);
|
|
println!("emitted:\n{}", writer);
|
|
let docs_new = match YamlLoader::load_from_str(&writer) {
|
|
Ok(y) => y,
|
|
Err(e) => panic!(format!("{}", e))
|
|
};
|
|
let doc_new = &docs_new[0];
|
|
|
|
assert_eq!(doc, doc_new);
|
|
}
|
|
|
|
#[test]
|
|
fn test_emit_complex() {
|
|
let s = r#"
|
|
cataloge:
|
|
product: &coffee { name: Coffee, price: 2.5 , unit: 1l }
|
|
product: &cookies { name: Cookies!, price: 3.40 , unit: 400g}
|
|
|
|
products:
|
|
*coffee:
|
|
amount: 4
|
|
*cookies:
|
|
amount: 4
|
|
[1,2,3,4]:
|
|
array key
|
|
2.4:
|
|
real key
|
|
true:
|
|
bool key
|
|
{}:
|
|
empty hash key
|
|
"#;
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
let docs_new = match YamlLoader::load_from_str(&writer) {
|
|
Ok(y) => y,
|
|
Err(e) => panic!(format!("{}", e))
|
|
};
|
|
let doc_new = &docs_new[0];
|
|
assert_eq!(doc, doc_new);
|
|
}
|
|
|
|
#[test]
|
|
fn test_emit_avoid_quotes() {
|
|
let s = r#"---
|
|
a7: 你好
|
|
boolean: "true"
|
|
boolean2: "false"
|
|
date: "2014-12-31"
|
|
empty_string: ""
|
|
empty_string1: " "
|
|
empty_string2: " a"
|
|
empty_string3: " a "
|
|
exp: "12e7"
|
|
field: ":"
|
|
field2: "{"
|
|
field3: "\\"
|
|
field4: "\n"
|
|
float: "2.6"
|
|
int: "4"
|
|
nullable: "null"
|
|
nullable2: "~"
|
|
products:
|
|
"*coffee":
|
|
amount: 4
|
|
"*cookies":
|
|
amount: 4
|
|
".milk":
|
|
amount: 1
|
|
"2.4": real key
|
|
"[1,2,3,4]": array key
|
|
"true": bool key
|
|
"{}": empty hash key
|
|
x: test
|
|
"y": "can't avoid quoting here"
|
|
z: string with spaces"#;
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
|
|
assert_eq!(s, writer, "actual:\n\n{}\n", writer);
|
|
}
|
|
|
|
#[test]
|
|
fn emit_quoted_bools() {
|
|
let input = r#"---
|
|
string0: yes
|
|
string1: no
|
|
string2: "true"
|
|
string3: "false"
|
|
string4: "~"
|
|
null0: ~
|
|
[true, false]: real_bools
|
|
[True, TRUE, False, FALSE, y,Y,yes,Yes,YES,n,N,no,No,NO,on,On,ON,off,Off,OFF]: false_bools
|
|
bool0: true
|
|
bool1: false"#;
|
|
let expected = r#"---
|
|
string0: "yes"
|
|
string1: "no"
|
|
string2: "true"
|
|
string3: "false"
|
|
string4: "~"
|
|
null0: ~
|
|
? - true
|
|
- false
|
|
: real_bools
|
|
? - "True"
|
|
- "TRUE"
|
|
- "False"
|
|
- "FALSE"
|
|
- "y"
|
|
- "Y"
|
|
- "yes"
|
|
- "Yes"
|
|
- "YES"
|
|
- "n"
|
|
- "N"
|
|
- "no"
|
|
- "No"
|
|
- "NO"
|
|
- "on"
|
|
- "On"
|
|
- "ON"
|
|
- "off"
|
|
- "Off"
|
|
- "OFF"
|
|
: false_bools
|
|
bool0: true
|
|
bool1: false"#;
|
|
|
|
let docs = YamlLoader::load_from_str(&input).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
|
|
assert_eq!(expected, writer, "actual:\n\n{}\n", writer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_and_nested() {
|
|
test_empty_and_nested_flag(false)
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_and_nested_compact() {
|
|
test_empty_and_nested_flag(true)
|
|
}
|
|
|
|
fn test_empty_and_nested_flag(compact: bool) {
|
|
let s = if compact { r#"---
|
|
a:
|
|
b:
|
|
c: hello
|
|
d: {}
|
|
e:
|
|
- f
|
|
- g
|
|
- h: []"# } else { r#"---
|
|
a:
|
|
b:
|
|
c: hello
|
|
d: {}
|
|
e:
|
|
- f
|
|
- g
|
|
-
|
|
h: []"# };
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.compact(compact);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
|
|
assert_eq!(s, writer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nested_arrays() {
|
|
let s = r#"---
|
|
a:
|
|
- b
|
|
- - c
|
|
- d
|
|
- - e
|
|
- f"#;
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
println!("original:\n{}", s);
|
|
println!("emitted:\n{}", writer);
|
|
|
|
assert_eq!(s, writer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deeply_nested_arrays() {
|
|
let s = r#"---
|
|
a:
|
|
- b
|
|
- - c
|
|
- d
|
|
- - e
|
|
- - f
|
|
- - e"#;
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
println!("original:\n{}", s);
|
|
println!("emitted:\n{}", writer);
|
|
|
|
assert_eq!(s, writer);
|
|
}
|
|
|
|
#[test]
|
|
fn test_nested_hashes() {
|
|
let s = r#"---
|
|
a:
|
|
b:
|
|
c:
|
|
d:
|
|
e: f"#;
|
|
|
|
let docs = YamlLoader::load_from_str(&s).unwrap();
|
|
let doc = &docs[0];
|
|
let mut writer = String::new();
|
|
{
|
|
let mut emitter = YamlEmitter::new(&mut writer);
|
|
emitter.dump(doc).unwrap();
|
|
}
|
|
println!("original:\n{}", s);
|
|
println!("emitted:\n{}", writer);
|
|
|
|
assert_eq!(s, writer);
|
|
}
|
|
|
|
}
|