diff --git a/src/de.rs b/src/de.rs index 85009ed..d41ca12 100644 --- a/src/de.rs +++ b/src/de.rs @@ -159,6 +159,9 @@ enum ErrorKind { /// A struct was expected but something else was found ExpectedString, + /// Dotted key attempted to extend something that is not a table. + DottedKeyInvalidType, + #[doc(hidden)] __Nonexhaustive, } @@ -210,7 +213,7 @@ impl<'de, 'b> de::Deserializer<'de> for &'b mut Deserializer<'de> { if cur_table.values.is_none() { cur_table.values = Some(Vec::new()); } - cur_table.values.as_mut().unwrap().push((key, value)); + self.add_dotted_key(key, value, cur_table.values.as_mut().unwrap())?; } } } @@ -796,7 +799,7 @@ impl<'a> Deserializer<'a> { } fn key_value(&mut self) -> Result, Error> { - let key = self.table_key()?; + let key = self.dotted_key()?; self.eat_whitespace()?; self.expect(Token::Equals)?; self.eat_whitespace()?; @@ -1095,11 +1098,12 @@ impl<'a> Deserializer<'a> { return Ok((span, ret)) } loop { - let key = self.table_key()?; + let key = self.dotted_key()?; self.eat_whitespace()?; self.expect(Token::Equals)?; self.eat_whitespace()?; - ret.push((key, self.value()?)); + let value = self.value()?; + self.add_dotted_key(key, value, &mut ret)?; self.eat_whitespace()?; if let Some(span) = self.eat_spanned(Token::RightBrace)? { @@ -1152,6 +1156,52 @@ impl<'a> Deserializer<'a> { self.tokens.table_key().map(|t| t.1).map_err(|e| self.token_error(e)) } + fn dotted_key(&mut self) -> Result>, Error> { + let mut result = Vec::new(); + result.push(self.table_key()?); + self.eat_whitespace()?; + while self.eat(Token::Period)? { + self.eat_whitespace()?; + result.push(self.table_key()?); + self.eat_whitespace()?; + } + Ok(result) + } + + fn add_dotted_key( + &self, + mut key_parts: Vec>, + value: Value<'a>, + values: &mut Vec<(Cow<'a, str>, Value<'a>)>, + ) -> Result<(), Error> { + let key = key_parts.remove(0); + if key_parts.is_empty() { + values.push((key, value)); + return Ok(()); + } + match values.iter_mut().find(|(k, _)| *k == key) { + Some((_, Value { e: E::InlineTable(ref mut v), .. })) => { + return self.add_dotted_key(key_parts, value, v); + } + Some((_, Value { start, .. })) => { + return Err(self.error(*start, ErrorKind::DottedKeyInvalidType)); + } + None => {} + } + // The start/end value is somewhat misleading here. + let inline_table = Value { + e: E::InlineTable(Vec::new()), + start: value.start, + end: value.end, + }; + values.push((key, inline_table)); + let last_i = values.len() - 1; + if let (_, Value { e: E::InlineTable(ref mut v), .. }) = values[last_i] { + self.add_dotted_key(key_parts, value, v)?; + } + Ok(()) + } + fn eat_whitespace(&mut self) -> Result<(), Error> { self.tokens.eat_whitespace().map_err(|e| self.token_error(e)) } @@ -1329,6 +1379,7 @@ impl fmt::Display for Error { ErrorKind::EmptyTableKey => "empty table key found".fmt(f)?, ErrorKind::Custom => self.inner.message.fmt(f)?, ErrorKind::ExpectedString => "expected string".fmt(f)?, + ErrorKind::DottedKeyInvalidType => "dotted key attempted to extend non-table type".fmt(f)?, ErrorKind::__Nonexhaustive => panic!(), } @@ -1372,6 +1423,7 @@ impl error::Error for Error { ErrorKind::EmptyTableKey => "empty table key found", ErrorKind::Custom => "a custom error", ErrorKind::ExpectedString => "expected string", + ErrorKind::DottedKeyInvalidType => "dotted key invalid type", ErrorKind::__Nonexhaustive => panic!(), } } @@ -1385,7 +1437,7 @@ impl de::Error for Error { enum Line<'a> { Table { at: usize, header: Header<'a>, array: bool }, - KeyValue(Cow<'a, str>, Value<'a>), + KeyValue(Vec>, Value<'a>), } struct Header<'a> { diff --git a/test-suite/tests/invalid-misc.rs b/test-suite/tests/invalid-misc.rs index 80421e3..5c7f779 100644 --- a/test-suite/tests/invalid-misc.rs +++ b/test-suite/tests/invalid-misc.rs @@ -17,4 +17,12 @@ fn bad() { bad("a = -0x1"); bad("a = 0x-1"); + + // Dotted keys. + bad("a.b.c = 1 + a.b = 2 + "); + bad("a = 1 + a.b = 2"); + bad("a = {k1 = 1, k1.name = \"joe\"}") } diff --git a/test-suite/tests/valid.rs b/test-suite/tests/valid.rs index b186800..d032ba5 100644 --- a/test-suite/tests/valid.rs +++ b/test-suite/tests/valid.rs @@ -45,7 +45,7 @@ fn to_json(toml: toml::Value) -> Json { fn run_pretty(toml: Toml) { // Assert toml == json println!("### pretty round trip parse."); - + // standard pretty let toml_raw = to_string_pretty(&toml).expect("to string"); let toml2 = toml_raw.parse().expect("from string"); @@ -247,3 +247,6 @@ test!(key_quote_newline, test!(table_array_nest_no_keys, include_str!("valid/table-array-nest-no-keys.toml"), include_str!("valid/table-array-nest-no-keys.json")); +test!(dotted_keys, + include_str!("valid/dotted-keys.toml"), + include_str!("valid/dotted-keys.json")); diff --git a/test-suite/tests/valid/dotted-keys.json b/test-suite/tests/valid/dotted-keys.json new file mode 100644 index 0000000..e32079a --- /dev/null +++ b/test-suite/tests/valid/dotted-keys.json @@ -0,0 +1,44 @@ +{ + "a": { + "b": { + "type": "integer", + "value": "123" + } + }, + "table": { + "a": { + "b": { + "c": { + "type": "integer", + "value": "1" + }, + "d": { + "type": "integer", + "value": "2" + } + } + }, + "in": { + "bar": { + "type": "integer", + "value": "2" + }, + "foo": { + "type": "integer", + "value": "1" + } + }, + "in2": { + "type": { + "color": { + "type": "string", + "value": "blue" + }, + "name": { + "type": "string", + "value": "cat" + } + } + } + } +} diff --git a/test-suite/tests/valid/dotted-keys.toml b/test-suite/tests/valid/dotted-keys.toml new file mode 100644 index 0000000..27dbd32 --- /dev/null +++ b/test-suite/tests/valid/dotted-keys.toml @@ -0,0 +1,9 @@ +a.b = 123 + +[table] +a.b.c = 1 +a . b . d = 2 +in = {foo = 1} +in.bar = 2 + +in2 = { type.name = "cat", type.color = "blue" }