mirror of https://github.com/kdl-org/kdl-rs.git
feat(serde): Add support for flags and #rest
Fixes: https://github.com/kdl-org/kdl-rs/issues/161
This commit is contained in:
parent
f824881cab
commit
9dad8b4c97
|
|
@ -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
331
src/de.rs
|
|
@ -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() {
|
||||||
|
|
|
||||||
14
src/entry.rs
14
src/entry.rs
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
12
src/node.rs
12
src/node.rs
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue