feat(serde): Add support for flags and #rest

Fixes: https://github.com/kdl-org/kdl-rs/issues/161
This commit is contained in:
Kat Marchán 2026-05-30 12:57:07 -07:00
parent f824881cab
commit 9dad8b4c97
6 changed files with 340 additions and 92 deletions

View File

@ -9,7 +9,7 @@ homepage = "https://kdl.dev"
repository = "https://github.com/kdl-org/kdl-rs" repository = "https://github.com/kdl-org/kdl-rs"
keywords = ["kdl", "document", "serialization", "config"] keywords = ["kdl", "document", "serialization", "config"]
rust-version = "1.95" rust-version = "1.95"
edition = "2021" edition = "2024"
[features] [features]
default = ["span", "serde"] default = ["span", "serde"]

331
src/de.rs
View File

@ -18,6 +18,8 @@
//! //!
//! - A **KDL document** is treated as a **map** (or struct), where each top-level //! - A **KDL document** is treated as a **map** (or struct), where each top-level
//! node name is a key and the node's content is the value. //! node name is a key and the node's content is the value.
//! - A **KDL node with no arguments** and no properties/children is treated as
//! a **boolean flag**.
//! - A **KDL node with only a single argument** and no properties/children is //! - A **KDL node with only a single argument** and no properties/children is
//! treated as a **scalar value** (the argument itself). //! treated as a **scalar value** (the argument itself).
//! - A **KDL node with multiple arguments** is treated as a **sequence** of those arguments. //! - A **KDL node with multiple arguments** is treated as a **sequence** of those arguments.
@ -36,20 +38,22 @@
//! struct Config { //! struct Config {
//! name: String, //! name: String,
//! port: u16, //! port: u16,
//! active: bool
//! } //! }
//! //!
//! let kdl = r#" //! let kdl = r#"
//! name my-app //! name my-app
//! port 8080 //! port 8080
//! active
//! "#; //! "#;
//! //!
//! let config: Config = kdl::de::from_str(kdl).unwrap(); //! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { name: "my-app".into(), port: 8080 }); //! assert_eq!(config, Config { name: "my-app".into(), port: 8080, active: true });
//! ``` //! ```
//! //!
//! ## Arguments mapping //! ## Arguments mapping
//! //!
//! This library supports two kinds of special renaming for arguments - `#{n}` and `#args`: //! This library supports three kinds of special renaming for arguments - `#{n}`, `#args`, and `#rest`:
//! //!
//! - `#{n}`: This is used when you want to access argument at a specific //! - `#{n}`: This is used when you want to access argument at a specific
//! position. The field name should start with `#`, then follow by the position //! position. The field name should start with `#`, then follow by the position
@ -103,6 +107,33 @@
//! assert_eq!(config, Config { server: Server { info: Vec::from(["my-app".into(), "https://example.com".into()]) }}); //! assert_eq!(config, Config { server: Server { info: Vec::from(["my-app".into(), "https://example.com".into()]) }});
//! ``` //! ```
//! //!
//! - `#rest`: This is used when you want to collect everything that wasn't
//! referenced by an `#{n}` rename.
//!
//! ```rust
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Config {
//! command: Command,
//! }
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Command {
//! #[serde(rename = "#0")]
//! cmd: String,
//! #[serde(rename = "#rest")]
//! args: Vec<String>,
//! }
//!
//! let kdl = r#"
//! command frob "https://example.com" x y
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { command: Command { cmd: "frob".into(), args: vec!["https://example.com".into(), "x".into(), "y".into()]}});
//! ```
//!
//! ## Properties mapping //! ## Properties mapping
//! //!
//! You can use `#@field-name` on a field that will be used for a property. //! You can use `#@field-name` on a field that will be used for a property.
@ -130,15 +161,52 @@
//! let config: Config = kdl::de::from_str(kdl).unwrap(); //! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { server: Server { name: "my-app".into(), port: 8080 }}); //! assert_eq!(config, Config { server: Server { name: "my-app".into(), port: 8080 }});
//! ``` //! ```
//!
//! ## Types mapping
//!
//! You can extract type annotations for nodes, arguments and properties by
//! using `#type`:
//!
//! ```rust
//! use serde::Deserialize;
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Config {
//! server: Server,
//! }
//!
//! #[derive(Deserialize, Debug, PartialEq)]
//! #[serde(rename_all = "kebab-case")]
//! enum ServerKind {
//! Big,
//! Small,
//! }
//! #[derive(Deserialize, Debug, PartialEq)]
//! struct Server {
//! #[serde(rename = "#type")]
//! server_kind: ServerKind,
//! #[serde(rename = "#0#type")]
//! app_type: String,
//! #[serde(rename = "#@port#type")]
//! port_type: String,
//! }
//!
//! let kdl = r#"
//! (big)server (cool)my-app port=(internal)8080
//! "#;
//!
//! let config: Config = kdl::de::from_str(kdl).unwrap();
//! assert_eq!(config, Config { server: Server { server_kind: ServerKind::Big, app_type: "cool".into(), port_type: "internal".into() }});
//! ```
use std::borrow::Cow; use std::borrow::Cow;
use std::sync::Arc; use std::sync::Arc;
use std::{fmt, iter}; use std::{fmt, iter};
use miette::{Diagnostic, LabeledSpan, Severity, SourceCode, SourceSpan}; use miette::{Diagnostic, LabeledSpan, Severity, SourceCode, SourceSpan};
use serde::Deserialize;
use serde::de::value::StringDeserializer; use serde::de::value::StringDeserializer;
use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor}; use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
use serde::Deserialize;
use crate::{KdlDiagnostic, KdlDocument, KdlEntry, KdlIdentifier, KdlNode, KdlValue}; use crate::{KdlDiagnostic, KdlDocument, KdlEntry, KdlIdentifier, KdlNode, KdlValue};
@ -698,8 +766,8 @@ impl<'de, 'a> de::Deserializer<'de> for NodeDeserializer<'a> {
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> { fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
if self.is_empty() { if self.is_empty() {
// Node with no data → null/unit // Node with no data → bool flag
return visitor.visit_unit(); return visitor.visit_bool(true);
} }
if self.is_scalar() { if self.is_scalar() {
// Single argument → deserialize as scalar // Single argument → deserialize as scalar
@ -726,6 +794,8 @@ impl<'de, 'a> de::Deserializer<'de> for NodeDeserializer<'a> {
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> { fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
if self.is_scalar() { if self.is_scalar() {
ValueDeserializer::new(self.args()[0], self.input).deserialize_any(visitor) ValueDeserializer::new(self.args()[0], self.input).deserialize_any(visitor)
} else if self.is_empty() {
visitor.visit_bool(true)
} else { } else {
Err(Error::new("expected a boolean value")).map_err(|err| { Err(Error::new("expected a boolean value")).map_err(|err| {
err.with_span( err.with_span(
@ -866,16 +936,16 @@ impl<'de, 'a> de::Deserializer<'de> for NodeDeserializer<'a> {
} }
fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> { fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_map(NodeMapAccess::new(self.node, self.input)) visitor.visit_map(NodeMapAccess::new(self.node, self.input, None))
} }
fn deserialize_struct<V: Visitor<'de>>( fn deserialize_struct<V: Visitor<'de>>(
self, self,
_name: &'static str, _name: &'static str,
_fields: &'static [&'static str], fields: &'static [&'static str],
visitor: V, visitor: V,
) -> Result<V::Value, Self::Error> { ) -> Result<V::Value, Self::Error> {
self.deserialize_map(visitor) visitor.visit_map(NodeMapAccess::new(self.node, self.input, Some(fields)))
} }
fn deserialize_enum<V: Visitor<'de>>( fn deserialize_enum<V: Visitor<'de>>(
@ -885,18 +955,18 @@ impl<'de, 'a> de::Deserializer<'de> for NodeDeserializer<'a> {
visitor: V, visitor: V,
) -> Result<V::Value, Self::Error> { ) -> Result<V::Value, Self::Error> {
// If the node is a scalar string, treat it as a unit variant name. // If the node is a scalar string, treat it as a unit variant name.
if self.is_scalar() { if self.is_scalar()
if let KdlValue::String(s) = self.args()[0].value() { && let KdlValue::String(s) = self.args()[0].value()
return visitor {
.visit_enum(str_deserializer(s.as_str())) return visitor
.map_err(|err| { .visit_enum(str_deserializer(s.as_str()))
err.with_span( .map_err(|err| {
self.input, err.with_span(
entry_span(self.args()[0]), self.input,
"while deserializing this enum variant", entry_span(self.args()[0]),
) "while deserializing this enum variant",
}); )
} });
} }
// Otherwise, try treating the node as an externally-tagged enum: // Otherwise, try treating the node as an externally-tagged enum:
@ -1042,6 +1112,7 @@ impl<'de, 'a> SeqAccess<'de> for NodeGroupSeqAccess<'a> {
} }
} }
#[derive(Debug)]
struct NodeMapAccess<'a> { struct NodeMapAccess<'a> {
/// Properties and children combined as (key, value_source). /// Properties and children combined as (key, value_source).
entries: Vec<(Cow<'a, str>, NodeMapValue<'a>)>, entries: Vec<(Cow<'a, str>, NodeMapValue<'a>)>,
@ -1049,51 +1120,86 @@ struct NodeMapAccess<'a> {
input: &'a Arc<String>, input: &'a Arc<String>,
} }
#[derive(Debug)]
enum NodeMapValue<'a> { enum NodeMapValue<'a> {
Ident(&'a KdlIdentifier), Ident(&'a KdlIdentifier),
Arg(&'a KdlEntry), Arg(&'a KdlEntry),
Args(Vec<&'a KdlEntry>), Args(Vec<&'a KdlEntry>),
SingleNode(&'a KdlNode), SingleNode(&'a KdlNode),
MultiNode(Vec<&'a KdlNode>), MultiNode(Vec<&'a KdlNode>),
Omitted,
} }
impl<'a> NodeMapAccess<'a> { impl<'a> NodeMapAccess<'a> {
fn new(node: &'a KdlNode, input: &'a Arc<String>) -> Self { fn new(
node: &'a KdlNode,
input: &'a Arc<String>,
fields: Option<&'static [&'static str]>,
) -> Self {
let mut entries: Vec<(Cow<'a, str>, NodeMapValue<'a>)> = Vec::new(); let mut entries: Vec<(Cow<'a, str>, NodeMapValue<'a>)> = Vec::new();
entries.push(("#name".into(), NodeMapValue::Ident(node.name()))); if let Some(fields) = fields {
if let Some(ty) = node.ty() { if fields.contains(&"#name") {
entries.push(("#type".into(), NodeMapValue::Ident(ty))) entries.push(("#name".into(), NodeMapValue::Ident(node.name())));
}
if fields.contains(&"#type")
&& let Some(ty) = node.ty()
{
entries.push(("#type".into(), NodeMapValue::Ident(ty)));
}
} }
let args: Vec<_> = node if let Some(fields) = fields
.entries() && let collect_all = fields.contains(&"#args")
.iter() && let collect_rest = fields.contains(&"#rest")
.filter(|e| e.name().is_none()) {
.collect(); let mut args = Vec::new();
let props: Vec<_> = node let mut rest = Vec::new();
.entries() for (i, arg) in node
.iter() .entries()
.filter(|e| e.name().is_some()) .iter()
.collect(); .filter(|e| e.name().is_none())
.enumerate()
for (i, arg) in args.iter().enumerate() { {
entries.push((format!("#{}", i).into(), NodeMapValue::Arg(arg))); if collect_all {
if let Some(ty) = arg.ty() { args.push(arg);
entries.push((format!("#{}#type", i).into(), NodeMapValue::Ident(ty))); }
let idx_name = format!("#{i}");
let ty_name = format!("{idx_name}#type");
if let Some(ty) = arg.ty() {
entries.push((ty_name.into(), NodeMapValue::Ident(ty)));
}
if fields.contains(&idx_name.as_ref()) {
entries.push((idx_name.into(), NodeMapValue::Arg(arg)));
} else if collect_rest {
rest.push(arg);
}
}
if collect_all {
entries.push(("#args".into(), NodeMapValue::Args(args)));
}
if collect_rest {
entries.push(("#rest".into(), NodeMapValue::Args(rest)));
} }
} }
if !args.is_empty() { let mut used_names = Vec::new();
entries.push(("#args".into(), NodeMapValue::Args(args.clone()))); for prop in node.entries().iter().filter(|e| e.name().is_some()) {
} // SAFETY: we just filtered for things with names.
for prop in &props {
let name = prop.name().unwrap().value(); let name = prop.name().unwrap().value();
entries.push((format!("#@{}", name).into(), NodeMapValue::Arg(prop))); if let Some(fields) = fields {
if let Some(ty) = prop.ty() { let hash_name = format!("#@{name}");
entries.push((format!("#@{}#type", name).into(), NodeMapValue::Ident(ty))); let ty_name = format!("{hash_name}#type");
if fields.contains(&hash_name.as_ref()) {
entries.push((hash_name.into(), NodeMapValue::Arg(prop)));
}
if let Some(ty) = prop.ty()
&& fields.contains(&ty_name.as_ref())
{
entries.push((ty_name.into(), NodeMapValue::Ident(ty)));
}
} }
entries.push((name.into(), NodeMapValue::Arg(prop))); entries.push((name.into(), NodeMapValue::Arg(prop)));
used_names.push(name);
} }
// Add children (grouped by node name) // Add children (grouped by node name)
@ -1103,6 +1209,7 @@ impl<'a> NodeMapAccess<'a> {
std::collections::HashMap::new(); std::collections::HashMap::new();
for child in children.nodes() { for child in children.nodes() {
let name = child.name().value(); let name = child.name().value();
used_names.push(name);
if !child_groups.contains_key(name) { if !child_groups.contains_key(name) {
child_keys.push(name); child_keys.push(name);
} }
@ -1117,7 +1224,15 @@ impl<'a> NodeMapAccess<'a> {
} }
} }
} }
if let Some(fields) = fields {
for field in fields {
if !field.starts_with('#') && !used_names.contains(field) {
entries.push((String::from(*field).into(), NodeMapValue::Omitted));
}
}
}
dbg!(&entries);
NodeMapAccess { NodeMapAccess {
entries, entries,
idx: 0, idx: 0,
@ -1166,6 +1281,7 @@ impl<'de, 'a> MapAccess<'de> for NodeMapAccess<'a> {
nodes, nodes,
input: self.input, input: self.input,
}), }),
NodeMapValue::Omitted => seed.deserialize(OmittedChildDeserializer),
} }
} }
} }
@ -1215,6 +1331,36 @@ impl<'de, 'a> Deserializer<'de> for ArgsSeqDeserializer<'a> {
} }
} }
struct OmittedChildDeserializer;
impl<'de> Deserializer<'de> for OmittedChildDeserializer {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Self::Error> {
visitor.visit_bool(false)
}
fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_bool(false)
}
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_none()
}
serde::forward_to_deserialize_any! {
i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf unit unit_struct tuple newtype_struct seq
tuple_struct map struct enum identifier ignored_any
}
}
struct NodeEnumAccess<'a> { struct NodeEnumAccess<'a> {
node: &'a KdlNode, node: &'a KdlNode,
input: &'a Arc<String>, input: &'a Arc<String>,
@ -1885,6 +2031,34 @@ h 8
); );
} }
#[test]
fn rename_rest_args() {
#[derive(Deserialize, Debug, PartialEq)]
struct Command {
#[serde(rename = "#0")]
name: String,
#[serde(rename = "#rest")]
args: Vec<String>,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Config {
command: Command,
}
let kdl = "command run --verbose --output result.txt";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
command: Command {
name: "run".into(),
args: vec!["--verbose".into(), "--output".into(), "result.txt".into(),],
},
}
);
}
#[test] #[test]
fn rename_children_args() { fn rename_children_args() {
#[derive(Deserialize, Debug, PartialEq)] #[derive(Deserialize, Debug, PartialEq)]
@ -1913,6 +2087,69 @@ h 8
); );
} }
#[test]
fn bool_flag_children() {
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
struct Server {
host: String,
active: bool,
}
#[derive(Deserialize, Debug, PartialEq)]
struct Config {
server: Server,
}
let kdl = "server host=localhost { active }";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
server: Server {
host: "localhost".into(),
active: true,
},
}
);
let kdl = "server host=localhost active=#true";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
server: Server {
host: "localhost".into(),
active: true,
},
}
);
let kdl = "server host=localhost";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
server: Server {
host: "localhost".into(),
active: false,
},
}
);
let kdl = "server host=localhost { }";
let config: Config = from_str(kdl).unwrap();
assert_eq!(
config,
Config {
server: Server {
host: "localhost".into(),
active: false,
},
}
);
}
// TODO(@zkat): Can't figure out how to do internally tagged stuff just yet... // TODO(@zkat): Can't figure out how to do internally tagged stuff just yet...
// #[test] // #[test]
// fn internal_tag_rename() { // fn internal_tag_rename() {

View File

@ -2,7 +2,7 @@
use miette::SourceSpan; use miette::SourceSpan;
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use crate::{v2_parser, KdlError, KdlIdentifier, KdlValue}; use crate::{KdlError, KdlIdentifier, KdlValue, v2_parser};
/// KDL Entries are the "arguments" to KDL nodes: either a (positional) /// KDL Entries are the "arguments" to KDL nodes: either a (positional)
/// [`Argument`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#argument) or /// [`Argument`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#argument) or
@ -230,7 +230,8 @@ impl KdlEntry {
let s = x.value_repr.trim(); let s = x.value_repr.trim();
// convert raw strings to new format // convert raw strings to new format
let s = s.strip_prefix('r').unwrap_or(s); let s = s.strip_prefix('r').unwrap_or(s);
let s = if crate::value::is_plain_ident(val) {
if crate::value::is_plain_ident(val) {
val.into() val.into()
} else if s } else if s
.find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c))) .find(|c| v2_parser::NEWLINES.iter().any(|nl| nl.contains(c)))
@ -258,8 +259,7 @@ impl KdlEntry {
} else { } else {
// We're all good! Let's move on. // We're all good! Let's move on.
s.to_string() s.to_string()
}; }
s
} }
// These have `#` prefixes now. The regular Display impl will // These have `#` prefixes now. The regular Display impl will
// take care of that. // take care of that.
@ -301,7 +301,8 @@ impl KdlEntry {
} else { } else {
s.to_string() s.to_string()
}; };
let s = if crate::value::is_plain_ident(val)
if crate::value::is_plain_ident(val)
&& !s.starts_with('\"') && !s.starts_with('\"')
&& !s.starts_with("r#") && !s.starts_with("r#")
{ {
@ -340,8 +341,7 @@ impl KdlEntry {
} else { } else {
// We're all good! Let's move on. // We're all good! Let's move on.
s.to_string() s.to_string()
}; }
s
} }
// No more # prefix for these // No more # prefix for these
KdlValue::Bool(b) => b.to_string(), KdlValue::Bool(b) => b.to_string(),

View File

@ -2,7 +2,7 @@
use miette::SourceSpan; use miette::SourceSpan;
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use crate::{v2_parser, KdlError, KdlValue}; use crate::{KdlError, KdlValue, v2_parser};
/// Represents a KDL /// Represents a KDL
/// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier). /// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier).

View File

@ -10,8 +10,8 @@ use std::{
use miette::SourceSpan; use miette::SourceSpan;
use crate::{ use crate::{
v2_parser, FormatConfig, KdlDocument, KdlDocumentFormat, KdlEntry, KdlError, KdlIdentifier, FormatConfig, KdlDocument, KdlDocumentFormat, KdlEntry, KdlError, KdlIdentifier, KdlValue,
KdlValue, v2_parser,
}; };
/// Represents an individual KDL /// Represents an individual KDL
@ -291,10 +291,10 @@ impl KdlNode {
if !terminator.starts_with('\n') { if !terminator.starts_with('\n') {
*terminator = "\n".into(); *terminator = "\n".into();
} }
if let Some(c) = trailing.chars().next() { if let Some(c) = trailing.chars().next()
if !c.is_whitespace() { && !c.is_whitespace()
trailing.insert(0, ' '); {
} trailing.insert(0, ' ');
} }
*before_children = " ".into(); *before_children = " ".into();

View File

@ -7,7 +7,8 @@ use miette::{Severity, SourceSpan};
use num_traits::CheckedMul; use num_traits::CheckedMul;
use winnow::{ use winnow::{
ascii::{digit1, hex_digit1, oct_digit1, Caseless}, LocatingSlice,
ascii::{Caseless, digit1, hex_digit1, oct_digit1},
combinator::{ combinator::{
alt, cut_err, empty, eof, fail, not, opt, peek, preceded, repeat, repeat_till, separated, alt, cut_err, empty, eof, fail, not, opt, peek, preceded, repeat, repeat_till, separated,
terminated, trace, terminated, trace,
@ -16,7 +17,6 @@ use winnow::{
prelude::*, prelude::*,
stream::{AsChar, Location, Recover, Recoverable, Stream}, stream::{AsChar, Location, Recover, Recoverable, Stream},
token::{any, none_of, one_of, take_while}, token::{any, none_of, one_of, take_while},
LocatingSlice,
}; };
use crate::{ use crate::{
@ -270,10 +270,10 @@ pub(crate) fn document(input: &mut Input<'_>) -> PResult<KdlDocument> {
if badend { if badend {
document.parse_next(input)?; document.parse_next(input)?;
} }
if let Some(bom) = bom { if let Some(bom) = bom
if let Some(fmt) = doc.format_mut() { && let Some(fmt) = doc.format_mut()
fmt.leading = format!("{bom}{}", fmt.leading); {
} fmt.leading = format!("{bom}{}", fmt.leading);
} }
Ok(doc) Ok(doc)
} }
@ -300,11 +300,11 @@ fn nodes(input: &mut Input<'_>) -> PResult<KdlDocument> {
// If there is a node, let it have the leading format // If there is a node, let it have the leading format
// This gives more consistent behavior // This gives more consistent behavior
if let Some(first_node) = ns.get_mut(0) { if let Some(first_node) = ns.get_mut(0)
if let Some(first_node_format) = first_node.format_mut() { && let Some(first_node_format) = first_node.format_mut()
first_node_format.leading = leading.into(); {
leading = ""; first_node_format.leading = leading.into();
} leading = "";
} }
Ok(KdlDocument { Ok(KdlDocument {
@ -1467,9 +1467,11 @@ mod string_tests {
Some(KdlValue::String("\"\"\"".into())) Some(KdlValue::String("\"\"\"".into()))
); );
assert!(string assert!(
.parse(new_input("\"\"\"\nfoo\n bar\n baz\n \"\"\"")) string
.is_err()); .parse(new_input("\"\"\"\nfoo\n bar\n baz\n \"\"\""))
.is_err()
);
} }
#[test] #[test]
@ -1507,9 +1509,11 @@ mod string_tests {
.unwrap(), .unwrap(),
Some(KdlValue::String("foo\n \\nbar\n baz".into())) Some(KdlValue::String("foo\n \\nbar\n baz".into()))
); );
assert!(string assert!(
.parse(new_input("#\"\"\"\nfoo\n bar\n baz\n \"\"\"#")) string
.is_err()); .parse(new_input("#\"\"\"\nfoo\n bar\n baz\n \"\"\"#"))
.is_err()
);
assert!(string.parse(new_input("#\"\nfoo\nbar\nbaz\n\"#")).is_err()); assert!(string.parse(new_input("#\"\nfoo\nbar\nbaz\n\"#")).is_err());
assert!(string.parse(new_input("\"\nfoo\nbar\nbaz\n\"")).is_err()); assert!(string.parse(new_input("\"\nfoo\nbar\nbaz\n\"")).is_err());
@ -1699,9 +1703,11 @@ fn multi_line_comment_test() {
assert!(multi_line_comment.parse(new_input("/*\nfoo*/")).is_ok()); assert!(multi_line_comment.parse(new_input("/*\nfoo*/")).is_ok());
assert!(multi_line_comment.parse(new_input("/*foo\n*/")).is_ok()); assert!(multi_line_comment.parse(new_input("/*foo\n*/")).is_ok());
assert!(multi_line_comment.parse(new_input("/* foo\n*/")).is_ok()); assert!(multi_line_comment.parse(new_input("/* foo\n*/")).is_ok());
assert!(multi_line_comment assert!(
.parse(new_input("/* /*bar*/ foo\n*/")) multi_line_comment
.is_ok()); .parse(new_input("/* /*bar*/ foo\n*/"))
.is_ok()
);
} }
/// slashdash := '/-' (node-space | line-space)* /// slashdash := '/-' (node-space | line-space)*
@ -1724,15 +1730,18 @@ fn slashdash_tests() {
assert!(node_entry.parse(new_input("/-commented tada")).is_ok()); assert!(node_entry.parse(new_input("/-commented tada")).is_ok());
assert!(node.parse(new_input("foo /- { }")).is_ok()); assert!(node.parse(new_input("foo /- { }")).is_ok());
assert!(node.parse(new_input("foo /- { bar }")).is_ok()); assert!(node.parse(new_input("foo /- { bar }")).is_ok());
assert!(node assert!(
.parse(new_input("/- foo bar\nnode /-1 2 { x }")) node.parse(new_input("/- foo bar\nnode /-1 2 { x }"))
.is_ok()); .is_ok()
assert!(node );
.parse(new_input("/- foo bar\nnode 2 /-3 { x }")) assert!(
.is_ok()); node.parse(new_input("/- foo bar\nnode 2 /-3 { x }"))
assert!(node .is_ok()
.parse(new_input("/- foo bar\nnode /-1 2 /-3 { x }")) );
.is_ok()); assert!(
node.parse(new_input("/- foo bar\nnode /-1 2 /-3 { x }"))
.is_ok()
);
} }
/// `number := keyword-number | hex | octal | binary | decimal` /// `number := keyword-number | hex | octal | binary | decimal`
@ -2031,7 +2040,9 @@ macro_rules! impl_from_str_radix {
}; };
} }
impl_from_str_radix!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize); impl_from_str_radix!(
i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
);
trait MaybeNegatable: CheckedMul { trait MaybeNegatable: CheckedMul {
fn negated(&self) -> Option<Self>; fn negated(&self) -> Option<Self>;