From b3275fb4327863163b67274bde95dea28612231d Mon Sep 17 00:00:00 2001 From: jihchi Date: Tue, 14 Sep 2021 16:40:53 +0800 Subject: [PATCH] Initial attempt of implementation --- Cargo.lock | 201 ++++++- Cargo.toml | 7 +- README.md | 36 +- src/cli.rs | 7 +- src/evaluation.rs | 378 ++++++++++++ src/kdlrs.rs | 391 +++++++++++++ src/lib.rs | 282 ++++++++- src/main.rs | 5 +- src/parser.rs | 553 ++++++++++++++++++ tests/accessor_multiple/mod.rs | 176 ++++++ tests/accessor_multiple/website.kdl | 48 ++ tests/accessor_single/closed_node_name.rs | 192 ++++++ .../closed_prop_name_explicit.rs | 182 ++++++ .../closed_prop_name_implicit.rs | 180 ++++++ tests/accessor_single/closed_props.rs | 146 +++++ tests/accessor_single/closed_type_tag.rs | 142 +++++ tests/accessor_single/closed_val.rs | 197 +++++++ tests/accessor_single/closed_values.rs | 146 +++++ .../identifier_closed_node_name.rs | 354 +++++++++++ .../identifier_closed_prop_name_explicit.rs | 332 +++++++++++ .../identifier_closed_prop_name_implicit.rs | 329 +++++++++++ .../identifier_closed_props.rs | 290 +++++++++ .../identifier_closed_type_tag.rs | 282 +++++++++ .../accessor_single/identifier_closed_val.rs | 376 ++++++++++++ .../identifier_closed_values.rs | 290 +++++++++ tests/accessor_single/mod.rs | 126 ++++ tests/e2e_test.rs | 15 + tests/example_test.rs | 104 ++++ 28 files changed, 5728 insertions(+), 39 deletions(-) create mode 100644 src/evaluation.rs create mode 100644 src/kdlrs.rs create mode 100644 src/parser.rs create mode 100644 tests/accessor_multiple/mod.rs create mode 100644 tests/accessor_multiple/website.kdl create mode 100644 tests/accessor_single/closed_node_name.rs create mode 100644 tests/accessor_single/closed_prop_name_explicit.rs create mode 100644 tests/accessor_single/closed_prop_name_implicit.rs create mode 100644 tests/accessor_single/closed_props.rs create mode 100644 tests/accessor_single/closed_type_tag.rs create mode 100644 tests/accessor_single/closed_val.rs create mode 100644 tests/accessor_single/closed_values.rs create mode 100644 tests/accessor_single/identifier_closed_node_name.rs create mode 100644 tests/accessor_single/identifier_closed_prop_name_explicit.rs create mode 100644 tests/accessor_single/identifier_closed_prop_name_implicit.rs create mode 100644 tests/accessor_single/identifier_closed_props.rs create mode 100644 tests/accessor_single/identifier_closed_type_tag.rs create mode 100644 tests/accessor_single/identifier_closed_val.rs create mode 100644 tests/accessor_single/identifier_closed_values.rs create mode 100644 tests/accessor_single/mod.rs create mode 100644 tests/e2e_test.rs create mode 100644 tests/example_test.rs diff --git a/Cargo.lock b/Cargo.lock index 11cc30e..b1d565f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,79 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "assert_cmd" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e996dc7940838b7ef1096b882e29ec30a3149a3a443cdc8dba19ed382eca1fe2" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "getopts" version = "0.2.21" @@ -28,13 +95,31 @@ dependencies = [ "wasi", ] +[[package]] +name = "indoc" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +dependencies = [ + "unindent", +] + +[[package]] +name = "itertools" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ddb889f9d0d08a67338271fa9b62996bc788c7796a5c18cf057420aaed5eaf" +dependencies = [ + "either", +] + [[package]] name = "kdl" -version = "1.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7465b53c2ea400a5d528cc642024f5356dbde9d53f36244f2321ee8ed63c39" +checksum = "114071e31456ec827056ca691d141f8e96327d9d9a29140da2e6fba9a5f17b83" dependencies = [ - "nom 6.2.1", + "nom", "phf", "thiserror", ] @@ -43,11 +128,20 @@ dependencies = [ name = "kq" version = "0.1.0" dependencies = [ + "assert_cmd", "getopts", + "indoc", "kdl", - "nom 7.0.0", + "nom", + "predicates", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.101" @@ -56,9 +150,9 @@ checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "minimal-lexical" @@ -68,23 +162,28 @@ checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" [[package]] name = "nom" -version = "6.2.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" dependencies = [ "memchr", + "minimal-lexical", "version_check", ] [[package]] -name = "nom" -version = "7.0.0" +name = "normalize-line-endings" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "memchr", - "minimal-lexical", - "version_check", + "autocfg", ] [[package]] @@ -137,6 +236,36 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "predicates" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6ce811d0b2e103743eec01db1c50612221f173084ce2f7941053e94b6bb474" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451" + +[[package]] +name = "predicates-tree" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dd0fd014130206c9352efbdc92be592751b2b9274dff685348341082c6ea3d" +dependencies = [ + "predicates-core", + "treeline", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -212,6 +341,29 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "siphasher" version = "0.3.7" @@ -249,6 +401,12 @@ dependencies = [ "syn", ] +[[package]] +name = "treeline" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" + [[package]] name = "unicode-width" version = "0.1.8" @@ -261,12 +419,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + [[package]] name = "version_check" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b2ec830..53e1020 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,12 @@ repository = "https://github.com/jihchi/kq.git" license-file = "LICENSE" keywords = ["kdl", "query", "transform", "config", "document"] +[dev-dependencies] +assert_cmd = "2.0.2" +predicates = "2.0.3" +indoc = "1.0.3" + [dependencies] -kdl = "1.1.0" +kdl = "3.0.0" nom = "7.0.0" getopts = "0.2" diff --git a/README.md b/README.md index 2cea04b..d9c286d 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,23 @@ [![Crates.io - download](https://img.shields.io/crates/d/kq)](https://crates.io/crates/kq) [![docs.rs](https://img.shields.io/docsrs/kq)](https://docs.rs/kq) -> 🚧 work in progress +A jq-like cli tool that can [query](https://github.com/kdl-org/kdl/blob/1.0.0/QUERY-SPEC.md) and transform [KDL](https://kdl.dev/) document right in the command line. -A jq-like cli tool that can [query](https://github.com/kdl-org/kdl/blob/main/QUERY-SPEC.md) and transform [KDL](https://kdl.dev/) document right in the command line. +> `||` and [Map Operator](https://github.com/kdl-org/kdl/blob/1.0.0/QUERY-SPEC.md#map-operator) are not supported yet. + +## Installation + +### Cargo + +```sh +cargo install kq +``` + +## Usage + +> Modified from https://github.com/kdl-org/kdl/blob/1.0.0/QUERY-SPEC.md#examples + +Given following content: ```console $ cat example.kdl @@ -20,10 +34,14 @@ package { miette "2.0.0" dev=true } } +``` +```console $ cat example.kdl | kq "package name" name "foo" +``` +```console $ cat example.kdl | kq "dependencies" dependencies platform="windows" { winapi "1.0.0" path="./crates/my-winapi-fork" @@ -31,23 +49,17 @@ dependencies platform="windows" { dependencies { miette "2.0.0" dev=true } +``` +```console $ cat example.kdl | kq "dependencies[platform]" dependencies platform="windows" { winapi "1.0.0" path="./crates/my-winapi-fork" } +``` +```console $ cat example.kdl | kq "dependencies > []" winapi "1.0.0" path="./crates/my-winapi-fork" miette "2.0.0" dev=true - -``` - -## Installation - -### Cargo - -```sh -cargo install kq ``` - diff --git a/src/cli.rs b/src/cli.rs index 39192de..43b9c99 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,9 +26,7 @@ impl Args { program, }) } -} -impl Args { pub fn help(&self) -> bool { self.matches.opt_present("h") } @@ -37,9 +35,8 @@ impl Args { self.matches.opt_present("v") } - pub fn get_free(&self) -> Option { - let some = !self.matches.free.is_empty(); - some.then(|| self.matches.free[0].clone()) + pub fn get_query(&self) -> Option<&String> { + self.matches.free.get(0) } pub fn print_help(&self) { diff --git a/src/evaluation.rs b/src/evaluation.rs new file mode 100644 index 0000000..8d8d2fb --- /dev/null +++ b/src/evaluation.rs @@ -0,0 +1,378 @@ +use crate::Operator; +use kdl::KdlValue; + +pub(crate) fn evaluate(lhs: &KdlValue, operator: &Operator, rhs: &KdlValue) -> bool { + match operator { + Operator::Contains => contains(lhs, rhs), + Operator::EndsWith => ends_with(lhs, rhs), + Operator::Equal => equal(lhs, rhs), + Operator::GreaterThan => greater_than(lhs, rhs), + Operator::GreaterThanOrEqualTo => greater_than_or_equal_to(lhs, rhs), + Operator::LessThan => less_than(lhs, rhs), + Operator::LessThanOrEqualTo => less_than_or_equal_to(lhs, rhs), + Operator::NotEqual => not_equal(lhs, rhs), + Operator::StartsWith => starts_with(lhs, rhs), + } +} + +fn contains(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(rhs) => lhs.contains(rhs), + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn ends_with(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(rhs) => lhs.ends_with(rhs), + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn equal(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs == rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + #[allow(clippy::float_cmp)] + KdlValue::Float(rhs) => lhs == rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(rhs) => lhs == rhs, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(rhs) => lhs == rhs, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => true, + }, + } +} + +fn greater_than(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs > rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(rhs) => lhs > rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn greater_than_or_equal_to(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs >= rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(rhs) => lhs >= rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn less_than(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs < rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(rhs) => lhs < rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn less_than_or_equal_to(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs <= rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(rhs) => lhs <= rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn not_equal(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(lhs) => match rhs { + KdlValue::Int(rhs) => lhs != rhs, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + #[allow(clippy::float_cmp)] + KdlValue::Float(rhs) => lhs != rhs, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(rhs) => lhs != rhs, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(rhs) => lhs != rhs, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} + +fn starts_with(lhs: &KdlValue, rhs: &KdlValue) -> bool { + match lhs { + KdlValue::Int(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Float(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::String(lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(rhs) => lhs.starts_with(rhs), + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Boolean(_lhs) => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + KdlValue::Null => match rhs { + KdlValue::Int(_rhs) => false, + KdlValue::Float(_rhs) => false, + KdlValue::String(_rhs) => false, + KdlValue::Boolean(_rhs) => false, + KdlValue::Null => false, + }, + } +} diff --git a/src/kdlrs.rs b/src/kdlrs.rs new file mode 100644 index 0000000..f8d77b5 --- /dev/null +++ b/src/kdlrs.rs @@ -0,0 +1,391 @@ +// The code of this file is modified from https://github.com/kdl-org/kdl-rs +// (mainly from https://github.com/kdl-org/kdl-rs/blob/main/src/parser.rs) +use kdl::KdlValue; +use nom::branch::alt; +use nom::bytes::complete::{tag, take_until, take_until1, take_while_m_n}; +use nom::character::complete::{anychar, char, none_of, one_of}; +use nom::combinator::{eof, map, map_opt, map_res, not, opt, recognize, value}; +use nom::multi::{fold_many0, many0, many1, many_till}; +use nom::sequence::{delimited, preceded, terminated, tuple}; +use nom::IResult; + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L148-L151 +/// `identifier := bare_identifier | string` +/// +// fn identifier(input: &str) -> IResult<&str, String, KdlParseError<&str>> { +pub(crate) fn identifier(input: &str) -> IResult<&str, String> { + alt((string, (map(bare_identifier, String::from))))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L124-L142 +/// `bare_identifier := ((identifier-char - digit - sign) identifier-char* | sign ((identifier-char - digit) identifier-char*)?) - keyword` +/// +// fn bare_identifier(input: &str) -> IResult<&str, &str, KdlParseError<&str>>> { +fn bare_identifier(input: &str) -> IResult<&str, &str> { + // fn left(input: &str) -> IResult<&str, (), KdlParseError<&str>> { + fn left(input: &str) -> IResult<&str, ()> { + not(keyword)(input)?; + not(one_of("0123456789"))(input)?; + not(one_of("+-"))(input)?; + let (input, _) = identifier_char(input)?; + let (input, _) = many0(identifier_char)(input)?; + Ok((input, ())) + } + // fn right(input: &str) -> IResult<&str, (), KdlParseError<&str>> { + fn right(input: &str) -> IResult<&str, ()> { + let (input, _) = one_of("+-")(input)?; + not(keyword)(input)?; + not(one_of("0123456789"))(input)?; + let (input, _) = opt(many1(identifier_char))(input)?; + Ok((input, ())) + } + recognize(alt((left, right)))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L144-L146 +/// `string := '"' character* '"'` +/// +// fn keyword(input: &str) -> IResult<&str, String, KdlParseError<&str>> { +fn keyword(input: &str) -> IResult<&str, String> { + map(alt((tag("true"), tag("false"), tag("null"))), String::from)(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L118-L122 +/// `identifier_char := unicode - linespace - [\/(){}<>;[]=,"] +/// +// fn identifier_char(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { +fn identifier_char(input: &str) -> IResult<&str, &str> { + not(linespace)(input)?; + recognize(none_of(r#"\/(){}<>;[]=,""#))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L118-L122 +/// `linespace := newline | ws | single-line-comment` +/// +// fn linespace(input: &str) -> IResult<&str, (), KdlParseError<&str>> { +fn linespace(input: &str) -> IResult<&str, ()> { + value((), alt((newline, whitespace, single_line_comment)))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L473-L487 +/// `newline := All line-break unicode white_space +/// +// fn newline(input: &str) -> IResult<&str, (), KdlParseError<&str>> { +fn newline(input: &str) -> IResult<&str, ()> { + value( + (), + alt(( + tag("\r\n"), + tag("\r"), + tag("\n"), + tag("\u{0085}"), + tag("\u{000C}"), + tag("\u{2028}"), + tag("\u{2029}"), + )), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L437-L448 +/// `ws := bom | unicode-space | multi-line-comment` +/// +// fn whitespace(input: &str) -> IResult<&str, (), KdlParseError<&str>> { +pub(crate) fn whitespace(input: &str) -> IResult<&str, ()> { + // TODO: bom? + value( + (), + alt(( + tag("\u{FEFF}"), + unicode_space, + recognize(multi_line_comment), + )), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L400-L405 +/// `single-line-comment := '//' ('\r' [^\n] | [^\r\n])* (newline | eof)` +/// +// fn single_line_comment(input: &str) -> IResult<&str, (), KdlParseError<&str>> { +fn single_line_comment(input: &str) -> IResult<&str, ()> { + let (input, _) = tag("//")(input)?; + let (input, _) = many_till(value((), anychar), alt((newline, value((), eof))))(input)?; + Ok((input, ())) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L407-L411 +/// `multi-line-comment := '/*' commented-block +/// +// fn multi_line_comment(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { +fn multi_line_comment(input: &str) -> IResult<&str, &str> { + let (input, _) = tag("/*")(input)?; + commented_block(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L413-L422 +/// `commented-block := '*/' | (multi-line-comment | '*' | '/' | [^*/]+) commented-block` +/// +// fn commented_block(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { +fn commented_block(input: &str) -> IResult<&str, &str> { + alt(( + tag("*/"), + terminated( + alt((multi_line_comment, take_until1("*/"), tag("*"), tag("/"))), + commented_block, + ), + ))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L450-L471 +/// +// fn unicode_space(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { +fn unicode_space(input: &str) -> IResult<&str, &str> { + alt(( + tag(" "), + tag("\t"), + tag("\u{00A0}"), + tag("\u{1680}"), + tag("\u{2000}"), + tag("\u{2001}"), + tag("\u{2002}"), + tag("\u{2003}"), + tag("\u{2004}"), + tag("\u{2005}"), + tag("\u{2006}"), + tag("\u{2007}"), + tag("\u{2008}"), + tag("\u{2009}"), + tag("\u{200A}"), + tag("\u{202F}"), + tag("\u{205F}"), + tag("\u{3000}"), + ))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L213-L223 +/// `string := '"' character* '"'` +/// +// fn string(input: &str) -> IResult<&str, String, KdlParseError<&str>> { +fn string(input: &str) -> IResult<&str, String> { + delimited( + char('"'), + fold_many0(character, String::new, |mut acc, ch| { + acc.push(ch); + acc + }), + char('"'), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L225-L228 +/// `character := '\' escape | [^\"]` +/// +// fn character(input: &str) -> IResult<&str, char, KdlParseError<&str>> { +fn character(input: &str) -> IResult<&str, char> { + alt((preceded(char('\\'), escape), none_of("\\\"")))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L237-L247 +/// a map and its inverse of escape-sequence<->char +/// +/// (instead of building a map by phf, use a function with pattern matching) +fn escape_chars(input: char) -> Option { + match input { + '"' => Some('"'), + '\\' => Some('\\'), + '/' => Some('/'), + 'b' => Some('\u{08}'), + 'f' => Some('\u{0C}'), + 'n' => Some('\n'), + 'r' => Some('\r'), + 't' => Some('\t'), + _ => None, + } +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L249-L255 +/// `escape := ["\\/bfnrt] | 'u{' hex-digit{1, 6} '}'` +/// +// fn escape(input: &str) -> IResult<&str, char, KdlParseError<&str>> { +fn escape(input: &str) -> IResult<&str, char> { + alt(( + delimited(tag("u{"), unicode, char('}')), + map_opt(anychar, escape_chars), + ))(input) +} + +// fn unicode(input: &str) -> IResult<&str, char, KdlParseError<&str>> { +fn unicode(input: &str) -> IResult<&str, char> { + map_opt( + map_res( + take_while_m_n(1, 6, |c: char| c.is_ascii_hexdigit()), + |hex| u32::from_str_radix(hex, 16), + ), + std::char::from_u32, + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L180-L190 +/// `value := type-annotation? (string | raw_string | number | boolean | 'null'`) +/// +// fn node_value(input: &str) -> IResult<&str, KdlValue, KdlParseError<&str>> { +pub(crate) fn node_value(input: &str) -> IResult<&str, KdlValue> { + // let (input, _ty) = opt(type_annotation)(input)?; + alt(( + map(string, KdlValue::String), + map(raw_string, |s| KdlValue::String(s.into())), + number, + boolean, + value(KdlValue::Null, tag("null")), + ))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L267-L278 +/// `raw-string := 'r' raw-string-hash` +/// `raw-string-hash := '#' raw-string-hash '#' | raw-string-quotes` +/// `raw-string-quotes := '"' .* '"'` +/// +// fn raw_string(input: &str) -> IResult<&str, &str, KdlParseError<&str>> { +fn raw_string(input: &str) -> IResult<&str, &str> { + let (input, _) = char('r')(input)?; + let (input, hashes) = recognize(many0(char('#')))(input)?; + let (input, _) = char('"')(input)?; + let close = format!("\"{}", hashes); + let (input, string) = take_until(&close[..])(input)?; + let (input, _) = tag(&close[..])(input)?; + Ok((input, string)) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L280-L289 +/// `number := decimal | hex | octal | binary` +/// +// fn number(input: &str) -> IResult<&str, KdlValue, KdlParseError<&str>> { +fn number(input: &str) -> IResult<&str, KdlValue> { + alt(( + map(hexadecimal, KdlValue::Int), + map(octal, KdlValue::Int), + map(binary, KdlValue::Int), + map(float, KdlValue::Float), + map(integer, KdlValue::Int), + ))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L331-L343 +/// +// fn sign(input: &str) -> IResult<&str, i64, KdlParseError<&str>> { +fn sign(input: &str) -> IResult<&str, i64> { + let (input, sign) = opt(alt((char('+'), char('-'))))(input)?; + let mult = if let Some(sign) = sign { + if sign == '+' { + 1 + } else { + -1 + } + } else { + 1 + }; + Ok((input, mult)) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L345-L358 +/// `hex := sign? '0x' [0-9a-fA-F] [0-9a-fA-F_]*` +/// +// fn hexadecimal(input: &str) -> IResult<&str, i64, KdlParseError<&str>> { +fn hexadecimal(input: &str) -> IResult<&str, i64> { + let (input, sign) = sign(input)?; + map_res( + preceded( + alt((tag("0x"), tag("0X"))), + recognize(many1(terminated( + one_of("0123456789abcdefABCDEF"), + many0(char('_')), + ))), + ), + move |out: &str| i64::from_str_radix(&str::replace(out, "_", ""), 16).map(|x| x * sign), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L360-L370 +/// +/// `octal := sign? '0o' [0-7] [0-7_]*` +// fn octal(input: &str) -> IResult<&str, i64, KdlParseError<&str>> { +fn octal(input: &str) -> IResult<&str, i64> { + let (input, sign) = sign(input)?; + map_res( + preceded( + alt((tag("0o"), tag("0O"))), + recognize(many1(terminated(one_of("01234567"), many0(char('_'))))), + ), + move |out: &str| i64::from_str_radix(&str::replace(out, "_", ""), 8).map(|x| x * sign), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L372-L382 +/// +// fn binary(input: &str) -> IResult<&str, i64, KdlParseError<&str>> { +fn binary(input: &str) -> IResult<&str, i64> { + let (input, sign) = sign(input)?; + map_res( + preceded( + alt((tag("0b"), tag("0B"))), + recognize(many1(terminated(one_of("01"), many0(char('_'))))), + ), + move |out: &str| i64::from_str_radix(&str::replace(out, "_", ""), 2).map(|x| x * sign), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L384-L390 +/// `boolean := 'true' | 'false'` +/// +// fn boolean(input: &str) -> IResult<&str, KdlValue, KdlParseError<&str>> { +fn boolean(input: &str) -> IResult<&str, KdlValue> { + alt(( + value(KdlValue::Boolean(true), tag("true")), + value(KdlValue::Boolean(false), tag("false")), + ))(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L291-L311 +/// ```text +/// decimal := integer ('.' [0-9]+)? exponent? +/// exponent := ('e' | 'E') integer +/// integer := sign? [1-9] [0-9_]* +/// sign := '+' | '-' +/// ``` +/// +// fn float(input: &str) -> IResult<&str, f64, KdlParseError<&str>> { +fn float(input: &str) -> IResult<&str, f64> { + map_res( + alt(( + recognize(tuple(( + integer, + opt(preceded(char('.'), integer)), + one_of("eE"), + opt(one_of("+-")), + integer, + ))), + recognize(tuple((integer, char('.'), integer))), + )), + |x| str::replace(x, "_", "").parse::(), + )(input) +} + +/// https://github.com/kdl-org/kdl-rs/blob/v3.0.0/src/parser.rs#L313-L329 +/// ```text +/// decimal := integer ('.' [0-9]+)? exponent? +/// exponent := ('e' | 'E') integer +/// integer := sign? [1-9] [0-9_]* +/// sign := '+' | '-' +/// ``` +/// +// fn integer(input: &str) -> IResult<&str, i64, KdlParseError<&str>> { +fn integer(input: &str) -> IResult<&str, i64> { + let (input, sign) = sign(input)?; + map_res( + recognize(many1(terminated(one_of("0123456789"), many0(char('_'))))), + move |out: &str| { + str::replace(out, "_", "") + .parse::() + .map(move |x| x * sign) + }, + )(input) +} diff --git a/src/lib.rs b/src/lib.rs index 773fc26..137bde8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,282 @@ -use kdl::{KdlError, KdlNode}; +use kdl::{KdlNode, KdlValue}; +use nom::combinator::all_consuming; +use nom::Finish; +use std::collections::VecDeque; +use std::iter; -pub fn query_document(_inpnt: I, raw_document: Vec) -> Result, KdlError> +mod evaluation; +mod kdlrs; +mod parser; + +use parser::{Accessor, Combinator, Entity, Matcher, Operator, Sibling}; + +pub fn query_document(input: &str, document: Vec) -> Result, String> { + let input = input.trim(); + if input.is_empty() { + Ok(document) + } else { + all_consuming(parser::selector)(input) + .finish() + .map(|(_input, selector)| query_by_selector(selector, document)) + .map_err(|error| error.to_string()) + } +} + +fn query_by_selector(selector: Vec, document: Vec) -> Vec { + selector + .iter() + .fold( + (&Accessor::Top, document), + |(previous, document), combinator| match combinator { + Combinator::Child(accessor, siblings) => { + let is_previous_sibling_top = match previous { + Accessor::AnyElement + | Accessor::AnyElementWithTypeTag(_) + | Accessor::Closed(_, _) + | Accessor::Sole(_) => false, + Accessor::Top => true, + }; + let document = query_by_child_combinator( + is_previous_sibling_top, + accessor, + siblings, + document, + ); + (accessor, document) + } + Combinator::Descendant(accessor, siblings) => { + let document = query_by_descendant_combinator(accessor, siblings, document); + (accessor, document) + } + }, + ) + .1 +} + +fn query_by_child_combinator( + is_previous_sibling_top: bool, + accessor: &Accessor, + siblings: &[(Sibling, Accessor)], + document: Vec, +) -> Vec { + if siblings.is_empty() { + match accessor { + Accessor::AnyElement => { + if is_previous_sibling_top { + document + } else { + document + .iter() + .flat_map(|node| &node.children) + .cloned() + .collect() + } + } + Accessor::AnyElementWithTypeTag(_identifier) => vec![], + Accessor::Closed(identifier, matcher) => identifier + .as_ref() + .map(|identifier| filter_by_identifier(identifier, &document)) + .unwrap_or(document) + .iter() + .filter(|node| match_by_matcher(matcher, node)) + .cloned() + .collect(), + Accessor::Sole(identifier) => { + if is_previous_sibling_top { + filter_by_identifier(identifier, &document) + } else { + document + .iter() + .flat_map(|node| filter_by_identifier(identifier, &node.children)) + .collect() + } + } + Accessor::Top => document, + } + } else if is_previous_sibling_top { + filter_by_siblings(accessor, siblings, &document) + } else { + document + .iter() + .flat_map(|node| filter_by_siblings(accessor, siblings, &node.children)) + .collect() + } +} + +fn query_by_descendant_combinator( + accessor: &Accessor, + siblings: &[(Sibling, Accessor)], + document: Vec, +) -> Vec { + if siblings.is_empty() { + match accessor { + Accessor::AnyElement => document, + Accessor::AnyElementWithTypeTag(_identifier) => vec![], + Accessor::Closed(identifier, matcher) => traverse( + |node| match_by_accessor_filter(identifier, matcher, node), + &document, + ), + Accessor::Sole(identifier) => traverse(|node| node.name == *identifier, &document), + Accessor::Top => document, + } + } else { + traverse_by_siblings(accessor, siblings, &document) + } +} + +fn filter_by_identifier(identifier: &str, document: &[KdlNode]) -> Vec { + document + .iter() + .filter(|node| node.name == *identifier) + .cloned() + .collect() +} + +fn filter_by_siblings( + accessor: &Accessor, + siblings: &[(Sibling, Accessor)], + document: &[KdlNode], +) -> Vec { + let head = (Sibling::General, accessor.clone()); + + document + .iter() + .enumerate() + .filter(|(i, node)| { + let mut siblings = iter::once(&head).chain(siblings.iter()).rev(); + let mut preceding = document[..*i].iter().rev().peekable(); + + let result = siblings + .next() + .and_then(|(sibling, accessor)| match_by_accessor(accessor, node).then(|| sibling)); + + let result = result.map(|sibling| { + let mut previous_sibling = sibling; + + siblings.all(|(sibling, accessor)| { + let is_sibling_matched = match previous_sibling { + Sibling::Adjacent => preceding + .next() + .map(|node| match_by_accessor(accessor, node)) + .unwrap_or(false), + Sibling::General => preceding.any(|node| match_by_accessor(accessor, node)), + }; + previous_sibling = sibling; + is_sibling_matched + }) + }); + + result.unwrap_or(false) + }) + .map(|(_i, node)| node) + .cloned() + .collect() +} + +fn match_by_matcher(matcher: &Matcher, node: &KdlNode) -> bool { + match matcher { + Matcher::Direct(entity) => match entity { + Entity::PropName(name) => node.properties.contains_key(name), + Entity::Val(index) => node.values.len() > *index, + // '[name()]', '[props()]',, and '[values()]' does not make sense by themselves in a matcher + // '[tag()]' is unsupported + Entity::NodeName | Entity::Props | Entity::TypeTag | Entity::Values => false, + }, + Matcher::Expression(entity, operator, value) => match entity { + Entity::PropName(name) => node + .properties + .get(name) + .map(|lhs| evaluation::evaluate(lhs, operator, value)) + .unwrap_or(false), + Entity::Val(index) => node + .values + .get(*index) + .map(|lhs| evaluation::evaluate(lhs, operator, value)) + .unwrap_or(false), + Entity::NodeName => match value { + KdlValue::String(string) => match operator { + Operator::Contains => node.name.contains(string), + Operator::EndsWith => node.name.ends_with(string), + Operator::Equal => &node.name == string, + Operator::GreaterThan => false, + Operator::GreaterThanOrEqualTo => false, + Operator::LessThan => false, + Operator::LessThanOrEqualTo => false, + Operator::NotEqual => &node.name != string, + Operator::StartsWith => node.name.starts_with(string), + }, + KdlValue::Int(_) | KdlValue::Float(_) | KdlValue::Boolean(_) | KdlValue::Null => { + false + } + }, + Entity::Props => false, + Entity::TypeTag => false, + Entity::Values => false, + }, + } +} + +fn match_by_accessor(accessor: &Accessor, node: &KdlNode) -> bool { + match accessor { + Accessor::AnyElement => true, + Accessor::AnyElementWithTypeTag(_identifier) => false, + Accessor::Closed(identifier, matcher) => { + match_by_accessor_filter(identifier, matcher, node) + } + Accessor::Sole(identifier) => node.name == *identifier, + Accessor::Top => true, + } +} + +fn match_by_accessor_filter( + identifier: &Option, + matcher: &Matcher, + node: &KdlNode, +) -> bool { + identifier + .as_ref() + .map(|identifier| node.name == *identifier) + .unwrap_or(true) + .then(|| match_by_matcher(matcher, node)) + .unwrap_or(false) +} + +fn traverse_by_siblings( + accessor: &Accessor, + siblings: &[(Sibling, Accessor)], + document: &[KdlNode], +) -> Vec { + let mut result = Vec::::new(); + let mut queue = VecDeque::<&[KdlNode]>::new(); + queue.push_back(document); + + while let Some(document) = queue.pop_front() { + for node in filter_by_siblings(accessor, siblings, document) { + result.push(node); + } + for node in document { + if !node.children.is_empty() { + queue.push_back(&node.children); + } + } + } + + result +} + +fn traverse(predicate: F, document: &[KdlNode]) -> Vec where - I: AsRef, + F: Fn(&KdlNode) -> bool, { - Ok(raw_document) + let mut result: Vec = vec![]; + let mut queue: VecDeque<&KdlNode> = document.iter().collect(); + + while let Some(node) = queue.pop_front() { + if predicate(node) { + result.push(node.clone()); + } + queue.extend(node.children.iter()); + } + + result } diff --git a/src/main.rs b/src/main.rs index 6302414..11aa371 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use kq; use std::error; use std::io::{self, Read}; @@ -17,8 +16,8 @@ fn main() -> Result<(), Box> { return Ok(()); } - let query = match args.get_free() { - Some(free) => free, + let query = match args.get_query() { + Some(query) => query, None => { args.print_help(); return Ok(()); diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..0f6badc --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,553 @@ +use kdl::KdlValue; +use nom::branch::alt; +use nom::bytes::complete::tag; +use nom::character::complete::digit0; +use nom::combinator::{iterator, map, opt, value}; +use nom::multi::many1; +use nom::sequence::{delimited, tuple}; +use nom::IResult; +use std::convert::TryFrom; + +use crate::kdlrs; + +#[derive(Debug, PartialEq)] +pub(crate) enum Combinator { + Child(Accessor, Vec<(Sibling, Accessor)>), + Descendant(Accessor, Vec<(Sibling, Accessor)>), +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Sibling { + Adjacent, + General, +} + +impl TryFrom<&ParsedCombinator> for Sibling { + type Error = &'static str; + + fn try_from(value: &ParsedCombinator) -> Result { + match value { + ParsedCombinator::Child => Err("Child can not convert to Sibling"), + ParsedCombinator::Descendant => Err("Descendant can not convert to Sibling"), + ParsedCombinator::GeneralSibling => Ok(Sibling::General), + ParsedCombinator::AdjacentSibling => Ok(Sibling::Adjacent), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Accessor { + AnyElement, + AnyElementWithTypeTag(Option), + Closed(Option, Matcher), + Sole(String), + Top, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Matcher { + Direct(Entity), + Expression(Entity, Operator, KdlValue), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Entity { + NodeName, + PropName(String), + Props, + TypeTag, + Val(usize), + Values, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Operator { + Contains, + EndsWith, + Equal, + GreaterThan, + GreaterThanOrEqualTo, + LessThan, + LessThanOrEqualTo, + NotEqual, + StartsWith, +} + +#[derive(Debug, Clone, PartialEq)] +enum ParsedCombinator { + AdjacentSibling, + Child, + Descendant, + GeneralSibling, +} + +pub(crate) fn selector(input: &str) -> IResult<&str, Vec> { + let (input, head) = accessor(input)?; + let mut it = iterator(input, tuple((combinator, accessor))); + let tail = it.collect::>(); + it.finish()?; + + let mut output = vec![Combinator::Descendant(head, vec![])]; + let mut iter = tail.iter(); + let mut it = iter.next(); + + while it.is_some() { + // push sibling combinator onto last hierarchy combinator as a sibling + while it.is_some() && is_sibling(it) { + let (combinator, accessor) = it.unwrap(); + let combinator = Sibling::try_from(combinator).unwrap(); + let sibling = (combinator, accessor.clone()); + match output.last_mut().unwrap() { + Combinator::Child(_head, siblings) | Combinator::Descendant(_head, siblings) => { + siblings.push(sibling) + } + }; + it = iter.next(); + } + // push hierarchy combinator onto output + if let Some((combinator, accessor)) = it { + match combinator { + ParsedCombinator::Child => output.push(Combinator::Child(accessor.clone(), vec![])), + ParsedCombinator::Descendant => { + output.push(Combinator::Descendant(accessor.clone(), vec![])); + } + ParsedCombinator::GeneralSibling | ParsedCombinator::AdjacentSibling => (), + }; + it = iter.next(); + } + } + + Ok(("", output)) +} + +fn is_sibling(value: Option<&(ParsedCombinator, Accessor)>) -> bool { + match value { + Some((combinator, _accessor)) => match combinator { + ParsedCombinator::Child | ParsedCombinator::Descendant => false, + ParsedCombinator::AdjacentSibling | ParsedCombinator::GeneralSibling => true, + }, + None => false, + } +} + +fn combinator(input: &str) -> IResult<&str, ParsedCombinator> { + alt(( + delimited( + many1(kdlrs::whitespace), + alt(( + value(ParsedCombinator::Child, tag(">")), + value(ParsedCombinator::AdjacentSibling, tag("+")), + value(ParsedCombinator::GeneralSibling, tag("~")), + )), + many1(kdlrs::whitespace), + ), + value(ParsedCombinator::Descendant, many1(kdlrs::whitespace)), + ))(input) +} + +/// ```text +/// accessor := +/// 'top()' | +/// '[]' | +/// '(' identifier? ')' | +/// identifier? matcher | +/// identifier +/// ``` +fn accessor(input: &str) -> IResult<&str, Accessor> { + alt(( + value(Accessor::Top, tag("top()")), + value(Accessor::AnyElement, tag("[]")), + map( + delimited(tag("("), opt(kdlrs::identifier), tag(")")), + Accessor::AnyElementWithTypeTag, + ), + map( + tuple((opt(kdlrs::identifier), matcher)), + |(identifier, matcher)| Accessor::Closed(identifier, matcher), + ), + map(kdlrs::identifier, Accessor::Sole), + ))(input) +} + +/// `matcher := '[' entity (ws+ operator ws+ kdl-value)? ']'` +fn matcher(input: &str) -> IResult<&str, Matcher> { + let (input, _) = tag("[")(input)?; + let (input, left_hand_side) = entity(input)?; + let (input, expression) = opt(tuple(( + delimited(many1(kdlrs::whitespace), operator, many1(kdlrs::whitespace)), + kdlrs::node_value, + )))(input)?; + let (input, _) = tag("]")(input)?; + + let output = match expression { + Some((operator, right_hand_side)) => { + Matcher::Expression(left_hand_side, operator, right_hand_side) + } + None => Matcher::Direct(left_hand_side), + }; + + Ok((input, output)) +} + +/// ```text +/// entity := +/// 'name()' | +/// 'tag()' | +/// 'props()' | +/// 'values()' | +/// 'val(' digit* ')' | +/// 'prop(' identifier ')' | +/// identifier '()'? +/// ``` +fn entity(input: &str) -> IResult<&str, Entity> { + alt(( + value(Entity::NodeName, tag("name()")), + value(Entity::TypeTag, tag("tag()")), + value(Entity::Props, tag("props()")), + value(Entity::Values, tag("values()")), + map(delimited(tag("val("), digit0, tag(")")), |input: &str| { + Entity::Val(input.parse::().unwrap_or(0)) + }), + map( + delimited(tag("prop("), kdlrs::identifier, tag(")")), + Entity::PropName, + ), + map(kdlrs::identifier, Entity::PropName), + ))(input) +} + +/// `operator := '=' | '!=' | '>' | '>=' | '<' | '<=' | '^=' | '$=' | '*='` +fn operator(input: &str) -> IResult<&str, Operator> { + alt(( + value(Operator::Contains, tag("*=")), + value(Operator::EndsWith, tag("$=")), + value(Operator::GreaterThanOrEqualTo, tag(">=")), + value(Operator::LessThanOrEqualTo, tag("<=")), + value(Operator::NotEqual, tag("!=")), + value(Operator::StartsWith, tag("^=")), + value(Operator::Equal, tag("=")), + value(Operator::GreaterThan, tag(">")), + value(Operator::LessThan, tag("<")), + ))(input) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_selector() { + assert_eq!( + selector("top()"), + Ok(("", vec![Combinator::Descendant(Accessor::Top, vec![]),])) + ); + + assert_eq!( + selector("top() > []"), + Ok(( + "", + vec![ + Combinator::Descendant(Accessor::Top, vec![]), + Combinator::Child(Accessor::AnyElement, vec![]) + ] + )) + ); + + assert_eq!( + selector("top() []"), + Ok(( + "", + vec![ + Combinator::Descendant(Accessor::Top, vec![]), + Combinator::Descendant(Accessor::AnyElement, vec![]) + ] + )) + ); + + assert_eq!( + selector("a + b ~ c"), + Ok(( + "", + vec![Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + )] + )) + ); + + assert_eq!( + selector("a + b ~ c > []"), + Ok(( + "", + vec![ + Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Child(Accessor::AnyElement, vec![]) + ] + )) + ); + + assert_eq!( + selector("a + b ~ c []"), + Ok(( + "", + vec![ + Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Descendant(Accessor::AnyElement, vec![]) + ] + )) + ); + + assert_eq!( + selector("a + b ~ c d ~ e + f"), + Ok(( + "", + vec![ + Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Descendant( + Accessor::Sole("d".to_owned()), + vec![ + (Sibling::General, Accessor::Sole("e".to_owned())), + (Sibling::Adjacent, Accessor::Sole("f".to_owned())) + ] + ), + ] + )) + ); + + assert_eq!( + selector("a + b ~ c > d ~ e + f"), + Ok(( + "", + vec![ + Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Child( + Accessor::Sole("d".to_owned()), + vec![ + (Sibling::General, Accessor::Sole("e".to_owned())), + (Sibling::Adjacent, Accessor::Sole("f".to_owned())) + ] + ), + ] + )) + ); + + assert_eq!( + selector("top() a + b ~ c > []"), + Ok(( + "", + vec![ + Combinator::Descendant(Accessor::Top, vec![]), + Combinator::Descendant( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Child(Accessor::AnyElement, vec![]) + ] + )) + ); + + assert_eq!( + selector("top() > a + b ~ c []"), + Ok(( + "", + vec![ + Combinator::Descendant(Accessor::Top, vec![]), + Combinator::Child( + Accessor::Sole("a".to_owned()), + vec![ + (Sibling::Adjacent, Accessor::Sole("b".to_owned())), + (Sibling::General, Accessor::Sole("c".to_owned())) + ] + ), + Combinator::Descendant(Accessor::AnyElement, vec![]) + ] + )) + ); + } + + #[test] + fn test_combinator() { + use super::ParsedCombinator::{AdjacentSibling, Child, Descendant, GeneralSibling}; + + assert_eq!(combinator(" > "), Ok(("", Child))); + assert_eq!(combinator(" + "), Ok(("", AdjacentSibling))); + assert_eq!(combinator(" ~ "), Ok(("", GeneralSibling))); + assert_eq!(combinator(" "), Ok(("", Descendant))); + } + + #[test] + fn test_accessor() { + use super::Accessor::{AnyElement, Closed, Sole, Top}; + + assert_eq!(accessor("[]"), Ok(("", AnyElement))); + assert_eq!(accessor("name"), Ok(("", Sole("name".to_owned())))); + assert_eq!(accessor("top()"), Ok(("", Top))); + assert_eq!( + accessor("[props()]"), + Ok(("", Closed(None, Matcher::Direct(Entity::Props)))) + ); + assert_eq!( + accessor("name[props()]"), + Ok(( + "", + Closed(Some("name".to_owned()), Matcher::Direct(Entity::Props)) + )) + ); + } + + #[test] + fn test_matcher() { + use super::Matcher::{Direct, Expression}; + + assert_eq!(matcher("[name()]"), Ok(("", Direct(Entity::NodeName)))); + assert_eq!(matcher("[tag()]"), Ok(("", Direct(Entity::TypeTag)))); + assert_eq!(matcher("[props()]"), Ok(("", Direct(Entity::Props)))); + assert_eq!(matcher("[values()]"), Ok(("", Direct(Entity::Values)))); + assert_eq!(matcher("[val()]"), Ok(("", Direct(Entity::Val(0))))); + assert_eq!(matcher("[val(777)]"), Ok(("", Direct(Entity::Val(777))))); + assert_eq!( + matcher("[prop(name)]"), + Ok(("", Direct(Entity::PropName("name".to_owned())))) + ); + assert_eq!( + matcher("[prop]"), + Ok(("", Direct(Entity::PropName("prop".to_owned())))) + ); + assert!(matcher("[some()]").is_err()); + + assert_eq!( + matcher(r#"[name() = "kdl"]"#), + Ok(( + "", + Expression(Entity::NodeName, Operator::Equal, "kdl".into()) + )) + ); + assert_eq!( + matcher(r#"[tag() = "kdl"]"#), + Ok(( + "", + Expression(Entity::TypeTag, Operator::Equal, "kdl".into()) + )) + ); + assert_eq!( + matcher(r#"[props() = "kdl"]"#), + Ok(("", Expression(Entity::Props, Operator::Equal, "kdl".into()))) + ); + assert_eq!( + matcher(r#"[values() = "kdl"]"#), + Ok(( + "", + Expression(Entity::Values, Operator::Equal, "kdl".into()) + )) + ); + assert_eq!( + matcher(r#"[val() = 777]"#), + Ok(("", Expression(Entity::Val(0), Operator::Equal, 777.into()))) + ); + assert_eq!( + matcher("[val(777) = 777]"), + Ok(( + "", + Expression(Entity::Val(777), Operator::Equal, 777.into()) + )) + ); + assert_eq!( + matcher("[prop(name) = 777]"), + Ok(( + "", + Expression( + Entity::PropName("name".to_owned()), + Operator::Equal, + 777.into() + ) + )) + ); + assert_eq!( + matcher("[prop = 777]"), + Ok(( + "", + Expression( + Entity::PropName("prop".to_owned()), + Operator::Equal, + 777.into() + ) + )) + ); + assert!(matcher("[some() = 777]").is_err()); + } + + #[test] + fn test_entity() { + use super::Entity::{NodeName, PropName, Props, TypeTag, Val, Values}; + + assert_eq!(entity("name()"), Ok(("", NodeName))); + assert_eq!(entity("tag()"), Ok(("", TypeTag))); + assert_eq!(entity("props()"), Ok(("", Props))); + assert_eq!(entity("values()"), Ok(("", Values))); + assert_eq!(entity("val()"), Ok(("", Val(0)))); + assert_eq!(entity("val(777)"), Ok(("", Val(777)))); + assert_eq!( + entity("val(3.14)"), + Ok(("(3.14)", PropName("val".to_owned()))) + ); + assert_eq!(entity("val(-0)"), Ok(("(-0)", PropName("val".to_owned())))); + assert_eq!(entity("prop(name)"), Ok(("", PropName("name".to_owned())))); + assert_eq!(entity("prop"), Ok(("", PropName("prop".to_owned())))); + assert_eq!(entity("prop()"), Ok(("()", PropName("prop".to_owned())))); + assert_eq!(entity("some()"), Ok(("()", PropName("some".to_owned())))); + + assert!(entity("0xEF").is_err()); + } + + #[test] + fn test_operator() { + use super::Operator::{ + Contains, EndsWith, Equal, GreaterThan, GreaterThanOrEqualTo, LessThan, + LessThanOrEqualTo, NotEqual, StartsWith, + }; + + assert_eq!(operator("!="), Ok(("", NotEqual))); + assert_eq!(operator("$="), Ok(("", EndsWith))); + assert_eq!(operator("*="), Ok(("", Contains))); + assert_eq!(operator("<="), Ok(("", LessThanOrEqualTo))); + assert_eq!(operator(">="), Ok(("", GreaterThanOrEqualTo))); + assert_eq!(operator("^="), Ok(("", StartsWith))); + assert_eq!(operator("<"), Ok(("", LessThan))); + assert_eq!(operator("="), Ok(("", Equal))); + assert_eq!(operator(">"), Ok(("", GreaterThan))); + + assert!(operator("?").is_err()); + assert!(operator("?=").is_err()); + assert!(operator("?= ...").is_err()); + } +} diff --git a/tests/accessor_multiple/mod.rs b/tests/accessor_multiple/mod.rs new file mode 100644 index 0000000..08db2a7 --- /dev/null +++ b/tests/accessor_multiple/mod.rs @@ -0,0 +1,176 @@ +use assert_cmd::Command; +use indoc::indoc; + +const INPUT: &str = include_str!("./website.kdl"); + +#[test] +fn top_descendant_any_element() { + Command::cargo_bin("kq") + .unwrap() + .arg("top() []") + .write_stdin(indoc! {r#" + name "CI" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" { + strategy { + matrix { + os "ubuntu-latest" "macOS-latest" "windows-latest" + } + } + } + } + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" { + strategy { + matrix { + os "ubuntu-latest" "macOS-latest" "windows-latest" + } + } + } + } + "#}); +} + +#[test] +fn top_child_any_element() { + Command::cargo_bin("kq") + .unwrap() + .arg("top() > []") + .write_stdin(indoc! {r#" + name "CI" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" { + strategy { + matrix { + os "ubuntu-latest" "macOS-latest" "windows-latest" + } + } + } + } + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" { + strategy { + matrix { + os "ubuntu-latest" "macOS-latest" "windows-latest" + } + } + } + } + "#}); +} + +#[test] +fn descendant_child() { + Command::cargo_bin("kq") + .unwrap() + .arg("html > body section > h2") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + h2 "Design and Discussion" + h2 "Design Principles" + "#}); +} + +#[test] +fn general_sibling() { + Command::cargo_bin("kq") + .unwrap() + .arg("html > head meta ~ title") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + title "kdl - Kat's Document Language" + "#}); +} + +#[test] +fn adjacent_sibling() { + Command::cargo_bin("kq") + .unwrap() + .arg("html body h2 + ol") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + ol { + li "Maintainability" + li "Flexibility" + li "Cognitive simplicity and Learnability" + li "Ease of de\/serialization" + li "Ease of implementation" + } + "#}); +} + +#[test] +fn general_adjacent_siblings() { + Command::cargo_bin("kq") + .unwrap() + .arg("html > head meta ~ title + link") + .write_stdin(INPUT) + .assert() + .success() + .stdout(predicates::str::starts_with("link")) + .stdout(predicates::str::contains(r#"href="\/styles\/global.css""#)) + .stdout(predicates::str::contains(r#"rel="stylesheet""#)); +} + +#[test] +fn adjacent_general_siblings() { + Command::cargo_bin("kq") + .unwrap() + .arg("html > head meta + meta ~ link") + .write_stdin(INPUT) + .assert() + .success() + .stdout(predicates::str::starts_with("link")) + .stdout(predicates::str::contains(r#"href="\/styles\/global.css""#)) + .stdout(predicates::str::contains(r#"rel="stylesheet""#)); +} + +#[test] +fn complex_single_level() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"li[val() = "Flexibility"] + [val() = "Cognitive simplicity and Learnability"] ~ [val() = "Ease of implementation"]"#) + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + li "Ease of implementation" + "#}); +} + +#[test] +fn complex_nested() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"header + section[prop(id) = "description"] ~ section[class = "kdl-section"] ol > li"#) + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + li "Maintainability" + li "Flexibility" + li "Cognitive simplicity and Learnability" + li "Ease of de\/serialization" + li "Ease of implementation" + "#}); +} diff --git a/tests/accessor_multiple/website.kdl b/tests/accessor_multiple/website.kdl new file mode 100644 index 0000000..2895cd6 --- /dev/null +++ b/tests/accessor_multiple/website.kdl @@ -0,0 +1,48 @@ +// source: https://github.com/kdl-org/kdl/blob/main/examples/website.kdl +!doctype "html" +html lang="en" { + head { + meta charset="utf-8" + meta name="viewport" content="width=device-width, initial-scale=1.0" + meta \ + name="description" \ + content="kdl is a document language, mostly based on SDLang, with xml-like semantics that looks like you're invoking a bunch of CLI commands!" + title "kdl - Kat's Document Language" + link rel="stylesheet" href="/styles/global.css" + } + body { + main { + header class="py-10 bg-gray-300" { + h1 class="text-4xl text-center" "kdl - Kat's Document Language" + } + section class="kdl-section" id="description" { + p { + - "kdl is a document language, mostly based on " + a href="https://sdlang.org" "SDLang" + - " with xml-like semantics that looks like you're invoking a bunch of CLI commands" + } + p "It's meant to be used both as a serialization format and a configuration language, and is relatively light on syntax compared to XML." + } + section class="kdl-section" id="design-and-discussion" { + h2 "Design and Discussion" + p { + - "kdl is still extremely new, and discussion about the format should happen over on the " + a href="https://github.com/kdoclang/kdl/discussions" { + - "discussions" + } + - " page in the Github repo. Feel free to jump in and give us your 2 cents!" + } + } + section class="kdl-section" id="design-principles" { + h2 "Design Principles" + ol { + li "Maintainability" + li "Flexibility" + li "Cognitive simplicity and Learnability" + li "Ease of de/serialization" + li "Ease of implementation" + } + } + } + } +} diff --git a/tests/accessor_single/closed_node_name.rs b/tests/accessor_single/closed_node_name.rs new file mode 100644 index 0000000..db54ce3 --- /dev/null +++ b/tests/accessor_single/closed_node_name.rs @@ -0,0 +1,192 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("[name()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() = "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() != "step"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() ^= "pro"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() $= "file"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() *= "rofil"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() > "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() >= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() < "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[name() <= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_prop_name_explicit.rs b/tests/accessor_single/closed_prop_name_explicit.rs new file mode 100644 index 0000000..6d11d24 --- /dev/null +++ b/tests/accessor_single/closed_prop_name_explicit.rs @@ -0,0 +1,182 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present() { + Command::cargo_bin("kq") + .unwrap() + .arg("[prop(uses)]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent() { + Command::cargo_bin("kq") + .unwrap() + .arg("[prop(does_not_exist)]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) *= "toolchain"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[prop(uses) <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_prop_name_implicit.rs b/tests/accessor_single/closed_prop_name_implicit.rs new file mode 100644 index 0000000..3b2465c --- /dev/null +++ b/tests/accessor_single/closed_prop_name_implicit.rs @@ -0,0 +1,180 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present() { + Command::cargo_bin("kq") + .unwrap() + .arg("[uses]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn absent() { + Command::cargo_bin("kq") + .unwrap() + .arg("[does_not_exist]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses *= "s/"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[uses <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_props.rs b/tests/accessor_single/closed_props.rs new file mode 100644 index 0000000..fa73095 --- /dev/null +++ b/tests/accessor_single/closed_props.rs @@ -0,0 +1,146 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("[props()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + props "props" props="props" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_type_tag.rs b/tests/accessor_single/closed_type_tag.rs new file mode 100644 index 0000000..cccb28c --- /dev/null +++ b/tests/accessor_single/closed_type_tag.rs @@ -0,0 +1,142 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("[tag()]") + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_val.rs b/tests/accessor_single/closed_val.rs new file mode 100644 index 0000000..3cdb05b --- /dev/null +++ b/tests/accessor_single/closed_val.rs @@ -0,0 +1,197 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present_without_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("[val()]") + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + name "CI" + "#}); +} + +#[test] +fn present_with_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("[val(1)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent() { + Command::cargo_bin("kq") + .unwrap() + .arg("[val(3)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(1) = "pull_request"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) != "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + "#}); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) ^= "pu"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) $= "sh"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) *= "us"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) > "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) >= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) < "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) <= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/closed_values.rs b/tests/accessor_single/closed_values.rs new file mode 100644 index 0000000..0ba3d2f --- /dev/null +++ b/tests/accessor_single/closed_values.rs @@ -0,0 +1,146 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("[values()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_node_name.rs b/tests/accessor_single/identifier_closed_node_name.rs new file mode 100644 index 0000000..dfcca77 --- /dev/null +++ b/tests/accessor_single/identifier_closed_node_name.rs @@ -0,0 +1,354 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[name()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() = "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[name() = "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() != "step"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"nonsense[name() != "step"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() ^= "pro"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[name() ^= "pro"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() $= "file"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[name() $= "file"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() *= "rofil"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + profile "minimal" + "#}); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[name() *= "rofil"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() > "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_matter[name() > "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() >= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_matter[name() >= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() < "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_matter[name() < "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"profile[name() <= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_matter[name() <= "profile"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_prop_name_explicit.rs b/tests/accessor_single/identifier_closed_prop_name_explicit.rs new file mode 100644 index 0000000..537823c --- /dev/null +++ b/tests/accessor_single/identifier_closed_prop_name_explicit.rs @@ -0,0 +1,332 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present_prop() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[prop(uses)]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_prop() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[prop(does_not_exist)]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[prop(uses)]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) *= "toolchain"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) *= "toolchain"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[prop(uses) <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[prop(uses) <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_prop_name_implicit.rs b/tests/accessor_single/identifier_closed_prop_name_implicit.rs new file mode 100644 index 0000000..5f8cba4 --- /dev/null +++ b/tests/accessor_single/identifier_closed_prop_name_implicit.rs @@ -0,0 +1,329 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present_prop() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[uses]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn absent_prop() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[does_not_exist]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[uses]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + "#}); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses = "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses != "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses ^= "actions"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses $= "@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses *= "s/"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(indoc! {r#" + step uses="actions\/checkout@v1" + step uses="actions-rs\/toolchain@v1" + "#}); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses *= "s/"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses > "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses >= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses < "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"step[uses <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[uses <= "actions/checkout@v1"]"#) + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step run="cargo test --all --verbose" + step uses="actions-rs/toolchain@v1" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_props.rs b/tests/accessor_single/identifier_closed_props.rs new file mode 100644 index 0000000..93d0509 --- /dev/null +++ b/tests/accessor_single/identifier_closed_props.rs @@ -0,0 +1,290 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[props()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + props "props" props="props" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[props()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + props "props" props="props" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[props() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[props() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_type_tag.rs b/tests/accessor_single/identifier_closed_type_tag.rs new file mode 100644 index 0000000..0aa5535 --- /dev/null +++ b/tests/accessor_single/identifier_closed_type_tag.rs @@ -0,0 +1,282 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("[tag()]") + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[tag()]") + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[tag() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[tag() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_val.rs b/tests/accessor_single/identifier_closed_val.rs new file mode 100644 index 0000000..066b488 --- /dev/null +++ b/tests/accessor_single/identifier_closed_val.rs @@ -0,0 +1,376 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn present_without_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("name[val()]") + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + "#}); +} + +#[test] +fn absent_identifier_present_without_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[val()]") + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn present_with_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("on[val(1)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent_identifier_present_with_index() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[val(1)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent() { + Command::cargo_bin("kq") + .unwrap() + .arg("[val(3)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_idenrifier_absent() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[val(3)]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(1) = "pull_request"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(1) = "pull_request"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) != "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + "#}); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) != "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) ^= "pu"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) ^= "pu"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) $= "sh"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) $= "sh"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) *= "us"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(indoc! {r#" + on "push" "pull_request" + "#}); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) *= "us"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) > "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) > "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) >= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) >= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) < "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) < "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[val(0) <= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[val(0) <= "push"]"#) + .write_stdin(indoc! {r#" + on "push" "pull_request" + name "CI" + jobs + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/identifier_closed_values.rs b/tests/accessor_single/identifier_closed_values.rs new file mode 100644 index 0000000..e8e0e51 --- /dev/null +++ b/tests/accessor_single/identifier_closed_values.rs @@ -0,0 +1,290 @@ +use assert_cmd::Command; +use indoc::indoc; + +#[test] +fn parentheses() { + Command::cargo_bin("kq") + .unwrap() + .arg("step[values()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("does_not_exist[values()]") + .write_stdin(indoc! {r#" + step uses="actions/checkout@v1" + step "Install Rust" uses="actions-rs/toolchain@v1" { + profile "minimal" + } + step "Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() = "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_not_equal() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() != "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_starts_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() ^= "asc"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_ends_with() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() $= "cii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_contains() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() *= "sci"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() > "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_greater_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() >= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() < "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"[values() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn absent_less_than_or_equal_to() { + Command::cargo_bin("kq") + .unwrap() + .arg(r#"does_not_exist[values() <= "ascii"]"#) + .write_stdin(indoc! {r#" + step (ascii)"Clippy" run="cargo clippy --all -- -D warnings" + step "Run tests" run="cargo test --all --verbose" + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} diff --git a/tests/accessor_single/mod.rs b/tests/accessor_single/mod.rs new file mode 100644 index 0000000..1cd591e --- /dev/null +++ b/tests/accessor_single/mod.rs @@ -0,0 +1,126 @@ +use assert_cmd::Command; +use indoc::indoc; + +mod closed_node_name; +mod closed_prop_name_explicit; +mod closed_prop_name_implicit; +mod closed_props; +mod closed_type_tag; +mod closed_val; +mod closed_values; +mod identifier_closed_node_name; +mod identifier_closed_prop_name_explicit; +mod identifier_closed_prop_name_implicit; +mod identifier_closed_props; +mod identifier_closed_type_tag; +mod identifier_closed_val; +mod identifier_closed_values; + +#[test] +fn top() { + Command::cargo_bin("kq") + .unwrap() + .arg("top()") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}); +} + +#[test] +fn type_tag() { + Command::cargo_bin("kq") + .unwrap() + .arg("()") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn type_type_with_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("(jobs)") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}) + .assert() + .success() + .stdout(predicates::str::is_empty()); +} + +#[test] +fn sole() { + Command::cargo_bin("kq") + .unwrap() + .arg("jobs") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}) + .assert() + .success() + .stdout(indoc! {r#" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}); +} + +#[test] +fn any_element() { + Command::cargo_bin("kq") + .unwrap() + .arg("[]") + .write_stdin(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}) + .assert() + .success() + .stdout(indoc! {r#" + name "CI" + on "push" "pull_request" + jobs { + fmt_and_docs "Check fmt & build docs" + build_and_test "Build & Test" + } + "#}); +} diff --git a/tests/e2e_test.rs b/tests/e2e_test.rs new file mode 100644 index 0000000..6f14821 --- /dev/null +++ b/tests/e2e_test.rs @@ -0,0 +1,15 @@ +use assert_cmd::Command; +use predicates::prelude::*; + +mod accessor_multiple; +mod accessor_single; + +#[test] +fn sanity() { + Command::cargo_bin("kq") + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("print this help menu")) + .stdout(predicate::str::contains("print the version")); +} diff --git a/tests/example_test.rs b/tests/example_test.rs new file mode 100644 index 0000000..046ddec --- /dev/null +++ b/tests/example_test.rs @@ -0,0 +1,104 @@ +// test cases are coming from https://github.com/kdl-org/kdl/blob/1.0.0/QUERY-SPEC.md#examples +use assert_cmd::Command; +use indoc::indoc; + +const INPUT: &str = indoc! {r#" + package { + name "foo" + version "1.0.0" + dependencies platform="windows" { + winapi "1.0.0" path="./crates/my-winapi-fork" + } + dependencies { + miette "2.0.0" dev=true + } + } +"#}; + +#[test] +fn descendant() { + Command::cargo_bin("kq") + .unwrap() + .arg("package name") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + name "foo" + "#}); +} + +#[test] +fn top_child() { + Command::cargo_bin("kq") + .unwrap() + .arg("top() > package name") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + name "foo" + "#}); +} + +#[test] +fn descendant_by_identifier() { + Command::cargo_bin("kq") + .unwrap() + .arg("dependencies") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + dependencies platform="windows" { + winapi "1.0.0" path=".\/crates\/my-winapi-fork" + } + dependencies { + miette "2.0.0" dev=true + } + "#}); +} + +#[test] +fn identifier_and_implicit_property_name() { + Command::cargo_bin("kq") + .unwrap() + .arg("dependencies[platform]") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + dependencies platform="windows" { + winapi "1.0.0" path=".\/crates\/my-winapi-fork" + } + "#}); +} + +#[test] +fn identifier_and_explicit_property_name() { + Command::cargo_bin("kq") + .unwrap() + .arg("dependencies[prop(platform)]") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + dependencies platform="windows" { + winapi "1.0.0" path=".\/crates\/my-winapi-fork" + } + "#}); +} + +#[test] +fn all_direct_children() { + Command::cargo_bin("kq") + .unwrap() + .arg("dependencies > []") + .write_stdin(INPUT) + .assert() + .success() + .stdout(indoc! {r#" + winapi "1.0.0" path=".\/crates\/my-winapi-fork" + miette "2.0.0" dev=true + "#}); +}