use std::collections::{BTreeMap, BTreeSet};
use chumsky::prelude::*;
use crate::ast::{Decimal, Integer, Literal, Node, Radix, TypeName, Value};
use crate::ast::{Document, SpannedName, SpannedNode};
use crate::errors::{ParseError as Error, TokenFormat};
use crate::span::Spanned;
use crate::traits::Span;
use chumsky::chain::Chain;
use chumsky::combinator::{Map, Then};
type MapChar<O, U> = fn(_: (O, U)) -> Vec<char>;
trait ChainChar<I: Clone, O> {
type Error;
fn chain_c<U, P>(self, other: P) -> Map<Then<Self, P>, MapChar<O, U>, (O, U)>
where
Self: Sized,
U: Chain<char>,
O: Chain<char>,
P: Parser<I, U, Error = Self::Error>;
}
impl<I: Clone, O, R: Parser<I, O>> ChainChar<I, O> for R {
type Error = <R as Parser<I, O>>::Error;
fn chain_c<U, P>(self, other: P) -> Map<Then<Self, P>, MapChar<O, U>, (O, U)>
where
Self: Sized,
U: Chain<char>,
O: Chain<char>,
P: Parser<I, U, Error = Self::Error>,
{
Parser::chain(self, other)
}
}
fn begin_comment<S: Span>(which: char) -> impl Parser<char, (), Error = Error<S>> + Clone {
just('/')
.map_err(|e: Error<S>| e.with_no_expected())
.ignore_then(just(which).ignored())
}
fn newline<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
just('\r')
.or_not()
.ignore_then(just('\n'))
.or(just('\r')) .or(just('\x0C')) .or(just('\x0B')) .or(just('\u{0085}')) .or(just('\u{2028}')) .or(just('\u{2029}')) .ignored()
.map_err(|e: Error<S>| e.with_expected_kind("newline"))
}
fn ws_char<S: Span>() -> impl Parser<char, (), Error = Error<S>> + Clone {
filter(|c| {
matches!(
c,
'\t' | ' ' | '\u{00a0}' | '\u{1680}' | '\u{2000}'
..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
)
})
.ignored()
}
fn id_char<S: Span>() -> impl Parser<char, char, Error = Error<S>> {
filter(|c| {
!matches!(c,
'\u{0000}'..='\u{0021}' |
'\\'|'/'|'('|')'|'{'|'}'|';'|'['|']'|'='|'"'|'#' |
'\u{00a0}' | '\u{1680}' |
'\u{2000}'..='\u{200A}' |
'\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}' |
'\u{0085}' | '\u{2028}' | '\u{2029}'
)
})
.map_err(|e: Error<S>| e.with_expected_kind("letter"))
}
fn id_sans_dig<S: Span>() -> impl Parser<char, char, Error = Error<S>> {
filter(|c| {
!matches!(c,
'0'..='9' |
'\u{0000}'..='\u{0020}' |
'\\'|'/'|'('|')'|'{'|'}'|';'|'['|']'|'='|'"'|'#' |
'\u{00a0}' | '\u{1680}' |
'\u{2000}'..='\u{200A}' |
'\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}' |
'\u{0085}' | '\u{2028}' | '\u{2029}'
)
})
.map_err(|e: Error<S>| e.with_expected_kind("letter"))
}
fn id_sans_dig_point<S: Span>() -> impl Parser<char, char, Error = Error<S>> {
filter(|c| {
!matches!(c,
'0'..='9' | '.' |
'\u{0000}'..='\u{0020}' |
'\\'|'/'|'('|')'|'{'|'}'|';'|'['|']'|'='|'"'|'#' |
'\u{00a0}' | '\u{1680}' |
'\u{2000}'..='\u{200A}' |
'\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}' |
'\u{0085}' | '\u{2028}' | '\u{2029}'
)
})
.map_err(|e: Error<S>| e.with_expected_kind("letter"))
}
fn id_sans_sign_dig_point<S: Span>() -> impl Parser<char, char, Error = Error<S>> {
filter(|c| {
!matches!(c,
'-'| '+' | '0'..='9' |
'\u{0000}'..='\u{0020}' |
'\\'|'/'|'('|')'|'{'|'}'|';'|'['|']'|'='|'"'|'#' |
'\u{00a0}' | '\u{1680}' |
'\u{2000}'..='\u{200A}' |
'\u{202F}' | '\u{205F}' | '\u{3000}' | '\u{FEFF}' |
'\u{0085}' | '\u{2028}' | '\u{2029}'
)
})
.map_err(|e: Error<S>| e.with_expected_kind("letter"))
}
fn ws<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
ws_char()
.repeated()
.at_least(1)
.ignored()
.or(ml_comment())
.map_err(|e| e.with_expected_kind("whitespace"))
}
fn comment<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
begin_comment('/')
.then(take_until(newline().or(end())))
.ignored()
}
fn ml_comment<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
recursive::<_, _, _, _, Error<S>>(|comment| {
choice((
comment,
none_of('*').ignored(),
just('*').then_ignore(none_of('/').rewind()).ignored(),
))
.repeated()
.ignored()
.delimited_by(begin_comment('*'), just("*/"))
})
.map_err_with_span(|e, span| {
if matches!(
&e,
Error::Unexpected {
found: TokenFormat::Eoi,
..
}
) && span.length() > 2
{
e.merge(Error::Unclosed {
label: "comment",
opened_at: span.at_start(2),
opened: "/*".into(),
expected_at: span.at_end(),
expected: "*/".into(),
found: None.into(),
})
} else {
e
}
})
}
fn raw_string<S: Span>() -> impl Parser<char, Box<str>, Error = Error<S>> {
just('#').repeated().at_least(1).map(|v| v.len())
.then_ignore(just('"'))
.then_with(|sharp_num| {
take_until(just('"').ignore_then(just('#').repeated().exactly(sharp_num).ignored()))
.map_err_with_span(move |e: Error<S>, span| {
if matches!(
&e,
Error::Unexpected {
found: TokenFormat::Eoi,
..
}
) {
e.merge(Error::Unclosed {
label: "raw string",
opened_at: span.before_start(sharp_num + 2),
opened: TokenFormat::OpenRaw(sharp_num),
expected_at: span.at_end(),
expected: TokenFormat::CloseRaw(sharp_num),
found: None.into(),
})
} else {
e
}
})
})
.map(|(text, ())| text.into_iter().collect::<String>().into())
}
fn string<S: Span>() -> impl Parser<char, Box<str>, Error = Error<S>> {
raw_string().or(escaped_string())
}
fn expected_kind(s: &'static str) -> BTreeSet<TokenFormat> {
[TokenFormat::Kind(s)].into_iter().collect()
}
fn esc_char<S: Span>() -> impl Parser<char, char, Error = Error<S>> {
filter_map(|span, c| match c {
'"' | '\\' => Ok(c),
'b' => Ok('\u{0008}'),
'f' => Ok('\u{000C}'),
'n' => Ok('\n'),
'r' => Ok('\r'),
't' => Ok('\t'),
's' => Ok(' '),
c => Err(Error::Unexpected {
label: Some("invalid escape char"),
span,
found: c.into(),
expected: "\"\\bfnrts".chars().map(|c| c.into()).collect(),
}),
})
.or(just('u').ignore_then(
filter_map(|span, c: char| {
c.is_ascii_hexdigit()
.then_some(c)
.ok_or_else(|| Error::Unexpected {
label: Some("unexpected character"),
span,
found: c.into(),
expected: expected_kind("hexadecimal digit"),
})
})
.repeated()
.at_least(1)
.at_most(6)
.delimited_by(just('{'), just('}'))
.try_map(|hex_chars, span| {
let s = hex_chars.into_iter().collect::<String>();
let c = u32::from_str_radix(&s, 16)
.map_err(|e| e.to_string())
.and_then(|n| char::try_from(n).map_err(|e| e.to_string()))
.map_err(|e| Error::Message {
label: Some("invalid character code"),
span,
message: e.to_string(),
})?;
Ok(c)
})
.recover_with(skip_until(['}', '"', '\\'], |_| '\0')),
))
}
fn escaped_string<S: Span>() -> impl Parser<char, Box<str>, Error = Error<S>> {
just('"').ignore_then(
choice((
filter(|&c| c != '"' && c != '\\'),
just('\\').ignore_then(esc_char()),
just('\\').then(ws_char().or(newline()).repeated().at_least(1)).map(|_| ' '),
))
.repeated()
.then_ignore(just('"'))
.map(|val| val.into_iter().collect::<String>().into())
.map_err_with_span(|e: Error<S>, span| {
if matches!(
&e,
Error::Unexpected {
found: TokenFormat::Eoi,
..
}
) {
e.merge(Error::Unclosed {
label: "string",
opened_at: span.before_start(1),
opened: '"'.into(),
expected_at: span.at_end(),
expected: '"'.into(),
found: None.into(),
})
} else {
e
}
}),
)
}
fn bare_ident<S: Span>() -> impl Parser<char, Box<str>, Error = Error<S>> {
let sign = just('+').or(just('-'));
choice((
id_sans_sign_dig_point().chain(id_char().repeated()),
sign.chain(id_sans_dig_point().chain(id_char().repeated()).or_not()),
sign.or_not().chain(just('.')).chain(id_sans_dig().chain(id_char().repeated()).or_not()),
))
.map(|v| v.into_iter().collect())
.try_map(|s: String, span| match &s[..] {
"true" | "false" | "null" | "nan" | "inf" | "-inf" => Err(Error::Message {
label: Some("illegal identifier"),
span,
message: format!("`{s}` is not allowed as a bare string"),
}),
"#true" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#true"),
expected: expected_kind("identifier"),
}),
"#false" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#false"),
expected: expected_kind("identifier"),
}),
"#null" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#null"),
expected: expected_kind("identifier"),
}),
"#nan" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#nan"),
expected: expected_kind("identifier"),
}),
"#inf" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#inf"),
expected: expected_kind("identifier"),
}),
"#-inf" => Err(Error::Unexpected {
label: Some("keyword"),
span,
found: TokenFormat::Token("#-inf"),
expected: expected_kind("identifier"),
}),
_ => Ok(s.into()),
})
}
fn ident<S: Span>() -> impl Parser<char, Box<str>, Error = Error<S>> {
choice((
number().map(Err),
bare_ident().map(Ok),
string().map(Ok),
))
.try_map(|res, span| {
res.map_err(|_| Error::Unexpected {
label: Some("unexpected number"),
span,
found: TokenFormat::Kind("number"),
expected: expected_kind("identifier"),
})
})
}
fn keyword<S: Span>() -> impl Parser<char, Literal, Error = Error<S>> {
choice((
just("#null")
.map_err(|e: Error<S>| e.with_expected_token("#null"))
.to(Literal::Null),
just("#true")
.map_err(|e: Error<S>| e.with_expected_token("#true"))
.to(Literal::Bool(true)),
just("#false")
.map_err(|e: Error<S>| e.with_expected_token("#false"))
.to(Literal::Bool(false)),
just("#nan")
.map_err(|e: Error<S>| e.with_expected_token("#nan"))
.to(Literal::Nan),
just("#inf")
.map_err(|e: Error<S>| e.with_expected_token("#inf"))
.to(Literal::Inf),
just("#-inf")
.map_err(|e: Error<S>| e.with_expected_token("#-inf"))
.to(Literal::NegInf),
))
}
fn digit<S: Span>(radix: u32) -> impl Parser<char, char, Error = Error<S>> {
filter(move |c: &char| c.is_digit(radix))
}
fn digits<S: Span>(radix: u32) -> impl Parser<char, Vec<char>, Error = Error<S>> {
filter(move |c: &char| c == &'_' || c.is_digit(radix)).repeated()
}
fn decimal_number<S: Span>() -> impl Parser<char, Literal, Error = Error<S>> {
just('-')
.or(just('+'))
.or_not()
.chain_c(digit(10))
.chain_c(digits(10))
.chain_c(
just('.')
.chain_c(digit(10))
.chain_c(digits(10))
.or_not()
.flatten(),
)
.chain_c(
just('e')
.or(just('E'))
.chain_c(just('-').or(just('+')).or_not())
.chain_c(digits(10))
.or_not()
.flatten(),
)
.map(|v| {
let is_decimal = v.iter().any(|c| matches!(c, '.' | 'e' | 'E'));
let s: String = v.into_iter().filter(|c| c != &'_').collect();
if is_decimal {
Literal::Decimal(Decimal(s.into()))
} else {
Literal::Int(Integer(Radix::Dec, s.into()))
}
})
}
fn radix_number<S: Span>() -> impl Parser<char, Literal, Error = Error<S>> {
just('-')
.or(just('+'))
.or_not()
.then_ignore(just('0'))
.then(choice((
just('b').ignore_then(digit(2).chain(digits(2)).map(|s| (Radix::Bin, s))),
just('o').ignore_then(digit(8).chain(digits(8)).map(|s| (Radix::Oct, s))),
just('x').ignore_then(digit(16).chain(digits(16)).map(|s| (Radix::Hex, s))),
)))
.map(|(sign, (radix, value))| {
let mut s = String::with_capacity(value.len() + sign.map_or(0, |_| 1));
if let Some(c) = sign {
s.push(c);
}
s.extend(value.into_iter().filter(|&c| c != '_'));
Literal::Int(Integer(radix, s.into()))
})
}
fn number<S: Span>() -> impl Parser<char, Literal, Error = Error<S>> {
radix_number().or(decimal_number())
}
fn literal<S: Span>() -> impl Parser<char, Literal, Error = Error<S>> {
choice((ident().map(Literal::String), keyword(), number()))
}
fn type_name<S: Span>() -> impl Parser<char, TypeName, Error = Error<S>> {
ident()
.delimited_by(
just('(').then(ws_char().repeated()),
ws_char().repeated().then(just(')'))
)
.map(TypeName::from_string)
}
fn spanned<T, S, P>(p: P) -> impl Parser<char, Spanned<T, S>, Error = Error<S>>
where
P: Parser<char, T, Error = Error<S>>,
S: Span,
{
p.map_with_span(|value, span| Spanned { span, value })
}
fn esc_line<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
just('\\')
.ignore_then(ws().repeated())
.ignore_then(comment().or(newline()).or(end()))
}
fn node_space<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
ws().or(esc_line())
}
fn node_terminator<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
choice((newline(), comment(), just(';').ignored(), end()))
}
enum PropOrArg<S> {
Prop(SpannedName<S>, Value<S>),
Arg(Value<S>),
Ignore,
}
fn type_name_value<S: Span>() -> impl Parser<char, Value<S>, Error = Error<S>> {
spanned(type_name().then_ignore(ws_char().repeated()))
.then(spanned(literal()))
.map(|(type_name, literal)| Value {
type_name: Some(type_name),
literal,
})
}
fn value<S: Span>() -> impl Parser<char, Value<S>, Error = Error<S>> {
type_name_value().or(spanned(literal()).map(|literal| Value {
type_name: None,
literal,
}))
}
fn prop_or_arg_inner<S: Span>() -> impl Parser<char, PropOrArg<S>, Error = Error<S>> {
use PropOrArg::*;
choice((
spanned(literal())
.then(
ws_char().repeated()
.then(just('='))
.then(ws_char().repeated())
.ignore_then(value()).or_not()
)
.try_map(|(name, value), _| {
let name_span = name.span;
match (name.value, value) {
(Literal::String(s), Some(value)) => {
let name = Spanned {
span: name_span,
value: s,
};
Ok(Prop(name, value))
}
(Literal::Bool(_) | Literal::Null | Literal::Nan | Literal::Inf | Literal::NegInf, Some(_)) => Err(Error::Unexpected {
label: Some("unexpected keyword"),
span: name_span,
found: TokenFormat::Kind("keyword"),
expected: [TokenFormat::Kind("identifier"), TokenFormat::Kind("string")]
.into_iter()
.collect(),
}),
(Literal::Int(_) | Literal::Decimal(_), Some(_)) => {
Err(Error::MessageWithHelp {
label: Some("unexpected number"),
span: name_span,
message: "numbers cannot be used as property names".into(),
help: "consider enclosing in double quotes \"..\"",
})
}
(value, None) => Ok(Arg(Value {
type_name: None,
literal: Spanned {
span: name_span,
value,
},
})),
}
}),
spanned(bare_ident())
.then(
ws_char().repeated()
.then(just('='))
.then(ws_char().repeated())
.ignore_then(value()).or_not()
)
.validate(|(name, value), span, emit| {
if value.is_none() {
emit(Error::MessageWithHelp {
label: Some("unexpected identifier"),
span,
message: "identifiers cannot be used as arguments".into(),
help: "consider enclosing in double quotes \"..\"",
});
}
(name, value)
})
.map(|(name, value)| {
if let Some(value) = value {
Prop(name, value)
} else {
Arg(Value {
type_name: None,
literal: name.map(Literal::String),
})
}
}),
type_name_value().map(Arg),
))
}
fn prop_or_arg<S: Span>() -> impl Parser<char, PropOrArg<S>, Error = Error<S>> {
begin_comment('-')
.ignore_then(line_space().repeated())
.ignore_then(prop_or_arg_inner())
.map(|_| PropOrArg::Ignore)
.or(prop_or_arg_inner())
}
fn line_space<S: Span>() -> impl Parser<char, (), Error = Error<S>> {
newline().or(ws()).or(comment())
}
fn nodes<S: Span>() -> impl Parser<char, Vec<SpannedNode<S>>, Error = Error<S>> {
use PropOrArg::*;
recursive(|nodes: chumsky::recursive::Recursive<char, _, Error<S>>| {
let braced_nodes =
just('{').ignore_then(nodes.then_ignore(just('}')).map_err_with_span(|e, span| {
if matches!(
&e,
Error::Unexpected {
found: TokenFormat::Eoi,
..
}
) {
e.merge(Error::Unclosed {
label: "curly braces",
opened_at: span.before_start(1),
opened: '{'.into(),
expected_at: span.at_end(),
expected: '}'.into(),
found: None.into(),
})
} else {
e
}
}));
let node = spanned(type_name().then_ignore(ws_char().repeated()))
.or_not()
.then(spanned(ident()))
.then(
node_space()
.repeated()
.at_least(1)
.ignore_then(prop_or_arg())
.repeated(),
)
.then(
node_space()
.repeated()
.ignore_then(
begin_comment('-')
.then_ignore(line_space().repeated())
.or_not(),
)
.then(spanned(braced_nodes))
.or_not(),
)
.then_ignore(node_space().repeated().then(node_terminator().or_not()))
.map(|(((type_name, node_name), line_items), opt_children)| {
let mut node = Node {
type_name,
node_name,
properties: BTreeMap::new(),
arguments: Vec::new(),
children: match opt_children {
Some((Some(_comment), _)) => None,
Some((None, children)) => Some(children),
None => None,
},
};
for item in line_items {
match item {
Prop(name, value) => {
node.properties.insert(name, value);
}
Arg(value) => {
node.arguments.push(value);
}
Ignore => {}
}
}
node
});
begin_comment('-')
.then_ignore(line_space().repeated())
.or_not()
.then(spanned(node))
.separated_by(line_space().repeated())
.allow_leading()
.allow_trailing()
.map(|vec| {
vec.into_iter()
.filter_map(
|(comment, node)| {
if comment.is_none() {
Some(node)
} else {
None
}
},
)
.collect()
})
})
}
pub(crate) fn document<S: Span>() -> impl Parser<char, Document<S>, Error = Error<S>> {
just('\u{FEFF}').or_not().ignore_then(nodes()).then_ignore(end()).map(|nodes| Document { nodes })
}
#[cfg(test)]
mod test {
use super::{comment, ident, literal, ml_comment, string, type_name, ws};
use super::{nodes, number};
use crate::ast::{Decimal, Integer, Literal, Radix, TypeName};
use crate::errors::{Error, ParseError};
use crate::span::Span;
use crate::traits::sealed::Sealed;
use chumsky::prelude::*;
use miette::NamedSource;
macro_rules! err_eq {
($left: expr, $right: expr) => {
let left = $left.unwrap_err();
let left: serde_json::Value = serde_json::from_str(&left).unwrap();
let right: serde_json::Value =
serde_json::from_str($right).unwrap();
assert_json_diff::assert_json_include!(
actual: left, expected: right);
}
}
fn parse<P, T>(p: P, text: &str) -> Result<T, String>
where
P: Parser<char, T, Error = ParseError<Span>>,
{
p.then_ignore(end())
.parse(Span::stream(text))
.map_err(|errors| {
let source = text.to_string() + " ";
let e = Error {
source_code: NamedSource::new("<test>", source),
errors: errors.into_iter().map(Into::into).collect(),
};
let mut buf = String::with_capacity(512);
miette::GraphicalReportHandler::new()
.render_report(&mut buf, &e)
.unwrap();
println!("{}", buf);
buf.truncate(0);
miette::JSONReportHandler::new()
.render_report(&mut buf, &e)
.unwrap();
buf
})
}
#[test]
fn parse_ws() {
parse(ws(), " ").unwrap();
parse(ws(), "text").unwrap_err();
}
#[test]
fn parse_comments() {
parse(comment(), "//hello").unwrap();
parse(comment(), "//hello\n").unwrap();
parse(ml_comment(), "/*nothing*/").unwrap();
parse(ml_comment(), "/*nothing**/").unwrap();
parse(ml_comment(), "/*no*thing*/").unwrap();
parse(ml_comment(), "/*no/**/thing*/").unwrap();
parse(ml_comment(), "/*no/*/**/*/thing*/").unwrap();
parse(ws().then(comment()), " // hello").unwrap();
parse(
ws().then(comment()).then(ws()).then(comment()),
" // hello\n //world",
)
.unwrap();
}
#[test]
fn parse_comment_err() {
err_eq!(
parse(ws(), r#"/* comment"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed comment `/*`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `*/`",
"span": {"offset": 10, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(ws(), r#"/* com/*ment *"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed comment `/*`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `*/`",
"span": {"offset": 14, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(ws(), r#"/* com/*me*/nt *"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed comment `/*`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `*/`",
"span": {"offset": 16, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(ws(), r#"/* comment *"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed comment `/*`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `*/`",
"span": {"offset": 12, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(ws(), r#"/*/"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed comment `/*`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `*/`",
"span": {"offset": 3, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(ws(), r#"xxx"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found `x`, expected whitespace",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected token",
"span": {"offset": 0, "length": 1}}
],
"related": []
}]
}"#
);
}
#[test]
fn parse_str() {
assert_eq!(&*parse(string(), r#""hello""#).unwrap(), "hello");
assert_eq!(&*parse(string(), r#""""#).unwrap(), "");
assert_eq!(&*parse(string(), r#""hel\"lo""#).unwrap(), "hel\"lo");
assert_eq!(
&*parse(string(), r#""hello\nworld!""#).unwrap(),
"hello\nworld!"
);
assert_eq!(&*parse(string(), r#""\u{1F680}""#).unwrap(), "🚀");
}
#[test]
fn parse_raw_str() {
assert_eq!(&*parse(string(), r#""hello""#).unwrap(), "hello");
assert_eq!(&*parse(string(), r##"#"world"#"##).unwrap(), "world");
assert_eq!(&*parse(string(), r##"#"world"#"##).unwrap(), "world");
assert_eq!(
&*parse(string(), r####"###"a\n"##b"###"####).unwrap(),
"a\\n\"##b"
);
}
#[test]
fn parse_str_err() {
err_eq!(
parse(string(), r#""hello"#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed string `\"`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 1}},
{"label": "expected `\"`",
"span": {"offset": 6, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(string(), r#""he\u{FFFFFF}llo""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "converted integer out of range for `char`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "invalid character code",
"span": {"offset": 5, "length": 8}}
],
"related": []
}]
}"#
);
err_eq!(
parse(string(), r#""he\u{1234567}llo""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found `7`, expected `}`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected token",
"span": {"offset": 12, "length": 1}}
],
"related": []
}]
}"#
);
err_eq!(
parse(string(), r#""he\u{1gh}llo""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found `g`, expected `}` or hexadecimal digit",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected token",
"span": {"offset": 7, "length": 1}}
],
"related": []
}]
}"#
);
err_eq!(
parse(string(), r#""he\x01llo""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"found `x`, expected `\"`, `\\`, `b`, `f`, `n`, `r`, `s`, `t` or `u`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "invalid escape char",
"span": {"offset": 4, "length": 1}}
],
"related": []
}]
}"#
);
err_eq!(
parse(string(), r#""he\u{FFFFFF}l\!lo""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "converted integer out of range for `char`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "invalid character code",
"span": {"offset": 5, "length": 8}}
],
"related": []
}, {
"message":
"found `!`, expected `\"`, `\\`, `b`, `f`, `n`, `r`, `s`, `t` or `u`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "invalid escape char",
"span": {"offset": 15, "length": 1}}
],
"related": []
}]
}"#
);
}
#[test]
fn parse_raw_str_err() {
err_eq!(
parse(string(), r#"#"hello"#),
r##"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed raw string `#\"`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `\"#`",
"span": {"offset": 7, "length": 0}}
],
"related": []
}]
}"##
);
err_eq!(
parse(string(), r###"#"hello""###),
r###"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed raw string `#\"`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 2}},
{"label": "expected `\"#`",
"span": {"offset": 8, "length": 0}}
],
"related": []
}]
}"###
);
err_eq!(
parse(string(), r####"###"hello"####),
r####"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed raw string `###\"`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 4}},
{"label": "expected `\"###`",
"span": {"offset": 9, "length": 0}}
],
"related": []
}]
}"####
);
err_eq!(
parse(string(), r####"###"hello"#world"####),
r####"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed raw string `###\"`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 0, "length": 4}},
{"label": "expected `\"###`",
"span": {"offset": 16, "length": 0}}
],
"related": []
}]
}"####
);
}
#[test]
fn parse_ident() {
assert_eq!(&*parse(ident(), "abcdef").unwrap(), "abcdef");
assert_eq!(&*parse(ident(), "xx_cd$yy").unwrap(), "xx_cd$yy");
assert_eq!(&*parse(ident(), "-").unwrap(), "-");
assert_eq!(&*parse(ident(), "--hello").unwrap(), "--hello");
assert_eq!(&*parse(ident(), "--hello1234").unwrap(), "--hello1234");
assert_eq!(&*parse(ident(), "--1").unwrap(), "--1");
assert_eq!(&*parse(ident(), "++1").unwrap(), "++1");
assert_eq!(&*parse(ident(), "-hello").unwrap(), "-hello");
assert_eq!(&*parse(ident(), "+hello").unwrap(), "+hello");
assert_eq!(&*parse(ident(), "-A").unwrap(), "-A");
assert_eq!(&*parse(ident(), "+b").unwrap(), "+b");
assert_eq!(
&*parse(ident().then_ignore(ws()), "adef ").unwrap(),
"adef"
);
assert_eq!(
&*parse(ident().then_ignore(ws()), "a123@ ").unwrap(),
"a123@"
);
parse(ident(), "1abc").unwrap_err();
parse(ident(), "-1").unwrap_err();
parse(ident(), "-1test").unwrap_err();
parse(ident(), "+1").unwrap_err();
}
#[test]
fn parse_literal() {
assert_eq!(parse(literal(), "#true").unwrap(), Literal::Bool(true));
assert_eq!(parse(literal(), "#false").unwrap(), Literal::Bool(false));
assert_eq!(parse(literal(), "#null").unwrap(), Literal::Null);
assert_eq!(parse(literal(), "#nan").unwrap(), Literal::Nan);
assert_eq!(parse(literal(), "#inf").unwrap(), Literal::Inf);
assert_eq!(parse(literal(), "#-inf").unwrap(), Literal::NegInf);
}
#[test]
fn exclude_keywords() {
parse(nodes(), "item #true").unwrap();
err_eq!(
parse(nodes(), "#true \"item\""),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"found `t`, expected `\"` or `#`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected token",
"span": {"offset": 1, "length": 1}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), "item #false=#true"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"found keyword, expected identifier or string",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected keyword",
"span": {"offset": 5, "length": 6}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), "item 2=2"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "numbers cannot be used as property names",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected number",
"span": {"offset": 5, "length": 1}}
],
"help": "consider enclosing in double quotes \"..\"",
"related": []
}]
}"#
);
}
#[test]
fn exclude_bare_keywords() {
err_eq!(
parse(nodes(), "item true"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"`true` is not allowed as a bare string",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "illegal identifier",
"span": {"offset": 5, "length": 4}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), r#"true "item""#),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"`true` is not allowed as a bare string",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "illegal identifier",
"span": {"offset": 0, "length": 4}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), "item false=#true"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message":
"`false` is not allowed as a bare string",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "illegal identifier",
"span": {"offset": 5, "length": 5}}
],
"related": []
}]
}"#
);
}
#[test]
fn parse_type() {
assert_eq!(
parse(type_name(), "(abcdef)").unwrap(),
TypeName::from_string("abcdef".into())
);
assert_eq!(
parse(type_name(), "(xx_cd$yy)").unwrap(),
TypeName::from_string("xx_cd$yy".into())
);
parse(type_name(), "(1abc)").unwrap_err();
assert_eq!(
parse(type_name(), "( abc)").unwrap(),
TypeName::from_string("abc".into())
);
assert_eq!(
parse(type_name(), "(abc )").unwrap(),
TypeName::from_string("abc".into())
);
}
#[test]
fn parse_type_err() {
err_eq!(
parse(type_name(), "(123)"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found number, expected identifier",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected number",
"span": {"offset": 1, "length": 3}}
],
"related": []
}]
}"#
);
err_eq!(
parse(type_name(), "(-1)"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found number, expected identifier",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected number",
"span": {"offset": 1, "length": 2}}
],
"related": []
}]
}"#
);
}
fn single<T, E: std::fmt::Debug>(r: Result<Vec<T>, E>) -> T {
let mut v = r.unwrap();
assert_eq!(v.len(), 1);
v.remove(0)
}
#[test]
fn parse_node() {
let nval = single(parse(nodes(), "hello"));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
let nval = single(parse(nodes(), "\"123\""));
assert_eq!(nval.node_name.as_ref(), "123");
assert_eq!(nval.type_name.as_ref(), None);
let nval = single(parse(nodes(), "(typ)other"));
assert_eq!(nval.node_name.as_ref(), "other");
assert_eq!(nval.type_name.as_ref().map(|x| &***x), Some("typ"));
let nval = single(parse(nodes(), "(typ) \tafter-ws"));
assert_eq!(nval.node_name.as_ref(), "after-ws");
assert_eq!(nval.type_name.as_ref().map(|x| &***x), Some("typ"));
let nval = single(parse(nodes(), "(\"std::duration\")\"timeout\""));
assert_eq!(nval.node_name.as_ref(), "timeout");
assert_eq!(
nval.type_name.as_ref().map(|x| &***x),
Some("std::duration")
);
let nval = single(parse(nodes(), "hello \"arg1\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&*nval.arguments[0].literal, &Literal::String("arg1".into()));
let nval = single(parse(nodes(), "node \"true\""));
assert_eq!(nval.node_name.as_ref(), "node");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&*nval.arguments[0].literal, &Literal::String("true".into()));
let nval = single(parse(nodes(), "hello (string)\"arg1\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&***nval.arguments[0].type_name.as_ref().unwrap(), "string");
assert_eq!(&*nval.arguments[0].literal, &Literal::String("arg1".into()));
let nval = single(parse(nodes(), "hello (typ) \t\"after whitespace\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&***nval.arguments[0].type_name.as_ref().unwrap(), "typ");
assert_eq!(&*nval.arguments[0].literal, &Literal::String("after whitespace".into()));
let nval = single(parse(nodes(), "hello key=(string)\"arg1\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 0);
assert_eq!(nval.properties.len(), 1);
assert_eq!(
&***nval
.properties
.get("key")
.unwrap()
.type_name
.as_ref()
.unwrap(),
"string"
);
assert_eq!(
&*nval.properties.get("key").unwrap().literal,
&Literal::String("arg1".into())
);
let nval = single(parse(nodes(), "hello key=\"arg1\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 0);
assert_eq!(nval.properties.len(), 1);
assert_eq!(
&*nval.properties.get("key").unwrap().literal,
&Literal::String("arg1".into())
);
let nval = single(parse(nodes(), "parent {\nchild\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.children().len(), 1);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child"
);
let nval = single(parse(nodes(), "parent {\nchild1\nchild2\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.children().len(), 2);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child1"
);
assert_eq!(
nval.children.as_ref().unwrap()[1].node_name.as_ref(),
"child2"
);
let nval = single(parse(nodes(), "parent{\nchild3\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.children().len(), 1);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child3"
);
let nval = single(parse(nodes(), "parent \"x\"=1 {\nchild4\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.properties.len(), 1);
assert_eq!(nval.children().len(), 1);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child4"
);
let nval = single(parse(nodes(), "parent \"x\" {\nchild4\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.children().len(), 1);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child4"
);
let nval = single(parse(nodes(), "parent \"x\"{\nchild5\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.children().len(), 1);
assert_eq!(
nval.children.as_ref().unwrap()[0].node_name.as_ref(),
"child5"
);
let nval = single(parse(nodes(), "hello /-\"skip_arg\" \"arg2\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&*nval.arguments[0].literal, &Literal::String("arg2".into()));
let nval = single(parse(nodes(), "hello /- \"skip_arg\" \"arg2\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 1);
assert_eq!(nval.properties.len(), 0);
assert_eq!(&*nval.arguments[0].literal, &Literal::String("arg2".into()));
let nval = single(parse(nodes(), "hello prop1=\"1\" /-prop1=\"2\""));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
assert_eq!(nval.arguments.len(), 0);
assert_eq!(nval.properties.len(), 1);
assert_eq!(
&*nval.properties.get("prop1").unwrap().literal,
&Literal::String("1".into())
);
let nval = single(parse(nodes(), "parent /-{\nchild\n}"));
assert_eq!(nval.node_name.as_ref(), "parent");
assert_eq!(nval.children().len(), 0);
}
#[test]
fn parse_node_whitespace() {
let nval = single(parse(nodes(), "hello { }"));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
let nval = single(parse(nodes(), "hello { } "));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
let nval = single(parse(nodes(), "hello "));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
let nval = single(parse(nodes(), "hello "));
assert_eq!(nval.node_name.as_ref(), "hello");
assert_eq!(nval.type_name.as_ref(), None);
}
#[test]
fn parse_node_err() {
err_eq!(
parse(nodes(), "hello{"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "unclosed curly braces `{`",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "opened here",
"span": {"offset": 5, "length": 1}},
{"label": "expected `}`",
"span": {"offset": 6, "length": 0}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), "1 + 2"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found number, expected identifier",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected number",
"span": {"offset": 0, "length": 1}}
],
"related": []
}]
}"#
);
err_eq!(
parse(nodes(), "-1 +2"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found number, expected identifier",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected number",
"span": {"offset": 0, "length": 2}}
],
"related": []
}]
}"#
);
}
#[test]
fn parse_nodes() {
let nval = parse(nodes(), "parent {\n/- child\n}").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "parent");
assert_eq!(nval[0].children().len(), 0);
let nval = parse(nodes(), "/-parent {\n child\n}\nsecond").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "second");
assert_eq!(nval[0].children().len(), 0);
}
#[test]
fn parse_number() {
assert_eq!(
parse(number(), "12").unwrap(),
Literal::Int(Integer(Radix::Dec, "12".into()))
);
assert_eq!(
parse(number(), "012").unwrap(),
Literal::Int(Integer(Radix::Dec, "012".into()))
);
assert_eq!(
parse(number(), "0").unwrap(),
Literal::Int(Integer(Radix::Dec, "0".into()))
);
assert_eq!(
parse(number(), "-012").unwrap(),
Literal::Int(Integer(Radix::Dec, "-012".into()))
);
assert_eq!(
parse(number(), "+0").unwrap(),
Literal::Int(Integer(Radix::Dec, "+0".into()))
);
assert_eq!(
parse(number(), "123_555").unwrap(),
Literal::Int(Integer(Radix::Dec, "123555".into()))
);
assert_eq!(
parse(number(), "123.555").unwrap(),
Literal::Decimal(Decimal("123.555".into()))
);
assert_eq!(
parse(number(), "+1_23.5_55E-17").unwrap(),
Literal::Decimal(Decimal("+123.555E-17".into()))
);
assert_eq!(
parse(number(), "123e+555").unwrap(),
Literal::Decimal(Decimal("123e+555".into()))
);
}
#[test]
fn parse_radix_number() {
assert_eq!(
parse(number(), "0x12").unwrap(),
Literal::Int(Integer(Radix::Hex, "12".into()))
);
assert_eq!(
parse(number(), "0xab_12").unwrap(),
Literal::Int(Integer(Radix::Hex, "ab12".into()))
);
assert_eq!(
parse(number(), "-0xab_12").unwrap(),
Literal::Int(Integer(Radix::Hex, "-ab12".into()))
);
assert_eq!(
parse(number(), "0o17").unwrap(),
Literal::Int(Integer(Radix::Oct, "17".into()))
);
assert_eq!(
parse(number(), "+0o17").unwrap(),
Literal::Int(Integer(Radix::Oct, "+17".into()))
);
assert_eq!(
parse(number(), "0b1010_101").unwrap(),
Literal::Int(Integer(Radix::Bin, "1010101".into()))
);
}
#[test]
fn parse_dashes() {
let nval = parse(nodes(), "-").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "-");
assert_eq!(nval[0].children().len(), 0);
let nval = parse(nodes(), "--").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "--");
assert_eq!(nval[0].children().len(), 0);
let nval = parse(nodes(), "--1").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "--1");
assert_eq!(nval[0].children().len(), 0);
let nval = parse(nodes(), "-\n-").unwrap();
assert_eq!(nval.len(), 2);
assert_eq!(nval[0].node_name.as_ref(), "-");
assert_eq!(nval[0].children().len(), 0);
assert_eq!(nval[1].node_name.as_ref(), "-");
assert_eq!(nval[1].children().len(), 0);
let nval = parse(nodes(), "node -1 --x=2").unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].arguments.len(), 1);
assert_eq!(nval[0].properties.len(), 1);
assert_eq!(
&*nval[0].arguments[0].literal,
&Literal::Int(Integer(Radix::Dec, "-1".into()))
);
assert_eq!(
&*nval[0].properties.get("--x").unwrap().literal,
&Literal::Int(Integer(Radix::Dec, "2".into()))
);
}
#[test]
fn parse_property_ws() {
let variants = [
"node a =b",
"node a= b",
"node a = b",
"node\ta\t=\tb",
];
for ea_variant in variants {
let nval = parse(nodes(), ea_variant).unwrap();
assert_eq!(nval.len(), 1);
assert_eq!(nval[0].node_name.as_ref(), "node");
assert_eq!(nval[0].arguments.len(), 0);
assert_eq!(nval[0].properties.len(), 1);
assert_eq!(
&*nval[0].properties.get("a").unwrap().literal,
&Literal::String("b".into())
);
}
err_eq!(
parse(nodes(), "node a\\\n=b"),
r#"{
"message": "error parsing KDL",
"severity": "error",
"labels": [],
"related": [{
"message": "found `=`, expected `\"`, `#`, `(`, `+`, `-`, `0`, `;`, `\\`, `{`, `#-inf`, `#false`, `#inf`, `#nan`, `#null`, `#true`, letter, newline, whitespace or end of input",
"severity": "error",
"filename": "<test>",
"labels": [
{"label": "unexpected token",
"span": {"offset": 8, "length": 1}}
],
"related": []
}]
}"#
);
}
}