From f66d8bcf33530c858a502bfa170f2383a8cbc204 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Sun, 29 Jan 2017 16:53:20 -0800 Subject: Rewrite crate with serde support from ground up This commit completely rewrites this crate from the ground up, supporting serde at the lowest levels as I believe serde support was intended to do. This is a major change from the previous versions of this crate, with a summary of changes being: * Serialization directly to TOML is now supported without going through a `Value` first. * Deserialization directly from TOML is now supported without going through a `Value`. Note that due to the TOML format some values still are buffered in intermediate memory, but overall this should be at a minimum now. * The API of `Value` was overhauled to match the API of `serde_json::Value`. The changes here were to: * Add `is_*` accessors * Add `get` and `get_mut` for one-field lookups. * Implement panicking lookups through `Index` The old `index` methods are now gone in favor of `get` and `Index` implementations. * A `Datetime` type has been added to represent a TOML datetime in a first-class fashion. Currently this type provides no accessors other than a `Display` implementation, but the idea is that this will grow support over time for decomposing the date. * Support for the `rustc-serialize` crate has been dropped, that'll stay on the 0.2 and 0.1 release trains. * This crate no longer supports the detection of unused fields, for that though you can use the `serde_ignored` crate on crates.io --- tests/datetime.rs | 58 ++++ tests/display.rs | 97 ++++++ tests/formatting.rs | 21 +- tests/invalid-misc.rs | 12 + tests/invalid.rs | 16 +- tests/invalid/datetime-malformed-no-z.toml | 1 - tests/parser.rs | 495 ++++++++++++++++++++++++++++ tests/serde.rs | 496 +++++++++++++++++++++++++++++ tests/valid.rs | 68 ++-- 9 files changed, 1205 insertions(+), 59 deletions(-) create mode 100644 tests/datetime.rs create mode 100644 tests/display.rs create mode 100644 tests/invalid-misc.rs delete mode 100644 tests/invalid/datetime-malformed-no-z.toml create mode 100644 tests/parser.rs create mode 100644 tests/serde.rs (limited to 'tests') diff --git a/tests/datetime.rs b/tests/datetime.rs new file mode 100644 index 0000000..948e863 --- /dev/null +++ b/tests/datetime.rs @@ -0,0 +1,58 @@ +extern crate toml; + +use std::str::FromStr; + +use toml::Value; + +#[test] +fn times() { + fn good(s: &str) { + let to_parse = format!("foo = {}", s); + let value = Value::from_str(&to_parse).unwrap(); + assert_eq!(value["foo"].as_datetime().unwrap().to_string(), s); + } + + good("1997-09-09T09:09:09Z"); + good("1997-09-09T09:09:09+09:09"); + good("1997-09-09T09:09:09-09:09"); + good("1997-09-09T09:09:09"); + good("1997-09-09"); + good("09:09:09"); + good("1997-09-09T09:09:09.09Z"); + good("1997-09-09T09:09:09.09+09:09"); + good("1997-09-09T09:09:09.09-09:09"); + good("1997-09-09T09:09:09.09"); + good("09:09:09.09"); +} + +#[test] +fn bad_times() { + fn bad(s: &str) { + let to_parse = format!("foo = {}", s); + assert!(Value::from_str(&to_parse).is_err()); + } + + bad("199-09-09"); + bad("199709-09"); + bad("1997-9-09"); + bad("1997-09-9"); + bad("1997-09-0909:09:09"); + bad("1997-09-09T09:09:09."); + bad("T"); + bad("T."); + bad("TZ"); + bad("1997-09-09T09:09:09.09+"); + bad("1997-09-09T09:09:09.09+09"); + bad("1997-09-09T09:09:09.09+09:9"); + bad("1997-09-09T09:09:09.09+0909"); + bad("1997-09-09T09:09:09.09-"); + bad("1997-09-09T09:09:09.09-09"); + bad("1997-09-09T09:09:09.09-09:9"); + bad("1997-09-09T09:09:09.09-0909"); + + bad("1997-00-09T09:09:09.09Z"); + bad("1997-09-00T09:09:09.09Z"); + bad("1997-09-09T30:09:09.09Z"); + bad("1997-09-09T12:69:09.09Z"); + bad("1997-09-09T12:09:69.09Z"); +} diff --git a/tests/display.rs b/tests/display.rs new file mode 100644 index 0000000..c38355a --- /dev/null +++ b/tests/display.rs @@ -0,0 +1,97 @@ +extern crate toml; + +use std::collections::BTreeMap; + +use toml::Value::{String, Integer, Float, Boolean, Array, Table}; + +macro_rules! map( ($($k:expr => $v:expr),*) => ({ + let mut _m = BTreeMap::new(); + $(_m.insert($k.to_string(), $v);)* + _m +}) ); + +#[test] +fn simple_show() { + assert_eq!(String("foo".to_string()).to_string(), + "\"foo\""); + assert_eq!(Integer(10).to_string(), + "10"); + assert_eq!(Float(10.0).to_string(), + "10.0"); + assert_eq!(Float(2.4).to_string(), + "2.4"); + assert_eq!(Boolean(true).to_string(), + "true"); + assert_eq!(Array(vec![]).to_string(), + "[]"); + assert_eq!(Array(vec![Integer(1), Integer(2)]).to_string(), + "[1, 2]"); +} + +#[test] +fn table() { + assert_eq!(Table(map! { }).to_string(), + ""); + assert_eq!(Table(map! { + "test" => Integer(2), + "test2" => Integer(3) }).to_string(), + "test = 2\ntest2 = 3\n"); + assert_eq!(Table(map! { + "test" => Integer(2), + "test2" => Table(map! { + "test" => String("wut".to_string()) + }) + }).to_string(), + "test = 2\n\ + \n\ + [test2]\n\ + test = \"wut\"\n"); + assert_eq!(Table(map! { + "test" => Integer(2), + "test2" => Table(map! { + "test" => String("wut".to_string()) + }) + }).to_string(), + "test = 2\n\ + \n\ + [test2]\n\ + test = \"wut\"\n"); + assert_eq!(Table(map! { + "test" => Integer(2), + "test2" => Array(vec![Table(map! { + "test" => String("wut".to_string()) + })]) + }).to_string(), + "test = 2\n\ + \n\ + [[test2]]\n\ + test = \"wut\"\n"); + assert_eq!(Table(map! { + "foo.bar" => Integer(2), + "foo\"bar" => Integer(2) + }).to_string(), + "\"foo\\\"bar\" = 2\n\ + \"foo.bar\" = 2\n"); + assert_eq!(Table(map! { + "test" => Integer(2), + "test2" => Array(vec![Table(map! { + "test" => Array(vec![Integer(2)]) + })]) + }).to_string(), + "test = 2\n\ + \n\ + [[test2]]\n\ + test = [2]\n"); + let table = Table(map! { + "test" => Integer(2), + "test2" => Array(vec![Table(map! { + "test" => Array(vec![Array(vec![Integer(2), Integer(3)]), + Array(vec![String("foo".to_string()), String("bar".to_string())])]) + })]) + }); + assert_eq!(table.to_string(), + "test = 2\n\ + \n\ + [[test2]]\n\ + test = [[2, 3], [\"foo\", \"bar\"]]\n"); +} diff --git a/tests/formatting.rs b/tests/formatting.rs index b8f4082..10fb165 100644 --- a/tests/formatting.rs +++ b/tests/formatting.rs @@ -1,19 +1,22 @@ -extern crate rustc_serialize; +extern crate serde; +#[macro_use] +extern crate serde_derive; extern crate toml; -use toml::encode_str; -#[derive(Debug, Clone, Hash, PartialEq, Eq, RustcEncodable, RustcDecodable)] +use toml::to_string; + +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] struct User { pub name: String, pub surname: String, } -#[derive(Debug, Clone, Hash, PartialEq, Eq, RustcEncodable, RustcDecodable)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] struct Users { pub user: Vec, } -#[derive(Debug, Clone, Hash, PartialEq, Eq, RustcEncodable, RustcDecodable)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] struct TwoUsers { pub user0: User, pub user1: User, @@ -21,7 +24,7 @@ struct TwoUsers { #[test] fn no_unnecessary_newlines_array() { - assert!(!encode_str(&Users { + assert!(!to_string(&Users { user: vec![ User { name: "John".to_string(), @@ -32,13 +35,13 @@ fn no_unnecessary_newlines_array() { surname: "Dough".to_string(), }, ], - }) + }).unwrap() .starts_with("\n")); } #[test] fn no_unnecessary_newlines_table() { - assert!(!encode_str(&TwoUsers { + assert!(!to_string(&TwoUsers { user0: User { name: "John".to_string(), surname: "Doe".to_string(), @@ -47,6 +50,6 @@ fn no_unnecessary_newlines_table() { name: "Jane".to_string(), surname: "Dough".to_string(), }, - }) + }).unwrap() .starts_with("\n")); } diff --git a/tests/invalid-misc.rs b/tests/invalid-misc.rs new file mode 100644 index 0000000..e8360fd --- /dev/null +++ b/tests/invalid-misc.rs @@ -0,0 +1,12 @@ +extern crate toml; + +#[test] +fn bad() { + fn bad(s: &str) { + assert!(s.parse::().is_err()); + } + + bad("a = 01"); + bad("a = 1__1"); + bad("a = 1_"); +} diff --git a/tests/invalid.rs b/tests/invalid.rs index 63e4de8..4679684 100644 --- a/tests/invalid.rs +++ b/tests/invalid.rs @@ -1,17 +1,9 @@ extern crate toml; -use toml::{Parser}; - fn run(toml: &str) { - let mut p = Parser::new(toml); - let table = p.parse(); - assert!(table.is_none()); - assert!(p.errors.len() > 0); - - // test Parser::to_linecol with the generated error offsets - for error in &p.errors { - p.to_linecol(error.lo); - p.to_linecol(error.hi); + println!("test if invalid:\n{}", toml); + if let Ok(e) = toml.parse::() { + panic!("parsed to: {:#?}", e); } } @@ -32,8 +24,6 @@ test!(datetime_malformed_no_secs, include_str!("invalid/datetime-malformed-no-secs.toml")); test!(datetime_malformed_no_t, include_str!("invalid/datetime-malformed-no-t.toml")); -test!(datetime_malformed_no_z, - include_str!("invalid/datetime-malformed-no-z.toml")); test!(datetime_malformed_with_milli, include_str!("invalid/datetime-malformed-with-milli.toml")); test!(duplicate_keys, diff --git a/tests/invalid/datetime-malformed-no-z.toml b/tests/invalid/datetime-malformed-no-z.toml deleted file mode 100644 index cf66b1e..0000000 --- a/tests/invalid/datetime-malformed-no-z.toml +++ /dev/null @@ -1 +0,0 @@ -no-z = 1987-07-05T17:45:00 diff --git a/tests/parser.rs b/tests/parser.rs new file mode 100644 index 0000000..2db2cfb --- /dev/null +++ b/tests/parser.rs @@ -0,0 +1,495 @@ +extern crate toml; + +use toml::Value; + +macro_rules! bad { + ($s:expr, $msg:expr) => ({ + match $s.parse::() { + Ok(s) => panic!("successfully parsed as {}", s), + Err(e) => { + let e = e.to_string(); + assert!(e.contains($msg), "error: {}", e); + } + } + }) +} + +#[test] +fn crlf() { + "\ +[project]\r\n\ +\r\n\ +name = \"splay\"\r\n\ +version = \"0.1.0\"\r\n\ +authors = [\"alex@crichton.co\"]\r\n\ +\r\n\ +[[lib]]\r\n\ +\r\n\ +path = \"lib.rs\"\r\n\ +name = \"splay\"\r\n\ +description = \"\"\"\ +A Rust implementation of a TAR file reader and writer. This library does not\r\n\ +currently handle compression, but it is abstract over all I/O readers and\r\n\ +writers. Additionally, great lengths are taken to ensure that the entire\r\n\ +contents are never required to be entirely resident in memory all at once.\r\n\ +\"\"\"\ +".parse::().unwrap(); +} + +#[test] +fn fun_with_strings() { + let table = r#" +bar = "\U00000000" +key1 = "One\nTwo" +key2 = """One\nTwo""" +key3 = """ +One +Two""" + +key4 = "The quick brown fox jumps over the lazy dog." +key5 = """ +The quick brown \ + + +fox jumps over \ +the lazy dog.""" +key6 = """\ + The quick brown \ + fox jumps over \ + the lazy dog.\ + """ +# What you see is what you get. +winpath = 'C:\Users\nodejs\templates' +winpath2 = '\\ServerX\admin$\system32\' +quoted = 'Tom "Dubs" Preston-Werner' +regex = '<\i\c*\s*>' + +regex2 = '''I [dw]on't need \d{2} apples''' +lines = ''' +The first newline is +trimmed in raw strings. +All other whitespace +is preserved. +''' +"#.parse::().unwrap(); + assert_eq!(table["bar"].as_str(), Some("\0")); + assert_eq!(table["key1"].as_str(), Some("One\nTwo")); + assert_eq!(table["key2"].as_str(), Some("One\nTwo")); + assert_eq!(table["key3"].as_str(), Some("One\nTwo")); + + let msg = "The quick brown fox jumps over the lazy dog."; + assert_eq!(table["key4"].as_str(), Some(msg)); + assert_eq!(table["key5"].as_str(), Some(msg)); + assert_eq!(table["key6"].as_str(), Some(msg)); + + assert_eq!(table["winpath"].as_str(), Some(r"C:\Users\nodejs\templates")); + assert_eq!(table["winpath2"].as_str(), Some(r"\\ServerX\admin$\system32\")); + assert_eq!(table["quoted"].as_str(), Some(r#"Tom "Dubs" Preston-Werner"#)); + assert_eq!(table["regex"].as_str(), Some(r"<\i\c*\s*>")); + assert_eq!(table["regex2"].as_str(), Some(r"I [dw]on't need \d{2} apples")); + assert_eq!(table["lines"].as_str(), + Some("The first newline is\n\ + trimmed in raw strings.\n\ + All other whitespace\n\ + is preserved.\n")); +} + +#[test] +fn tables_in_arrays() { + let table = r#" +[[foo]] +#… +[foo.bar] +#… + +[[foo]] # ... +#… +[foo.bar] +#... +"#.parse::().unwrap(); + table["foo"][0]["bar"].as_table().unwrap(); + table["foo"][1]["bar"].as_table().unwrap(); +} + +#[test] +fn empty_table() { + let table = r#" +[foo]"#.parse::().unwrap(); + table["foo"].as_table().unwrap(); +} + +#[test] +fn fruit() { + let table = r#" +[[fruit]] +name = "apple" + +[fruit.physical] +color = "red" +shape = "round" + +[[fruit.variety]] +name = "red delicious" + +[[fruit.variety]] +name = "granny smith" + +[[fruit]] +name = "banana" + +[[fruit.variety]] +name = "plantain" +"#.parse::().unwrap(); + assert_eq!(table["fruit"][0]["name"].as_str(), Some("apple")); + assert_eq!(table["fruit"][0]["physical"]["color"].as_str(), Some("red")); + assert_eq!(table["fruit"][0]["physical"]["shape"].as_str(), Some("round")); + assert_eq!(table["fruit"][0]["variety"][0]["name"].as_str(), Some("red delicious")); + assert_eq!(table["fruit"][0]["variety"][1]["name"].as_str(), Some("granny smith")); + assert_eq!(table["fruit"][1]["name"].as_str(), Some("banana")); + assert_eq!(table["fruit"][1]["variety"][0]["name"].as_str(), Some("plantain")); +} + +#[test] +fn stray_cr() { + "\r".parse::().unwrap_err(); + "a = [ \r ]".parse::().unwrap_err(); + "a = \"\"\"\r\"\"\"".parse::().unwrap_err(); + "a = \"\"\"\\ \r \"\"\"".parse::().unwrap_err(); + "a = '''\r'''".parse::().unwrap_err(); + "a = '\r'".parse::().unwrap_err(); + "a = \"\r\"".parse::().unwrap_err(); +} + +#[test] +fn blank_literal_string() { + let table = "foo = ''".parse::().unwrap(); + assert_eq!(table["foo"].as_str(), Some("")); +} + +#[test] +fn many_blank() { + let table = "foo = \"\"\"\n\n\n\"\"\"".parse::().unwrap(); + assert_eq!(table["foo"].as_str(), Some("\n\n")); +} + +#[test] +fn literal_eats_crlf() { + let table = " + foo = \"\"\"\\\r\n\"\"\" + bar = \"\"\"\\\r\n \r\n \r\n a\"\"\" + ".parse::().unwrap(); + assert_eq!(table["foo"].as_str(), Some("")); + assert_eq!(table["bar"].as_str(), Some("a")); +} + +#[test] +fn string_no_newline() { + "a = \"\n\"".parse::().unwrap_err(); + "a = '\n'".parse::().unwrap_err(); +} + +#[test] +fn bad_leading_zeros() { + "a = 00".parse::().unwrap_err(); + "a = -00".parse::().unwrap_err(); + "a = +00".parse::().unwrap_err(); + "a = 00.0".parse::().unwrap_err(); + "a = -00.0".parse::().unwrap_err(); + "a = +00.0".parse::().unwrap_err(); + "a = 9223372036854775808".parse::().unwrap_err(); + "a = -9223372036854775809".parse::().unwrap_err(); +} + +#[test] +fn bad_floats() { + "a = 0.".parse::().unwrap_err(); + "a = 0.e".parse::().unwrap_err(); + "a = 0.E".parse::().unwrap_err(); + "a = 0.0E".parse::().unwrap_err(); + "a = 0.0e".parse::().unwrap_err(); + "a = 0.0e-".parse::().unwrap_err(); + "a = 0.0e+".parse::().unwrap_err(); + "a = 0.0e+00".parse::().unwrap_err(); +} + +#[test] +fn floats() { + macro_rules! t { + ($actual:expr, $expected:expr) => ({ + let f = format!("foo = {}", $actual); + println!("{}", f); + let a = f.parse::().unwrap(); + assert_eq!(a["foo"].as_float().unwrap(), $expected); + }) + } + + t!("1.0", 1.0); + t!("1.0e0", 1.0); + t!("1.0e+0", 1.0); + t!("1.0e-0", 1.0); + t!("1.001e-0", 1.001); + t!("2e10", 2e10); + t!("2e+10", 2e10); + t!("2e-10", 2e-10); + t!("2_0.0", 20.0); + t!("2_0.0_0e1_0", 20.0e10); + t!("2_0.1_0e1_0", 20.1e10); +} + +#[test] +fn bare_key_names() { + let a = " + foo = 3 + foo_3 = 3 + foo_-2--3--r23f--4-f2-4 = 3 + _ = 3 + - = 3 + 8 = 8 + \"a\" = 3 + \"!\" = 3 + \"a^b\" = 3 + \"\\\"\" = 3 + \"character encoding\" = \"value\" + 'ʎǝʞ' = \"value\" + ".parse::().unwrap(); + &a["foo"]; + &a["-"]; + &a["_"]; + &a["8"]; + &a["foo_3"]; + &a["foo_-2--3--r23f--4-f2-4"]; + &a["a"]; + &a["!"]; + &a["\""]; + &a["character encoding"]; + &a["ʎǝʞ"]; +} + +#[test] +fn bad_keys() { + "key\n=3".parse::().unwrap_err(); + "key=\n3".parse::().unwrap_err(); + "key|=3".parse::().unwrap_err(); + "\"\"=3".parse::().unwrap_err(); + "=3".parse::().unwrap_err(); + "\"\"|=3".parse::().unwrap_err(); + "\"\n\"|=3".parse::().unwrap_err(); + "\"\r\"|=3".parse::().unwrap_err(); +} + +#[test] +fn bad_table_names() { + "[]".parse::().unwrap_err(); + "[.]".parse::().unwrap_err(); + "[\"\".\"\"]".parse::().unwrap_err(); + "[a.]".parse::().unwrap_err(); + "[\"\"]".parse::().unwrap_err(); + "[!]".parse::().unwrap_err(); + "[\"\n\"]".parse::().unwrap_err(); + "[a.b]\n[a.\"b\"]".parse::().unwrap_err(); + "[']".parse::().unwrap_err(); + "[''']".parse::().unwrap_err(); + "['''''']".parse::().unwrap_err(); + "['\n']".parse::().unwrap_err(); + "['\r\n']".parse::().unwrap_err(); +} + +#[test] +fn table_names() { + let a = " + [a.\"b\"] + [\"f f\"] + [\"f.f\"] + [\"\\\"\"] + ['a.a'] + ['\"\"'] + ".parse::().unwrap(); + println!("{:?}", a); + &a["a"]["b"]; + &a["f f"]; + &a["f.f"]; + &a["\""]; + &a["\"\""]; +} + +#[test] +fn invalid_bare_numeral() { + "4".parse::().unwrap_err(); +} + +#[test] +fn inline_tables() { + "a = {}".parse::().unwrap(); + "a = {b=1}".parse::().unwrap(); + "a = { b = 1 }".parse::().unwrap(); + "a = {a=1,b=2}".parse::().unwrap(); + "a = {a=1,b=2,c={}}".parse::().unwrap(); + "a = {a=1,}".parse::().unwrap_err(); + "a = {,}".parse::().unwrap_err(); + "a = {a=1,a=1}".parse::().unwrap_err(); + "a = {\n}".parse::().unwrap_err(); + "a = {".parse::().unwrap_err(); + "a = {a=[\n]}".parse::().unwrap(); + "a = {\"a\"=[\n]}".parse::().unwrap(); + "a = [\n{},\n{},\n]".parse::().unwrap(); +} + +#[test] +fn number_underscores() { + macro_rules! t { + ($actual:expr, $expected:expr) => ({ + let f = format!("foo = {}", $actual); + let table = f.parse::().unwrap(); + assert_eq!(table["foo"].as_integer().unwrap(), $expected); + }) + } + + t!("1_0", 10); + t!("1_0_0", 100); + t!("1_000", 1000); + t!("+1_000", 1000); + t!("-1_000", -1000); +} + +#[test] +fn bad_underscores() { + bad!("foo = 0_", "invalid number"); + bad!("foo = 0__0", "invalid number"); + bad!("foo = __0", "invalid number"); + bad!("foo = 1_0_", "invalid number"); +} + +#[test] +fn bad_unicode_codepoint() { + bad!("foo = \"\\uD800\"", "invalid escape value"); +} + +#[test] +fn bad_strings() { + bad!("foo = \"\\uxx\"", "invalid hex escape"); + bad!("foo = \"\\u\"", "invalid hex escape"); + bad!("foo = \"\\", "unterminated"); + bad!("foo = '", "unterminated"); +} + +#[test] +fn empty_string() { + assert_eq!("foo = \"\"".parse::() + .unwrap()["foo"] + .as_str() + .unwrap(), + ""); +} + +#[test] +fn booleans() { + let table = "foo = true".parse::().unwrap(); + assert_eq!(table["foo"].as_bool(), Some(true)); + + let table = "foo = false".parse::().unwrap(); + assert_eq!(table["foo"].as_bool(), Some(false)); + + assert!("foo = true2".parse::().is_err()); + assert!("foo = false2".parse::().is_err()); + assert!("foo = t1".parse::().is_err()); + assert!("foo = f2".parse::().is_err()); +} + +#[test] +fn bad_nesting() { + bad!(" + a = [2] + [[a]] + b = 5 + ", "duplicate key: `a`"); + bad!(" + a = 1 + [a.b] + ", "duplicate key: `a`"); + bad!(" + a = [] + [a.b] + ", "duplicate key: `a`"); + bad!(" + a = [] + [[a.b]] + ", "duplicate key: `a`"); + bad!(" + [a] + b = { c = 2, d = {} } + [a.b] + c = 2 + ", "duplicate key: `b`"); +} + +#[test] +fn bad_table_redefine() { + bad!(" + [a] + foo=\"bar\" + [a.b] + foo=\"bar\" + [a] + ", "redefinition of table `a`"); + bad!(" + [a] + foo=\"bar\" + b = { foo = \"bar\" } + [a] + ", "redefinition of table `a`"); + bad!(" + [a] + b = {} + [a.b] + ", "duplicate key: `b`"); + + bad!(" + [a] + b = {} + [a] + ", "redefinition of table `a`"); +} + +#[test] +fn datetimes() { + macro_rules! t { + ($actual:expr) => ({ + let f = format!("foo = {}", $actual); + let toml = f.parse::().expect(&format!("failed: {}", f)); + assert_eq!(toml["foo"].as_datetime().unwrap().to_string(), $actual); + }) + } + + t!("2016-09-09T09:09:09Z"); + t!("2016-09-09T09:09:09.1Z"); + t!("2016-09-09T09:09:09.2+10:00"); + t!("2016-09-09T09:09:09.0123456789-02:00"); + bad!("foo = 2016-09-09T09:09:09.Z", "failed to parse date"); + bad!("foo = 2016-9-09T09:09:09Z", "failed to parse date"); + bad!("foo = 2016-09-09T09:09:09+2:00", "failed to parse date"); + bad!("foo = 2016-09-09T09:09:09-2:00", "failed to parse date"); + bad!("foo = 2016-09-09T09:09:09Z-2:00", "failed to parse date"); +} + +#[test] +fn require_newline_after_value() { + bad!("0=0r=false", "invalid number at line 1"); + bad!(r#" +0=""o=""m=""r=""00="0"q="""0"""e="""0""" +"#, "expected newline"); + bad!(r#" +[[0000l0]] +0="0"[[0000l0]] +0="0"[[0000l0]] +0="0"l="0" +"#, "expected newline"); + bad!(r#" +0=[0]00=[0,0,0]t=["0","0","0"]s=[1000-00-00T00:00:00Z,2000-00-00T00:00:00Z] +"#, "expected newline"); + bad!(r#" +0=0r0=0r=false +"#, "invalid number at line 2"); + bad!(r#" +0=0r0=0r=falsefal=false +"#, "invalid number at line 2"); +} diff --git a/tests/serde.rs b/tests/serde.rs new file mode 100644 index 0000000..e4d88fc --- /dev/null +++ b/tests/serde.rs @@ -0,0 +1,496 @@ +extern crate serde; +extern crate toml; +#[macro_use] +extern crate serde_derive; + +use std::collections::{BTreeMap, HashSet}; +use serde::{Deserialize, Deserializer}; + +use toml::Value; +use toml::Value::{Table, Integer, Array, Float}; + +macro_rules! t { + ($e:expr) => (match $e { + Ok(t) => t, + Err(e) => panic!("{} failed with {}", stringify!($e), e), + }) +} + +macro_rules! equivalent { + ($literal:expr, $toml:expr,) => ({ + let toml = $toml; + let literal = $literal; + + // In/out of Value is equivalent + println!("try_from"); + assert_eq!(t!(Value::try_from(literal.clone())), toml); + println!("try_into"); + assert_eq!(literal, t!(toml.clone().try_into())); + + // Through a string equivalent + println!("to_string(literal)"); + assert_eq!(t!(toml::to_string(&literal)), toml.to_string()); + println!("to_string(toml)"); + assert_eq!(t!(toml::to_string(&toml)), toml.to_string()); + println!("literal, from_str(toml)"); + assert_eq!(literal, t!(toml::from_str(&toml.to_string()))); + println!("toml, from_str(toml)"); + assert_eq!(toml, t!(toml::from_str(&toml.to_string()))); + }) +} + +macro_rules! error { + ($ty:ty, $toml:expr, $error:expr) => ({ + println!("attempting parsing"); + match toml::from_str::<$ty>(&$toml.to_string()) { + Ok(_) => panic!("successful"), + Err(e) => { + assert!(e.to_string().contains($error), + "bad error: {}", e); + } + } + + println!("attempting toml decoding"); + match $toml.try_into::<$ty>() { + Ok(_) => panic!("successful"), + Err(e) => { + assert!(e.to_string().contains($error), + "bad error: {}", e); + } + } + }) +} + +macro_rules! decode( ($t:expr) => ({ + t!($t.try_into()) +}) ); + +macro_rules! map( ($($k:ident: $v:expr),*) => ({ + let mut _m = BTreeMap::new(); + $(_m.insert(stringify!($k).to_string(), $v);)* + _m +}) ); + +#[test] +fn smoke() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: isize } + + equivalent!( + Foo { a: 2 }, + Table(map! { a: Integer(2) }), + ); +} + +#[test] +fn smoke_hyphen() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { + a_b: isize, + } + + equivalent! { + Foo { a_b: 2 }, + Table(map! { a_b: Integer(2) }), + } + + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo2 { + #[serde(rename = "a-b")] + a_b: isize, + } + + let mut m = BTreeMap::new(); + m.insert("a-b".to_string(), Integer(2)); + equivalent! { + Foo2 { a_b: 2 }, + Table(m), + } +} + +#[test] +fn nested() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: isize, b: Bar } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Bar { a: String } + + equivalent! { + Foo { a: 2, b: Bar { a: "test".to_string() } }, + Table(map! { + a: Integer(2), + b: Table(map! { + a: Value::String("test".to_string()) + }) + }), + } +} + +#[test] +fn application_decode_error() { + #[derive(PartialEq, Debug)] + struct Range10(usize); + impl Deserialize for Range10 { + fn deserialize(d: D) -> Result { + let x: usize = try!(Deserialize::deserialize(d)); + if x > 10 { + Err(serde::de::Error::custom("more than 10")) + } else { + Ok(Range10(x)) + } + } + } + let d_good = Integer(5); + let d_bad1 = Value::String("not an isize".to_string()); + let d_bad2 = Integer(11); + + assert_eq!(Range10(5), d_good.try_into().unwrap()); + + let err1: Result = d_bad1.try_into(); + assert!(err1.is_err()); + let err2: Result = d_bad2.try_into(); + assert!(err2.is_err()); +} + +#[test] +fn array() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: Vec } + + equivalent! { + Foo { a: vec![1, 2, 3, 4] }, + Table(map! { + a: Array(vec![ + Integer(1), + Integer(2), + Integer(3), + Integer(4) + ]) + }), + }; +} + +#[test] +fn inner_structs_with_options() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { + a: Option>, + b: Bar, + } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Bar { + a: String, + b: f64, + } + + equivalent! { + Foo { + a: Some(Box::new(Foo { + a: None, + b: Bar { a: "foo".to_string(), b: 4.5 }, + })), + b: Bar { a: "bar".to_string(), b: 1.0 }, + }, + Table(map! { + a: Table(map! { + b: Table(map! { + a: Value::String("foo".to_string()), + b: Float(4.5) + }) + }), + b: Table(map! { + a: Value::String("bar".to_string()), + b: Float(1.0) + }) + }), + } +} + +#[test] +fn hashmap() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { + set: HashSet, + map: BTreeMap, + } + + equivalent! { + Foo { + map: { + let mut m = BTreeMap::new(); + m.insert("foo".to_string(), 10); + m.insert("bar".to_string(), 4); + m + }, + set: { + let mut s = HashSet::new(); + s.insert('a'); + s + }, + }, + Table(map! { + map: Table(map! { + foo: Integer(10), + bar: Integer(4) + }), + set: Array(vec![Value::String("a".to_string())]) + }), + } +} + +#[test] +fn table_array() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: Vec, } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Bar { a: isize } + + equivalent! { + Foo { a: vec![Bar { a: 1 }, Bar { a: 2 }] }, + Table(map! { + a: Array(vec![ + Table(map!{ a: Integer(1) }), + Table(map!{ a: Integer(2) }), + ]) + }), + } +} + +#[test] +fn type_errors() { + #[derive(Deserialize)] + #[allow(dead_code)] + struct Foo { bar: isize } + + error! { + Foo, + Table(map! { + bar: Value::String("a".to_string()) + }), + "invalid type: string \"a\", expected isize for key `bar`" + } + + #[derive(Deserialize)] + #[allow(dead_code)] + struct Bar { foo: Foo } + + error! { + Bar, + Table(map! { + foo: Table(map! { + bar: Value::String("a".to_string()) + }) + }), + "invalid type: string \"a\", expected isize for key `foo.bar`" + } +} + +#[test] +fn missing_errors() { + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct Foo { bar: isize } + + error! { + Foo, + Table(map! { }), + "missing field `bar`" + } +} + +#[test] +fn parse_enum() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: E } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + #[serde(untagged)] + enum E { + Bar(isize), + Baz(String), + Last(Foo2), + } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo2 { + test: String, + } + + equivalent! { + Foo { a: E::Bar(10) }, + Table(map! { a: Integer(10) }), + } + + equivalent! { + Foo { a: E::Baz("foo".to_string()) }, + Table(map! { a: Value::String("foo".to_string()) }), + } + + equivalent! { + Foo { a: E::Last(Foo2 { test: "test".to_string() }) }, + Table(map! { a: Table(map! { test: Value::String("test".to_string()) }) }), + } +} + +// #[test] +// fn unused_fields() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: isize } +// +// let v = Foo { a: 2 }; +// let mut d = Decoder::new(Table(map! { +// a, Integer(2), +// b, Integer(5) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, Some(Table(map! { +// b, Integer(5) +// }))); +// } +// +// #[test] +// fn unused_fields2() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: Bar } +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Bar { a: isize } +// +// let v = Foo { a: Bar { a: 2 } }; +// let mut d = Decoder::new(Table(map! { +// a, Table(map! { +// a, Integer(2), +// b, Integer(5) +// }) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, Some(Table(map! { +// a, Table(map! { +// b, Integer(5) +// }) +// }))); +// } +// +// #[test] +// fn unused_fields3() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: Bar } +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Bar { a: isize } +// +// let v = Foo { a: Bar { a: 2 } }; +// let mut d = Decoder::new(Table(map! { +// a, Table(map! { +// a, Integer(2) +// }) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, None); +// } +// +// #[test] +// fn unused_fields4() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: BTreeMap } +// +// let v = Foo { a: map! { a, "foo".to_string() } }; +// let mut d = Decoder::new(Table(map! { +// a, Table(map! { +// a, Value::String("foo".to_string()) +// }) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, None); +// } +// +// #[test] +// fn unused_fields5() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: Vec } +// +// let v = Foo { a: vec!["a".to_string()] }; +// let mut d = Decoder::new(Table(map! { +// a, Array(vec![Value::String("a".to_string())]) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, None); +// } +// +// #[test] +// fn unused_fields6() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: Option> } +// +// let v = Foo { a: Some(vec![]) }; +// let mut d = Decoder::new(Table(map! { +// a, Array(vec![]) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, None); +// } +// +// #[test] +// fn unused_fields7() { +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Foo { a: Vec } +// #[derive(Serialize, Deserialize, PartialEq, Debug)] +// struct Bar { a: isize } +// +// let v = Foo { a: vec![Bar { a: 1 }] }; +// let mut d = Decoder::new(Table(map! { +// a, Array(vec![Table(map! { +// a, Integer(1), +// b, Integer(2) +// })]) +// })); +// assert_eq!(v, t!(Deserialize::deserialize(&mut d))); +// +// assert_eq!(d.toml, Some(Table(map! { +// a, Array(vec![Table(map! { +// b, Integer(2) +// })]) +// }))); +// } + +#[test] +fn empty_arrays() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: Vec } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Bar; + + equivalent! { + Foo { a: vec![] }, + Table(map! {a: Array(Vec::new())}), + } +} + +#[test] +fn empty_arrays2() { + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Foo { a: Option> } + #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] + struct Bar; + + equivalent! { + Foo { a: None }, + Table(map! {}), + } + + equivalent!{ + Foo { a: Some(vec![]) }, + Table(map! { a: Array(vec![]) }), + } +} + +#[test] +fn extra_keys() { + #[derive(Serialize, Deserialize)] + struct Foo { a: isize } + + let toml = Table(map! { a: Integer(2), b: Integer(2) }); + assert!(toml.clone().try_into::().is_ok()); + assert!(toml::from_str::(&toml.to_string()).is_ok()); +} diff --git a/tests/valid.rs b/tests/valid.rs index 09589c9..4229f1c 100644 --- a/tests/valid.rs +++ b/tests/valid.rs @@ -1,65 +1,61 @@ -extern crate rustc_serialize; extern crate toml; +extern crate serde_json; -use std::collections::BTreeMap; -use rustc_serialize::json::Json; +use toml::Value as Toml; +use serde_json::Value as Json; -use toml::{Parser, Value}; -use toml::Value::{Table, Integer, Float, Boolean, Datetime, Array}; - -fn to_json(toml: Value) -> Json { +fn to_json(toml: toml::Value) -> Json { fn doit(s: &str, json: Json) -> Json { - let mut map = BTreeMap::new(); - map.insert(format!("{}", "type"), Json::String(format!("{}", s))); - map.insert(format!("{}", "value"), json); + let mut map = serde_json::Map::new(); + map.insert("type".to_string(), Json::String(s.to_string())); + map.insert("value".to_string(), json); Json::Object(map) } + match toml { - Value::String(s) => doit("string", Json::String(s)), - Integer(i) => doit("integer", Json::String(format!("{}", i))), - Float(f) => doit("float", Json::String({ + Toml::String(s) => doit("string", Json::String(s)), + Toml::Integer(i) => doit("integer", Json::String(i.to_string())), + Toml::Float(f) => doit("float", Json::String({ let s = format!("{:.15}", f); let s = format!("{}", s.trim_right_matches('0')); if s.ends_with(".") {format!("{}0", s)} else {s} })), - Boolean(b) => doit("bool", Json::String(format!("{}", b))), - Datetime(s) => doit("datetime", Json::String(s)), - Array(arr) => { + Toml::Boolean(b) => doit("bool", Json::String(format!("{}", b))), + Toml::Datetime(s) => doit("datetime", Json::String(s.to_string())), + Toml::Array(arr) => { let is_table = match arr.first() { - Some(&Table(..)) => true, + Some(&Toml::Table(..)) => true, _ => false, }; let json = Json::Array(arr.into_iter().map(to_json).collect()); if is_table {json} else {doit("array", json)} } - Table(table) => Json::Object(table.into_iter().map(|(k, v)| { - (k, to_json(v)) - }).collect()), + Toml::Table(table) => { + let mut map = serde_json::Map::new(); + for (k, v) in table { + map.insert(k, to_json(v)); + } + Json::Object(map) + } } } fn run(toml: &str, json: &str) { - let mut p = Parser::new(toml); - let table = p.parse(); - assert!(p.errors.len() == 0, "had_errors: {:?}", - p.errors.iter().map(|e| { - (e.desc.clone(), &toml[e.lo - 5..e.hi + 5]) - }).collect::>()); - assert!(table.is_some()); - let toml = Table(table.unwrap()); - let toml_string = format!("{}", toml); + println!("parsing:\n{}", toml); + let toml: Toml = toml.parse().unwrap(); + let json: Json = json.parse().unwrap(); - let json = Json::from_str(json).unwrap(); + // Assert toml == json let toml_json = to_json(toml.clone()); assert!(json == toml_json, "expected\n{}\ngot\n{}\n", - json.pretty(), - toml_json.pretty()); + serde_json::to_string_pretty(&json).unwrap(), + serde_json::to_string_pretty(&toml_json).unwrap()); - let table2 = Parser::new(&toml_string).parse().unwrap(); - // floats are a little lossy - if table2.values().any(|v| v.as_float().is_some()) { return } - assert_eq!(toml, Table(table2)); + // Assert round trip + println!("round trip parse: {}", toml); + let toml2 = toml.to_string().parse().unwrap(); + assert_eq!(toml, toml2); } macro_rules! test( ($name:ident, $toml:expr, $json:expr) => ( -- cgit v1.2.3