mirror of https://github.com/kdl-org/kdl-rs.git
fix(compliance): pull in spec test suite and fix issues (#40)
This commit includes a whole bunch of fixes, some of which are significant changes to the parser and some related functionality. But I consider all changes to be bugfixes because they were compliance failures.
This commit is contained in:
parent
71df712c0c
commit
58a40fdf48
26
README.md
26
README.md
|
|
@ -96,6 +96,32 @@ Error:
|
|||
╰────
|
||||
help: Floating point numbers must be base 10, and have numbers after the decimal point.
|
||||
```
|
||||
|
||||
### Quirks
|
||||
|
||||
#### Properties
|
||||
|
||||
Multiple properties with the same name are allowed, and all duplicated
|
||||
**will be preserved**, meaning those documents will correctly round-trip.
|
||||
When using `node.get()`/`node["key"]` & company, the _last_ property with
|
||||
that name's value will be returned.
|
||||
|
||||
#### Numbers
|
||||
|
||||
KDL itself does not specify a particular representation for numbers and
|
||||
accepts just about anything valid, no matter how large and how small. This
|
||||
means a few things:
|
||||
|
||||
* Numbers without a decimal point are interpreted as u64.
|
||||
* Numbers with a decimal point are interpreted as f64.
|
||||
* Floating point numbers that evaluate to f64::INFINITY or
|
||||
f64::NEG_INFINITY or NaN will be represented as such in the values,
|
||||
instead of the original numbers.
|
||||
* A similar restriction applies to overflowed u64 values.
|
||||
* The original _representation_ of these numbers will be preserved, unless
|
||||
you `doc.fmt()`, in which case the original representation will be
|
||||
thrown away and the actual value will be used when serializing.
|
||||
|
||||
### License
|
||||
|
||||
The code in this repository is covered by [the Apache-2.0
|
||||
|
|
|
|||
|
|
@ -188,7 +188,12 @@ impl KdlDocument {
|
|||
/// Auto-formats this Document, making everything nice while preserving
|
||||
/// comments.
|
||||
pub fn fmt(&mut self) {
|
||||
self.fmt_impl(0);
|
||||
self.fmt_impl(0, false);
|
||||
}
|
||||
|
||||
/// Formats the document and removes all comments from the document.
|
||||
pub fn fmt_no_comments(&mut self) {
|
||||
self.fmt_impl(0, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,15 +204,20 @@ impl Display for KdlDocument {
|
|||
}
|
||||
|
||||
impl KdlDocument {
|
||||
pub(crate) fn fmt_impl(&mut self, indent: usize) {
|
||||
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
|
||||
if let Some(s) = self.leading.as_mut() {
|
||||
crate::fmt::fmt_leading(s, indent);
|
||||
crate::fmt::fmt_leading(s, indent, no_comments);
|
||||
}
|
||||
let mut has_nodes = false;
|
||||
for node in &mut self.nodes {
|
||||
has_nodes = true;
|
||||
node.fmt_impl(indent, no_comments);
|
||||
}
|
||||
if let Some(s) = self.trailing.as_mut() {
|
||||
crate::fmt::fmt_trailing(s);
|
||||
}
|
||||
for node in &mut self.nodes {
|
||||
node.fmt_impl(indent);
|
||||
crate::fmt::fmt_trailing(s, no_comments);
|
||||
if !has_nodes {
|
||||
s.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
10
src/entry.rs
10
src/entry.rs
|
|
@ -124,12 +124,12 @@ impl Display for KdlEntry {
|
|||
if let Some(leading) = &self.leading {
|
||||
write!(f, "{}", leading)?;
|
||||
}
|
||||
if let Some(ty) = &self.ty {
|
||||
write!(f, "({})", ty)?;
|
||||
}
|
||||
if let Some(name) = &self.name {
|
||||
write!(f, "{}=", name)?;
|
||||
}
|
||||
if let Some(ty) = &self.ty {
|
||||
write!(f, "({})", ty)?;
|
||||
}
|
||||
if let Some(repr) = &self.value_repr {
|
||||
write!(f, "{}", repr)?;
|
||||
} else {
|
||||
|
|
@ -165,7 +165,7 @@ impl FromStr for KdlEntry {
|
|||
type Err = KdlError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parser::parse(s, parser::entry_with_node_space)
|
||||
parser::parse(s, parser::entry_with_trailing)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -217,7 +217,7 @@ mod test {
|
|||
}
|
||||
);
|
||||
|
||||
let entry: KdlEntry = " \\\n (\"m\\\"eh\")\"foo\"=0xDEADbeef\t\\\n".parse()?;
|
||||
let entry: KdlEntry = " \\\n \"foo\"=(\"m\\\"eh\")0xDEADbeef\t\\\n".parse()?;
|
||||
assert_eq!(
|
||||
entry,
|
||||
KdlEntry {
|
||||
|
|
|
|||
28
src/fmt.rs
28
src/fmt.rs
|
|
@ -1,30 +1,34 @@
|
|||
pub(crate) fn fmt_leading(leading: &mut String, indent: usize) {
|
||||
pub(crate) fn fmt_leading(leading: &mut String, indent: usize, no_comments: bool) {
|
||||
if leading.is_empty() {
|
||||
return;
|
||||
}
|
||||
let comments = crate::parser::parse(leading.trim(), crate::parser::leading_comments)
|
||||
.expect("invalid leading text");
|
||||
let mut result = String::new();
|
||||
for line in comments {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
result.push_str(&format!("{:indent$}{}\n", "", trimmed, indent = indent));
|
||||
if !no_comments {
|
||||
let comments = crate::parser::parse(leading.trim(), crate::parser::leading_comments)
|
||||
.expect("invalid leading text");
|
||||
for line in comments {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
result.push_str(&format!("{:indent$}{}\n", "", trimmed, indent = indent));
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push_str(&format!("{:indent$}", "", indent = indent));
|
||||
*leading = result;
|
||||
}
|
||||
|
||||
pub(crate) fn fmt_trailing(decor: &mut String) {
|
||||
pub(crate) fn fmt_trailing(decor: &mut String, no_comments: bool) {
|
||||
if decor.is_empty() {
|
||||
return;
|
||||
}
|
||||
*decor = decor.trim().to_string();
|
||||
let mut result = String::new();
|
||||
let comments = crate::parser::parse(decor, crate::parser::trailing_comments)
|
||||
.expect("invalid trailing text");
|
||||
for comment in comments {
|
||||
result.push_str(comment);
|
||||
if !no_comments {
|
||||
let comments = crate::parser::parse(decor, crate::parser::trailing_comments)
|
||||
.expect("invalid trailing text");
|
||||
for comment in comments {
|
||||
result.push_str(comment);
|
||||
}
|
||||
}
|
||||
*decor = result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::{parser, KdlError};
|
|||
|
||||
/// Represents a KDL
|
||||
/// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct KdlIdentifier {
|
||||
pub(crate) value: String,
|
||||
pub(crate) repr: Option<String>,
|
||||
|
|
@ -199,9 +199,6 @@ mod test {
|
|||
let invalid = "\"x";
|
||||
assert!(invalid.parse::<KdlIdentifier>().is_err());
|
||||
|
||||
let invalid = "r#\"foo\"#";
|
||||
assert!(invalid.parse::<KdlIdentifier>().is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
27
src/lib.rs
27
src/lib.rs
|
|
@ -94,6 +94,32 @@
|
|||
//! ╰────
|
||||
//! help: Floating point numbers must be base 10, and have numbers after the decimal point.
|
||||
//! ```
|
||||
//!
|
||||
//! ## Quirks
|
||||
//!
|
||||
//! ### Properties
|
||||
//!
|
||||
//! Multiple properties with the same name are allowed, and all duplicated
|
||||
//! **will be preserved**, meaning those documents will correctly round-trip.
|
||||
//! When using `node.get()`/`node["key"]` & company, the _last_ property with
|
||||
//! that name's value will be returned.
|
||||
//!
|
||||
//! ### Numbers
|
||||
//!
|
||||
//! KDL itself does not specify a particular representation for numbers and
|
||||
//! accepts just about anything valid, no matter how large and how small. This
|
||||
//! means a few things:
|
||||
//!
|
||||
//! * Numbers without a decimal point are interpreted as [`u64`].
|
||||
//! * Numbers with a decimal point are interpreted as [`f64`].
|
||||
//! * Floating point numbers that evaluate to [`f64::INFINITY`] or
|
||||
//! [`f64::NEG_INFINITY`] or NaN will be represented as such in the values,
|
||||
//! instead of the original numbers.
|
||||
//! * A similar restriction applies to overflowed [`u64`] values.
|
||||
//! * The original _representation_ of these numbers will be preserved, unless
|
||||
//! you [`KdlDocument::fmt`] in which case the original representation will be
|
||||
//! thrown away and the actual value will be used when serializing.
|
||||
//!
|
||||
//! ## License
|
||||
//!
|
||||
//! The code in this repository is covered by [the Apache-2.0
|
||||
|
|
@ -102,6 +128,7 @@
|
|||
#![deny(missing_debug_implementations, nonstandard_style)]
|
||||
#![warn(missing_docs, unreachable_pub, rust_2018_idioms, unreachable_pub)]
|
||||
#![cfg_attr(test, deny(warnings))]
|
||||
#![doc(html_favicon_url = "https://kdl.dev/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://kdl.dev/logo.svg")]
|
||||
|
||||
pub use document::*;
|
||||
|
|
|
|||
34
src/node.rs
34
src/node.rs
|
|
@ -134,14 +134,15 @@ impl KdlNode {
|
|||
fn get_impl(&self, key: NodeKey) -> Option<&KdlEntry> {
|
||||
match key {
|
||||
NodeKey::Key(key) => {
|
||||
let mut current = None;
|
||||
for entry in &self.entries {
|
||||
if entry.name.is_some()
|
||||
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
|
||||
{
|
||||
return Some(entry);
|
||||
current = Some(entry);
|
||||
}
|
||||
}
|
||||
None
|
||||
current
|
||||
}
|
||||
NodeKey::Index(idx) => {
|
||||
let mut current_idx = 0;
|
||||
|
|
@ -170,14 +171,15 @@ impl KdlNode {
|
|||
fn get_mut_impl(&mut self, key: NodeKey) -> Option<&mut KdlEntry> {
|
||||
match key {
|
||||
NodeKey::Key(key) => {
|
||||
let mut current = None;
|
||||
for entry in &mut self.entries {
|
||||
if entry.name.is_some()
|
||||
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
|
||||
{
|
||||
return Some(entry);
|
||||
current = Some(entry);
|
||||
}
|
||||
}
|
||||
None
|
||||
current
|
||||
}
|
||||
NodeKey::Index(idx) => {
|
||||
let mut current_idx = 0;
|
||||
|
|
@ -340,7 +342,12 @@ impl KdlNode {
|
|||
|
||||
/// Auto-formats this node and its contents.
|
||||
pub fn fmt(&mut self) {
|
||||
self.fmt_impl(0);
|
||||
self.fmt_impl(0, false);
|
||||
}
|
||||
|
||||
/// Auto-formats this node and its contents, stripping comments.
|
||||
pub fn fmt_no_comments(&mut self) {
|
||||
self.fmt_impl(0, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -421,12 +428,12 @@ impl Display for KdlNode {
|
|||
}
|
||||
|
||||
impl KdlNode {
|
||||
pub(crate) fn fmt_impl(&mut self, indent: usize) {
|
||||
pub(crate) fn fmt_impl(&mut self, indent: usize, no_comments: bool) {
|
||||
if let Some(s) = self.leading.as_mut() {
|
||||
crate::fmt::fmt_leading(s, indent);
|
||||
crate::fmt::fmt_leading(s, indent, no_comments);
|
||||
}
|
||||
if let Some(s) = self.trailing.as_mut() {
|
||||
crate::fmt::fmt_trailing(s);
|
||||
crate::fmt::fmt_trailing(s, no_comments);
|
||||
if s.starts_with(';') {
|
||||
s.remove(0);
|
||||
}
|
||||
|
|
@ -446,7 +453,7 @@ impl KdlNode {
|
|||
entry.fmt();
|
||||
}
|
||||
if let Some(children) = self.children.as_mut() {
|
||||
children.fmt_impl(indent + 4);
|
||||
children.fmt_impl(indent + 4, no_comments);
|
||||
if let Some(leading) = children.leading.as_mut() {
|
||||
leading.push('\n');
|
||||
}
|
||||
|
|
@ -511,6 +518,11 @@ mod test {
|
|||
assert_eq!(node.name(), &"\"node\"".parse()?);
|
||||
assert_eq!(node.get(0), Some(&"0xDEADbeef".parse()?));
|
||||
|
||||
r#"
|
||||
node "test" {
|
||||
link "blah" anything="self"
|
||||
}"#
|
||||
.parse::<KdlNode>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -528,5 +540,9 @@ mod test {
|
|||
|
||||
assert_eq!(node[0], false.into());
|
||||
assert_eq!(node["foo"], KdlValue::Null);
|
||||
|
||||
node.entries_mut().push(KdlEntry::new_prop("x", 1));
|
||||
node.entries_mut().push(KdlEntry::new_prop("x", 2));
|
||||
assert_eq!(&node["x"], &2.into())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::ops::RangeTo;
|
|||
|
||||
use crate::nom_compat::{many0, many1, many_till};
|
||||
use nom::branch::alt;
|
||||
use nom::bytes::complete::{tag, take_until, take_until1, take_while, take_while_m_n};
|
||||
use nom::bytes::complete::{tag, take_until, take_while, take_while_m_n};
|
||||
use nom::character::complete::{anychar, char, none_of, one_of};
|
||||
use nom::combinator::{all_consuming, cut, eof, map, map_opt, map_res, opt, recognize};
|
||||
use nom::combinator::{all_consuming, cut, eof, map, map_opt, map_res, opt, peek, recognize};
|
||||
use nom::error::{context, ParseError};
|
||||
use nom::sequence::{delimited, preceded, terminated, tuple};
|
||||
use nom::{Finish, IResult, Offset, Parser, Slice};
|
||||
|
|
@ -81,7 +81,7 @@ pub(crate) fn node(input: &str) -> IResult<&str, KdlNode, KdlParseError<&str>> {
|
|||
cut(recognize(preceded(
|
||||
many0(node_space),
|
||||
alt((
|
||||
terminated(recognize(opt(tag(";"))), alt((linespace, eof))),
|
||||
terminated(recognize(tag(";")), opt(alt((linespace, eof)))),
|
||||
alt((newline, single_line_comment, eof)),
|
||||
)),
|
||||
))),
|
||||
|
|
@ -109,7 +109,7 @@ pub(crate) fn node(input: &str) -> IResult<&str, KdlNode, KdlParseError<&str>> {
|
|||
}
|
||||
|
||||
pub(crate) fn identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&str>> {
|
||||
alt((plain_identifier, quoted_identifier))(input)
|
||||
alt((quoted_identifier, plain_identifier))(input)
|
||||
}
|
||||
|
||||
pub(crate) fn leading_comments(input: &str) -> IResult<&str, Vec<&str>, KdlParseError<&str>> {
|
||||
|
|
@ -145,22 +145,38 @@ fn plain_identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&
|
|||
take_while_m_n(1, 1, KdlIdentifier::is_initial_char),
|
||||
cut(take_while(KdlIdentifier::is_identifier_char)),
|
||||
))(input).map_err(|e| set_details(e, start, Some("invalid identifier character"), Some("See https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier for an explanation of valid KDL identifiers.")))?;
|
||||
match name {
|
||||
"false" | "true" | "null" => {
|
||||
return Err(nom::Err::Error(KdlParseError {
|
||||
input,
|
||||
context: Some("non-keyword identifier"),
|
||||
len: name.len(),
|
||||
label: Some("reserved keyword"),
|
||||
help: Some("Reserved keywords cannot be used as identifiers."),
|
||||
kind: None,
|
||||
touched: false,
|
||||
}))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let mut ident = KdlIdentifier::from(name);
|
||||
ident.set_repr(name);
|
||||
Ok((input, ident))
|
||||
}
|
||||
|
||||
fn quoted_identifier(input: &str) -> IResult<&str, KdlIdentifier, KdlParseError<&str>> {
|
||||
let (input, (raw, val)) = string(input)?;
|
||||
let (input, (raw, val)) = alt((string, raw_string))(input)?;
|
||||
let mut ident = KdlIdentifier::from(val.as_string().unwrap());
|
||||
ident.set_repr(raw);
|
||||
Ok((input, ident))
|
||||
}
|
||||
|
||||
pub(crate) fn entry_with_node_space(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
let (input, leading) = recognize(many0(node_space))(input)?;
|
||||
let leading = if leading.is_empty() { " " } else { leading };
|
||||
let (input, mut entry) = entry(input)?;
|
||||
pub(crate) fn entry_with_trailing(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
let (input, mut leading) = recognize(many0(node_space))(input)?;
|
||||
if leading.is_empty() {
|
||||
leading = " ";
|
||||
};
|
||||
let (input, mut entry) = alt((property, argument))(input)?;
|
||||
let (input, trailing) = recognize(many0(node_space))(input)?;
|
||||
entry.set_leading(leading);
|
||||
entry.set_trailing(trailing);
|
||||
|
|
@ -168,25 +184,32 @@ pub(crate) fn entry_with_node_space(input: &str) -> IResult<&str, KdlEntry, KdlP
|
|||
}
|
||||
|
||||
fn entry(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
alt((property, argument))(input)
|
||||
let (input, leading) = recognize(many1(node_space))(input)?;
|
||||
let (input, mut entry) = alt((property, argument))(input)?;
|
||||
entry.set_leading(leading);
|
||||
Ok((input, entry))
|
||||
}
|
||||
|
||||
fn entry_maybe_space(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
let (input, leading) = recognize(many0(node_space))(input)?;
|
||||
let (input, mut entry) = alt((property, argument))(input)?;
|
||||
entry.set_leading(leading);
|
||||
Ok((input, entry))
|
||||
}
|
||||
|
||||
fn property(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
let (input, leading) = recognize(many0(node_space))(input)?;
|
||||
let (input, ty) = opt(annotation)(input)?;
|
||||
let (input, name) = identifier(input)?;
|
||||
let (input, _) = context("'=' after property name", tag("="))(input)?;
|
||||
let (input, ty) = opt(annotation)(input)?;
|
||||
let (input, (raw, value)) = context("property value", cut(value))(input).map_err(|e| set_details(e, input, Some("invalid value"), Some("Please refer to https://github.com/kdl-org/kdl/blob/main/SPEC.md#value for valid KDL value syntaxes.")))?;
|
||||
let mut entry = KdlEntry::new_prop(name, value);
|
||||
entry.ty = ty;
|
||||
entry.set_leading(leading);
|
||||
entry.set_trailing("");
|
||||
entry.set_value_repr(raw);
|
||||
Ok((input, entry))
|
||||
}
|
||||
|
||||
fn argument(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
||||
let (input, leading) = recognize(many0(node_space))(input)?;
|
||||
let (input, ty) = opt(annotation)(input)?;
|
||||
let (input, (raw, value)) = if ty.is_some() {
|
||||
context("valid value", cut(value))(input)
|
||||
|
|
@ -195,7 +218,6 @@ fn argument(input: &str) -> IResult<&str, KdlEntry, KdlParseError<&str>> {
|
|||
}?;
|
||||
let mut entry = KdlEntry::new(value);
|
||||
entry.ty = ty;
|
||||
entry.set_leading(leading);
|
||||
entry.set_trailing("");
|
||||
entry.set_value_repr(raw);
|
||||
Ok((input, entry))
|
||||
|
|
@ -329,7 +351,7 @@ fn multi_line_comment(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
|
|||
tag("/*"),
|
||||
context("comment block body", cut(commented_block)),
|
||||
))(input)
|
||||
.map_err(|e| set_details(e, input, Some("comment"), None))
|
||||
.map_err(|e| set_details(e, input, Some("comment"), Some("multi-line comments must start with /* and be terminated with a matching */. They may be nested, but their */ must match.")))
|
||||
}
|
||||
|
||||
/// `commented-block := '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block`
|
||||
|
|
@ -337,7 +359,12 @@ fn commented_block(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
|
|||
alt((
|
||||
tag("*/"),
|
||||
terminated(
|
||||
alt((multi_line_comment, take_until1("*/"), tag("*"), tag("/"))),
|
||||
alt((
|
||||
multi_line_comment,
|
||||
tag("*"),
|
||||
tag("/"),
|
||||
recognize(many_till(anychar, peek(alt((tag("*"), tag("/")))))),
|
||||
)),
|
||||
commented_block,
|
||||
),
|
||||
))(input)
|
||||
|
|
@ -348,7 +375,7 @@ fn node_slashdash(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
|
|||
tag("/-"),
|
||||
context(
|
||||
"node following a slashdash",
|
||||
cut(alt((recognize(entry), recognize(children)))),
|
||||
cut(alt((recognize(entry_maybe_space), recognize(children)))),
|
||||
),
|
||||
))(input)
|
||||
.map_err(|e| set_details(e, input, Some("slashdash"), None))
|
||||
|
|
@ -430,6 +457,7 @@ fn escape(input: &str) -> IResult<&str, char, KdlParseError<&str>> {
|
|||
}
|
||||
|
||||
fn unicode(input: &str) -> IResult<&str, char, KdlParseError<&str>> {
|
||||
// TODO: This should only accept up to 0x10FFFF.
|
||||
map_opt(
|
||||
map_res(
|
||||
take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()),
|
||||
|
|
@ -448,7 +476,7 @@ fn raw_string(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&s
|
|||
raw.push('r');
|
||||
let (input, hashes) = recognize(many0(char('#')))(input)?;
|
||||
raw.push_str(hashes);
|
||||
let (input, _) = cut(char('"'))(input)?;
|
||||
let (input, _) = char('"')(input)?;
|
||||
raw.push('"');
|
||||
let close = format!("\"{}", hashes);
|
||||
let (input, value) = take_until(&close[..])(input)?;
|
||||
|
|
@ -528,6 +556,7 @@ fn hexadecimal(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&
|
|||
)),
|
||||
move |(raw_body, hex): (&str, &str)| {
|
||||
raw.push_str(raw_body);
|
||||
// TODO: Failure in case of int overflow!
|
||||
i64::from_str_radix(&str::replace(hex, "_", ""), 16)
|
||||
.map(|x| x * sign)
|
||||
.map(|x| (raw.clone(), KdlValue::Base16(x)))
|
||||
|
|
@ -664,6 +693,10 @@ mod comment_tests {
|
|||
#[test]
|
||||
fn multi_line() {
|
||||
assert_eq!(comment("/* Hello world */"), Ok(("", "/* Hello world */")));
|
||||
assert_eq!(
|
||||
comment("/* Hello /* world */ blah */"),
|
||||
Ok(("", "/* Hello /* world */ blah */"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -752,6 +785,8 @@ mod value_tests {
|
|||
)
|
||||
))
|
||||
);
|
||||
let (_, n) = node("node 0x0123_4567_89ab_cdef").expect("failed to parse node");
|
||||
assert_eq!(&n[0], &KdlValue::Base16(0x0123456789abcdef));
|
||||
assert_eq!(
|
||||
value("0x123_4567"),
|
||||
Ok(("", ("0x123_4567".into(), KdlValue::Base16(0x1234567))))
|
||||
|
|
|
|||
35
src/value.rs
35
src/value.rs
|
|
@ -151,11 +151,23 @@ impl Display for KdlValue {
|
|||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::RawString(_) => self.write_raw_string(f),
|
||||
Self::String(string) => write!(f, "{:?}", string),
|
||||
Self::String(_) => self.write_string(f),
|
||||
Self::Base2(value) => write!(f, "0b{:b}", value),
|
||||
Self::Base8(value) => write!(f, "0o{:o}", value),
|
||||
Self::Base10(value) => write!(f, "{}", value),
|
||||
Self::Base10Float(value) => write!(f, "{}", value),
|
||||
Self::Base10(value) => write!(f, "{:?}", value),
|
||||
Self::Base10Float(value) => write!(
|
||||
f,
|
||||
"{:?}",
|
||||
if value == &f64::INFINITY {
|
||||
f64::MAX
|
||||
} else if value == &f64::NEG_INFINITY {
|
||||
-f64::MAX
|
||||
} else if value.is_nan() {
|
||||
0.0
|
||||
} else {
|
||||
*value
|
||||
}
|
||||
),
|
||||
Self::Base16(value) => write!(f, "0x{:x}", value),
|
||||
Self::Bool(value) => write!(f, "{}", value),
|
||||
Self::Null => write!(f, "null"),
|
||||
|
|
@ -164,6 +176,23 @@ impl Display for KdlValue {
|
|||
}
|
||||
|
||||
impl KdlValue {
|
||||
fn write_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let string = self.as_string().unwrap();
|
||||
write!(f, "\"")?;
|
||||
for char in string.chars() {
|
||||
match char {
|
||||
'\\' | '"' => write!(f, "\\{}", char)?,
|
||||
'\n' => write!(f, "\\n")?,
|
||||
'\r' => write!(f, "\\r")?,
|
||||
'\t' => write!(f, "\\t")?,
|
||||
'\u{08}' => write!(f, "\\b")?,
|
||||
'\u{0C}' => write!(f, "\\f")?,
|
||||
_ => write!(f, "{}", char)?,
|
||||
}
|
||||
}
|
||||
write!(f, "\"")?;
|
||||
Ok(())
|
||||
}
|
||||
fn write_raw_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let raw = self.as_string().unwrap();
|
||||
let mut consecutive = 0usize;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
# Full Document Test Cases
|
||||
|
||||
The `input` folder contains test cases for KDL parsers. The `expected_kdl`
|
||||
folder contains files with the same name as those in `input` with the expected
|
||||
output after being run through the parser and printed out again. If there's no
|
||||
file in `expected_kdl` with a name corresponding to one in `input` it
|
||||
indicates that parsing for that case should fail.
|
||||
|
||||
## Translation Rules
|
||||
|
||||
By necessity, the files in `expected_kdl` are not identical to their
|
||||
corresponding inputs. They are instead pretty-printed according to the
|
||||
following rules:
|
||||
|
||||
* All comments removed
|
||||
* Extra empty lines removed except for a newline after the last node
|
||||
* All nodes should be reformatted without escaped newlines
|
||||
* Node fields should be `identifier <values> <properties> <children only if non-empty>`
|
||||
* All values and all children must be in the same order as they were defined.
|
||||
* Properties must be in _alphabetical order_ and separated by a single space.
|
||||
* All strings must be represented as regular strings, with appropriate escapes
|
||||
for invalid bare characters. That means that raw strings must be converted
|
||||
to plain strings, and escaped.
|
||||
* Any literal newlines or other ascii escape characters in escaped strings
|
||||
replaced with their escape sequences.
|
||||
* All identifiers must be unquoted unless they _must_ be quoted. That means
|
||||
`"foo"` becomes `foo`, and `"foo bar"` stays that way.
|
||||
* Any duplicate properties must be removed, with only the rightmost one
|
||||
remaining. This also means duplicate properties must be allowed.
|
||||
* 4 space indents
|
||||
* All numbers must be converted to their simplest decimal representation. That
|
||||
means that hex, octal, and binary must all be converted to decimals. All
|
||||
floats must be represented using `E` notation, with a single digit left of
|
||||
the decimal point if the float is less than 1. While parsers are required to
|
||||
_consume_ different number syntaxes, they are under no obligation to
|
||||
represent numbers in any particular way.
|
||||
|
||||
Data may be manipulated as you wish in order to output the expected KDL. This
|
||||
test suite verifies the ability to **parse**, not specific quirks about
|
||||
internal representations.
|
||||
|
||||
## What to do if a test fails for you
|
||||
|
||||
This test suite was originally designed for a pre-1.0 version of the KDL
|
||||
specification. If you encounter a failure, it's likely that the test suite
|
||||
will need to be updated, rather than your parser itself. This test suite is
|
||||
NOT AUTHORITATIVE. If this test suite disagrees with the KDL spec in any way,
|
||||
the most desirable resolution is to send a PR to this repository to fix the
|
||||
test itself. Likewise, if you think a test succeeded but should not have,
|
||||
please send a PR.
|
||||
|
||||
If you think the disagreement is due to a genuine error or oversight in the
|
||||
KDL specification, please open an issue explaining the matter and the change
|
||||
will be considered for the next version of the KDL spec.
|
||||
|
||||
## Credit
|
||||
|
||||
This test suite was extracted from
|
||||
[`kdl4j`](https://github.com/hkolbeck/kdl4j), the original Java
|
||||
implementation of KDL, with huge thanks to
|
||||
[@hkolbeck](https://github.com/hkolbeck) for authoring them!
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use kdl::{KdlDocument, KdlError, KdlIdentifier, KdlValue};
|
||||
use miette::IntoDiagnostic;
|
||||
|
||||
#[test]
|
||||
fn spec_compliance() -> miette::Result<()> {
|
||||
let input = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests")
|
||||
.join("test_cases")
|
||||
.join("input");
|
||||
for test_name in fs::read_dir(&input).into_diagnostic()? {
|
||||
let test_path = test_name.into_diagnostic()?.path();
|
||||
println!(
|
||||
"parsing {}:",
|
||||
PathBuf::from(test_path.file_name().unwrap()).display()
|
||||
);
|
||||
let src = normalize_line_endings(fs::read_to_string(&test_path).into_diagnostic()?);
|
||||
println!("src: {}", src);
|
||||
let res: Result<KdlDocument, KdlError> = src.parse();
|
||||
validate_res(res, &test_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_res(res: Result<KdlDocument, KdlError>, path: &Path) -> miette::Result<()> {
|
||||
let file_name = path.file_name().unwrap();
|
||||
let expected_dir = path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("expected_kdl");
|
||||
let expected_path = expected_dir.join(file_name);
|
||||
let underscored = expected_dir.join(&format!("_{}", PathBuf::from(file_name).display()));
|
||||
if expected_path.exists() {
|
||||
let doc = res?;
|
||||
let expected =
|
||||
normalize_line_endings(fs::read_to_string(&expected_path).into_diagnostic()?);
|
||||
println!("expected: {}", expected);
|
||||
let stringified = stringify_to_expected(doc);
|
||||
println!("stringified: {}", stringified);
|
||||
assert_eq!(stringified, expected);
|
||||
} else if underscored.exists() {
|
||||
println!(
|
||||
"skipped reserialization for {}",
|
||||
PathBuf::from(file_name).display()
|
||||
);
|
||||
} else {
|
||||
assert!(res.is_err(), "parse should not have succeeded");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_line_endings(src: String) -> String {
|
||||
src.replace("\r\n", "\n")
|
||||
}
|
||||
|
||||
fn stringify_to_expected(mut doc: KdlDocument) -> String {
|
||||
doc.fmt_no_comments();
|
||||
normalize_numbers(&mut doc);
|
||||
normalize_strings(&mut doc);
|
||||
dedupe_props(&mut doc);
|
||||
remove_empty_children(&mut doc);
|
||||
doc.to_string()
|
||||
}
|
||||
|
||||
fn normalize_numbers(doc: &mut KdlDocument) {
|
||||
for node in doc.nodes_mut() {
|
||||
for entry in node.entries_mut() {
|
||||
if let Some(value) = entry.value().as_i64() {
|
||||
*entry.value_mut() = KdlValue::Base10(value);
|
||||
}
|
||||
}
|
||||
if let Some(children) = node.children_mut() {
|
||||
normalize_numbers(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_strings(doc: &mut KdlDocument) {
|
||||
for node in doc.nodes_mut() {
|
||||
for entry in node.entries_mut() {
|
||||
if let Some(value) = entry.value().as_string() {
|
||||
*entry.value_mut() = KdlValue::String(value.to_string());
|
||||
}
|
||||
}
|
||||
if let Some(children) = node.children_mut() {
|
||||
normalize_strings(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dedupe_props(doc: &mut KdlDocument) {
|
||||
for node in doc.nodes_mut() {
|
||||
let mut props = HashMap::<KdlIdentifier, Vec<usize>>::new();
|
||||
for (idx, entry) in node.entries_mut().iter_mut().enumerate() {
|
||||
if let Some(name) = entry.name() {
|
||||
if !props.contains_key(name) {
|
||||
props.insert(name.clone(), Vec::new());
|
||||
}
|
||||
if let Some(indices) = props.get_mut(name) {
|
||||
indices.push(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
let new_entries = node
|
||||
.entries()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, entry)| {
|
||||
if let Some(name) = entry.name() {
|
||||
if let Some(indices) = props.get(name) {
|
||||
if &idx == indices.last().unwrap() {
|
||||
return Some(entry.clone());
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(entry.clone())
|
||||
});
|
||||
*node.entries_mut() = new_entries.collect();
|
||||
if let Some(children) = node.children_mut() {
|
||||
dedupe_props(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_empty_children(doc: &mut KdlDocument) {
|
||||
for node in doc.nodes_mut() {
|
||||
let maybe_children = node.children_mut();
|
||||
if maybe_children.is_some() && maybe_children.as_ref().unwrap().nodes().is_empty() {
|
||||
*maybe_children = None;
|
||||
}
|
||||
if let Some(children) = maybe_children {
|
||||
remove_empty_children(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1e-10
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1 1.0 10000000000.0 1e-10 1 7 2 "arg" "arg\\\\" true false null
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 10000000000.0
|
||||
|
|
@ -0,0 +1 @@
|
|||
node prop=1.23E+1000
|
||||
|
|
@ -0,0 +1 @@
|
|||
node prop=1.23E-1000
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1e-100
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "\"\\/\b\f\n\r\t"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
node "arg" prop="val" {
|
||||
inner_node
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg" arg="val"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)false
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)2.5
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)16
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)null
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)"str"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)"str"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)true
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)"arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node (type)0
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
😁 "happy!"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node ("")10
|
||||
|
|
@ -0,0 +1 @@
|
|||
("")node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node key=("")true
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
node false true
|
||||
|
|
@ -0,0 +1 @@
|
|||
node prop1=true prop2=false
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg2"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node_2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node_2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
node1
|
||||
node2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "😀"
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
"" "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node ""="empty"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node ""
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "hello\nworld"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "hello\nworld"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg" "arg2\n"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
node1
|
||||
node2
|
||||
|
|
@ -0,0 +1 @@
|
|||
false_id
|
||||
|
|
@ -0,0 +1 @@
|
|||
node false_id=1
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1311768467294899695
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1311768467294899695
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 737894400291
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1234
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
node {
|
||||
inner_node
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1 @@
|
|||
node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 11
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg1" "arg2"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node " hey\neveryone\nhow goes?\n"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node -1.0 key=-10.0
|
||||
|
|
@ -0,0 +1 @@
|
|||
node -10 prop=-15
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
node1 {
|
||||
node2 {
|
||||
node
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
node1
|
||||
node2
|
||||
|
|
@ -0,0 +1 @@
|
|||
node "arg"
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 10000000000.0
|
||||
|
|
@ -0,0 +1 @@
|
|||
node false
|
||||
|
|
@ -0,0 +1 @@
|
|||
node true
|
||||
|
|
@ -0,0 +1 @@
|
|||
(type)node
|
||||
|
|
@ -0,0 +1 @@
|
|||
node null
|
||||
|
|
@ -0,0 +1 @@
|
|||
null_id
|
||||
|
|
@ -0,0 +1 @@
|
|||
node null_id=1
|
||||
|
|
@ -0,0 +1 @@
|
|||
node prop=null
|
||||
|
|
@ -0,0 +1 @@
|
|||
node 15.7
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue