feat(kql): implement KQL query engine (#61)

Fixes: https://github.com/kdl-org/kdl-rs/issues/32

This implements a proposed draft of a KQL engine,
allowing CSS selector-style querying of KDL documents (and nodes)
in a variety of different ways.
This commit is contained in:
Kat Marchán 2022-12-04 19:06:50 -08:00
parent 04471a537e
commit 6d1a516eb9
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
14 changed files with 1993 additions and 13 deletions

View File

@ -15,6 +15,10 @@ default = ["span"]
span = []
[dependencies]
miette = "5.3.0"
nom = { version = "7.1.1", default-features = false }
miette = "5.5.0"
nom = "7.1.1"
thiserror = "1.0.30"
[dev-dependencies]
miette = { version = "5.5.0", features = ["fancy"] }
pretty_assertions = "1.3.0"

View File

@ -29,7 +29,7 @@ words {
word_nodes.sort_by(sort_by_name);
words_section.fmt();
println!("{}", doc.to_string());
println!("{}", doc);
// output:
// words {

View File

@ -2,7 +2,7 @@
use miette::SourceSpan;
use std::{fmt::Display, str::FromStr};
use crate::{parser, KdlError, KdlNode, KdlValue};
use crate::{parser, IntoKdlQuery, KdlError, KdlNode, KdlQueryIterator, KdlValue, NodeKey};
/// Represents a KDL
/// [`Document`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#document).
@ -18,7 +18,7 @@ use crate::{parser, KdlError, KdlNode, KdlValue};
/// # use kdl::KdlDocument;
/// let kdl: KdlDocument = "foo 1 2 3\nbar 4 5 6".parse().expect("parse failed");
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq)]
pub struct KdlDocument {
pub(crate) leading: Option<String>,
pub(crate) nodes: Vec<KdlNode>,
@ -36,6 +36,15 @@ impl PartialEq for KdlDocument {
}
}
impl std::hash::Hash for KdlDocument {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.nodes.hash(state);
self.trailing.hash(state);
// Intentionally omitted: self.span.hash(state)
}
}
impl Default for KdlDocument {
fn default() -> Self {
Self {
@ -241,6 +250,70 @@ impl KdlDocument {
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
}
/// Queries this Document's children according to the KQL query language,
/// returning an iterator over all matching nodes.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_all(&self, query: impl IntoKdlQuery) -> Result<KdlQueryIterator<'_>, KdlError> {
let parsed = query.into_query()?;
Ok(KdlQueryIterator::new(None, Some(self), parsed))
}
/// Queries this Document's children according to the KQL query language,
/// returning the first match, if any.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlError> {
let mut iter = self.query_all(query)?;
Ok(iter.next())
}
/// Queries this Document's children according to the KQL query language,
/// picking the first match, and calling `.get(key)` on it, if the query
/// succeeded.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_get(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<Option<&KdlValue>, KdlError> {
Ok(self.query(query)?.and_then(|node| node.get(key)))
}
/// Queries this Document's children according to the KQL query language,
/// returning an iterator over all matching nodes, returning the requested
/// field from each of those nodes and filtering out nodes that don't have
/// it.
///
/// # NOTE
///
/// Any query selectors that try to select the toplevel `scope()` will
/// fail to match when using this method, since there's no [`KdlNode`] to
/// return in this case.
pub fn query_get_all(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<impl Iterator<Item = &KdlValue>, KdlError> {
let key: NodeKey = key.into();
Ok(self
.query_all(query)?
.filter_map(move |node| node.get(key.clone())))
}
}
impl Display for KdlDocument {

View File

@ -8,7 +8,7 @@ use crate::{parser, KdlError, KdlIdentifier, KdlValue};
/// [`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)]
#[derive(Debug, Clone, Eq)]
pub struct KdlEntry {
pub(crate) leading: Option<String>,
pub(crate) ty: Option<KdlIdentifier>,
@ -32,6 +32,18 @@ impl PartialEq for KdlEntry {
}
}
impl std::hash::Hash for KdlEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.ty.hash(state);
self.value.hash(state);
self.value_repr.hash(state);
self.name.hash(state);
self.trailing.hash(state);
// intentionally omitted: self.span.hash(state)
}
}
impl KdlEntry {
/// Creates a new Argument (positional) KdlEntry.
pub fn new(value: impl Into<KdlValue>) -> Self {

View File

@ -136,6 +136,7 @@ pub use entry::*;
pub use error::*;
pub use identifier::*;
pub use node::*;
pub use query::*;
pub use value::*;
mod document;
@ -146,4 +147,6 @@ mod identifier;
mod node;
mod nom_compat;
mod parser;
mod query;
mod query_parser;
mod value;

View File

@ -7,12 +7,15 @@ use std::{
#[cfg(feature = "span")]
use miette::SourceSpan;
use crate::{parser, KdlDocument, KdlEntry, KdlError, KdlIdentifier, KdlValue};
use crate::{
parser, IntoKdlQuery, KdlDocument, KdlEntry, KdlError, KdlIdentifier, KdlQueryIterator,
KdlValue,
};
/// Represents an individual KDL
/// [`Node`](https://github.com/kdl-org/kdl/blob/main/SPEC.md#node) inside a
/// KDL Document.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq)]
pub struct KdlNode {
pub(crate) leading: Option<String>,
pub(crate) ty: Option<KdlIdentifier>,
@ -39,6 +42,19 @@ impl PartialEq for KdlNode {
}
}
impl std::hash::Hash for KdlNode {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.leading.hash(state);
self.ty.hash(state);
self.name.hash(state);
self.entries.hash(state);
self.before_children.hash(state);
self.children.hash(state);
self.trailing.hash(state);
// Intentionally omitted: self.span.hash(state);
}
}
impl KdlNode {
/// Creates a new KdlNode with a given name.
pub fn new(name: impl Into<KdlIdentifier>) -> Self {
@ -414,6 +430,45 @@ impl KdlNode {
pub fn fmt_no_comments(&mut self) {
self.fmt_impl(0, true);
}
/// Queries this Node according to the KQL query language,
/// returning an iterator over all matching nodes.
pub fn query_all(&self, query: impl IntoKdlQuery) -> Result<KdlQueryIterator<'_>, KdlError> {
let q = query.into_query()?;
Ok(KdlQueryIterator::new(Some(self), None, q))
}
/// Queries this Node according to the KQL query language,
/// returning the first match, if any.
pub fn query(&self, query: impl IntoKdlQuery) -> Result<Option<&KdlNode>, KdlError> {
Ok(self.query_all(query)?.next())
}
/// Queries this Node according to the KQL query language,
/// picking the first match, and calling `.get(key)` on it, if the query
/// succeeded.
pub fn query_get(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<Option<&KdlValue>, KdlError> {
Ok(self.query(query)?.and_then(|node| node.get(key)))
}
/// Queries this Node according to the KQL query language,
/// returning an iterator over all matching nodes, returning the requested
/// field from each of those nodes and filtering out nodes that don't have
/// it.
pub fn query_get_all(
&self,
query: impl IntoKdlQuery,
key: impl Into<NodeKey>,
) -> Result<impl Iterator<Item = &KdlValue>, KdlError> {
let key: NodeKey = key.into();
Ok(self
.query_all(query)?
.filter_map(move |node| node.get(key.clone())))
}
}
/// Represents a [`KdlNode`]'s entry key.

View File

@ -89,7 +89,7 @@ impl<'a> KdlParser<'a> {
///
/// Note that substr must be a literal substring, as in it must be
/// a pointer into the same string!
fn span_from_substr(&self, substr: &str) -> SourceSpan {
pub(crate) fn span_from_substr(&self, substr: &str) -> SourceSpan {
let base_addr = self.full_input.as_ptr() as usize;
let substr_addr = substr.as_ptr() as usize;
assert!(
@ -388,7 +388,7 @@ fn argument<'a: 'b, 'b>(
}
}
fn value(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&str>> {
pub(crate) fn value(input: &str) -> IResult<&str, (String, KdlValue), KdlParseError<&str>> {
alt((
null,
boolean,
@ -416,7 +416,7 @@ fn children<'a: 'b, 'b>(
}
}
fn annotation<'a: 'b, 'b>(
pub(crate) fn annotation<'a: 'b, 'b>(
kdl_parser: &'b KdlParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlIdentifier, KdlParseError<&'a str>> + 'b {
move |input| {
@ -471,7 +471,7 @@ fn escline(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
))(input).map_err(|e| set_details(e, input, Some("line escape starts here"), Some("line escapes can only be followed by whitespace plus a newline (or single-line comment).")))
}
fn unicode_space(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
pub(crate) fn unicode_space(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
alt((
tag(" "),
tag("\t"),

334
src/query.rs Normal file
View File

@ -0,0 +1,334 @@
use std::{collections::VecDeque, str::FromStr, sync::Arc};
use crate::{query_parser::KdlQueryParser, KdlDocument, KdlError, KdlNode, KdlValue};
/// A parsed KQL query. For details on the syntax, see the [KQL
/// spec](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md).
#[derive(Debug, Clone, PartialEq)]
pub struct KdlQuery(pub(crate) Vec<KdlQuerySelector>);
impl FromStr for KdlQuery {
type Err = KdlError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parser = KdlQueryParser::new(s);
parser.parse(crate::query_parser::query(&parser))
}
}
/// A trait that tries to convert something into a [`KdlQuery`].
pub trait IntoKdlQuery: IntoQuerySealed {}
impl IntoKdlQuery for KdlQuery {}
impl IntoKdlQuery for String {}
impl<'a> IntoKdlQuery for &'a str {}
impl<'a> IntoKdlQuery for &'a String {}
#[doc(hidden)]
pub trait IntoQuerySealed {
fn into_query(self) -> Result<KdlQuery, KdlError>;
}
impl IntoQuerySealed for KdlQuery {
fn into_query(self) -> Result<KdlQuery, KdlError> {
Ok(self)
}
}
impl IntoQuerySealed for &str {
fn into_query(self) -> Result<KdlQuery, KdlError> {
self.parse()
}
}
impl IntoQuerySealed for String {
fn into_query(self) -> Result<KdlQuery, KdlError> {
self.parse()
}
}
impl IntoQuerySealed for &String {
fn into_query(self) -> Result<KdlQuery, KdlError> {
self.parse()
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct KdlQuerySelector(pub(crate) Vec<KdlQuerySelectorSegment>);
impl KdlQuerySelector {
fn matches(&self, crumb: Arc<Breadcrumb<'_>>, scope: Option<&KdlNode>) -> bool {
if self.0.is_empty() {
// I don't think this is possible, but just in case.
return false;
}
let mut segments = self.0.iter().rev();
let end = segments
.next()
.expect("This should've had at least one item.");
// When doing a query from a node, instead of a document, we have to
// skip matching on the node itself, unless the query is just
// `scope()`.
if let Some(scope) = &scope {
// We're in node-query mode.
if crumb.next.is_none() {
// We're almost definitely looking at the scope node itself,
// but just check. We'll do no further processing.
if end.is_scope() {
return crumb.node == *scope;
} else {
return false;
}
}
}
if !end.matcher.matches(crumb.node) {
// If the final segment doesn't even match the node, don't bother
// looking any further.
return false;
}
let mut node = crumb.node;
let mut next = crumb.next.clone();
let mut parent_doc = crumb.parent_doc;
'segments: for segment in segments {
use KdlSegmentCombinator::*;
match segment.op.as_ref().expect("This should've had an op.") {
Child | Descendant => {
while let Some(crumb) = next.clone() {
if segment.matcher.matches(crumb.node) {
continue 'segments;
}
// We only loop once if the op is `Child`. Otherwise,
// we keep going up the tree!
if segment.op == Some(Child) {
break;
}
next = crumb.next.clone();
if let Some(crumb) = &next {
node = crumb.node;
}
parent_doc = crumb.parent_doc;
}
if segment.is_scope() {
return next.map(|crumb| crumb.node) == scope;
}
return false;
}
Neighbor | Sibling => {
if let Some(parent) = &parent_doc {
for n in parent
.nodes()
.iter()
.rev()
.skip_while(|n| !std::ptr::eq(*n, node))
.skip(1)
{
if segment.matcher.matches(n) {
node = n;
continue 'segments;
}
if segment.op == Some(Neighbor) {
break;
}
}
}
return false;
}
}
}
true
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct KdlQuerySelectorSegment {
pub(crate) op: Option<KdlSegmentCombinator>,
pub(crate) matcher: KdlQueryMatcher,
}
impl KdlQuerySelectorSegment {
fn is_scope(&self) -> bool {
self.matcher.0.len() == 1 && self.matcher.0[0].accessor == KdlQueryMatcherAccessor::Scope
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum KdlSegmentCombinator {
Child,
Descendant,
Neighbor,
Sibling,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct KdlQueryMatcher(pub(crate) Vec<KdlQueryMatcherDetails>);
impl KdlQueryMatcher {
pub(crate) fn matches(&self, node: &KdlNode) -> bool {
self.0.iter().all(|m| m.matches(node))
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct KdlQueryMatcherDetails {
pub(crate) accessor: KdlQueryMatcherAccessor,
pub(crate) op: KdlQueryAttributeOp,
pub(crate) value: Option<KdlValue>,
}
impl KdlQueryMatcherDetails {
pub(crate) fn matches(&self, node: &KdlNode) -> bool {
use KdlQueryAttributeOp::*;
use KdlQueryMatcherAccessor::*;
match (&self.accessor, &self.op, &self.value) {
(Scope, _, _) => false,
(Annotation | Node, op, Some(KdlValue::String(s) | KdlValue::RawString(s))) => {
let lhs = match &self.accessor {
Annotation => node.ty().map(|ty| ty.value()),
Node => Some(node.name().value()),
_ => unreachable!(),
};
let ss = Some(&s[..]);
match op {
Equal => lhs == ss,
NotEqual => lhs != ss,
Gt => lhs > ss,
Gte => lhs >= ss,
Lt => lhs < ss,
Lte => lhs <= ss,
StartsWith => lhs.map(|lhs| lhs.starts_with(s)).unwrap_or(false),
EndsWith => lhs.map(|lhs| lhs.ends_with(s)).unwrap_or(false),
Contains => lhs.map(|lhs| lhs.contains(s)).unwrap_or(false),
}
}
(Annotation | Node, _op, Some(_)) => false,
// This is `()blah`.
(Annotation, _, None) => node.ty().is_some(),
// This is `[]`.
(Node, _, None) => true,
(Arg(_) | Prop(_), op, val @ Some(_)) => {
let val = val.as_ref();
let lhs = match &self.accessor {
Arg(Some(idx)) => node.get(*idx),
Arg(None) => node.get(0),
Prop(name) => node.get(&name[..]),
_ => unreachable!(),
};
match &op {
Equal => lhs == val,
NotEqual => lhs != val,
Gt => lhs > val,
Gte => lhs >= val,
Lt => lhs < val,
Lte => lhs <= val,
StartsWith | EndsWith | Contains => {
unreachable!("This should have been caught by the parser")
}
}
}
(Arg(_) | Prop(_), _op, None) => match &self.accessor {
Arg(Some(idx)) => node.get(*idx).is_some(),
Arg(None) => node.get(0).is_some(),
Prop(name) => node.get(&name[..]).is_some(),
_ => unreachable!(),
},
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum KdlQueryAttributeOp {
Equal,
NotEqual,
Gt,
Gte,
Lt,
Lte,
StartsWith,
EndsWith,
Contains,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum KdlQueryMatcherAccessor {
Scope,
Node,
Annotation,
Arg(Option<usize>),
Prop(String),
}
#[derive(Debug, Clone)]
struct Breadcrumb<'a> {
node: &'a KdlNode,
parent_doc: Option<&'a KdlDocument>,
next: Option<Arc<Breadcrumb<'a>>>,
}
/// Iterator for results of a KDL query over a [`KdlDocument`].
#[derive(Debug, Clone)]
pub struct KdlQueryIterator<'a> {
scope: Option<&'a KdlNode>,
query: KdlQuery,
q: VecDeque<Arc<Breadcrumb<'a>>>,
}
impl<'a> KdlQueryIterator<'a> {
pub(crate) fn new(
scope: Option<&'a KdlNode>,
ctx_doc: Option<&'a KdlDocument>,
query: KdlQuery,
) -> Self {
let mut q = VecDeque::new();
if let Some(scope) = scope {
q.push_back(Arc::new(Breadcrumb {
node: scope,
parent_doc: None,
next: None,
}));
} else if let Some(doc) = ctx_doc {
for node in doc.nodes() {
q.push_front(Arc::new(Breadcrumb {
node,
parent_doc: Some(doc),
next: None,
}));
}
}
Self { scope, query, q }
}
}
impl<'a> Iterator for KdlQueryIterator<'a> {
type Item = &'a KdlNode;
fn next(&mut self) -> Option<Self::Item> {
while let Some(crumb) = self.q.pop_back() {
if let Some(children) = crumb.node.children() {
for node in children.nodes().iter().rev() {
self.q.push_back(Arc::new(Breadcrumb {
node,
parent_doc: Some(children),
next: Some(crumb.clone()),
}));
}
}
for selector in &self.query.0 {
if selector.matches(crumb.clone(), self.scope) {
return Some(crumb.node);
}
}
}
// Otherwise, we're done! Just return None and the iterator's done.
None
}
}

591
src/query_parser.rs Normal file
View File

@ -0,0 +1,591 @@
use crate::nom_compat::many0;
use crate::parser::{value, KdlParser};
use crate::query::{
KdlQuery, KdlQueryAttributeOp, KdlQueryMatcher, KdlQueryMatcherAccessor,
KdlQueryMatcherDetails, KdlQuerySelector, KdlQuerySelectorSegment, KdlSegmentCombinator,
};
use crate::{KdlError, KdlErrorKind, KdlParseError, KdlValue};
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::combinator::{all_consuming, cut, map, opt, recognize};
use nom::error::context;
use nom::multi::separated_list1;
use nom::sequence::{delimited, preceded, terminated};
use nom::{Finish, IResult, Offset, Parser};
pub(crate) struct KdlQueryParser<'a>(KdlParser<'a>);
impl<'a> KdlQueryParser<'a> {
pub(crate) fn new(full_input: &'a str) -> Self {
Self(KdlParser::new(full_input))
}
pub(crate) fn parse<T, P>(&self, parser: P) -> Result<T, KdlError>
where
P: Parser<&'a str, T, KdlParseError<&'a str>>,
{
all_consuming(parser)(self.0.full_input)
.finish()
.map(|(_, arg)| arg)
.map_err(|e| {
let span_substr = &e.input[..e.len];
KdlError {
input: self.0.full_input.into(),
span: self.0.span_from_substr(span_substr),
help: if let Some(help) = e.help {
Some(help)
} else if e.kind.is_none() && e.context.is_none() {
Some("The general syntax for queries is '(type)nodename[prop=value], anothernode, etc'. For more details, please see https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md")
} else {
None
},
label: e.label,
kind: if let Some(kind) = e.kind {
kind
} else if let Some(ctx) = e.context {
KdlErrorKind::Context(ctx)
} else {
KdlErrorKind::Context("a valid KQL query")
},
}
})
}
}
fn set_details<'a>(
mut err: nom::Err<KdlParseError<&'a str>>,
start: &'a str,
label: Option<&'static str>,
help: Option<&'static str>,
) -> nom::Err<KdlParseError<&'a str>> {
match &mut err {
nom::Err::Error(e) | nom::Err::Failure(e) => {
if !e.touched {
e.len = start.offset(e.input);
e.input = start;
e.label = label;
e.help = help;
e.touched = true;
}
}
_ => {}
}
err
}
pub(crate) fn query<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl FnMut(&'a str) -> IResult<&'a str, KdlQuery, KdlParseError<&'a str>> + 'b {
map(
separated_list1(
delimited(whitespace, tag(","), whitespace),
query_selector(kdl_parser),
),
KdlQuery,
)
}
fn query_selector<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQuerySelector, KdlParseError<&'a str>> + 'b {
move |input| {
let mut segments = Vec::new();
let mut is_scope = true;
let mut input = input;
loop {
let (inp, _) = whitespace(input)?;
input = inp;
let (inp, matchers) = node_matchers(kdl_parser, is_scope)(input)?;
input = inp;
let (inp, _) = whitespace(input)?;
input = inp;
let (inp, op) = opt(segment_combinator)(input)?;
input = inp;
let is_last = op.is_none();
segments.push(KdlQuerySelectorSegment {
op,
matcher: KdlQueryMatcher(matchers),
});
if is_last {
break;
}
is_scope = false;
}
let (input, _) = whitespace(input)?;
Ok((input, KdlQuerySelector(segments)))
}
}
fn segment_combinator(input: &str) -> IResult<&str, KdlSegmentCombinator, KdlParseError<&str>> {
alt((
map(tag(">>"), |_| KdlSegmentCombinator::Descendant),
map(tag(">"), |_| KdlSegmentCombinator::Child),
map(tag("++"), |_| KdlSegmentCombinator::Sibling),
map(tag("+"), |_| KdlSegmentCombinator::Neighbor),
))(input)
}
fn node_matchers<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
is_scope: bool,
) -> impl Fn(&'a str) -> IResult<&'a str, Vec<KdlQueryMatcherDetails>, KdlParseError<&'a str>> + 'b
{
move |input| {
let mut matchers = Vec::new();
let (input, _) = whitespace(input)?;
let start = input;
let (input, scope) = opt(scope_accessor)(input)?;
if let Some(xsr) = scope {
if is_scope {
matchers.push(KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
accessor: xsr,
value: None,
});
return Ok((input, matchers));
} else {
return Err(nom::Err::Error(KdlParseError {
input: start,
len: start.len() - input.len(),
kind: None,
label: Some("scope()"),
help: Some("Make sure scope() precedes any other items within a (comma-separated) selector."),
touched: false,
context: Some("scope() to be the first item in this selector"),
}));
}
}
let (input, details) = opt(annotation_matcher(kdl_parser))(input)?;
if let Some(details) = details {
matchers.push(details);
let start = input;
let (input, typed) = opt(annotation_matcher(kdl_parser))(input)?;
if typed.is_some() {
return Err(nom::Err::Error(KdlParseError {
input: start,
len: start.len() - input.len(),
kind: None,
label: Some("type annotation"),
help: Some("The syntax for node selectors is (type)node[attribute=value]."),
touched: false,
context: Some("only one type annotation per selector"),
}));
}
}
let (input, node) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
if let Some(node) = node {
matchers.push(KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
value: Some(KdlValue::String(node.value().to_owned())),
accessor: KdlQueryMatcherAccessor::Node,
});
}
let start = input;
let (input, typed) = opt(annotation_matcher(kdl_parser))(input)?;
if typed.is_some() {
return Err(nom::Err::Error(KdlParseError {
input: start,
len: start.len() - input.len(),
kind: None,
label: Some("type annotation"),
help: Some("The syntax for node selectors is (type)node[attribute=value]."),
touched: false,
context: Some("type annotation to not be used after a node name"),
}));
}
let start = input;
let (input, mut attribute_matchers) = many0(attribute_matcher(kdl_parser))(input)?;
matchers.append(&mut attribute_matchers);
if matchers.is_empty() {
Err(nom::Err::Error(KdlParseError {
input: start,
len: 0,
kind: None,
label: Some("node matcher"),
help: Some("node matcher must not be empty"),
touched: false,
context: Some("a valid node matcher"),
}))
} else {
// Check for trailing type annotations.
let start = input;
let (end, typed) = opt(annotation_matcher(kdl_parser))(input)?;
if typed.is_some() {
return Err(nom::Err::Error(KdlParseError {
input: start,
len: start.len() - end.len(),
kind: None,
label: Some("type annotation"),
help: Some("The syntax for node selectors is (type)node[attribute=value]."),
touched: false,
context: Some("type annotation to come before attribute matcher(s)"),
}));
}
// Check for trailing node name matcher.
let (end, ident) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
if ident.is_some() {
return Err(nom::Err::Error(KdlParseError {
input: start,
len: start.len() - end.len(),
kind: None,
label: Some("node name"),
help: Some("The syntax for node selectors is (type)node[attribute=value]."),
touched: false,
context: Some("node name to come before attribute matcher(s)"),
}));
}
Ok((input, matchers))
}
}
}
fn attribute_matcher<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherDetails, KdlParseError<&'a str>> + 'b {
move |input| {
let start = input;
let (input, _) = tag("[")(input)?;
let (input, _) = whitespace(input)?;
let (input, matcher) = attribute_matcher_inner(kdl_parser)(input)?;
let (input, _) = whitespace(input)?;
let (input, _) = context("a closing ']' for this attribute matcher", cut(tag("]")))(input)
.map_err(|e| set_details(e, start, Some("partial attribute matcher"), None))?;
Ok((input, matcher))
}
}
fn attribute_matcher_inner<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherDetails, KdlParseError<&'a str>> + 'b {
move |input| {
let (input, xsr) = opt(accessor(kdl_parser))(input)?;
if let Some(xsr) = xsr {
let (input, _) = whitespace(input)?;
let (input, op) = opt(attribute_op)(input)?;
let (input, _) = whitespace(input)?;
if let Some(op) = op {
let prev = input;
let (input, val) = opt(crate::parser::value)(input)?;
// Make sure it's a syntax error to try and use string
// operators with non-string arguments.
if let Some((_, value)) = val {
if matches!(
op,
KdlQueryAttributeOp::StartsWith
| KdlQueryAttributeOp::EndsWith
| KdlQueryAttributeOp::Contains
) {
if value.is_string() {
Ok((
input,
KdlQueryMatcherDetails {
op,
value: Some(value),
accessor: xsr,
},
))
} else {
Err(nom::Err::Failure(KdlParseError {
input: prev,
len: prev.len() - input.len(),
kind: None,
label: Some("non-string operator value"),
help: Some("Only strings can be used as arguments for string-related operators (*=, ^=, $=)."),
touched: false,
context: Some("a string as an operator value"),
}))
}
} else {
Ok((
input,
KdlQueryMatcherDetails {
op,
value: Some(value),
accessor: xsr,
},
))
}
} else {
Err(nom::Err::Failure(KdlParseError {
input: prev,
len: 0,
kind: None,
label: Some("operator value"),
help: Some("Only valid KDL values can be used on the right hand side of attribute matcher operators."),
touched: false,
context: Some("a valid operator argument"),
}))
}
} else {
Ok((
input,
KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
value: None,
accessor: xsr,
},
))
}
} else {
Ok((
input,
KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
value: None,
accessor: KdlQueryMatcherAccessor::Node,
},
))
}
}
}
fn attribute_op(input: &str) -> IResult<&str, KdlQueryAttributeOp, KdlParseError<&str>> {
alt((
map(tag("="), |_| KdlQueryAttributeOp::Equal),
map(tag("!="), |_| KdlQueryAttributeOp::NotEqual),
map(tag(">"), |_| KdlQueryAttributeOp::Gt),
map(tag(">="), |_| KdlQueryAttributeOp::Gte),
map(tag("<"), |_| KdlQueryAttributeOp::Lt),
map(tag("<="), |_| KdlQueryAttributeOp::Lte),
map(tag("^="), |_| KdlQueryAttributeOp::StartsWith),
map(tag("$="), |_| KdlQueryAttributeOp::EndsWith),
map(tag("*="), |_| KdlQueryAttributeOp::Contains),
))(input)
}
fn annotation_matcher<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherDetails, KdlParseError<&'a str>> + 'b {
move |input| {
let start = input;
let (input, _) = tag("(")(input)?;
let (input, _) = whitespace(input)?;
let (input, ty) = opt(crate::parser::identifier(&kdl_parser.0))(input)?;
let (input, _) = context("closing ')' for type annotation", cut(tag(")")))(input)
.map_err(|e| set_details(e, start, Some("annotation"), Some("annotations can only be KDL identifiers (including string identifiers), and can't have any space inside the parentheses.")))?;
Ok((
input,
KdlQueryMatcherDetails {
op: KdlQueryAttributeOp::Equal,
value: ty.map(|ident| KdlValue::String(ident.value().to_owned())),
accessor: KdlQueryMatcherAccessor::Annotation,
},
))
}
}
fn scope_accessor(input: &str) -> IResult<&str, KdlQueryMatcherAccessor, KdlParseError<&str>> {
let start = input;
let (input, _) = tag("scope(")(input)?;
let (input, _) = context(
"a valid scope accessor",
cut(preceded(whitespace, tag(")"))),
)(input)
.map_err(|e| set_details(e, start, Some("partial scope accessor"), None))?;
Ok((input, KdlQueryMatcherAccessor::Scope))
}
fn accessor<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherAccessor, KdlParseError<&'a str>> + 'b {
move |input| {
let (input, accessor) = alt((
type_accessor,
arg_accessor,
prop_accessor(kdl_parser),
prop_name_accessor(kdl_parser),
bad_accessor(kdl_parser),
))(input)?;
Ok((input, accessor))
}
}
fn type_accessor(input: &str) -> IResult<&str, KdlQueryMatcherAccessor, KdlParseError<&str>> {
let start = input;
let (input, _) = tag("type")(input)?;
let (input, _) = context(
"an opening '(' for a 'type()' accessor",
preceded(whitespace, tag("(")),
)(input)
.map_err(|e| set_details(e, start, Some("partial type accessor"), None))?;
let (input, _) = context(
"a closing ')' for this 'type()' accessor",
cut(preceded(whitespace, tag(")"))),
)(input)
.map_err(|e| {
set_details(
e,
start,
Some("partial type accessor"),
Some("type() accessors don't take any arguments. Use e.g. [type() = \"foo\"] instead."),
)
})?;
Ok((input, KdlQueryMatcherAccessor::Annotation))
}
fn arg_accessor(input: &str) -> IResult<&str, KdlQueryMatcherAccessor, KdlParseError<&str>> {
let (input, _) = tag("arg")(input)?;
let (input, arg) = parenthesized_arg(input)?;
if let Some(arg) = arg {
if let Some(index) = arg
.as_i64()
.and_then(|arg| -> Option<usize> { arg.try_into().ok() })
{
Ok((input, KdlQueryMatcherAccessor::Arg(Some(index))))
} else {
Err(nom::Err::Error(KdlParseError {
input,
len: 0,
kind: None,
label: Some("arg accessor"),
help: Some("arg accessor must be an integer"),
touched: false,
context: Some("a valid arg accessor"),
}))
}
} else {
Ok((input, KdlQueryMatcherAccessor::Arg(None)))
}
}
fn prop_name_accessor<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherAccessor, KdlParseError<&'a str>> + 'b {
move |input| {
let start = input;
let (input, prop_name) = crate::parser::identifier(&kdl_parser.0)(input)?;
let (_, paren) = opt(preceded(whitespace, tag("(")))(input)?;
if paren.is_some() {
Err(nom::Err::Error(KdlParseError {
input: start,
len: 0,
kind: None,
label: Some("accessor"),
help: Some("accessor must be one of: type(), arg(), prop(), propname"),
touched: false,
context: Some("a valid accessor"),
}))
} else {
Ok((
input,
KdlQueryMatcherAccessor::Prop(prop_name.value().to_owned()),
))
}
}
}
fn prop_accessor<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherAccessor, KdlParseError<&'a str>> + 'b {
move |input| {
let (input, _) = tag("prop")(input)?;
let (input, val) = parenthesized_prop(kdl_parser)(input)?;
Ok((input, KdlQueryMatcherAccessor::Prop(val)))
}
}
fn parenthesized_arg(input: &str) -> IResult<&str, Option<KdlValue>, KdlParseError<&str>> {
let (input, _) = tag("(")(input)?;
let (input, maybe_value) = opt(value)(input)?;
let (input, _) = tag(")")(input)?;
if let Some((_, val)) = maybe_value {
Ok((input, Some(val)))
} else {
Ok((input, None))
}
}
fn parenthesized_prop<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, String, KdlParseError<&'a str>> + 'b {
move |input| {
let (input, _) = tag("(")(input)?;
let (input, prop) = crate::parser::identifier(&kdl_parser.0)(input)?;
let (input, _) = tag(")")(input)?;
Ok((input, prop.value().to_owned()))
}
}
fn bad_accessor<'a: 'b, 'b>(
kdl_parser: &'b KdlQueryParser<'a>,
) -> impl Fn(&'a str) -> IResult<&'a str, KdlQueryMatcherAccessor, KdlParseError<&'a str>> + 'b {
move |input| {
let start = input;
let (input, scope) = opt(preceded(
tag("scope"),
preceded(
whitespace,
opt(terminated(tag("("), opt(preceded(whitespace, tag(")"))))),
),
))(input)?;
if scope.is_some() {
return Err(nom::Err::Failure(KdlParseError {
input: start,
len: start.len() - input.len(),
kind: None,
label: Some("incorrect scope() accessor"),
help: Some("Accessors must be one of: type(), arg(), prop(), propname"),
touched: false,
context: Some(
"'scope()' to be the first item only at the top level of the query selector",
),
}));
}
let (input, ident) = opt(terminated(
crate::parser::identifier(&kdl_parser.0),
preceded(
whitespace,
terminated(tag("("), opt(preceded(whitespace, tag(")")))),
),
))(input)?;
if let Some(ident) = ident {
match ident.value() {
"type" | "arg" | "prop" | "val" => {}
_ => {
return Err(nom::Err::Failure(KdlParseError {
input: start,
len: start.len() - input.len(),
kind: None,
label: Some("invalid attribute accessor"),
help: Some("Accessors must be one of: type(), arg(), prop(), propname"),
touched: false,
context: Some("a valid attribute accessor"),
}));
}
}
}
Err(nom::Err::Error(KdlParseError {
input: start,
len: 0,
kind: None,
label: Some("accessor"),
help: Some("accessor must be one of: type(), arg(), prop(), propname"),
touched: false,
context: Some("a valid accessor"),
}))
}
}
fn whitespace(input: &str) -> IResult<&str, &str, KdlParseError<&str>> {
recognize(many0(alt((
crate::parser::unicode_space,
crate::parser::newline,
))))(input)
}

View File

@ -1,7 +1,7 @@
use std::fmt::Display;
/// A specific [KDL Value](https://github.com/kdl-org/kdl/blob/main/SPEC.md#value).
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum KdlValue {
/// A [KDL Raw String](https://github.com/kdl-org/kdl/blob/main/SPEC.md#raw-string).
RawString(String),
@ -41,6 +41,41 @@ pub enum KdlValue {
Null,
}
impl Eq for KdlValue {}
// NOTE: I know, I know. This is terrible and I shouldn't do it, but it's
// better than not being able to hash KdlValue at all.
#[allow(clippy::derive_hash_xor_eq)]
impl std::hash::Hash for KdlValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
KdlValue::RawString(val) => val.hash(state),
KdlValue::String(val) => val.hash(state),
KdlValue::Base2(val) => val.hash(state),
KdlValue::Base8(val) => val.hash(state),
KdlValue::Base10(val) => val.hash(state),
KdlValue::Base10Float(val) => {
let val = if val == &f64::INFINITY {
f64::MAX
} else if val == &f64::NEG_INFINITY {
-f64::MAX
} else if val.is_nan() {
// We collapse NaN to 0.0 because we're evil like that.
0.0
} else {
*val
};
// Good enough to be close-ish for our purposes.
(val.trunc() as i64).hash(state);
(val.fract() as i64).hash(state);
}
KdlValue::Base16(val) => val.hash(state),
KdlValue::Bool(val) => val.hash(state),
KdlValue::Null => core::mem::discriminant(self).hash(state),
}
}
}
impl KdlValue {
/// Returns `true` if the value is a [`KdlValue::RawString`].
pub fn is_raw_string(&self) -> bool {

182
tests/query_api.rs Normal file
View File

@ -0,0 +1,182 @@
use kdl::{KdlDocument, KdlQuery};
use miette::Result;
#[test]
fn document_query_all() -> Result<()> {
let doc = "foo\nbar\nbaz".parse::<KdlDocument>()?;
let results = doc.query_all("bar")?;
assert_eq!(results.count(), 1);
let results = doc.query_all(String::from("bar"))?;
assert_eq!(results.count(), 1);
let results = doc.query_all(&String::from("bar"))?;
assert_eq!(results.count(), 1);
let results = doc.query_all("bar".parse::<KdlQuery>()?)?;
assert_eq!(results.count(), 1);
let results = doc.query_all("scope()")?;
assert_eq!(
results.count(),
0,
"scope() on its own doesn't return anything if querying from a doc."
);
Ok(())
}
#[test]
fn document_query() -> Result<()> {
let doc = "foo\nbar\nbaz".parse::<KdlDocument>()?;
assert!(doc.query("bar")?.is_some());
assert!(doc.query(String::from("bar"))?.is_some());
assert!(doc.query(&String::from("bar"))?.is_some());
assert!(doc.query("bar".parse::<KdlQuery>()?)?.is_some());
assert!(doc.query("scope()")?.is_none());
Ok(())
}
#[test]
fn document_query_get() -> Result<()> {
let doc = "foo\nbar true\nbaz".parse::<KdlDocument>()?;
assert_eq!(doc.query_get("bar", 0)?, Some(&true.into()));
assert_eq!(doc.query_get(String::from("bar"), 0)?, Some(&true.into()));
assert_eq!(doc.query_get(&String::from("bar"), 0)?, Some(&true.into()));
assert_eq!(
doc.query_get("bar".parse::<KdlQuery>()?, 0)?,
Some(&true.into())
);
Ok(())
}
#[test]
fn document_query_get_all() -> Result<()> {
let doc = "foo\nbar true\nbaz false".parse::<KdlDocument>()?;
assert_eq!(
doc.query_get_all("[]", 0)?.collect::<Vec<_>>(),
vec![&true.into(), &false.into()]
);
assert_eq!(doc.query_get_all(String::from("[]"), 0)?.count(), 2);
assert_eq!(doc.query_get_all(&String::from("[]"), 0)?.count(), 2);
assert_eq!(doc.query_get_all("[]".parse::<KdlQuery>()?, 0)?.count(), 2);
Ok(())
}
#[test]
fn node_query_all() -> Result<()> {
let doc = r#"
foo
bar {
a {
b
}
}
baz
"#
.parse::<KdlDocument>()?;
let node = doc.query("bar")?.unwrap();
let results = node.query_all("b")?;
assert_eq!(results.count(), 1);
let results = node.query_all(String::from("b"))?;
assert_eq!(results.count(), 1);
let results = node.query_all(&String::from("b"))?;
assert_eq!(results.count(), 1);
let results = node.query_all("b".parse::<KdlQuery>()?)?;
assert_eq!(results.count(), 1);
let results = node.query_all("scope()")?.collect::<Vec<_>>();
assert_eq!(results[0], node);
let results = node.query_all("scope() > a".parse::<KdlQuery>()?)?;
assert_eq!(results.count(), 1);
let results = node.query_all("scope() > b".parse::<KdlQuery>()?)?;
assert_eq!(results.count(), 0);
Ok(())
}
#[test]
fn node_query() -> Result<()> {
let doc = r#"
foo
bar {
a {
b
}
}
baz
"#
.parse::<KdlDocument>()?;
let node = doc.query("bar")?.unwrap();
assert!(node.query("b")?.is_some());
assert!(node.query(String::from("b"))?.is_some());
assert!(node.query(&String::from("b"))?.is_some());
assert!(node.query("b".parse::<KdlQuery>()?)?.is_some());
assert_eq!(node.query("scope()")?, Some(node));
assert!(node.query("scope() > a")?.is_some());
assert!(node.query("scope() > b")?.is_none());
Ok(())
}
#[test]
fn node_query_get() -> Result<()> {
let doc = r#"
foo
bar 1 2 3 {
a false {
b true
}
}
baz
"#
.parse::<KdlDocument>()?;
let node = doc.query("bar")?.unwrap();
assert_eq!(node.query_get("b", 0)?, Some(&true.into()));
assert_eq!(node.query_get(String::from("b"), 0)?, Some(&true.into()));
assert_eq!(node.query_get(&String::from("b"), 0)?, Some(&true.into()));
assert_eq!(
node.query_get("b".parse::<KdlQuery>()?, 0)?,
Some(&true.into())
);
assert_eq!(node.query_get("scope()", 0)?, Some(&1.into()));
assert_eq!(node.query_get("scope() > a", 0)?, Some(&false.into()));
assert!(node.query_get("scope() > b", "prop")?.is_none());
Ok(())
}
#[test]
fn node_query_get_all() -> Result<()> {
let doc = r#"
foo
bar 1 2 3 {
a false {
b true
}
}
baz
"#
.parse::<KdlDocument>()?;
let node = doc.query("bar")?.unwrap();
assert_eq!(
node.query_get_all("[]", 0)?.collect::<Vec<_>>(),
vec![&false.into(), &true.into()]
);
assert_eq!(node.query_get_all(String::from("[]"), 0)?.count(), 2);
assert_eq!(node.query_get_all(&String::from("[]"), 0)?.count(), 2);
assert_eq!(node.query_get_all("[]".parse::<KdlQuery>()?, 0)?.count(), 2);
Ok(())
}

314
tests/query_matchers.rs Normal file
View File

@ -0,0 +1,314 @@
use kdl::{KdlDocument, KdlNode};
use miette::Result;
use pretty_assertions::assert_eq;
#[test]
fn scope_alone() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
"#
.parse()?;
let results = doc.query_all("scope()")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, Vec::<&KdlNode>::new());
let results = doc.nodes()[0]
.query_all("scope()")?
.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[0]]);
Ok(())
}
#[test]
fn scope_only_at_top() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
}
"#
.parse()?;
assert!(
doc.query_all("foo >> scope()").is_err(),
"scope() must be at the top level"
);
Ok(())
}
#[test]
fn any_descendants() -> Result<()> {
let doc: KdlDocument = r#"
foo
bar
baz
"#
.parse()?;
let results = doc.query_all("bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[1]]);
Ok(())
}
#[test]
fn prop_matcher() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar p=1
baz
}
bar p=1
baz p=2 {
foo {
bar p=1 {
bar p=2
}
}
}
"#
.parse()?;
let results = doc.query_all("[p = 2]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[2],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
let results = doc.query_all("[p = 1]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[1],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
assert_eq!(
doc.query_all("[prop(p) = 1]")?.collect::<Vec<&KdlNode>>(),
results
);
let results = doc.query_all("[p]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[1],
&doc.nodes()[2],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
assert_eq!(
doc.query_all("[prop(p)]")?.collect::<Vec<&KdlNode>>(),
results
);
Ok(())
}
#[test]
fn empty_arg_matcher() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar 1
baz
}
bar 2
baz {
foo {
bar 1 {
bar
}
}
}
"#
.parse()?;
let results = doc.query_all("[arg() = 1]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
let results = doc.query_all("[arg()]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[1],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
Ok(())
}
#[test]
fn indexed_arg_matcher() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar 1 2
baz
}
bar 2 1
baz {
foo {
bar 1 2 {
bar 1 3 2
}
}
}
"#
.parse()?;
let results = doc.query_all("[arg(1) = 2]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
let results = doc.query_all("[arg(2) = 2]")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
Ok(())
}
#[test]
fn type_annotation_matcher() -> Result<()> {
let doc: KdlDocument = r#"
foo {
(here)bar
baz
}
bar
baz {
(here)foo {
bar {
(here)bar
}
}
}
"#
.parse()?;
let results = doc.query_all("(here)")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
let results = doc
.query_all("[type() = \"here\"]")?
.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
let results = doc.query_all("()")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
Ok(())
}

321
tests/query_ops.rs Normal file
View File

@ -0,0 +1,321 @@
use kdl::{KdlDocument, KdlNode};
use miette::Result;
use pretty_assertions::assert_eq;
#[test]
fn scope_with_all_children() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
"#
.parse()?;
let results = doc.query_all("scope() > []")?.collect::<Vec<&KdlNode>>();
assert_eq!(&results, &doc.nodes().iter().collect::<Vec<&KdlNode>>());
Ok(())
}
#[test]
fn scope_child_by_name() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar {
a
b
}
baz
"#
.parse()?;
let results = doc.query_all("scope() > bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[1]]);
// Scope from a specific node.
let results = results[0]
.query_all("scope() > a")?
.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![&doc.nodes()[1].children().unwrap().nodes()[0]]
);
Ok(())
}
#[test]
fn scope_descendants() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
"#
.parse()?;
let results = doc.query_all("scope() >> bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[1]
]
);
Ok(())
}
#[test]
fn scope_only_at_top() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
}
"#
.parse()?;
assert!(
doc.query_all("foo >> scope()").is_err(),
"scope() must be at the top level"
);
Ok(())
}
#[test]
fn any_descendants() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
"#
.parse()?;
let results = doc.query_all("bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[1]
]
);
Ok(())
}
#[test]
fn node_descendants() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz {
foo {
bar {
bar
}
}
}
"#
.parse()?;
let results = doc.query_all("foo >> bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
Ok(())
}
#[test]
fn node_children() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz {
foo {
bar {
bar
}
}
}
"#
.parse()?;
let results = doc.query_all("foo > bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0],
&doc.nodes()[2].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
]
);
Ok(())
}
#[test]
fn node_neighbor() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
"#
.parse()?;
let results = doc.query_all("foo + bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[1]]);
let results = doc.query_all("foo + bar + baz")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[2]]);
Ok(())
}
#[test]
fn node_sibling() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz
quux
other
"#
.parse()?;
let results = doc.query_all("foo ++ bar")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[1]]);
let results = doc.query_all("foo ++ baz")?.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[2]]);
let results = doc
.query_all("foo ++ bar ++ other")?
.collect::<Vec<&KdlNode>>();
assert_eq!(results, vec![&doc.nodes()[4]]);
Ok(())
}
#[test]
fn multiple_selectors() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar
baz
}
bar
baz {
foo {
bar {
bar
}
}
}
"#
.parse()?;
let results = doc.query_all("foo, baz")?.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0],
&doc.nodes()[0].children().unwrap().nodes()[1],
&doc.nodes()[2],
&doc.nodes()[2].children().unwrap().nodes()[0]
],
"First match all the `foo`s, then all the `baz`s."
);
Ok(())
}
#[test]
fn all_combined() -> Result<()> {
let doc: KdlDocument = r#"
foo {
bar {
baz {
foo {
bar {
bar
}
}
bar
baz
quux
other
}
}
}
bar
baz
"#
.parse()?;
let results = doc
.query_all("foo >> baz > foo + bar ++ other")?
.collect::<Vec<&KdlNode>>();
assert_eq!(
results,
vec![
&doc.nodes()[0].children().unwrap().nodes()[0]
.children()
.unwrap()
.nodes()[0]
.children()
.unwrap()
.nodes()[4]
]
);
Ok(())
}

56
tests/query_syntax.rs Normal file
View File

@ -0,0 +1,56 @@
use kdl::KdlDocument;
use miette::Result;
#[test]
fn syntax_errors() -> Result<()> {
macro_rules! assert_syntax_errors {
($(($input:expr, $msg:expr, ($offset:expr, $len:expr))),*) => {
$(
let err = "node".parse::<KdlDocument>()
.unwrap()
.query_all($input)
.expect_err("query parse should've failed.");
assert_eq!(err.to_string(), $msg, "unexpected error message");
assert_eq!(err.span.offset(), $offset, "unexpected span offset");
assert_eq!(err.span.len(), $len, "unexpected span length");
)*
}
}
assert_syntax_errors! {
("", "Expected a valid node matcher.", (0, 0)),
(" scope(", "Expected a valid scope accessor.", (1, 6)),
("(", "Expected closing ')' for type annotation.", (0, 1)),
(")", "Expected a valid node matcher.", (0, 0)),
("[", "Expected a closing ']' for this attribute matcher.", (0, 1)),
("]", "Expected a valid node matcher.", (0, 0)),
("a b", "Expected a valid KQL query.", (2, 0)),
("a\nb", "Expected a valid KQL query.", (2, 0)),
(",", "Expected a valid node matcher.", (0, 0)),
("[] > scope( )", "Expected scope() to be the first item in this selector.", (5, 8)),
("()(type)", "Expected only one type annotation per selector.", (2, 6)),
("(type)()", "Expected only one type annotation per selector.", (6, 2)),
("name(type)", "Expected type annotation to not be used after a node name.", (4, 6)),
("[]name", "Expected node name to come before attribute matcher(s).", (2, 4)),
("[]()", "Expected type annotation to come before attribute matcher(s).", (2, 2)),
("[type(blah)]", "Expected a closing ')' for this 'type()' accessor.", (1, 5)),
("[scope()]", "Expected 'scope()' to be the first item only at the top level of the query selector.", (1, 7)),
("[scope ( )]", "Expected 'scope()' to be the first item only at the top level of the query selector.", (1, 9)),
("[other()]", "Expected a valid attribute accessor.", (1, 7)),
("[arg()1]", "Expected a closing ']' for this attribute matcher.", (0, 6)),
("[arg() 1]", "Expected a closing ']' for this attribute matcher.", (0, 7)),
("[arg()=identifier]", "Expected a valid operator argument.", (7, 0)),
// // Only string values are allowed here.
("[arg()*=1]", "Expected a string as an operator value.", (8, 1)),
("[arg()^=1]", "Expected a string as an operator value.", (8, 1)),
("[arg()$=1]", "Expected a string as an operator value.", (8, 1)),
("[arg()*=null]", "Expected a string as an operator value.", (8, 4)),
("[arg()^=null]", "Expected a string as an operator value.", (8, 4)),
("[arg()$=null]", "Expected a string as an operator value.", (8, 4)),
("[arg()*=true]", "Expected a string as an operator value.", (8, 4)),
("[arg()^=true]", "Expected a string as an operator value.", (8, 4)),
("[arg()$=true]", "Expected a string as an operator value.", (8, 4))
}
Ok(())
}