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 }}
|
||||
strategy:
|
||||
matrix:
|
||||
rust: [1.46.0, stable]
|
||||
rust: [1.56.0, stable]
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
[package]
|
||||
name = "kdl"
|
||||
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"]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
homepage = "https://kdl.dev"
|
||||
repository = "https://github.com/kdl-org/kdl-rs"
|
||||
keywords = ["kdl", "document", "serialization", "config"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
|
||||
[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"] }
|
||||
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
|
||||
semantics that looks like you're invoking a bunch of CLI commands!
|
||||
You can think of this crate as
|
||||
[`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
|
||||
language, and is relatively light on syntax compared to XML.
|
||||
### Example
|
||||
|
||||
There's a living
|
||||
[specification](https://github.com/kdl-org/kdl/blob/main/SPEC.md), as well as
|
||||
[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.
|
||||
```rust
|
||||
use kdl::KdlDocument;
|
||||
|
||||
This repository is the official/reference implementation in Rust, and
|
||||
corresponds to [the kdl crate](https://crates.io/crates/kdl)
|
||||
|
||||
## Design and Discussion
|
||||
|
||||
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"
|
||||
}
|
||||
let doc: KdlDocument = r#"
|
||||
hello 1 2 3
|
||||
world prop="value" {
|
||||
child 1
|
||||
child 2
|
||||
}
|
||||
"#.parse().expect("failed to parse KDL");
|
||||
|
||||
// unicode! comments!
|
||||
π 3.14159
|
||||
```
|
||||
|
||||
## 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![],
|
||||
}
|
||||
]
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 miette::Diagnostic;
|
||||
use nom::error::{ContextError, ErrorKind, FromExternalError, ParseError};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(doc)]
|
||||
|
|
@ -11,16 +11,13 @@ use {
|
|||
};
|
||||
|
||||
/// An error that occurs when parsing a KDL document.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Error)]
|
||||
#[error("Error parsing document at line {line} column {column}. {kind}")]
|
||||
#[derive(Debug, Diagnostic, Clone, Eq, PartialEq, Error)]
|
||||
#[error("Error parsing document: {kind}")]
|
||||
#[diagnostic(code("{kind.code()}"))]
|
||||
pub struct KdlError {
|
||||
pub input: String,
|
||||
/// Offset in chars of the error.
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +28,7 @@ pub enum KdlErrorKind {
|
|||
ParseIntError(ParseIntError),
|
||||
#[error(transparent)]
|
||||
ParseFloatError(ParseFloatError),
|
||||
#[error("Failed to parse {0} component of semver string.")]
|
||||
#[error("Failed to parse `{0}` component.")]
|
||||
Context(&'static str),
|
||||
#[error("An unspecified error occurred.")]
|
||||
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 = include_str!("../README.md")]
|
||||
|
||||
use nom::combinator::all_consuming;
|
||||
use nom::Finish;
|
||||
|
||||
pub use crate::error::{KdlError, KdlErrorKind, TryFromKdlNodeValueError};
|
||||
pub use crate::node::{KdlNode, KdlValue};
|
||||
/// `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.
|
||||
///
|
||||
/// You can think of this crate as
|
||||
/// [`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 identifier;
|
||||
mod node;
|
||||
mod nom_compat;
|
||||
mod parser;
|
||||
|
||||
/// 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
|
||||
}
|
||||
mod value;
|
||||
|
|
|
|||
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.
|
||||
///
|
||||
/// 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>,
|
||||
}
|
||||
use crate::{KdlDocument, KdlEntry, KdlError, KdlErrorKind, KdlIdentifier, KdlValue};
|
||||
|
||||
/// 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)]
|
||||
pub enum KdlValue {
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
String(String),
|
||||
Boolean(bool),
|
||||
Null,
|
||||
pub struct KdlNode {
|
||||
pub(crate) leading: Option<String>,
|
||||
pub(crate) ty: Option<String>,
|
||||
pub(crate) name: KdlIdentifier,
|
||||
// TODO: consider using `hashlink` for this instead, later.
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.write(f, 0)
|
||||
impl KdlNode {
|
||||
/// Creates a new KdlNode with a given name.
|
||||
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 {
|
||||
fn write(&self, f: &mut fmt::Formatter<'_>, indent: usize) -> fmt::Result {
|
||||
write!(f, "{:indent$}", "", indent = indent)?;
|
||||
/// Parse a KDL document from a string into a [`KdlDocument`] object model.
|
||||
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)?;
|
||||
for arg in &self.values {
|
||||
write!(f, " {}", arg)?;
|
||||
}
|
||||
for (prop, value) in &self.properties {
|
||||
write!(f, " ")?;
|
||||
display_identifier(f, prop)?;
|
||||
write!(f, "={}", value)?;
|
||||
}
|
||||
impl FromStr for KdlNode {
|
||||
type Err = KdlError;
|
||||
|
||||
if self.children.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
KdlNode::parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(f, " {{")?;
|
||||
for child in &self.children {
|
||||
child.write(f, indent + 4)?;
|
||||
writeln!(f)?;
|
||||
}
|
||||
write!(f, "{:indent$}}}", "", indent = indent)?;
|
||||
impl Display for KdlNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.stringify(f, 0)
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
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)]
|
||||
mod tests {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_value() {
|
||||
assert_eq!("1", format!("{}", KdlValue::Int(1)));
|
||||
assert_eq!("1.5", format!("{}", KdlValue::Float(1.5)));
|
||||
assert_eq!("true", format!("{}", KdlValue::Boolean(true)));
|
||||
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()))
|
||||
);
|
||||
}
|
||||
fn indexing() {
|
||||
let mut node = KdlNode::new("foo");
|
||||
node.push("bar");
|
||||
node["foo"] = 1.into();
|
||||
|
||||
#[test]
|
||||
fn display_node() {
|
||||
let mut value = KdlNode {
|
||||
name: "foo".into(),
|
||||
values: vec![1.into(), "two".into()],
|
||||
properties: HashMap::new(),
|
||||
children: vec![],
|
||||
};
|
||||
assert_eq!(node[0], "bar".into());
|
||||
assert_eq!(node["foo"], 1.into());
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
#[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>."
|
||||
);
|
||||
assert_eq!(node[0], false.into());
|
||||
assert_eq!(node["foo"], KdlValue::Null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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