7 Commits

10 changed files with 341 additions and 23 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/target
/expand.rs

5
.rusty-hook.toml Normal file
View File

@@ -0,0 +1,5 @@
[hooks]
pre-push = "cargo fmt --check && cargo test && cargo clippy -- -D warnings"
[logging]
verbose = true

22
CHANGELOG.md Normal file
View File

@@ -0,0 +1,22 @@
# Changelog
All notable changes to this project will be documented in this file.
## [1.1.0] - 2025-01-11
### 🚀 Features
- Add more support for patterns and conditional match
### 🐛 Bug Fixes
- Properly parse Expressions, Idents, and Literals
### ⚙️ Miscellaneous Tasks
- Clean up parse logic for Value
- Add git hooks
- Remove earthly check in release just script
- Add CHANGELOG.md
<!-- generated by git-cliff -->

110
Cargo.lock generated
View File

@@ -23,22 +23,48 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ci_info"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24f638c70e8c5753795cc9a8c07c44da91554a09e4cf11a7326e8161b0a3c45e"
dependencies = [
"envmnt",
]
[[package]]
name = "comlexr"
version = "1.0.0"
version = "1.1.0"
dependencies = [
"proc-macro2",
"quote",
"rstest",
"rusty-hook",
"syn",
]
[[package]]
name = "envmnt"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d328fc287c61314c4a61af7cfdcbd7e678e39778488c7cb13ec133ce0f4059"
dependencies = [
"fsio",
"indexmap 1.9.3",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "fsio"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fd087255f739f4f1aeea69f11b72f8080e9c2e7645cd06955dad4a178a49e3"
[[package]]
name = "futures-core"
version = "0.3.31"
@@ -82,18 +108,43 @@ dependencies = [
"slab",
]
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.7.0"
@@ -101,7 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.15.2",
]
[[package]]
@@ -110,6 +161,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nias"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -223,12 +280,44 @@ dependencies = [
"semver",
]
[[package]]
name = "rusty-hook"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cee9be61be7e1cbadd851e58ed7449c29c620f00b23df937cb9cbc04ac21a3"
dependencies = [
"ci_info",
"getopts",
"nias",
"toml",
]
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "serde"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "slab"
version = "0.4.9"
@@ -249,6 +338,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
@@ -261,7 +359,7 @@ version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"indexmap 2.7.0",
"toml_datetime",
"winnow",
]
@@ -272,6 +370,12 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "winnow"
version = "0.6.24"

View File

@@ -2,7 +2,7 @@
name = "comlexr"
description = "Dynamically build Command objects with conditional expressions"
repository = "https://gitlab.com/wunker-bunker/comlexr"
version = "1.0.0"
version = "1.1.0"
edition = "2018"
license = "MIT"
@@ -16,6 +16,7 @@ syn = { version = "2", features = ["full"] }
[dev-dependencies]
rstest = "0.24"
rusty-hook = "0.11"
[lints.rust]
unsafe_code = "forbid"
@@ -27,3 +28,9 @@ perf = "deny"
style = "deny"
nursery = "deny"
pedantic = "deny"
[package.metadata.release]
pre-release-hook = ["git", "cliff", "-o", "CHANGELOG.md", "--tag", "{{version}}"]
pre-release-replacements = [
{ file = "README.md", search = "comlexr = \"\\d+.\\d+.\\d+\"", replace = "comlexr = \"{{version}}\"" }
]

View File

@@ -8,7 +8,7 @@ Add `comlexr` to your project's `Cargo.toml`:
```toml
[dependencies]
comlexr = "1.0.0"
comlexr = "1.1.0"
```
### Rust Edition

59
justfile Normal file
View File

@@ -0,0 +1,59 @@
export RUST_BACKTRACE := "1"
set dotenv-load := true
set positional-arguments := true
# default recipe to display help information
default:
@just --list
# Clean up development files and images
clean:
cargo clean
# Run unit tests
test:
cargo test --workspace -- --show-output
# Run unit tests for all features
test-all-features:
cargo test --workspace --all-features -- --show-output
# Run clippy
lint:
cargo clippy
# Run clippy for all features
lint-all-features:
cargo clippy --all-features
# Expand the macros of a module for debugging
expand *args:
cargo expand $@ > ./expand.rs
$EDITOR ./expand.rs
# Installs cargo tools that help with development
tools:
rustup toolchain install stable nightly
rustup component add --toolchain stable rust-analyzer clippy rustfmt
cargo install --locked cargo-watch cargo-expand bacon
# Run cargo release and push the tag separately
release *args:
#!/usr/bin/env bash
set -euxo pipefail
# --workspace: updating all crates in the workspace
# --no-tag: do not push tag for each new version
# --no-confirm: don't look for user input, just run the command
# --execute: not a dry run
cargo release $1 -v \
--workspace \
--no-tag \
--no-confirm \
--execute
VERSION=$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "comlexr") .version')
echo "Pushing tag: v${VERSION}"
git tag "v${VERSION}"
git push origin "v${VERSION}"

View File

@@ -1,5 +1,10 @@
use quote::{quote, ToTokens};
use syn::{braced, bracketed, parse::Parse, punctuated::Punctuated, token, Token};
use syn::{
braced, bracketed,
parse::{discouraged::Speculative, Parse},
punctuated::Punctuated,
token, Token,
};
pub struct Command {
program: Value,
@@ -59,7 +64,10 @@ enum LogicArg {
impl Parse for LogicArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(Token![for]) {
if input.peek3(Token![in]) {
let pat_fork = input.fork();
_ = pat_fork.parse::<Token![for]>()?;
if pat_fork.call(syn::Pat::parse_single).is_ok() && pat_fork.peek(Token![in]) {
input.parse().map(Self::ForIn)
} else {
input.parse().map(Self::ForIter)
@@ -258,28 +266,54 @@ impl ToTokens for Match {
struct MatchArm {
pattern: syn::Pat,
if_expr: Option<Value>,
args: Arguments,
}
impl Parse for MatchArm {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let pattern = input.call(syn::Pat::parse_multi)?;
let if_expr = if input.peek(Token![if]) {
_ = input.parse::<Token![if]>()?;
Some(input.parse()?)
} else {
None
};
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self { pattern, args })
Ok(Self {
pattern,
if_expr,
args,
})
}
}
impl ToTokens for MatchArm {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { pattern, args } = self;
let Self {
pattern,
if_expr,
args,
} = self;
tokens.extend(quote! {
tokens.extend(if_expr.as_ref().map_or_else(
|| {
quote! {
#pattern => {
#args
}
});
}
},
|if_expr| {
quote! {
#pattern if #if_expr => {
#args
}
}
},
));
}
}
@@ -374,13 +408,25 @@ enum Value {
impl Parse for Value {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(syn::Lit) {
input.parse().map(Self::Lit)
} else if input.peek(syn::Ident) {
let expr_fork = input.fork();
expr_fork
.parse()
.map(|expr| {
input.advance_to(&expr_fork);
Self::Expr(expr)
})
.or_else(|_| {
if input.peek(syn::Ident) {
input.parse().map(Self::Ident)
} else if input.peek(syn::Lit) {
input.parse().map(Self::Lit)
} else {
input.parse().map(Self::Expr)
Err(syn::Error::new(
input.span(),
"Expected an expression, ident, or literal",
))
}
})
}
}

View File

@@ -86,6 +86,29 @@ mod cmd;
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "arg1" "arg2" "multi" "multi1" "multi" "multi2""#.to_string());
/// ```
///
/// ## Match Statements
/// ```
/// use comlexr::cmd;
///
/// enum TestArgs {
/// Arg1,
/// Arg2,
/// Arg3,
/// }
///
/// let match_arg = TestArgs::Arg2;
/// let command = cmd!(
/// "echo",
/// "test",
/// match match_arg {
/// TestArgs::Arg1 => "arg1",
/// TestArgs::Arg2 => ["arg1", "arg2"],
/// TestArgs::Arg3 => ["arg1", "arg2", "arg3"],
/// }
/// );
/// assert_eq!(format!("{command:?}"), r#""echo" "test" "arg1" "arg2""#.to_string());
/// ```
///
/// ## Dynamic Closures
/// ```
/// use comlexr::cmd;

View File

@@ -62,6 +62,31 @@ fn for_in(#[case] single_iter: &[&str], #[case] multi_iter: &[&str], #[case] exp
assert_eq!(format!("{command:?}"), expected.to_string());
}
struct TestStruct(&'static str);
#[rstest]
#[case(&[], &[], r#""echo" "test""#)]
#[case(&[TestStruct("1"), TestStruct("2")], &[], r#""echo" "test" "1" "2""#)]
#[case(&[], &[TestStruct("3"), TestStruct("4")], r#""echo" "test" "multi" "3" "multi" "4""#)]
#[case(&[TestStruct("1"), TestStruct("2")], &[TestStruct("3"), TestStruct("4")], r#""echo" "test" "1" "2" "multi" "3" "multi" "4""#)]
fn for_in_pat(
#[case] single_iter: &[TestStruct],
#[case] multi_iter: &[TestStruct],
#[case] expected: &str,
) {
let command = cmd!(
"echo",
"test",
for TestStruct(arg) in single_iter => arg,
for TestStruct(arg) in multi_iter => [
"multi",
arg,
],
);
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(None, None, r#""echo" "test""#)]
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
@@ -105,11 +130,37 @@ fn match_statement(#[case] match_arg: TestArgs, #[case] expected: &str) {
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(TestArgs::Arg1, true, r#""echo" "test" "arg1""#)]
#[case(TestArgs::Arg1, false, r#""echo" "test""#)]
#[case(TestArgs::Arg2, true, r#""echo" "test" "arg1" "arg2""#)]
#[case(TestArgs::Arg2, false, r#""echo" "test""#)]
#[case(TestArgs::Arg3, true, r#""echo" "test" "arg1" "arg2" "arg3""#)]
#[case(TestArgs::Arg3, false, r#""echo" "test""#)]
fn match_statement_conditional(
#[case] match_arg: TestArgs,
#[case] flag: bool,
#[case] expected: &str,
) {
let command = cmd!(
"echo",
"test",
match match_arg {
TestArgs::Arg1 if flag => "arg1",
TestArgs::Arg2 if flag => ["arg1", "arg2"],
TestArgs::Arg3 if flag => ["arg1", "arg2", "arg3"],
_ => [],
}
);
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(None, None, r#""echo" "test""#)]
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
#[case(None, Some("arg"), r#""echo" "test" "multi" "arg""#)]
#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi" "2""#)]
#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi=2""#)]
fn multi_match(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case] expected: &str) {
let command = cmd!(
"echo",
@@ -118,7 +169,7 @@ fn multi_match(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case
(None, None) => [],
(Some(single), None) => single,
(None, Some(multi)) => ["multi", multi],
(Some(single), Some(multi)) => [single, "multi", multi],
(Some(single), Some(multi)) => [single, format!("multi={multi}")],
},
);