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:
Kat Marchán 2022-04-22 02:20:30 -07:00 committed by GitHub
parent d52e101ff9
commit 364ea6173c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2060 additions and 1541 deletions

12
.editorconfig Normal file
View File

@ -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

View File

@ -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:

View File

@ -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"

View File

@ -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

360
src/document.rs Normal file
View File

@ -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)
);
}
}

219
src/entry.rs Normal file
View File

@ -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");
}
}

View File

@ -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,

158
src/identifier.rs Normal file
View File

@ -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""#);
}
}

View File

@ -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
}

View File

@ -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>."
);
} }
} }

File diff suppressed because it is too large Load Diff

266
src/value.rs Normal file
View File

@ -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");
}
}

View File

@ -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");
}