mirror of https://github.com/kdl-org/kdl-rs.git
feat(api): complete rewrite into document-oriented parser (#29)
This new version of kdl-rs is a complete rewrite that introduces a formatting-aware-and-preserving parser, much like toml_edit et al. BREAKING CHANGE: Completely new API and bumped MSRV to 1.56.0.
This commit is contained in:
parent
d52e101ff9
commit
364ea6173c
|
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
rust: [1.46.0, stable]
|
rust: [1.56.0, stable]
|
||||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
[package]
|
[package]
|
||||||
name = "kdl"
|
name = "kdl"
|
||||||
version = "3.0.1-alpha.0"
|
version = "3.0.1-alpha.0"
|
||||||
description = "Official Rust KDL parser"
|
description = "Document-oriented KDL parser and API. Allows formatting/whitespace/comment-preserving parsing and modification of KDL text."
|
||||||
authors = ["Kat Marchán <kzm@zkat.tech>", "KDL Community"]
|
authors = ["Kat Marchán <kzm@zkat.tech>", "KDL Community"]
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://kdl.dev"
|
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"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nom = { version = "7.0.0", default-features = false }
|
miette = "4.5.0"
|
||||||
|
nom = { version = "7.1.1", default-features = false }
|
||||||
phf = { version = "0.8.0", features = ["macros"] }
|
phf = { version = "0.8.0", features = ["macros"] }
|
||||||
thiserror = "1.0.22"
|
thiserror = "1.0.22"
|
||||||
|
|
|
||||||
72
README.md
72
README.md
|
|
@ -1,64 +1,26 @@
|
||||||
# KDL - The KDL Document Language
|
`kdl` is a "document-oriented" parser and API. That means that, unlike
|
||||||
|
serde-based implementations, it's meant to preserve formatting when editing,
|
||||||
|
as well as inserting values with custom formatting. This is useful when
|
||||||
|
working with human-maintained KDL files.
|
||||||
|
|
||||||
[KDL](https://github.com/kdl-org/kdl) is a document language with xml-like
|
You can think of this crate as
|
||||||
semantics that looks like you're invoking a bunch of CLI commands!
|
[`toml_edit`](https://crates.io/crates/toml_edit), but for KDL.
|
||||||
|
|
||||||
It's meant to be used both as a serialization format and a configuration
|
### Example
|
||||||
language, and is relatively light on syntax compared to XML.
|
|
||||||
|
|
||||||
There's a living
|
```rust
|
||||||
[specification](https://github.com/kdl-org/kdl/blob/main/SPEC.md), as well as
|
use kdl::KdlDocument;
|
||||||
[various implementations](https://github.com/kdl-org/kdl#implementations). The language is based on
|
|
||||||
[SDLang](https://sdlang.org), with a number of modifications and
|
|
||||||
clarifications on its syntax and behavior.
|
|
||||||
|
|
||||||
This repository is the official/reference implementation in Rust, and
|
let doc: KdlDocument = r#"
|
||||||
corresponds to [the kdl crate](https://crates.io/crates/kdl)
|
hello 1 2 3
|
||||||
|
world prop="value" {
|
||||||
## Design and Discussion
|
child 1
|
||||||
|
child 2
|
||||||
KDL is still extremely new, and discussion about the format should happen over
|
|
||||||
on the [spec repo's discussions
|
|
||||||
page](https://github.com/kdoclang/kdl/discussions). Feel free to jump in and
|
|
||||||
give us your 2 cents!
|
|
||||||
|
|
||||||
## Example KDL File
|
|
||||||
|
|
||||||
```text
|
|
||||||
author "Alex Monad" email="alex@example.com" active=true
|
|
||||||
|
|
||||||
contents {
|
|
||||||
section "First section" {
|
|
||||||
paragraph "This is the first paragraph"
|
|
||||||
paragraph "This is the second paragraph"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
"#.parse().expect("failed to parse KDL");
|
||||||
|
|
||||||
// unicode! comments!
|
assert_eq!(doc.get_args("hello"), vec![&1.into(), &2.into(), &3.into()]);
|
||||||
π 3.14159
|
assert_eq!(doc.get("world").map(|node| &node["prop"]), Some(&"value".into()));
|
||||||
```
|
|
||||||
|
|
||||||
## Basic Library Example
|
|
||||||
|
|
||||||
```
|
|
||||||
use kdl::{KdlNode, KdlValue};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
kdl::parse_document("node 1 key=true").unwrap(),
|
|
||||||
vec![
|
|
||||||
KdlNode {
|
|
||||||
name: String::from("node"),
|
|
||||||
values: vec![KdlValue::Int(1)],
|
|
||||||
properties: {
|
|
||||||
let mut temp = HashMap::new();
|
|
||||||
temp.insert(String::from("key"), KdlValue::Boolean(true));
|
|
||||||
temp
|
|
||||||
},
|
|
||||||
children: vec![],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use nom::{combinator::all_consuming, Finish};
|
||||||
|
|
||||||
|
use crate::{parser::document, KdlError, KdlErrorKind, KdlNode, KdlValue};
|
||||||
|
|
||||||
|
/// Represents a KDL
|
||||||
|
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
|
||||||
|
///
|
||||||
|
/// This type is also used to manage a [`KdlNode`]'s [`Children
|
||||||
|
/// Block`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#children-block),
|
||||||
|
/// when present.
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq)]
|
||||||
|
pub struct KdlDocument {
|
||||||
|
pub(crate) leading: Option<String>,
|
||||||
|
// TODO: Consider using `hashlink` for this, later?
|
||||||
|
pub(crate) nodes: Vec<KdlNode>,
|
||||||
|
pub(crate) trailing: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlDocument {
|
||||||
|
/// Creates a new Document.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the first child node with a matching name.
|
||||||
|
pub fn get(&self, name: &str) -> Option<&KdlNode> {
|
||||||
|
self.nodes.iter().find(move |n| n.name().value() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a reference to the first child node with a matching name.
|
||||||
|
pub fn get_mut(&mut self, name: &str) -> Option<&mut KdlNode> {
|
||||||
|
self.nodes
|
||||||
|
.iter_mut()
|
||||||
|
.find(move |n| n.name().value() == name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the first argument (value) of the first child node with a
|
||||||
|
/// matching name. This is a shorthand utility for cases where a document
|
||||||
|
/// is being used as a key/value store.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Given a document like this:
|
||||||
|
/// ```kdl
|
||||||
|
/// foo 1
|
||||||
|
/// bar false
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// You can fetch the value of `foo` in a single call like this:
|
||||||
|
/// ```rust
|
||||||
|
/// # use kdl::{KdlDocument, KdlValue};
|
||||||
|
/// # let doc: KdlDocument = "foo 1\nbar false".parse().unwrap();
|
||||||
|
/// assert_eq!(doc.get_arg("foo"), Some(&1.into()));
|
||||||
|
/// ```
|
||||||
|
pub fn get_arg(&self, name: &str) -> Option<&KdlValue> {
|
||||||
|
self.get(name)
|
||||||
|
.and_then(|node| node.get(0))
|
||||||
|
.map(|e| e.value())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the all node arguments (value) of the first child node with a
|
||||||
|
/// matching name. This is a shorthand utility for cases where a document
|
||||||
|
/// is being used as a key/value store and the value is expected to be
|
||||||
|
/// array-ish.
|
||||||
|
///
|
||||||
|
/// If a node has no arguments, this will return an empty array.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Given a document like this:
|
||||||
|
/// ```kdl
|
||||||
|
/// foo 1 2 3
|
||||||
|
/// bar false
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// You can fetch the arguments for `foo` in a single call like this:
|
||||||
|
/// ```rust
|
||||||
|
/// # use kdl::{KdlDocument, KdlValue};
|
||||||
|
/// # let doc: KdlDocument = "foo 1 2 3\nbar false".parse().unwrap();
|
||||||
|
/// assert_eq!(doc.get_args("foo"), vec![&1.into(), &2.into(), &3.into()]);
|
||||||
|
/// ```
|
||||||
|
pub fn get_args(&self, name: &str) -> Vec<&KdlValue> {
|
||||||
|
self.get(name)
|
||||||
|
.map(|n| n.entries())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.name().is_none())
|
||||||
|
.map(|e| e.value())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_arg_mut(&mut self, name: &str) -> Option<&mut KdlValue> {
|
||||||
|
self.get_mut(name)
|
||||||
|
.and_then(|node| node.get_mut(0))
|
||||||
|
.map(|e| e.value_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This utility makes it easy to interact with a KDL convention where
|
||||||
|
/// child nodes named `-` are treated as array-ish values.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Given a document like this:
|
||||||
|
/// ```kdl
|
||||||
|
/// foo {
|
||||||
|
/// - 1
|
||||||
|
/// - 2
|
||||||
|
/// - false
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// You can fetch the dashed child values of `foo` in a single call like this:
|
||||||
|
/// ```rust
|
||||||
|
/// # use kdl::{KdlDocument, KdlValue};
|
||||||
|
/// # let doc: KdlDocument = "foo {\n - 1\n - 2\n - false\n}".parse().unwrap();
|
||||||
|
/// assert_eq!(doc.get_dash_vals("foo"), vec![&1.into(), &2.into(), &false.into()]);
|
||||||
|
/// ```
|
||||||
|
pub fn get_dash_vals(&self, name: &str) -> Vec<&KdlValue> {
|
||||||
|
self.get(name)
|
||||||
|
.and_then(|n| n.children())
|
||||||
|
.map(|doc| doc.nodes())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.name().value() == "-")
|
||||||
|
.map(|e| e.get(0))
|
||||||
|
.filter(|v| v.is_some())
|
||||||
|
.map(|v| v.unwrap().value())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to this document's child nodes.
|
||||||
|
pub fn nodes(&self) -> &[KdlNode] {
|
||||||
|
&self.nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to this document's child nodes.
|
||||||
|
pub fn nodes_mut(&mut self) -> &mut Vec<KdlNode> {
|
||||||
|
&mut self.nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets leading text (whitespace, comments) for this KdlDocument.
|
||||||
|
pub fn leading(&self) -> Option<&str> {
|
||||||
|
self.leading.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets leading text (whitespace, comments) for this KdlDocument.
|
||||||
|
pub fn set_leading(&mut self, leading: impl Into<String>) {
|
||||||
|
self.leading = Some(leading.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets trailing text (whitespace, comments) for this KdlDocument.
|
||||||
|
pub fn trailing(&self) -> Option<&str> {
|
||||||
|
self.trailing.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets trailing text (whitespace, comments) for this KdlDocument.
|
||||||
|
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
|
||||||
|
self.trailing = Some(trailing.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto-formats this Document.
|
||||||
|
///
|
||||||
|
/// Note: This currently removes comments as well.
|
||||||
|
pub fn fmt(&mut self) {
|
||||||
|
self.leading = None;
|
||||||
|
self.trailing = None;
|
||||||
|
for node in &mut self.nodes {
|
||||||
|
node.fmt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KdlDocument {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.stringify(f, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlDocument {
|
||||||
|
pub(crate) fn stringify(
|
||||||
|
&self,
|
||||||
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
indent: usize,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
if let Some(leading) = &self.leading {
|
||||||
|
write!(f, "{}", leading)?;
|
||||||
|
}
|
||||||
|
for node in &self.nodes {
|
||||||
|
node.stringify(f, indent)?;
|
||||||
|
if node.trailing.is_none() {
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(trailing) = &self.trailing {
|
||||||
|
write!(f, "{}", trailing)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for KdlDocument {
|
||||||
|
type Item = KdlNode;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.nodes.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlDocument {
|
||||||
|
/// Parse a KDL document from a string into a [`KdlDocument`] object model.
|
||||||
|
fn parse(input: &str) -> Result<KdlDocument, KdlError> {
|
||||||
|
all_consuming(document)(input)
|
||||||
|
.finish()
|
||||||
|
.map(|(_, arg)| arg)
|
||||||
|
.map_err(|e| {
|
||||||
|
let prefix = &input[..(input.len() - e.input.len())];
|
||||||
|
KdlError {
|
||||||
|
input: input.into(),
|
||||||
|
offset: prefix.chars().count(),
|
||||||
|
kind: if let Some(kind) = e.kind {
|
||||||
|
kind
|
||||||
|
} else if let Some(ctx) = e.context {
|
||||||
|
KdlErrorKind::Context(ctx)
|
||||||
|
} else {
|
||||||
|
KdlErrorKind::Other
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for KdlDocument {
|
||||||
|
type Err = KdlError;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
KdlDocument::parse(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{KdlEntry, KdlValue};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing() {
|
||||||
|
let src = "
|
||||||
|
// This is the first node
|
||||||
|
foo 1 2 \"three\" null true bar=\"baz\" {
|
||||||
|
- 1
|
||||||
|
- 2
|
||||||
|
- \"three\"
|
||||||
|
something \"else\"\r
|
||||||
|
}
|
||||||
|
|
||||||
|
null_id null_prop=null
|
||||||
|
true_id true_prop=null
|
||||||
|
+false true
|
||||||
|
|
||||||
|
bar \"indented\" // trailing whitespace after this\t
|
||||||
|
/*
|
||||||
|
Some random comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
a; b; c;
|
||||||
|
|
||||||
|
/-commented \"node\"
|
||||||
|
|
||||||
|
another /*foo*/ \"node\" /-1 /*bar*/ null;
|
||||||
|
final;";
|
||||||
|
let mut doc: KdlDocument = src.parse().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(doc.leading, Some("".into()));
|
||||||
|
assert_eq!(doc.get_arg("foo"), Some(&1.into()));
|
||||||
|
assert_eq!(
|
||||||
|
doc.get_dash_vals("foo"),
|
||||||
|
vec![&1.into(), &2.into(), &"three".into()]
|
||||||
|
);
|
||||||
|
|
||||||
|
let foo = doc.get("foo").expect("expected a foo node");
|
||||||
|
assert_eq!(foo.leading, Some("\n// This is the first node\n".into()));
|
||||||
|
assert_eq!(&foo[2], &"three".into());
|
||||||
|
assert_eq!(&foo["bar"], &"baz".into());
|
||||||
|
assert_eq!(
|
||||||
|
foo.children().unwrap().get_arg("something"),
|
||||||
|
Some(&"else".into())
|
||||||
|
);
|
||||||
|
assert_eq!(doc.get_arg("another"), Some(&"node".into()));
|
||||||
|
|
||||||
|
let null = doc.get("null_id").expect("expected a null_id node");
|
||||||
|
assert_eq!(&null["null_prop"], &KdlValue::Null);
|
||||||
|
|
||||||
|
let tru = doc.get("true_id").expect("expected a true_id node");
|
||||||
|
assert_eq!(&tru["true_prop"], &KdlValue::Null);
|
||||||
|
|
||||||
|
let plusfalse = doc.get("+false").expect("expected a +false node");
|
||||||
|
assert_eq!(&plusfalse[0], &KdlValue::Bool(true));
|
||||||
|
|
||||||
|
let bar = doc.get("bar").expect("expected a bar node");
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", bar),
|
||||||
|
"\n bar \"indented\" // trailing whitespace after this\t\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
let a = doc.get("a").expect("expected a node");
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", a),
|
||||||
|
"/*\nSome random comment\n */\n\na; ".to_string()
|
||||||
|
);
|
||||||
|
|
||||||
|
let b = doc.get("b").expect("expected a node");
|
||||||
|
assert_eq!(format!("{}", b), "b; ".to_string());
|
||||||
|
|
||||||
|
// Round-tripping works.
|
||||||
|
assert_eq!(format!("{}", doc), src);
|
||||||
|
|
||||||
|
// Programmatic manipulation works.
|
||||||
|
let mut node: KdlNode = "new\n".parse().unwrap();
|
||||||
|
// Manual entry parsing preserves formatting/reprs.
|
||||||
|
node.push("\"blah\"=0xDEADbeef".parse::<KdlEntry>().unwrap());
|
||||||
|
doc.nodes_mut().push(node);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", doc),
|
||||||
|
format!("{}new \"blah\"=0xDEADbeef\n", src)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn construction() {
|
||||||
|
let mut doc = KdlDocument::new();
|
||||||
|
doc.nodes_mut().push(KdlNode::new("foo"));
|
||||||
|
|
||||||
|
let mut bar = KdlNode::new("bar");
|
||||||
|
bar.insert("prop", "value");
|
||||||
|
bar.push(1);
|
||||||
|
bar.push(2);
|
||||||
|
bar.push(false);
|
||||||
|
bar.push(KdlValue::Null);
|
||||||
|
|
||||||
|
let subdoc = bar.ensure_children();
|
||||||
|
subdoc.nodes_mut().push(KdlNode::new("barchild"));
|
||||||
|
doc.nodes_mut().push(bar);
|
||||||
|
doc.nodes_mut().push(KdlNode::new("baz"));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"foo
|
||||||
|
bar prop="value" 1 2 false null {
|
||||||
|
barchild
|
||||||
|
}
|
||||||
|
baz
|
||||||
|
"#,
|
||||||
|
format!("{}", doc)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use nom::{combinator::all_consuming, Finish};
|
||||||
|
|
||||||
|
use crate::{KdlError, KdlErrorKind, KdlIdentifier, KdlValue};
|
||||||
|
|
||||||
|
/// KDL Entries are the "arguments" to KDL nodes: either a (positional)
|
||||||
|
/// [`Argument`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#argument) or
|
||||||
|
/// a (key/value)
|
||||||
|
/// [`Property`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#property)
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct KdlEntry {
|
||||||
|
pub(crate) leading: Option<String>,
|
||||||
|
pub(crate) ty: Option<String>,
|
||||||
|
pub(crate) value: KdlValue,
|
||||||
|
pub(crate) value_repr: Option<String>,
|
||||||
|
pub(crate) name: Option<KdlIdentifier>,
|
||||||
|
pub(crate) trailing: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlEntry {
|
||||||
|
/// Creates a new Argument (positional) KdlEntry.
|
||||||
|
pub fn new(value: impl Into<KdlValue>) -> Self {
|
||||||
|
KdlEntry {
|
||||||
|
leading: None,
|
||||||
|
ty: None,
|
||||||
|
value: value.into(),
|
||||||
|
value_repr: None,
|
||||||
|
name: None,
|
||||||
|
trailing: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Option<&KdlIdentifier> {
|
||||||
|
self.name.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the entry's value.
|
||||||
|
pub fn value(&self) -> &KdlValue {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a mutable reference to this entry's value.
|
||||||
|
pub fn value_mut(&mut self) -> &mut KdlValue {
|
||||||
|
&mut self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the entry's value.
|
||||||
|
pub fn set_value(&mut self, value: impl Into<KdlValue>) {
|
||||||
|
self.value = value.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Property (key/value) KdlEntry.
|
||||||
|
pub fn new_prop(key: impl Into<KdlIdentifier>, value: impl Into<KdlValue>) -> Self {
|
||||||
|
KdlEntry {
|
||||||
|
leading: None,
|
||||||
|
ty: None,
|
||||||
|
value: value.into(),
|
||||||
|
value_repr: None,
|
||||||
|
name: Some(key.into()),
|
||||||
|
trailing: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets leading text (whitespace, comments) for this KdlEntry.
|
||||||
|
pub fn leading(&self) -> Option<&str> {
|
||||||
|
self.leading.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets leading text (whitespace, comments) for this KdlEntry.
|
||||||
|
pub fn set_leading(&mut self, leading: impl Into<String>) {
|
||||||
|
self.leading = Some(leading.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets trailing text (whitespace, comments) for this KdlEntry.
|
||||||
|
pub fn trailing(&self) -> Option<&str> {
|
||||||
|
self.trailing.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets trailing text (whitespace, comments) for this KdlEntry.
|
||||||
|
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
|
||||||
|
self.trailing = Some(trailing.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the custom string representation for this KdlEntry's [`KdlValue`].
|
||||||
|
pub fn value_repr(&self) -> Option<&str> {
|
||||||
|
self.value_repr.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a custom string representation for this KdlEntry's [`KdlValue`].
|
||||||
|
pub fn set_value_repr(&mut self, repr: impl Into<String>) {
|
||||||
|
self.value_repr = Some(repr.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto-formats this entry.
|
||||||
|
pub fn fmt(&mut self) {
|
||||||
|
self.leading = None;
|
||||||
|
self.trailing = None;
|
||||||
|
self.value_repr = None;
|
||||||
|
if let Some(name) = &mut self.name {
|
||||||
|
name.fmt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KdlEntry {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(leading) = &self.leading {
|
||||||
|
write!(f, "{}", leading)?;
|
||||||
|
}
|
||||||
|
if let Some(ty) = &self.ty {
|
||||||
|
write!(f, "{}", ty)?;
|
||||||
|
}
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
write!(f, "{}=", name)?;
|
||||||
|
}
|
||||||
|
if let Some(repr) = &self.value_repr {
|
||||||
|
write!(f, "{}", repr)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", self.value)?;
|
||||||
|
}
|
||||||
|
if let Some(trailing) = &self.trailing {
|
||||||
|
write!(f, "{}", trailing)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for KdlEntry
|
||||||
|
where
|
||||||
|
T: Into<KdlValue>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
KdlEntry::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V> From<(K, V)> for KdlEntry
|
||||||
|
where
|
||||||
|
K: Into<KdlIdentifier>,
|
||||||
|
V: Into<KdlValue>,
|
||||||
|
{
|
||||||
|
fn from((key, value): (K, V)) -> Self {
|
||||||
|
KdlEntry::new_prop(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for KdlEntry {
|
||||||
|
type Err = KdlError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
KdlEntry::parse(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlEntry {
|
||||||
|
/// Parse a KDL document from a string into a [`KdlDocument`] object model.
|
||||||
|
fn parse(input: &str) -> Result<KdlEntry, KdlError> {
|
||||||
|
all_consuming(crate::parser::entry)(input)
|
||||||
|
.finish()
|
||||||
|
.map(|(_, arg)| arg)
|
||||||
|
.map_err(|e| {
|
||||||
|
let prefix = &input[..(input.len() - e.input.len())];
|
||||||
|
KdlError {
|
||||||
|
input: input.into(),
|
||||||
|
offset: prefix.chars().count(),
|
||||||
|
kind: if let Some(kind) = e.kind {
|
||||||
|
kind
|
||||||
|
} else if let Some(ctx) = e.context {
|
||||||
|
KdlErrorKind::Context(ctx)
|
||||||
|
} else {
|
||||||
|
KdlErrorKind::Other
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new() {
|
||||||
|
let entry = KdlEntry::new(42);
|
||||||
|
assert_eq!(
|
||||||
|
entry,
|
||||||
|
KdlEntry {
|
||||||
|
leading: None,
|
||||||
|
ty: None,
|
||||||
|
value: KdlValue::Base10(42),
|
||||||
|
value_repr: None,
|
||||||
|
name: None,
|
||||||
|
trailing: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let entry = KdlEntry::new_prop("name", 42);
|
||||||
|
assert_eq!(
|
||||||
|
entry,
|
||||||
|
KdlEntry {
|
||||||
|
leading: None,
|
||||||
|
ty: None,
|
||||||
|
value: KdlValue::Base10(42),
|
||||||
|
value_repr: None,
|
||||||
|
name: Some("name".into()),
|
||||||
|
trailing: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn display() {
|
||||||
|
let entry = KdlEntry::new(KdlValue::Base10(42));
|
||||||
|
assert_eq!(format!("{}", entry), "42");
|
||||||
|
|
||||||
|
let entry = KdlEntry::new_prop("name", KdlValue::Base10(42));
|
||||||
|
assert_eq!(format!("{}", entry), "name=42");
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/error.rs
13
src/error.rs
|
|
@ -1,7 +1,7 @@
|
||||||
use std::num::{ParseFloatError, ParseIntError};
|
use std::num::{ParseFloatError, ParseIntError};
|
||||||
|
|
||||||
|
use miette::Diagnostic;
|
||||||
use nom::error::{ContextError, ErrorKind, FromExternalError, ParseError};
|
use nom::error::{ContextError, ErrorKind, FromExternalError, ParseError};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[cfg(doc)]
|
#[cfg(doc)]
|
||||||
|
|
@ -11,16 +11,13 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An error that occurs when parsing a KDL document.
|
/// An error that occurs when parsing a KDL document.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Error)]
|
#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)]
|
||||||
#[error("Error parsing document at line {line} column {column}. {kind}")]
|
#[error("Error parsing document: {kind}")]
|
||||||
|
#[diagnostic(code("{kind.code()}"))]
|
||||||
pub struct KdlError {
|
pub struct KdlError {
|
||||||
pub input: String,
|
pub input: String,
|
||||||
/// Offset in chars of the error.
|
/// Offset in chars of the error.
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
/// 1-based line number of the error.
|
|
||||||
pub line: usize,
|
|
||||||
/// 1-based column number (in chars) of the error.
|
|
||||||
pub column: usize,
|
|
||||||
pub kind: KdlErrorKind,
|
pub kind: KdlErrorKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,7 +28,7 @@ pub enum KdlErrorKind {
|
||||||
ParseIntError(ParseIntError),
|
ParseIntError(ParseIntError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ParseFloatError(ParseFloatError),
|
ParseFloatError(ParseFloatError),
|
||||||
#[error("Failed to parse {0} component of semver string.")]
|
#[error("Failed to parse `{0}` component.")]
|
||||||
Context(&'static str),
|
Context(&'static str),
|
||||||
#[error("An unspecified error occurred.")]
|
#[error("An unspecified error occurred.")]
|
||||||
Other,
|
Other,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
/// Represents a KDL
|
||||||
|
/// [Identifier](https://github.com/kdl-org/kdl/blob/main/SPEC.md#identifier).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct KdlIdentifier {
|
||||||
|
pub(crate) value: String,
|
||||||
|
pub(crate) repr: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlIdentifier {
|
||||||
|
/// Gets the string value for this identifier.
|
||||||
|
pub fn value(&self) -> &str {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the string value for this identifier.
|
||||||
|
pub fn set_value(&mut self, value: impl Into<String>) {
|
||||||
|
self.value = value.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the custom string representation for this identifier, if any.
|
||||||
|
pub fn repr(&self) -> Option<&str> {
|
||||||
|
self.repr.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a custom string representation for this identifier.
|
||||||
|
pub fn set_repr(&mut self, repr: impl Into<String>) {
|
||||||
|
self.repr = Some(repr.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto-formats this identifier.
|
||||||
|
pub fn fmt(&mut self) {
|
||||||
|
self.repr = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KdlIdentifier {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(repr) = &self.repr {
|
||||||
|
write!(f, "{}", repr)
|
||||||
|
} else if self.plain_value() {
|
||||||
|
write!(f, "{}", self.value)
|
||||||
|
} else {
|
||||||
|
write!(f, "{:?}", self.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlIdentifier {
|
||||||
|
pub(crate) fn is_identifier_char(c: char) -> bool {
|
||||||
|
!((c as u32) < 0x20
|
||||||
|
|| (c as u32) > 0x10ffff
|
||||||
|
|| matches!(
|
||||||
|
c,
|
||||||
|
'\\' | '/'
|
||||||
|
| '('
|
||||||
|
| ')'
|
||||||
|
| '{'
|
||||||
|
| '}'
|
||||||
|
| '<'
|
||||||
|
| '>'
|
||||||
|
| ';'
|
||||||
|
| '['
|
||||||
|
| ']'
|
||||||
|
| '='
|
||||||
|
| ','
|
||||||
|
| '"'
|
||||||
|
// Newlines
|
||||||
|
| '\r'
|
||||||
|
| '\n'
|
||||||
|
| '\u{0085}'
|
||||||
|
| '\u{000C}'
|
||||||
|
| '\u{2028}'
|
||||||
|
| '\u{2029}'
|
||||||
|
// Whitespace
|
||||||
|
| ' '
|
||||||
|
| '\t'
|
||||||
|
| '\u{FEFF}'
|
||||||
|
| '\u{00A0}'
|
||||||
|
| '\u{1680}'
|
||||||
|
| '\u{2000}'
|
||||||
|
| '\u{2001}'
|
||||||
|
| '\u{2002}'
|
||||||
|
| '\u{2003}'
|
||||||
|
| '\u{2004}'
|
||||||
|
| '\u{2005}'
|
||||||
|
| '\u{2006}'
|
||||||
|
| '\u{2007}'
|
||||||
|
| '\u{2008}'
|
||||||
|
| '\u{2009}'
|
||||||
|
| '\u{200A}'
|
||||||
|
| '\u{202F}'
|
||||||
|
| '\u{205F}'
|
||||||
|
| '\u{3000}'
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_initial_char(c: char) -> bool {
|
||||||
|
!c.is_numeric() && Self::is_identifier_char(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plain_value(&self) -> bool {
|
||||||
|
let mut iter = self.value.chars();
|
||||||
|
if let Some(c) = iter.next() {
|
||||||
|
if !Self::is_initial_char(c) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for char in iter {
|
||||||
|
if !Self::is_identifier_char(char) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for KdlIdentifier {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
KdlIdentifier {
|
||||||
|
value: value.to_string(),
|
||||||
|
repr: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for KdlIdentifier {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
KdlIdentifier { value, repr: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<KdlIdentifier> for String {
|
||||||
|
fn from(value: KdlIdentifier) -> Self {
|
||||||
|
value.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formatting() {
|
||||||
|
let plain = KdlIdentifier::from("foo");
|
||||||
|
assert_eq!(format!("{}", plain), "foo");
|
||||||
|
|
||||||
|
let quoted = KdlIdentifier::from("foo\"bar");
|
||||||
|
assert_eq!(format!("{}", quoted), r#""foo\"bar""#);
|
||||||
|
|
||||||
|
let mut custom_repr = KdlIdentifier::from("foo");
|
||||||
|
custom_repr.set_repr(r#""foo/bar""#.to_string());
|
||||||
|
assert_eq!(format!("{}", custom_repr), r#""foo/bar""#);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/lib.rs
103
src/lib.rs
|
|
@ -1,71 +1,44 @@
|
||||||
#![doc(html_logo_url = "https://kdl.dev/logo.svg")]
|
#![doc(html_logo_url = "https://kdl.dev/logo.svg")]
|
||||||
#![doc = include_str!("../README.md")]
|
/// `kdl` is a "document-oriented" parser and API. That means that, unlike
|
||||||
|
/// serde-based implementations, it's meant to preserve formatting when editing,
|
||||||
use nom::combinator::all_consuming;
|
/// as well as inserting values with custom formatting. This is useful when
|
||||||
use nom::Finish;
|
/// working with human-maintained KDL files.
|
||||||
|
///
|
||||||
pub use crate::error::{KdlError, KdlErrorKind, TryFromKdlNodeValueError};
|
/// You can think of this crate as
|
||||||
pub use crate::node::{KdlNode, KdlValue};
|
/// [`toml_edit`](https://crates.io/crates/toml_edit), but for KDL.
|
||||||
|
///
|
||||||
|
/// ### Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use kdl::KdlDocument;
|
||||||
|
///
|
||||||
|
/// let doc: KdlDocument = r#"
|
||||||
|
/// hello 1 2 3
|
||||||
|
/// world prop="value" {
|
||||||
|
/// child 1
|
||||||
|
/// child 2
|
||||||
|
/// }
|
||||||
|
/// "#.parse().expect("failed to parse KDL");
|
||||||
|
///
|
||||||
|
/// assert_eq!(doc.get_args("hello"), vec![&1.into(), &2.into(), &3.into()]);
|
||||||
|
/// assert_eq!(doc.get("world").map(|node| &node["prop"]), Some(&"value".into()));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## License
|
||||||
|
///
|
||||||
|
/// The code in this repository is covered by [the Apache-2.0 License](LICENSE.md).
|
||||||
|
pub use document::*;
|
||||||
|
pub use entry::*;
|
||||||
|
pub use error::*;
|
||||||
|
pub use identifier::*;
|
||||||
|
pub use node::*;
|
||||||
|
pub use value::*;
|
||||||
|
|
||||||
|
mod document;
|
||||||
|
mod entry;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod identifier;
|
||||||
mod node;
|
mod node;
|
||||||
mod nom_compat;
|
mod nom_compat;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
mod value;
|
||||||
/// Parse a KDL document from a string into a list of [`KdlNode`]s.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use kdl::{KdlNode, KdlValue};
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// kdl::parse_document("node 1 key=true").unwrap(),
|
|
||||||
/// vec![
|
|
||||||
/// KdlNode {
|
|
||||||
/// name: String::from("node"),
|
|
||||||
/// values: vec![KdlValue::Int(1)],
|
|
||||||
/// properties: {
|
|
||||||
/// let mut temp = HashMap::new();
|
|
||||||
/// temp.insert(String::from("key"), KdlValue::Boolean(true));
|
|
||||||
/// temp
|
|
||||||
/// },
|
|
||||||
/// children: vec![],
|
|
||||||
/// }
|
|
||||||
/// ]
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
pub fn parse_document<I>(input: I) -> Result<Vec<KdlNode>, KdlError>
|
|
||||||
where
|
|
||||||
I: AsRef<str>,
|
|
||||||
{
|
|
||||||
let input = input.as_ref();
|
|
||||||
all_consuming(parser::nodes)(input)
|
|
||||||
.finish()
|
|
||||||
.map(|(_, arg)| arg)
|
|
||||||
.map_err(|e| {
|
|
||||||
let prefix = &input[..(input.len() - e.input.len())];
|
|
||||||
let (line, column) = calculate_line_column(prefix);
|
|
||||||
KdlError {
|
|
||||||
input: input.into(),
|
|
||||||
offset: prefix.chars().count(),
|
|
||||||
line,
|
|
||||||
column,
|
|
||||||
kind: if let Some(kind) = e.kind {
|
|
||||||
kind
|
|
||||||
} else if let Some(ctx) = e.context {
|
|
||||||
KdlErrorKind::Context(ctx)
|
|
||||||
} else {
|
|
||||||
KdlErrorKind::Other
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the line and column of the end of a `&str`.
|
|
||||||
///
|
|
||||||
/// If the line ends on a newline, the (line, column) pair is placed on the previous line instead.
|
|
||||||
fn calculate_line_column(input: &str) -> (usize, usize) {
|
|
||||||
let (input, skipped_lines) = parser::count_leading_lines(input);
|
|
||||||
let input = parser::strip_trailing_newline(input);
|
|
||||||
(skipped_lines + 1, input.len() + 1) // +1 as we're 1-based
|
|
||||||
}
|
|
||||||
|
|
|
||||||
790
src/node.rs
790
src/node.rs
|
|
@ -1,365 +1,479 @@
|
||||||
use std::{collections::HashMap, convert::TryFrom, fmt};
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
ops::{Index, IndexMut},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::TryFromKdlNodeValueError;
|
use nom::{combinator::all_consuming, Finish};
|
||||||
|
|
||||||
/// A node representing the smallest unit of a KDL document.
|
use crate::{KdlDocument, KdlEntry, KdlError, KdlErrorKind, KdlIdentifier, KdlValue};
|
||||||
///
|
|
||||||
/// The anatomy of a node:
|
|
||||||
/// ```text
|
|
||||||
/// name "value" property_key="property value" {
|
|
||||||
/// child
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ## Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use kdl::{KdlNode, KdlValue};
|
|
||||||
/// use std::collections::HashMap;
|
|
||||||
///
|
|
||||||
/// const DOCUMENT: &str = r#"
|
|
||||||
/// name "value" property_key="property value" {
|
|
||||||
/// child
|
|
||||||
/// }
|
|
||||||
/// "#;
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// kdl::parse_document(DOCUMENT).unwrap(),
|
|
||||||
/// vec![
|
|
||||||
/// KdlNode {
|
|
||||||
/// name: String::from("name"),
|
|
||||||
/// values: vec![KdlValue::String("value".into())],
|
|
||||||
/// properties: {
|
|
||||||
/// let mut temp = HashMap::new();
|
|
||||||
/// temp.insert(
|
|
||||||
/// String::from("property_key"),
|
|
||||||
/// KdlValue::String("property value".into())
|
|
||||||
/// );
|
|
||||||
/// temp
|
|
||||||
/// },
|
|
||||||
/// children: vec![
|
|
||||||
/// KdlNode {
|
|
||||||
/// name: String::from("child"),
|
|
||||||
/// ..Default::default()
|
|
||||||
/// }
|
|
||||||
/// ],
|
|
||||||
/// }
|
|
||||||
/// ]
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq)]
|
|
||||||
pub struct KdlNode {
|
|
||||||
pub name: String,
|
|
||||||
pub values: Vec<KdlValue>,
|
|
||||||
pub properties: HashMap<String, KdlValue>,
|
|
||||||
pub children: Vec<KdlNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A value present in either a node's values or in a node's properties.
|
/// Represents an individual KDL
|
||||||
|
/// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a
|
||||||
|
/// KDL Document.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum KdlValue {
|
pub struct KdlNode {
|
||||||
Int(i64),
|
pub(crate) leading: Option<String>,
|
||||||
Float(f64),
|
pub(crate) ty: Option<String>,
|
||||||
String(String),
|
pub(crate) name: KdlIdentifier,
|
||||||
Boolean(bool),
|
// TODO: consider using `hashlink` for this instead, later.
|
||||||
Null,
|
pub(crate) entries: Vec<KdlEntry>,
|
||||||
|
pub(crate) before_children: Option<String>,
|
||||||
|
pub(crate) children: Option<KdlDocument>,
|
||||||
|
pub(crate) trailing: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for KdlNode {
|
impl KdlNode {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
/// Creates a new KdlNode with a given name.
|
||||||
self.write(f, 0)
|
pub fn new(name: impl Into<KdlIdentifier>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
leading: None,
|
||||||
|
ty: None,
|
||||||
|
entries: Vec::new(),
|
||||||
|
before_children: None,
|
||||||
|
children: None,
|
||||||
|
trailing: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets this node's name.
|
||||||
|
pub fn name(&self) -> &KdlIdentifier {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a mutable reference to this node's name.
|
||||||
|
pub fn name_mut(&mut self) -> &mut KdlIdentifier {
|
||||||
|
&mut self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets this node's name.
|
||||||
|
pub fn set_name(&mut self, name: impl Into<KdlIdentifier>) {
|
||||||
|
self.name = name.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to this node's entries (arguments and properties).
|
||||||
|
pub fn entries(&self) -> &[KdlEntry] {
|
||||||
|
&self.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to this node's entries (arguments and
|
||||||
|
/// properties).
|
||||||
|
pub fn entries_mut(&mut self) -> &mut Vec<KdlEntry> {
|
||||||
|
&mut self.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets leading text (whitespace, comments) for this node.
|
||||||
|
pub fn leading(&self) -> Option<&str> {
|
||||||
|
self.leading.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets leading text (whitespace, comments) for this node.
|
||||||
|
pub fn set_leading(&mut self, leading: impl Into<String>) {
|
||||||
|
self.leading = Some(leading.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets text (whitespace, comments) right before the children block's starting `{`.
|
||||||
|
pub fn before_children(&self) -> Option<&str> {
|
||||||
|
self.before_children.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets text (whitespace, comments) right before the children block's starting `{`.
|
||||||
|
pub fn set_before_children(&mut self, before: impl Into<String>) {
|
||||||
|
self.before_children = Some(before.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets trailing text (whitespace, comments) for this node.
|
||||||
|
pub fn trailing(&self) -> Option<&str> {
|
||||||
|
self.trailing.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets trailing text (whitespace, comments) for this node.
|
||||||
|
pub fn set_trailing(&mut self, trailing: impl Into<String>) {
|
||||||
|
self.trailing = Some(trailing.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches an entry by key. Number keys will look up arguments, strings
|
||||||
|
/// will look up properties.
|
||||||
|
pub fn get(&self, key: impl Into<NodeKey>) -> Option<&KdlEntry> {
|
||||||
|
self.get_impl(key.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_impl(&self, key: NodeKey) -> Option<&KdlEntry> {
|
||||||
|
match key {
|
||||||
|
NodeKey::Key(key) => {
|
||||||
|
for entry in &self.entries {
|
||||||
|
if entry.name.is_some()
|
||||||
|
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
|
||||||
|
{
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
NodeKey::Index(idx) => {
|
||||||
|
let mut current_idx = 0;
|
||||||
|
for entry in &self.entries {
|
||||||
|
if entry.name.is_none() {
|
||||||
|
if current_idx == idx {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
current_idx += 1;
|
||||||
|
if current_idx > idx + 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches a mutable referene to an entry by key. Number keys will look
|
||||||
|
/// up arguments, strings will look up properties.
|
||||||
|
pub fn get_mut(&mut self, key: impl Into<NodeKey>) -> Option<&mut KdlEntry> {
|
||||||
|
self.get_mut_impl(key.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut_impl(&mut self, key: NodeKey) -> Option<&mut KdlEntry> {
|
||||||
|
match key {
|
||||||
|
NodeKey::Key(key) => {
|
||||||
|
for entry in &mut self.entries {
|
||||||
|
if entry.name.is_some()
|
||||||
|
&& entry.name.as_ref().map(|i| i.value()) == Some(key.value())
|
||||||
|
{
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
NodeKey::Index(idx) => {
|
||||||
|
let mut current_idx = 0;
|
||||||
|
for entry in &mut self.entries {
|
||||||
|
if entry.name.is_none() {
|
||||||
|
if current_idx >= idx {
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
current_idx += 1;
|
||||||
|
if current_idx >= idx {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts an entry into this node. If an entry already exists with the
|
||||||
|
/// same key, it will be replaced and the previous entry will be returned.
|
||||||
|
///
|
||||||
|
/// Numerical keys will insert arguments, string keys will insert
|
||||||
|
/// properties.
|
||||||
|
pub fn insert(
|
||||||
|
&mut self,
|
||||||
|
key: impl Into<NodeKey>,
|
||||||
|
entry: impl Into<KdlEntry>,
|
||||||
|
) -> Option<KdlEntry> {
|
||||||
|
self.insert_impl(key.into(), entry.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_impl(&mut self, key: NodeKey, mut entry: KdlEntry) -> Option<KdlEntry> {
|
||||||
|
match key {
|
||||||
|
NodeKey::Key(ref key_val) => {
|
||||||
|
if entry.name.is_none() {
|
||||||
|
entry.name = Some(key_val.clone());
|
||||||
|
}
|
||||||
|
if entry.name.as_ref().map(|i| i.value()) != Some(key_val.value()) {
|
||||||
|
panic!("Property name mismatch");
|
||||||
|
}
|
||||||
|
if let Some(existing) = self.get_mut(key) {
|
||||||
|
std::mem::swap(existing, &mut entry);
|
||||||
|
Some(entry)
|
||||||
|
} else {
|
||||||
|
self.entries.push(entry);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeKey::Index(idx) => {
|
||||||
|
if entry.name.is_some() {
|
||||||
|
panic!("Cannot insert property with name under a numerical key");
|
||||||
|
}
|
||||||
|
if let Some(existing) = self.get_mut(key) {
|
||||||
|
std::mem::swap(existing, &mut entry);
|
||||||
|
Some(entry)
|
||||||
|
} else {
|
||||||
|
let mut current_idx = 0;
|
||||||
|
for existing in &mut self.entries {
|
||||||
|
if existing.name.is_none() {
|
||||||
|
if current_idx == idx {
|
||||||
|
std::mem::swap(existing, &mut entry);
|
||||||
|
return Some(entry);
|
||||||
|
}
|
||||||
|
current_idx += 1;
|
||||||
|
if current_idx >= idx {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx > current_idx {
|
||||||
|
panic!(
|
||||||
|
"Insertion index (is {}) should be <= len (is {})",
|
||||||
|
idx, current_idx
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.entries.push(entry);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entry from this node. If an entry already exists with the
|
||||||
|
/// same key, it will be returned.
|
||||||
|
///
|
||||||
|
/// Numerical keys will remove arguments, string keys will remove
|
||||||
|
/// properties.
|
||||||
|
pub fn remove(&mut self, key: impl Into<NodeKey>) -> Option<KdlEntry> {
|
||||||
|
self.remove_impl(key.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_impl(&mut self, key: NodeKey) -> Option<KdlEntry> {
|
||||||
|
match key {
|
||||||
|
NodeKey::Key(key) => {
|
||||||
|
for (idx, entry) in self.entries.iter_mut().enumerate() {
|
||||||
|
if entry.name.is_some() && entry.name.as_ref() == Some(&key) {
|
||||||
|
return Some(self.entries.remove(idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
NodeKey::Index(idx) => {
|
||||||
|
let mut current_idx = 0;
|
||||||
|
for entry in &mut self.entries {
|
||||||
|
if entry.name.is_none() {
|
||||||
|
if current_idx == idx {
|
||||||
|
return Some(self.entries.remove(idx));
|
||||||
|
}
|
||||||
|
current_idx += 1;
|
||||||
|
if current_idx >= idx {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for `self.entries_mut().push(entry)`.
|
||||||
|
pub fn push(&mut self, entry: impl Into<KdlEntry>) {
|
||||||
|
self.entries.push(entry.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand for `self.entries_mut().clear()`
|
||||||
|
pub fn clear_entries(&mut self) {
|
||||||
|
self.entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to this node's children, if any.
|
||||||
|
pub fn children(&self) -> Option<&KdlDocument> {
|
||||||
|
self.children.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to this node's children, if any.
|
||||||
|
pub fn children_mut(&mut self) -> &mut Option<KdlDocument> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the KdlDocument representing this node's children.
|
||||||
|
pub fn set_children(&mut self, children: KdlDocument) {
|
||||||
|
self.children = Some(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes this node's children completely.
|
||||||
|
pub fn clear_children(&mut self) {
|
||||||
|
self.children = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to this node's children [`KdlDocument`],
|
||||||
|
/// creating one first if one does not already exist.
|
||||||
|
pub fn ensure_children(&mut self) -> &mut KdlDocument {
|
||||||
|
if self.children.is_none() {
|
||||||
|
self.children = Some(KdlDocument::new());
|
||||||
|
}
|
||||||
|
self.children_mut().as_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auto-formats this node and its contents.
|
||||||
|
pub fn fmt(&mut self) {
|
||||||
|
self.leading = None;
|
||||||
|
self.trailing = None;
|
||||||
|
for entry in &mut self.entries {
|
||||||
|
entry.fmt();
|
||||||
|
}
|
||||||
|
if let Some(children) = &mut self.children {
|
||||||
|
children.fmt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a [`KdlNode`]'s entry key.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum NodeKey {
|
||||||
|
Key(KdlIdentifier),
|
||||||
|
Index(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for NodeKey {
|
||||||
|
fn from(key: &str) -> Self {
|
||||||
|
NodeKey::Key(key.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for NodeKey {
|
||||||
|
fn from(key: String) -> Self {
|
||||||
|
NodeKey::Key(key.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usize> for NodeKey {
|
||||||
|
fn from(key: usize) -> Self {
|
||||||
|
NodeKey::Index(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for KdlNode {
|
||||||
|
type Output = KdlValue;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
self.get(index).expect("Argument out of range.").value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<usize> for KdlNode {
|
||||||
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
||||||
|
self.get_mut(index)
|
||||||
|
.expect("Argument out of range.")
|
||||||
|
.value_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<&str> for KdlNode {
|
||||||
|
type Output = KdlValue;
|
||||||
|
|
||||||
|
fn index(&self, key: &str) -> &Self::Output {
|
||||||
|
self.get(key).expect("No such property.").value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<&str> for KdlNode {
|
||||||
|
fn index_mut(&mut self, key: &str) -> &mut Self::Output {
|
||||||
|
if self.get(key).is_none() {
|
||||||
|
self.push((key, KdlValue::Null));
|
||||||
|
}
|
||||||
|
self.get_mut(key)
|
||||||
|
.expect("Something went wrong.")
|
||||||
|
.value_mut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KdlNode {
|
impl KdlNode {
|
||||||
fn write(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result {
|
/// Parse a KDL document from a string into a [`KdlDocument`] object model.
|
||||||
write!(f, "{:indent$}", "", indent = indent)?;
|
fn parse(input: &str) -> Result<KdlNode, KdlError> {
|
||||||
|
all_consuming(crate::parser::node)(input)
|
||||||
|
.finish()
|
||||||
|
.map(|(_, arg)| arg)
|
||||||
|
.map_err(|e| {
|
||||||
|
let prefix = &input[..(input.len() - e.input.len())];
|
||||||
|
KdlError {
|
||||||
|
input: input.into(),
|
||||||
|
offset: prefix.chars().count(),
|
||||||
|
kind: if let Some(kind) = e.kind {
|
||||||
|
kind
|
||||||
|
} else if let Some(ctx) = e.context {
|
||||||
|
KdlErrorKind::Context(ctx)
|
||||||
|
} else {
|
||||||
|
KdlErrorKind::Other
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
display_identifier(f, &self.name)?;
|
impl FromStr for KdlNode {
|
||||||
for arg in &self.values {
|
type Err = KdlError;
|
||||||
write!(f, " {}", arg)?;
|
|
||||||
}
|
|
||||||
for (prop, value) in &self.properties {
|
|
||||||
write!(f, " ")?;
|
|
||||||
display_identifier(f, prop)?;
|
|
||||||
write!(f, "={}", value)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.children.is_empty() {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
return Ok(());
|
KdlNode::parse(input)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
writeln!(f, " {{")?;
|
impl Display for KdlNode {
|
||||||
for child in &self.children {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
child.write(f, indent + 4)?;
|
self.stringify(f, 0)
|
||||||
writeln!(f)?;
|
}
|
||||||
}
|
}
|
||||||
write!(f, "{:indent$}}}", "", indent = indent)?;
|
|
||||||
|
|
||||||
|
impl KdlNode {
|
||||||
|
pub(crate) fn stringify(
|
||||||
|
&self,
|
||||||
|
f: &mut std::fmt::Formatter<'_>,
|
||||||
|
indent: usize,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
if let Some(leading) = &self.leading {
|
||||||
|
write!(f, "{}", leading)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "{:indent$}", "", indent = indent)?;
|
||||||
|
}
|
||||||
|
if let Some(ty) = &self.ty {
|
||||||
|
write!(f, "({})", ty)?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", self.name)?;
|
||||||
|
let mut space_before_children = true;
|
||||||
|
for entry in &self.entries {
|
||||||
|
if entry.leading.is_none() {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", entry)?;
|
||||||
|
space_before_children = entry.trailing.is_none();
|
||||||
|
}
|
||||||
|
if let Some(children) = &self.children {
|
||||||
|
if space_before_children {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
write!(f, "{{")?;
|
||||||
|
if children.leading.is_none() {
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
children.stringify(f, indent + 4)?;
|
||||||
|
write!(f, "}}")?;
|
||||||
|
}
|
||||||
|
if let Some(trailing) = &self.trailing {
|
||||||
|
write!(f, "{}", trailing)?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl fmt::Display for KdlValue {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
use KdlValue::*;
|
|
||||||
match self {
|
|
||||||
Int(x) => write!(f, "{}", x),
|
|
||||||
Float(x) => write!(f, "{}", x),
|
|
||||||
String(x) => display_string(f, x),
|
|
||||||
Boolean(x) => write!(f, "{}", x),
|
|
||||||
Null => write!(f, "null"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_identifier(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
|
|
||||||
if let Ok(("", identifier)) = crate::parser::bare_identifier(s) {
|
|
||||||
write!(f, "{}", identifier)
|
|
||||||
} else {
|
|
||||||
display_string(f, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn display_string(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
|
|
||||||
write!(f, "\"")?;
|
|
||||||
for c in s.chars() {
|
|
||||||
match crate::parser::ESCAPE_CHARS.1.get(&c) {
|
|
||||||
None => write!(f, "{}", c)?,
|
|
||||||
Some(c) => write!(f, "\\{}", c)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write!(f, "\"")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support conversions from base types into KdlNodeValue
|
|
||||||
|
|
||||||
impl From<i64> for KdlValue {
|
|
||||||
fn from(v: i64) -> Self {
|
|
||||||
Self::Int(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<f64> for KdlValue {
|
|
||||||
fn from(v: f64) -> Self {
|
|
||||||
Self::Float(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for KdlValue {
|
|
||||||
fn from(v: String) -> Self {
|
|
||||||
Self::String(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for KdlValue {
|
|
||||||
fn from(v: &str) -> Self {
|
|
||||||
Self::String(v.to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bool> for KdlValue {
|
|
||||||
fn from(v: bool) -> Self {
|
|
||||||
Self::Boolean(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> From<Option<T>> for KdlValue
|
|
||||||
where
|
|
||||||
T: Into<KdlValue>,
|
|
||||||
{
|
|
||||||
fn from(v: Option<T>) -> Self {
|
|
||||||
v.map_or(KdlValue::Null, |v| v.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Support reverse conversions using TryFrom
|
|
||||||
|
|
||||||
// Synthesizes a TryFrom impl for both the base type and an Option variant.
|
|
||||||
//
|
|
||||||
// We need the Option variant because we can't write a blanket impl due to the existing
|
|
||||||
// impl<T, U> TryFrom<U> for T where U: Into<T>
|
|
||||||
// even though KdlNodeValue does not implement Into<Option<_>>.
|
|
||||||
macro_rules! impl_try_from {
|
|
||||||
(<$($lt:lifetime)?> $source:ty => $typ:ty, $($good:pat => $value:expr),+; $($bad:ident),+) => {
|
|
||||||
impl<$($lt)?> TryFrom<$source> for $typ {
|
|
||||||
type Error = TryFromKdlNodeValueError;
|
|
||||||
fn try_from(value: $source) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
$( $good => Ok($value), )+
|
|
||||||
$( KdlValue::$bad(_) => Err(TryFromKdlNodeValueError {
|
|
||||||
expected: stringify!($typ),
|
|
||||||
variant: stringify!($bad)
|
|
||||||
}), )+
|
|
||||||
KdlValue::Null => Err(TryFromKdlNodeValueError {
|
|
||||||
expected: stringify!($typ),
|
|
||||||
variant: "Null"
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<$($lt)?> TryFrom<$source> for Option<$typ> {
|
|
||||||
type Error = TryFromKdlNodeValueError;
|
|
||||||
fn try_from(value: $source) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
$( $good => Ok(Some($value)), )+
|
|
||||||
$( KdlValue::$bad(_) => Err(TryFromKdlNodeValueError {
|
|
||||||
expected: concat!("Option::<", stringify!($typ), ">"),
|
|
||||||
variant: stringify!($bad)
|
|
||||||
}), )+
|
|
||||||
KdlValue::Null => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
(& $($lt:lifetime)?, $typ:ty, $($tt:tt)*) => {
|
|
||||||
impl_try_from!(<$($lt)?> & $($lt)? KdlValue => $typ, $($tt)*);
|
|
||||||
};
|
|
||||||
($typ:ty, $($tt:tt)*) => {
|
|
||||||
impl_try_from!(<> KdlValue => $typ, $($tt)*);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_try_from!(i64, KdlValue::Int(v) => v; Float, String, Boolean);
|
|
||||||
impl_try_from!(&, i64, KdlValue::Int(v) => *v; Float, String, Boolean);
|
|
||||||
impl_try_from!(f64, KdlValue::Float(v) => v; Int, String, Boolean);
|
|
||||||
impl_try_from!(&, f64, KdlValue::Float(v) => *v; Int, String, Boolean);
|
|
||||||
impl_try_from!(String, KdlValue::String(v) => v; Int, Float, Boolean);
|
|
||||||
impl_try_from!(&'a, &'a str, KdlValue::String(v) => &v[..]; Int, Float, Boolean);
|
|
||||||
impl_try_from!(bool, KdlValue::Boolean(v) => v; Int, Float, String);
|
|
||||||
impl_try_from!(&, bool, KdlValue::Boolean(v) => *v; Int, Float, String);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn display_value() {
|
fn indexing() {
|
||||||
assert_eq!("1", format!("{}", KdlValue::Int(1)));
|
let mut node = KdlNode::new("foo");
|
||||||
assert_eq!("1.5", format!("{}", KdlValue::Float(1.5)));
|
node.push("bar");
|
||||||
assert_eq!("true", format!("{}", KdlValue::Boolean(true)));
|
node["foo"] = 1.into();
|
||||||
assert_eq!("false", format!("{}", KdlValue::Boolean(false)));
|
|
||||||
assert_eq!("null", format!("{}", KdlValue::Null));
|
|
||||||
assert_eq!(
|
|
||||||
r#""foo""#,
|
|
||||||
format!("{}", KdlValue::String("foo".to_owned()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
r#""foo \"bar\" baz""#,
|
|
||||||
format!("{}", KdlValue::String(r#"foo "bar" baz"#.to_owned()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
assert_eq!(node[0], "bar".into());
|
||||||
fn display_node() {
|
assert_eq!(node["foo"], 1.into());
|
||||||
let mut value = KdlNode {
|
|
||||||
name: "foo".into(),
|
|
||||||
values: vec![1.into(), "two".into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
value.properties.insert("three".to_owned(), 3.into());
|
node[0] = false.into();
|
||||||
|
node["foo"] = KdlValue::Null;
|
||||||
|
|
||||||
assert_eq!(r#"foo 1 "two" three=3"#, format!("{}", value));
|
assert_eq!(node[0], false.into());
|
||||||
}
|
assert_eq!(node["foo"], KdlValue::Null);
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn display_nested_node() {
|
|
||||||
let value = KdlNode {
|
|
||||||
name: "a1".into(),
|
|
||||||
values: vec!["a".into(), 1.into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![
|
|
||||||
KdlNode {
|
|
||||||
name: "b1".into(),
|
|
||||||
values: vec!["b".into(), 1.into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![KdlNode {
|
|
||||||
name: "c1".into(),
|
|
||||||
values: vec!["c".into(), 1.into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
KdlNode {
|
|
||||||
name: "b2".into(),
|
|
||||||
values: vec!["b".into(), 2.into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![KdlNode {
|
|
||||||
name: "c2".into(),
|
|
||||||
values: vec!["c".into(), 2.into()],
|
|
||||||
properties: HashMap::new(),
|
|
||||||
children: vec![],
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
r#"
|
|
||||||
a1 "a" 1 {
|
|
||||||
b1 "b" 1 {
|
|
||||||
c1 "c" 1
|
|
||||||
}
|
|
||||||
b2 "b" 2 {
|
|
||||||
c2 "c" 2
|
|
||||||
}
|
|
||||||
}"#,
|
|
||||||
format!("\n{}", value)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from() {
|
|
||||||
assert_eq!(KdlValue::from(1), KdlValue::Int(1));
|
|
||||||
assert_eq!(KdlValue::from(1.5), KdlValue::Float(1.5));
|
|
||||||
assert_eq!(
|
|
||||||
KdlValue::from("foo".to_owned()),
|
|
||||||
KdlValue::String("foo".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(KdlValue::from("bar"), KdlValue::String("bar".to_owned()));
|
|
||||||
assert_eq!(KdlValue::from(true), KdlValue::Boolean(true));
|
|
||||||
|
|
||||||
assert_eq!(KdlValue::from(None::<i64>), KdlValue::Null);
|
|
||||||
assert_eq!(KdlValue::from(Some(1)), KdlValue::Int(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_from_success() {
|
|
||||||
assert_eq!(i64::try_from(KdlValue::Int(1)), Ok(1));
|
|
||||||
assert_eq!(i64::try_from(&KdlValue::Int(1)), Ok(1));
|
|
||||||
assert_eq!(f64::try_from(KdlValue::Float(1.5)), Ok(1.5));
|
|
||||||
assert_eq!(f64::try_from(&KdlValue::Float(1.5)), Ok(1.5));
|
|
||||||
assert_eq!(
|
|
||||||
String::try_from(KdlValue::String("foo".to_owned())),
|
|
||||||
Ok("foo".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
<&str as TryFrom<_>>::try_from(&KdlValue::String("foo".to_owned())),
|
|
||||||
Ok("foo")
|
|
||||||
);
|
|
||||||
assert_eq!(bool::try_from(KdlValue::Boolean(true)), Ok(true));
|
|
||||||
assert_eq!(bool::try_from(&KdlValue::Boolean(true)), Ok(true));
|
|
||||||
|
|
||||||
assert_eq!(Option::<i64>::try_from(KdlValue::Int(1)), Ok(Some(1)));
|
|
||||||
assert_eq!(Option::<i64>::try_from(KdlValue::Null), Ok(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn try_from_failure() {
|
|
||||||
// We don't expose the internal format of the error type, so let's just test the message
|
|
||||||
// for a couple of cases.
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", i64::try_from(KdlValue::Float(1.5)).unwrap_err()),
|
|
||||||
"Failed to convert from KdlNodeValue::Float to i64."
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
Option::<i64>::try_from(KdlValue::Float(1.5)).unwrap_err()
|
|
||||||
),
|
|
||||||
"Failed to convert from KdlNodeValue::Float to Option::<i64>."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1438
src/parser.rs
1438
src/parser.rs
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,266 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
/// A specific [KDL Value](https://github.com/kdl-org/kdl/blob/main/SPEC.md#value).
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum KdlValue {
|
||||||
|
/// A [KDL Raw String](https://github.com/kdl-org/kdl/blob/main/SPEC.md#raw-string).
|
||||||
|
RawString(String),
|
||||||
|
|
||||||
|
/// A [KDL String](https://github.com/kdl-org/kdl/blob/main/SPEC.md#string).
|
||||||
|
String(String),
|
||||||
|
|
||||||
|
/// A [KDL
|
||||||
|
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
|
||||||
|
/// binary form (e.g. `0b010101`).
|
||||||
|
Base2(i64),
|
||||||
|
|
||||||
|
/// A [KDL
|
||||||
|
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
|
||||||
|
/// octal form (e.g. `0o12345670`).
|
||||||
|
Base8(i64),
|
||||||
|
|
||||||
|
/// A [KDL
|
||||||
|
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
|
||||||
|
/// decimal form (e.g. `1234567890`).
|
||||||
|
Base10(i64),
|
||||||
|
|
||||||
|
/// A [KDL
|
||||||
|
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
|
||||||
|
/// decimal form (e.g. `1234567890.123`), interpreted as a Rust f64.
|
||||||
|
Base10Float(f64),
|
||||||
|
|
||||||
|
/// A [KDL
|
||||||
|
/// Number](https://github.com/kdl-org/kdl/blob/main/SPEC.md#number) in
|
||||||
|
/// hexadecimal form (e.g. `1234567890abcdef`).
|
||||||
|
Base16(i64),
|
||||||
|
|
||||||
|
/// A [KDL Boolean](https://github.com/kdl-org/kdl/blob/main/SPEC.md#boolean).
|
||||||
|
Bool(bool),
|
||||||
|
|
||||||
|
/// The [KDL Null Value](https://github.com/kdl-org/kdl/blob/main/SPEC.md#null).
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlValue {
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::RawString`].
|
||||||
|
pub fn is_raw_string(&self) -> bool {
|
||||||
|
matches!(self, Self::RawString(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::String`].
|
||||||
|
pub fn is_string(&self) -> bool {
|
||||||
|
matches!(self, Self::String(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::String`] or [`KdlValue::RawString`].
|
||||||
|
pub fn is_string_value(&self) -> bool {
|
||||||
|
matches!(self, Self::String(..) | Self::RawString(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base2`].
|
||||||
|
pub fn is_base2(&self) -> bool {
|
||||||
|
matches!(self, Self::Base2(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base8`].
|
||||||
|
pub fn is_base8(&self) -> bool {
|
||||||
|
matches!(self, Self::Base8(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base10`].
|
||||||
|
pub fn is_base10(&self) -> bool {
|
||||||
|
matches!(self, Self::Base10(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base16`].
|
||||||
|
pub fn is_base16(&self) -> bool {
|
||||||
|
matches!(self, Self::Base16(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base2`],
|
||||||
|
/// [`KdlValue::Base8`], [`KdlValue::Base10`], or [`KdlValue::Base16`].
|
||||||
|
pub fn is_i64_value(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
Self::Base2(..) | Self::Base8(..) | Self::Base10(..) | Self::Base16(..)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base10Float`].
|
||||||
|
pub fn is_base10_float(&self) -> bool {
|
||||||
|
matches!(self, Self::Base10Float(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Base10Float`].
|
||||||
|
pub fn is_float_value(&self) -> bool {
|
||||||
|
matches!(self, Self::Base10Float(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Bool`].
|
||||||
|
pub fn is_bool(&self) -> bool {
|
||||||
|
matches!(self, Self::Bool(..))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the value is a [`KdlValue::Null`].
|
||||||
|
pub fn is_null(&self) -> bool {
|
||||||
|
matches!(self, Self::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(&str)` if the `KdlValue` is a [`KdlValue::RawString`] or a
|
||||||
|
/// [`KdlValue::String`], otherwise returns `None`.
|
||||||
|
pub fn as_string(&self) -> Option<&str> {
|
||||||
|
use KdlValue::*;
|
||||||
|
match self {
|
||||||
|
String(s) | RawString(s) => Some(s),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(i64)` if the `KdlValue` is a [`KdlValue::Base2`],
|
||||||
|
/// [`KdlValue::Base8`], [`KdlValue::Base10`], or [`KdlValue::Base16`],
|
||||||
|
/// otherwise returns `None`.
|
||||||
|
pub fn as_i64(&self) -> Option<i64> {
|
||||||
|
use KdlValue::*;
|
||||||
|
match self {
|
||||||
|
Base2(i) | Base8(i) | Base10(i) | Base16(i) => Some(*i),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(f64)` if the `KdlValue` is a [`KdlValue::Base10Float`],
|
||||||
|
/// otherwise returns `None`.
|
||||||
|
pub fn as_f64(&self) -> Option<f64> {
|
||||||
|
if let Self::Base10Float(v) = self {
|
||||||
|
Some(*v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `Some(bool)` if the `KdlValue` is a [`KdlValue::Bool`], otherwise returns `None`.
|
||||||
|
pub fn as_bool(&self) -> Option<bool> {
|
||||||
|
if let Self::Bool(v) = self {
|
||||||
|
Some(*v)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KdlValue {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::RawString(_) => self.write_raw_string(f),
|
||||||
|
Self::String(string) => write!(f, "{:?}", string),
|
||||||
|
Self::Base2(value) => write!(f, "0b{:b}", value),
|
||||||
|
Self::Base8(value) => write!(f, "0o{:o}", value),
|
||||||
|
Self::Base10(value) => write!(f, "{}", value),
|
||||||
|
Self::Base10Float(value) => write!(f, "{}", value),
|
||||||
|
Self::Base16(value) => write!(f, "0x{:x}", value),
|
||||||
|
Self::Bool(value) => write!(f, "{}", value),
|
||||||
|
Self::Null => write!(f, "null"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KdlValue {
|
||||||
|
fn write_raw_string(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "r")?;
|
||||||
|
let raw = self.as_string().unwrap();
|
||||||
|
let mut consecutive = 0usize;
|
||||||
|
let mut maxhash = 0usize;
|
||||||
|
for char in raw.chars() {
|
||||||
|
if char == '#' {
|
||||||
|
consecutive += 1;
|
||||||
|
} else if char == '"' {
|
||||||
|
maxhash = maxhash.max(consecutive);
|
||||||
|
} else {
|
||||||
|
consecutive = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "{}", "#".repeat(maxhash + 1))?;
|
||||||
|
write!(f, "\"{}\"", raw)?;
|
||||||
|
write!(f, "{}", "#".repeat(maxhash + 1))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for KdlValue {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
KdlValue::Base10(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for KdlValue {
|
||||||
|
fn from(value: f64) -> Self {
|
||||||
|
KdlValue::Base10Float(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for KdlValue {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
KdlValue::String(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for KdlValue {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
KdlValue::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for KdlValue {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
KdlValue::Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Option<T>> for KdlValue
|
||||||
|
where
|
||||||
|
T: Into<KdlValue>,
|
||||||
|
{
|
||||||
|
fn from(value: Option<T>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some(value) => value.into(),
|
||||||
|
None => KdlValue::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formatting() {
|
||||||
|
let raw = KdlValue::RawString(r###"r##"foor#"bar"#baz"##"###.into());
|
||||||
|
assert_eq!(
|
||||||
|
format!("{}", raw),
|
||||||
|
r####"r###"r##"foor#"bar"#baz"##"###"####
|
||||||
|
);
|
||||||
|
|
||||||
|
let string = KdlValue::String("foo\n".into());
|
||||||
|
assert_eq!(format!("{}", string), r#""foo\n""#);
|
||||||
|
|
||||||
|
let base2 = KdlValue::Base2(0b1010_1010);
|
||||||
|
assert_eq!(format!("{}", base2), "0b10101010");
|
||||||
|
|
||||||
|
let base8 = KdlValue::Base8(0o12345670);
|
||||||
|
assert_eq!(format!("{}", base8), "0o12345670");
|
||||||
|
|
||||||
|
let base10 = KdlValue::Base10(1234567890);
|
||||||
|
assert_eq!(format!("{}", base10), "1234567890");
|
||||||
|
|
||||||
|
let base10float = KdlValue::Base10Float(1234567890.12345);
|
||||||
|
assert_eq!(format!("{}", base10float), "1234567890.12345");
|
||||||
|
|
||||||
|
let base16 = KdlValue::Base16(0x1234567890ABCDEF);
|
||||||
|
assert_eq!(format!("{}", base16), "0x1234567890abcdef");
|
||||||
|
|
||||||
|
let boolean = KdlValue::Bool(true);
|
||||||
|
assert_eq!(format!("{}", boolean), "true");
|
||||||
|
|
||||||
|
let null = KdlValue::Null;
|
||||||
|
assert_eq!(format!("{}", null), "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
//! Tests the kdl files in the examples directory.
|
|
||||||
|
|
||||||
use kdl::*;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Helper for constructing nodes.
|
|
||||||
///
|
|
||||||
/// This takes input that's similar to KDL itself, but each node must be terminated with either
|
|
||||||
/// a semicolon or a braced block. Nodes whose name contains characters not valid in Rust
|
|
||||||
/// identifiers must be written as a string literal instead.
|
|
||||||
macro_rules! nodes {
|
|
||||||
([$v:ident]:name) => {};
|
|
||||||
([$v:ident]:name $name:ident $($tt:tt)*) => {
|
|
||||||
nodes!([$v]:values stringify!($name); {} {} $($tt)*)
|
|
||||||
};
|
|
||||||
([$v:ident]:name $name:literal $($tt:tt)*) => {
|
|
||||||
nodes!([$v]:values $name; {} {} $($tt)*)
|
|
||||||
};
|
|
||||||
([$v:ident]:values $name:expr; {$($value:literal,)*} $props:tt $new_value:literal $($tt:tt)*) => {
|
|
||||||
nodes!([$v]:values $name; {$($value,)* $new_value,} $props $($tt)*)
|
|
||||||
};
|
|
||||||
([$v:ident]:values $name:expr; $values:tt {$($key:ident=$prop:literal,)*} $new_key:ident=$new_prop:literal $($tt:tt)*) => {
|
|
||||||
nodes!([$v]:values $name; $values {$($key=$prop,)* $new_key=$new_prop,} $($tt)*)
|
|
||||||
};
|
|
||||||
([$v:ident]:values $name:expr; $values:tt $props:tt $(; $($tt:tt)*)?) => {
|
|
||||||
nodes!([$v]:values $name; $values $props {} $($($tt)*)?)
|
|
||||||
};
|
|
||||||
([$v:ident]:values $name:expr; {$($value:literal,)*} {$($key:ident=$prop:literal,)*} {$($child:tt)*} $($tail:tt)*) => {
|
|
||||||
$v.push(KdlNode {
|
|
||||||
name: $name.to_owned(),
|
|
||||||
values: vec![$( $value.to_owned().into() ),*],
|
|
||||||
properties: {
|
|
||||||
#[allow(unused_mut)]
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
$(
|
|
||||||
map.insert(stringify!($key).to_owned(), $prop.to_owned().into());
|
|
||||||
)*
|
|
||||||
map
|
|
||||||
},
|
|
||||||
children: nodes!($($child)*),
|
|
||||||
});
|
|
||||||
nodes!([$v]:name $($tail)*);
|
|
||||||
};
|
|
||||||
// Explicitly match literal and ident at the start instead of $($tt:tt)*
|
|
||||||
// so we get better errors than "recursion limit exceeded" if we fail to match.
|
|
||||||
(:start $($tt:tt)+) => {{
|
|
||||||
let mut v = Vec::new();
|
|
||||||
nodes!([v]:name $($tt)+);
|
|
||||||
v
|
|
||||||
}};
|
|
||||||
($name:literal $($tt:tt)*) => {
|
|
||||||
nodes!(:start $name $($tt)*)
|
|
||||||
};
|
|
||||||
($name:ident $($tt:tt)*) => {
|
|
||||||
nodes!(:start $name $($tt)*)
|
|
||||||
};
|
|
||||||
() => { vec![] }
|
|
||||||
}
|
|
||||||
|
|
||||||
const NUMBERS: &str = r#"
|
|
||||||
hex 0x32;
|
|
||||||
float 0.5;
|
|
||||||
binary 0b0110;
|
|
||||||
octal 0o755;
|
|
||||||
bignum 1_000_000;
|
|
||||||
scientific 1.234e-10;
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_numbers() {
|
|
||||||
let doc = parse_document(NUMBERS);
|
|
||||||
assert_eq!(
|
|
||||||
doc,
|
|
||||||
Ok(nodes! {
|
|
||||||
hex 0x32;
|
|
||||||
float 0.5;
|
|
||||||
binary 0b0110;
|
|
||||||
octal 0o755;
|
|
||||||
bignum 1_000_000;
|
|
||||||
scientific 1.234e-10;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ci() {
|
|
||||||
let doc = parse_document(include_str!("../examples/ci.kdl"));
|
|
||||||
let nodes = nodes! {
|
|
||||||
name "CI";
|
|
||||||
on "push" "pull_request";
|
|
||||||
env {
|
|
||||||
RUSTFLAGS "-Dwarnings"
|
|
||||||
}
|
|
||||||
jobs {
|
|
||||||
fmt_and_docs "Check fmt & build docs" {
|
|
||||||
"runs-on" "ubuntu-latest";
|
|
||||||
steps {
|
|
||||||
step uses="actions/checkout@v1";
|
|
||||||
step "Install Rust" uses="actions-rs/toolchain@v1" {
|
|
||||||
profile "minimal";
|
|
||||||
toolchain "stable";
|
|
||||||
components "rustfmt";
|
|
||||||
override true;
|
|
||||||
}
|
|
||||||
step "rustfmt" run="cargo fmt --all -- --check";
|
|
||||||
step "docs" run="cargo doc --no-deps";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
build_and_test "Build & Test" {
|
|
||||||
"runs-on" "${{ matrix.os }}";
|
|
||||||
strategy {
|
|
||||||
matrix {
|
|
||||||
rust "1.46.0" "stable";
|
|
||||||
os "ubuntu-latest" "macOS-latest" "windows-latest";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
steps {
|
|
||||||
step uses="actions/checkout@v1";
|
|
||||||
step "Install Rust" uses="actions-rs/toolchain@v1" {
|
|
||||||
profile "minimal";
|
|
||||||
toolchain "${{ matrix.rust }}";
|
|
||||||
components "clippy";
|
|
||||||
override true;
|
|
||||||
}
|
|
||||||
step "Clippy" run="cargo clippy --all -- -D warnings";
|
|
||||||
step "Run tests" run="cargo test --all --verbose";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
assert_eq!(doc, Ok(nodes));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cargo() {
|
|
||||||
let doc = parse_document(include_str!("../examples/Cargo.kdl"));
|
|
||||||
let nodes = nodes! {
|
|
||||||
package {
|
|
||||||
name "kdl";
|
|
||||||
version "0.0.0";
|
|
||||||
description "kat's document language";
|
|
||||||
authors "Kat Marchán <kzm@zkat.tech>";
|
|
||||||
"license-file" "LICENSE.md";
|
|
||||||
edition "2018";
|
|
||||||
}
|
|
||||||
dependencies {
|
|
||||||
nom "6.0.1";
|
|
||||||
thiserror "1.0.22";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
assert_eq!(doc, Ok(nodes));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_nuget() {
|
|
||||||
let doc = parse_document(include_str!("../examples/nuget.kdl"));
|
|
||||||
// This file is particularly large. It would be nice to validate it, but for now
|
|
||||||
// I'm just going to settle for making sure it parses.
|
|
||||||
doc.expect("Parsing failed");
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue