Skip to content

Commit 1acdacd

Browse files
committed
feat: add custom default values
1 parent d625270 commit 1acdacd

4 files changed

Lines changed: 133 additions & 5 deletions

File tree

embedded-cli-macros/src/command/model.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use convert_case::{Case, Casing};
22
use darling::{Error, FromField, FromMeta, FromVariant, Result};
33
use proc_macro2::{Ident, TokenStream};
44
use quote::quote;
5-
use syn::{Field, Fields, FieldsNamed, FieldsUnnamed, Variant};
5+
use syn::{Expr, Field, Fields, FieldsNamed, FieldsUnnamed, Variant};
66

77
use super::args::{ArgType, TypedArg};
88

@@ -64,9 +64,27 @@ impl FromMeta for ShortName {
6464
}
6565
}
6666

67+
#[derive(Debug)]
68+
enum TypedDefault {
69+
Generated,
70+
Fixed(Expr),
71+
}
72+
73+
impl FromMeta for TypedDefault {
74+
fn from_expr(value: &Expr) -> Result<Self> {
75+
Ok(Self::Fixed(value.clone()))
76+
}
77+
78+
fn from_word() -> Result<Self> {
79+
Ok(Self::Generated)
80+
}
81+
}
82+
6783
#[derive(Debug, FromField, Default)]
6884
#[darling(default, attributes(arg), forward_attrs(allow, doc, cfg))]
6985
struct ArgAttrs {
86+
default_value: Option<String>,
87+
default_value_t: Option<TypedDefault>,
7088
short: Option<ShortName>,
7189
long: Option<LongName>,
7290
value_name: Option<String>,
@@ -111,6 +129,7 @@ impl CommandArgType {
111129

112130
pub struct CommandArg {
113131
pub arg_type: CommandArgType,
132+
pub default_value: Option<TokenStream>,
114133
pub field_name: String,
115134
pub field_type: TokenStream,
116135
#[cfg(feature = "help")]
@@ -146,9 +165,9 @@ impl CommandArg {
146165
LongName::Fixed(name) => name,
147166
});
148167
if let Some(long) = &long {
149-
if long.chars().any(|c| !c.is_ascii_alphabetic() && c != '-') {
168+
if long.chars().any(|c| !c.is_ascii_alphanumeric() && c != '-') {
150169
return Err(Error::custom(
151-
"Option name must consist of alphabetic ASCII chars",
170+
"Option name must consist of alphanumeric ASCII chars",
152171
));
153172
}
154173
}
@@ -167,8 +186,29 @@ impl CommandArg {
167186
} else {
168187
CommandArgType::Positional
169188
};
189+
190+
let default_value = match (arg_attrs.default_value, arg_attrs.default_value_t) {
191+
(Some(value), None) => Some(quote! {
192+
<#field_type as _cli::arguments::FromArgument>::from_arg(#value)?,
193+
}),
194+
(None, Some(value)) => {
195+
let value = match value {
196+
TypedDefault::Generated => quote! { Default::default() },
197+
TypedDefault::Fixed(expr) => quote! { #expr },
198+
};
199+
Some(quote! { #value })
200+
}
201+
(None, None) => None,
202+
_ => {
203+
return Err(Error::custom(
204+
"Only one of default_value or default_value_t is allowed",
205+
))
206+
}
207+
};
208+
170209
Ok(Self {
171210
arg_type,
211+
default_value,
172212
field_name,
173213
field_type,
174214
#[cfg(feature = "help")]

embedded-cli-macros/src/command/parse.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fn create_arg_parsing(command: &Command) -> (TokenStream, Vec<TokenStream>) {
101101
));
102102
}
103103
CommandArgType::Option { long, short } => {
104-
arg_default = None;
104+
arg_default = arg.default_value.clone();
105105
let state = format_ident!(
106106
"Expect{}",
107107
arg.field_name.from_case(Case::Snake).to_case(Case::Pascal)
@@ -123,7 +123,7 @@ fn create_arg_parsing(command: &Command) -> (TokenStream, Vec<TokenStream>) {
123123
));
124124
}
125125
CommandArgType::Positional => {
126-
arg_default = None;
126+
arg_default = arg.default_value.clone();
127127
let parse_value = create_parse_arg_value(ty);
128128

129129
positional_value_arms.push(quote! {

embedded-cli/tests/cli/defaults.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use embedded_cli::Command;
2+
use rstest::rstest;
3+
4+
use crate::impl_convert;
5+
use crate::wrapper::CliWrapper;
6+
7+
use crate::terminal::assert_terminal;
8+
9+
#[derive(Debug, Clone, Command, PartialEq)]
10+
enum CliTestCommand<'a> {
11+
Cmd {
12+
#[arg(long, default_value = "default name")]
13+
name: &'a str,
14+
15+
#[arg(long, default_value = "8")]
16+
level: u8,
17+
18+
#[arg(long, default_value_t = 9)]
19+
level2: u8,
20+
21+
#[arg(long, default_value_t)]
22+
level3: u8,
23+
},
24+
}
25+
26+
#[derive(Debug, Clone, PartialEq)]
27+
enum TestCommand {
28+
Cmd {
29+
name: String,
30+
level: u8,
31+
level2: u8,
32+
level3: u8,
33+
},
34+
}
35+
36+
impl_convert! {CliTestCommand<'_> => TestCommand, command, {
37+
match command {
38+
cmd => cmd.into(),
39+
}
40+
}}
41+
42+
impl<'a> From<CliTestCommand<'a>> for TestCommand {
43+
fn from(value: CliTestCommand<'a>) -> Self {
44+
match value {
45+
CliTestCommand::Cmd {
46+
name,
47+
level,
48+
level2,
49+
level3,
50+
} => Self::Cmd {
51+
name: name.to_string(),
52+
level,
53+
level2,
54+
level3,
55+
},
56+
}
57+
}
58+
}
59+
60+
#[rstest]
61+
#[case("cmd --name test-name --level 1 --level2 2 --level3 3", TestCommand::Cmd {
62+
name: "test-name".to_string(),
63+
level: 1,
64+
level2: 2,
65+
level3: 3,
66+
})]
67+
#[case("cmd", TestCommand::Cmd {
68+
name: "default name".to_string(),
69+
level: 8,
70+
level2: 9,
71+
level3: 0,
72+
})]
73+
fn options_parsing(#[case] command: &str, #[case] expected: TestCommand) {
74+
let mut cli = CliWrapper::new();
75+
76+
cli.process_str(command);
77+
78+
cli.send_enter();
79+
80+
assert_terminal!(
81+
cli.terminal(),
82+
2,
83+
vec![format!("$ {}", command), "$".to_string()]
84+
);
85+
86+
assert_eq!(cli.received_commands(), vec![Ok(expected)]);
87+
}

embedded-cli/tests/cli/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod autocomplete;
55
#[cfg(not(feature = "autocomplete"))]
66
mod autocomplete_disabled;
77
mod base;
8+
mod defaults;
89
#[cfg(feature = "help")]
910
mod help_simple;
1011
#[cfg(feature = "help")]

0 commit comments

Comments
 (0)