Skip to content

Commit 27cd054

Browse files
authored
Merge pull request #3 from inthehack/feat-add-static-commands
feat: add foundations for static commands
2 parents da5382a + 68b388c commit 27cd054

28 files changed

Lines changed: 1962 additions & 447 deletions

Cargo.lock

Lines changed: 861 additions & 46 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ resolver = "2"
1313
members = ["noshell-parser", "noshell-macros", "noshell"]
1414

1515
[workspace.dependencies]
16-
defmt = { version = "0.3.10", default-features = false }
16+
# Core dependencies.
17+
defmt = "0.3.10"
18+
heapless = "0.8.0"
19+
nom = { version = "8.0.0", default-features = false }
1720
thiserror = { version = "2.0.11", default-features = false }
1821

22+
# Dev dependencies.
1923
googletest = "0.13.0"

Justfile

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,39 @@ mod init 'just/init.just'
1515
help:
1616
just --list
1717

18-
# Build the current workspace using the given PROFILE.
18+
# Build sources (cargo build).
1919
[group('build')]
20-
build PROFILE *OPTS:
21-
#!/usr/bin/env bash
22-
readonly PROFILES=( "dev" "release" )
20+
build *OPTS:
21+
cargo build --workspace {{ OPTS }}
2322

24-
validate() {
25-
local lookup="$1"
23+
# Build sources (cargo build).
24+
[group('build')]
25+
build-pkg pkg *OPTS:
26+
cargo build -p {{ pkg }} {{ OPTS }}
27+
28+
# Check if sources are compliant with lint rules (cargo clippy).
29+
[group('quality')]
30+
lint *OPTS:
31+
cargo clippy --workspace {{ OPTS }} -- -D warnings
32+
33+
# Check if sources are compliant with lint rules (cargo clippy --fix).
34+
[group('quality')]
35+
fix *OPTS: && (lint OPTS "--fix")
2636

27-
for value in "${PROFILES[@]}"; do
28-
[[ x"${lookup}" == x"${value}" ]] && return 0
29-
done
37+
# Format source code (nix fmt).
38+
[group('quality')]
39+
format *OPTS:
40+
nix fmt {{ OPTS }}
3041

31-
return 1
32-
}
42+
alias fmt := format
3343

34-
validate "{{ PROFILE }}" || {
35-
echo "error: invalid profile '{{ PROFILE }}'"
36-
exit 1
37-
}
44+
# Test all.
45+
test *OPTS:
46+
cargo nextest run --workspace {{ OPTS }}
3847

39-
nix build {{ OPTS }} '.#{{ PROFILE }}'
48+
# Test package.
49+
test-pkg pkg *OPTS:
50+
cargo nextest run -p {{ pkg }} {{ OPTS }}
4051

4152
# Clean the cargo build artifacts.
4253
[group('utility')]

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ This crate is still a work in progress and is subject to huge changes in its API
4040

4141
# Roadmap
4242

43-
- \[ \] Add support for positional arguments.
44-
- \[ \] Add support for `noline` crate, waiting for a custom handling of terminal escape codes.
45-
- \[x\] Add more parsers for destination value like `Option<Vec<_>>` or `Vec<_>` for instance.
46-
- \[ \] Add automatic generation of help output.
47-
- \[ \] Add completion thanks to escape codes (i.e. a press on Tab for instance).
48-
- \[ \] Add support for subcommands and related global and local arguments
43+
- [ ] Add support for positional arguments.
44+
- [ ] Add support for `noline` crate, waiting for a custom handling of terminal escape codes.
45+
- [x] Add more parsers for destination value like `Option<Vec<_>>` or `Vec<_>` for instance.
46+
- [ ] Add automatic generation of help output.
47+
- [ ] Add completion thanks to escape codes (i.e. a press on Tab for instance).
48+
- [ ] Add support for subcommands and related global and local arguments
4949

5050
Please feel free to email me with suggestions or directly propose a Pull Request with some valuable
5151
contribution. As it is the beginning of the project, I will take time to studi every contribution.

flake.nix

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
inputs.treefmt-nix.flakeModule
2121
];
2222

23+
# Applied systems.
24+
systems = [
25+
"x86_64-linux"
26+
"aarch64-linux"
27+
];
28+
29+
# Per-system configuration.
2330
perSystem =
2431
{ pkgs, system, ... }:
2532

@@ -85,6 +92,7 @@
8592
# Build dependencies.
8693
bacon
8794
cargo-deny
95+
cargo-insta
8896
cargo-nextest
8997
cargo-semver-checks
9098
clippy
@@ -96,13 +104,5 @@
96104
];
97105
};
98106
};
99-
100-
# Applied systems.
101-
systems = [
102-
"x86_64-linux"
103-
"aarch64-linux"
104-
"x86_64-darwin"
105-
"aarch64-darwin"
106-
];
107107
};
108108
}

noshell-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ syn = { version = "2.0.99", features = ["full"] }
2121

2222
[dev-dependencies]
2323
anyhow = "1.0.97"
24+
insta = "1.46.0"

noshell-macros/src/derive.rs

Lines changed: 43 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::collections::HashSet;
44

55
use proc_macro2::TokenStream;
66
use quote::{ToTokens, format_ident, quote, quote_spanned};
7+
use syn::Ident;
78
use syn::ext::IdentExt;
89
use syn::{
910
Data, DataStruct, DeriveInput, Expr, ExprLit, Fields, FieldsNamed, Lit, LitStr,
@@ -42,26 +43,38 @@ pub fn try_run(input: &DeriveInput) -> Result<TokenStream, syn::Error> {
4243
..
4344
}) => {
4445
let args = collect_args_meta(fields)?;
45-
let init = build_args_init(&args)?;
46+
let init = build_args_init(&args, format_ident!("args"))?;
4647

4748
let (ids_ty, ids) = parse_and_build_arg_ids(&args)?;
4849

4950
let attrs = Attr::parse_all(&input.attrs)?;
50-
let limit = get_noshell_attr_limit_arg_value(&attrs)?.unwrap_or(PARSER_ARG_COUNT_MAX);
51+
let size = get_noshell_attr_limit_arg_value(&attrs)?.unwrap_or(PARSER_ARG_COUNT_MAX);
5152

5253
Ok(quote! {
5354
impl #ident {
54-
pub fn parse<'a>(__argv: &'a [&'a str]) -> Result<Self, noshell::Error> {
55+
pub fn try_parse_from<'a, IterTy>(input: IterTy) -> Result<Self, noshell::Error>
56+
where
57+
IterTy: Iterator<Item = &'a str> + Clone,
58+
{
5559
use noshell::parser::ParsedArgs;
5660

57-
let __ids = Self::ids();
58-
let __tokens = noshell::parser::Tokens::new(__argv);
59-
let __args: ParsedArgs<'_, #limit> = noshell::parser::ParsedArgs::parse(__tokens, __ids);
61+
let flags = Self::parsed_flag_ids();
62+
let args = ParsedArgs::<'_, _, #size>::try_parse_from(input, flags.iter())?;
6063

6164
Ok(#ident #init)
6265
}
6366

64-
pub fn ids() -> &'static [(noshell::parser::lexer::Flag<'static>, &'static str)] {
67+
pub fn parse_from<'a, IterTy>(iter: IterTy) -> Self
68+
where
69+
IterTy: Iterator<Item = &'a str> + Clone,
70+
{
71+
Self::try_parse_from(iter).expect("should parse arguments from iterator")
72+
}
73+
74+
pub fn parsed_flag_ids() -> &'static [
75+
(noshell::parser::lexer::Flag<'static>, &'static str)
76+
]
77+
{
6578
static IDS: #ids_ty = #ids;
6679
&IDS
6780
}
@@ -91,10 +104,10 @@ fn collect_args_meta(fields: &FieldsNamed) -> Result<Vec<MetaArg>, syn::Error> {
91104
Ok(meta)
92105
}
93106

94-
fn build_args_init(fields: &[MetaArg]) -> Result<TokenStream, syn::Error> {
107+
fn build_args_init(fields: &[MetaArg], ident: Ident) -> Result<TokenStream, syn::Error> {
95108
let args = fields
96109
.iter()
97-
.map(build_arg_parser)
110+
.map(|x| build_arg_parser(x, ident.clone()))
98111
.collect::<Result<Vec<_>, syn::Error>>()?;
99112

100113
Ok(quote! {{
@@ -104,23 +117,22 @@ fn build_args_init(fields: &[MetaArg]) -> Result<TokenStream, syn::Error> {
104117
}})
105118
}
106119

107-
fn build_arg_parser(arg: &MetaArg) -> Result<TokenStream, syn::Error> {
120+
fn build_arg_parser(arg: &MetaArg, args_ident: Ident) -> Result<TokenStream, syn::Error> {
108121
let ty = &arg.ty;
109122
let inner_ty = get_inner_ty(ty);
110123

111-
let args = format_ident!("__args");
112124
let try_get_one = quote_spanned!(inner_ty.span()=> try_get_one::<#inner_ty>);
113125
let try_get_many = quote_spanned!(inner_ty.span()=> try_get_many::<_, #inner_ty>);
114126

115-
let ident = arg.id.unraw();
116-
let id = ident.to_string();
127+
let arg_ident = arg.id.unraw();
128+
let arg_id = arg_ident.to_string();
117129

118130
let value = match Ty::from_syn_ty(ty) {
119131
// Optional argument with required value.
120132
Ty::Option => quote_spanned! { ty.span()=>
121-
if #args.contains(#id) {
133+
if #args_ident.contains(#arg_id) {
122134
Some(
123-
#args.#try_get_one(#id)
135+
#args_ident.#try_get_one(#arg_id)
124136
.map(Option::unwrap)
125137
.and_then(noshell::parser::utils::check_value_is_missing)
126138
.map(Option::unwrap)?
@@ -132,9 +144,9 @@ fn build_arg_parser(arg: &MetaArg) -> Result<TokenStream, syn::Error> {
132144

133145
// Optional argument with optional value.
134146
Ty::OptionOption => quote_spanned! { ty.span()=>
135-
if #args.contains(#id) {
147+
if #args_ident.contains(#arg_id) {
136148
Some(
137-
#args.#try_get_one(#id).map(Option::flatten)?
149+
#args_ident.#try_get_one(#arg_id).map(Option::flatten)?
138150
)
139151
} else {
140152
None
@@ -143,9 +155,9 @@ fn build_arg_parser(arg: &MetaArg) -> Result<TokenStream, syn::Error> {
143155

144156
// Optional argument with required non-empty sequence of values.
145157
Ty::OptionVec => quote_spanned! { ty.span()=>
146-
if #args.contains(#id) {
158+
if #args_ident.contains(#arg_id) {
147159
Some(
148-
#args.#try_get_many(#id)
160+
#args_ident.#try_get_many(#arg_id)
149161
.map(Option::unwrap)
150162
.and_then(noshell::parser::utils::check_vec_is_missing)?
151163
)
@@ -156,15 +168,15 @@ fn build_arg_parser(arg: &MetaArg) -> Result<TokenStream, syn::Error> {
156168

157169
// Required argument with required non-empty sequence of values.
158170
Ty::Vec => quote_spanned! { ty.span()=>
159-
#args.#try_get_many(#id)
171+
#args_ident.#try_get_many(#arg_id)
160172
.and_then(noshell::parser::utils::check_arg_is_missing)
161173
.map(Option::unwrap)
162174
.and_then(noshell::parser::utils::check_vec_is_missing)?
163175
},
164176

165177
// Required argument with required value.
166178
Ty::Simple => quote_spanned! { ty.span()=>
167-
#args.#try_get_one(#id)
179+
#args_ident.#try_get_one(#arg_id)
168180
.and_then(noshell::parser::utils::check_arg_is_missing)
169181
.map(Option::unwrap)
170182
.and_then(noshell::parser::utils::check_value_is_missing)
@@ -173,7 +185,7 @@ fn build_arg_parser(arg: &MetaArg) -> Result<TokenStream, syn::Error> {
173185
};
174186

175187
Ok(quote_spanned! { arg.span=>
176-
#ident: #value
188+
#arg_ident: #value
177189
})
178190
}
179191

@@ -388,6 +400,8 @@ fn parse_and_build_arg_ids(args: &[MetaArg]) -> Result<(TokenStream, TokenStream
388400
mod tests {
389401
use syn::Field;
390402

403+
use crate::tests::utils::format_rust_token_stream;
404+
391405
use super::*;
392406

393407
#[test]
@@ -536,7 +550,7 @@ mod tests {
536550

537551
let attrs = Attr::parse_all(&field.attrs)?;
538552
let meta = MetaArg::new(&field, attrs);
539-
let given = build_arg_parser(&meta)?;
553+
let given = build_arg_parser(&meta, format_ident!("__args"))?;
540554

541555
let expected = quote!(
542556
value: __args.try_get_one::<u32>("value")
@@ -557,7 +571,7 @@ mod tests {
557571

558572
let attrs = Attr::parse_all(&field.attrs)?;
559573
let meta = MetaArg::new(&field, attrs);
560-
let given = build_arg_parser(&meta)?;
574+
let given = build_arg_parser(&meta, format_ident!("__args"))?;
561575

562576
let expected = quote!(
563577
value: if __args.contains("value") {
@@ -583,7 +597,7 @@ mod tests {
583597

584598
let attrs = Attr::parse_all(&field.attrs)?;
585599
let meta = MetaArg::new(&field, attrs);
586-
let given = build_arg_parser(&meta)?;
600+
let given = build_arg_parser(&meta, format_ident!("__args"))?;
587601

588602
let expected = quote!(value:
589603
if __args.contains("value") {
@@ -606,7 +620,7 @@ mod tests {
606620

607621
let attrs = Attr::parse_all(&field.attrs)?;
608622
let meta = MetaArg::new(&field, attrs);
609-
let given = build_arg_parser(&meta)?;
623+
let given = build_arg_parser(&meta, format_ident!("__args"))?;
610624

611625
let expected = quote!(value:
612626
if __args.contains("value") {
@@ -631,7 +645,7 @@ mod tests {
631645

632646
let attrs = Attr::parse_all(&field.attrs)?;
633647
let meta = MetaArg::new(&field, attrs);
634-
let given = build_arg_parser(&meta)?;
648+
let given = build_arg_parser(&meta, format_ident!("__args"))?;
635649

636650
let expected = quote!(
637651
value: __args.try_get_many::<_, u32>("value")
@@ -654,43 +668,8 @@ mod tests {
654668
}
655669
};
656670

657-
let given = try_run(&derive)?;
658-
659-
let expected = quote! {
660-
impl MyArgs {
661-
pub fn parse<'a>(__argv: &'a [&'a str]) -> Result<Self, noshell::Error> {
662-
use noshell::parser::ParsedArgs;
663-
664-
let __ids = Self::ids();
665-
let __tokens = noshell::parser::Tokens::new(__argv);
666-
let __args: ParsedArgs<'_, 16usize> = noshell::parser::ParsedArgs::parse(__tokens, __ids);
667-
668-
Ok(MyArgs {
669-
value1: __args.try_get_one::<u32>("value1")
670-
.and_then(noshell::parser::utils::check_arg_is_missing)
671-
.map(Option::unwrap)
672-
.and_then(noshell::parser::utils::check_value_is_missing)
673-
.map(Option::unwrap)?,
674-
675-
value2: __args.try_get_one::<u32>("value2")
676-
.and_then(noshell::parser::utils::check_arg_is_missing)
677-
.map(Option::unwrap)
678-
.and_then(noshell::parser::utils::check_value_is_missing)
679-
.map(Option::unwrap)?
680-
})
681-
}
682-
683-
pub fn ids() -> &'static [(noshell::parser::lexer::Flag<'static>, &'static str)] {
684-
static IDS: [(noshell::parser::lexer::Flag<'static>, &'static str); 2usize] = [
685-
(noshell::parser::lexer::Flag::Long("value1"), "value1"),
686-
(noshell::parser::lexer::Flag::Long("value2"), "value2"),
687-
];
688-
&IDS
689-
}
690-
}
691-
};
692-
693-
assert_eq!(expected.to_string(), given.to_string());
671+
let output = format_rust_token_stream(try_run(&derive)?);
672+
insta::assert_snapshot!(output);
694673

695674
Ok(())
696675
}

noshell-macros/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ mod derive;
1010
mod helpers;
1111
mod ty;
1212

13+
#[cfg(test)]
14+
mod tests;
15+
1316
/// `Parser` derive macro.
1417
#[proc_macro_derive(Parser, attributes(arg))]
1518
pub fn derive_parser(item: TokenStream) -> TokenStream {

0 commit comments

Comments
 (0)