Skip to content

Commit 52912db

Browse files
authored
new: added Cmd#parse_for, OptCfg::make_cfgs_for and OptStore derive macro (#20)
1 parent 3ed4fba commit 52912db

File tree

13 files changed

+2967
-9
lines changed

13 files changed

+2967
-9
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ categories = ["command-line interface"]
1515
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1616

1717
[dependencies]
18+
cliargs_derive = { version = "0.1", path = "cliargs_derive" }
1819

1920
[dev-dependencies]
2021
trybuild = "1.0"
22+
23+
[workspace]
24+
members = ["cliargs_derive"]

cliargs_derive/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "cliargs_derive"
3+
version = "0.1.0"
4+
authors = ["Takayuki Sato <[email protected]>"]
5+
edition = "2021"
6+
rust-version = "1.74.1"
7+
description = ""
8+
documentation = "https://docs.rs/cliargs_derive"
9+
readme = "README.md"
10+
repository = "https://github.com/sttk/cliargs-rust"
11+
license = "MIT"
12+
keywords = ["cli", "command", "line", "argument", "parse"]
13+
categories = ["command-line interface"]
14+
15+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
16+
17+
[dependencies]
18+
proc-macro2 = "1.0"
19+
syn = "2.0"
20+
quote = "1.0"
21+
22+
[lib]
23+
proc-macro = true

cliargs_derive/src/errors.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
2+
// This program is free software under MIT License.
3+
// See the file LICENSE in this distribution for more details.
4+
5+
pub enum OptStoreErr {
6+
MustPutOnStruct,
7+
MustNotPutOnTuple,
8+
BadFieldType,
9+
BadAttrMetaName,
10+
BadAttrMetaValueCfg,
11+
BadAttrMetaValueDesc,
12+
BadAttrMetaValueArg,
13+
MustNotHasDefaults(String),
14+
InvalidNumberFormat(String, String),
15+
}
16+
17+
impl OptStoreErr {
18+
fn msg(&self) -> String {
19+
match self {
20+
OptStoreErr::MustPutOnStruct => String::from("must be attached to a struct"),
21+
OptStoreErr::MustNotPutOnTuple => String::from("must not be attached to a tuple"),
22+
OptStoreErr::BadFieldType => String::from(
23+
"accept only bool, primitive number types, String, \
24+
Vec<> of primitive number, Vec<> of String, \
25+
Option<> primitive number, Option<> of String",
26+
),
27+
OptStoreErr::BadAttrMetaName => String::from(
28+
"field attribute `opt` accepts only cfg=\"...\", \
29+
desc=\"...\", and arg=\"...\"",
30+
),
31+
OptStoreErr::BadAttrMetaValueCfg => {
32+
String::from("`cfg` in field attributes `opt` must be a string literal")
33+
}
34+
OptStoreErr::BadAttrMetaValueDesc => {
35+
String::from("`desc` in field attributes `opt` must be a string literal")
36+
}
37+
OptStoreErr::BadAttrMetaValueArg => {
38+
String::from("`arg` in field attributes `opt` must be a string literal")
39+
}
40+
OptStoreErr::MustNotHasDefaults(fld) => {
41+
format!("`{fld}` is bool, so the default value cannot be specified")
42+
}
43+
OptStoreErr::InvalidNumberFormat(fld, ty) => {
44+
format!("`{fld}` is {ty}, but the default value is invalid format")
45+
}
46+
}
47+
}
48+
49+
pub fn at(&self, span: proc_macro2::Span) -> syn::Error {
50+
const PREFIX: &str = "[derive macro `OptStore`]";
51+
syn::Error::new(span, format!("{PREFIX} {}", self.msg()))
52+
}
53+
}

cliargs_derive/src/lib.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// Copyright (C) 2024 Takayuki Sato. All Rights Reserved.
2+
// This program is free software under MIT License.
3+
// See the file LICENSE in this distribution for more details.
4+
5+
mod errors;
6+
mod option_field;
7+
mod scalar_field;
8+
mod util;
9+
mod vector_field;
10+
11+
use errors::OptStoreErr;
12+
use util::identify_field_type;
13+
14+
use proc_macro::TokenStream;
15+
use std::collections::HashMap;
16+
17+
/// Is attached to a struct which holds command line option values, and automatically implements
18+
/// its method to generate `OptCfg`s from its fields, and other methods.
19+
///
20+
/// This macro automatically implements the method to generates a vector of `OptCfg` from the field
21+
/// definitions and `opt` field attributes, and this also implements the method that instantiates
22+
/// the struct using the default values specified in `opt` field attributes, and implements the
23+
/// method to updates the field values with the values from the passed `HashMap1.
24+
///
25+
/// The `opt` field attribute can have the following pairs of name and value: one is `cfg` to
26+
/// specify `names` and `defaults` of `OptCfg` struct, another is `desc` to specify `desc` of
27+
/// `OptCfg` struct, and yet another is `arg` to specify `arg_in_help` of `OptCfg` struct.
28+
///
29+
/// The format of `cfg` is like `cfg="f,foo=123"`.
30+
/// The left side of the equal sign is the option name(s), and the right side is the default
31+
/// value(s).
32+
/// If there is no equal sign, it is determined that only the option name is specified.
33+
/// If you want to specify multiple option names, separate them with commas.
34+
/// If you want to specify multiple default values, separate them with commas and round them with
35+
/// square brackets, like `[1,2,3]`.
36+
/// If you want to use your favorite carachter as a separator, you can use it by putting it on the
37+
/// left side of the open square bracket, like `/[1/2/3]`.
38+
///
39+
/// The following code is a sample of a option store struct.
40+
///
41+
/// ```rust
42+
/// extern crate cliargs_derive;
43+
/// use cliargs_derive::OptStore;
44+
///
45+
/// #[derive(OptStore)]
46+
/// struct MyOptions {
47+
/// #[opt(cfg="f,foo-bar", desc="The description of foo-bar.")]
48+
/// foo_bar: bool,
49+
///
50+
/// #[opt(cfg="b", desc="The description of baz.", arg="text")]
51+
/// baz: String,
52+
///
53+
/// #[opt(cfg="q=[1,2,3]", desc="The description of qux.", arg="num...")]
54+
/// qux: Vec<u8>,
55+
/// }
56+
/// ```
57+
#[proc_macro_derive(OptStore, attributes(opt))]
58+
pub fn opt_store_derive(input: TokenStream) -> TokenStream {
59+
let input = &syn::parse_macro_input!(input as syn::DeriveInput);
60+
61+
match generate_opt_store_impl(input) {
62+
Ok(generated) => generated,
63+
Err(err) => err.to_compile_error().into(),
64+
}
65+
}
66+
67+
fn generate_opt_store_impl(input: &syn::DeriveInput) -> Result<TokenStream, syn::Error> {
68+
let struct_name = &input.ident;
69+
let (impl_generics, _, where_clause) = &input.generics.split_for_impl();
70+
71+
let struct_data = match &input.data {
72+
syn::Data::Struct(data) => data,
73+
_ => return Err(OptStoreErr::MustPutOnStruct.at(input.ident.span())),
74+
};
75+
76+
let struct_span = input.ident.span();
77+
78+
let mut cfg_vec = Vec::<proc_macro2::TokenStream>::new();
79+
let mut init_vec = Vec::<proc_macro2::TokenStream>::new();
80+
let mut set_vec = Vec::<proc_macro2::TokenStream>::new();
81+
for field in &struct_data.fields {
82+
collect_impl_for_field(
83+
field,
84+
&mut cfg_vec,
85+
&mut init_vec,
86+
&mut set_vec,
87+
struct_span,
88+
)?;
89+
}
90+
91+
let expanded = quote::quote! {
92+
#[automatically_derived]
93+
impl #impl_generics #struct_name #where_clause {
94+
pub fn with_defaults() -> #struct_name {
95+
#struct_name {
96+
#(#init_vec),*
97+
}
98+
}
99+
}
100+
101+
#[automatically_derived]
102+
impl #impl_generics cliargs::OptStore for #struct_name #where_clause {
103+
fn make_opt_cfgs(&self) -> Vec<cliargs::OptCfg> {
104+
vec![
105+
#(#cfg_vec),*
106+
]
107+
}
108+
109+
fn set_field_values(&mut self, m: &std::collections::HashMap<&str, Vec<&str>>) -> Result<(), cliargs::errors::InvalidOption> {
110+
#(#set_vec)*
111+
Ok(())
112+
}
113+
}
114+
};
115+
116+
//println!("{}", expanded);
117+
Ok(expanded.into())
118+
}
119+
120+
fn collect_impl_for_field(
121+
field: &syn::Field,
122+
cfg_vec: &mut Vec<proc_macro2::TokenStream>,
123+
init_vec: &mut Vec<proc_macro2::TokenStream>,
124+
set_vec: &mut Vec<proc_macro2::TokenStream>,
125+
struct_span: proc_macro2::Span,
126+
) -> Result<(), syn::Error> {
127+
let field_name = match field.ident.as_ref() {
128+
Some(ident) => ident,
129+
None => return Err(OptStoreErr::MustNotPutOnTuple.at(struct_span)),
130+
};
131+
let field_span = field_name.span();
132+
133+
let mut attr_map = HashMap::<String, String>::new();
134+
let mut attr_span: Option<proc_macro2::Span> = None;
135+
for attr in &field.attrs {
136+
if attr.path().is_ident("opt") {
137+
let span = attr.path().get_ident().unwrap().span();
138+
attr_span = Some(span);
139+
140+
collect_impl_for_field_attr(attr, &mut attr_map, span)?;
141+
}
142+
}
143+
144+
if let Some((ty_ident, in_vec, in_opt)) = identify_field_type(&field.ty) {
145+
if in_opt {
146+
return option_field::collect_impl(
147+
field_name, ty_ident, cfg_vec, init_vec, set_vec, &attr_map, attr_span, field_span,
148+
);
149+
} else if in_vec {
150+
return vector_field::collect_impl(
151+
field_name, ty_ident, cfg_vec, init_vec, set_vec, &attr_map, attr_span, field_span,
152+
);
153+
} else {
154+
return scalar_field::collect_impl(
155+
field_name, ty_ident, cfg_vec, init_vec, set_vec, &attr_map, attr_span, field_span,
156+
);
157+
}
158+
}
159+
160+
Err(OptStoreErr::BadFieldType.at(field_span))
161+
}
162+
163+
fn collect_impl_for_field_attr(
164+
attr: &syn::Attribute,
165+
attr_map: &mut HashMap<String, String>,
166+
attr_span: proc_macro2::Span,
167+
) -> Result<(), syn::Error> {
168+
let nested = attr.parse_args_with(
169+
syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
170+
)?;
171+
for meta in nested {
172+
match meta {
173+
syn::Meta::NameValue(meta) => {
174+
if meta.path.is_ident("cfg") {
175+
match meta.value {
176+
syn::Expr::Lit(lit) => match lit.lit {
177+
syn::Lit::Str(s) => {
178+
let value = s.value();
179+
match &value.split_once("=") {
180+
Some((lhs, rhs)) => {
181+
attr_map.insert("names".to_string(), lhs.to_string());
182+
attr_map.insert("defaults".to_string(), rhs.to_string());
183+
}
184+
None => {
185+
attr_map.insert("names".to_string(), value);
186+
}
187+
}
188+
}
189+
_ => return Err(OptStoreErr::BadAttrMetaValueCfg.at(attr_span)),
190+
},
191+
_ => return Err(OptStoreErr::BadAttrMetaValueCfg.at(attr_span)),
192+
}
193+
} else if meta.path.is_ident("desc") {
194+
match meta.value {
195+
syn::Expr::Lit(lit) => match lit.lit {
196+
syn::Lit::Str(s) => {
197+
attr_map.insert("desc".to_string(), s.value());
198+
}
199+
_ => return Err(OptStoreErr::BadAttrMetaValueDesc.at(attr_span)),
200+
},
201+
_ => return Err(OptStoreErr::BadAttrMetaValueDesc.at(attr_span)),
202+
}
203+
} else if meta.path.is_ident("arg") {
204+
match meta.value {
205+
syn::Expr::Lit(lit) => match lit.lit {
206+
syn::Lit::Str(s) => {
207+
attr_map.insert("arg".to_string(), s.value());
208+
}
209+
_ => return Err(OptStoreErr::BadAttrMetaValueArg.at(attr_span)),
210+
},
211+
_ => return Err(OptStoreErr::BadAttrMetaValueArg.at(attr_span)),
212+
}
213+
} else {
214+
return Err(OptStoreErr::BadAttrMetaName.at(attr_span));
215+
}
216+
}
217+
_ => return Err(OptStoreErr::BadAttrMetaName.at(attr_span)),
218+
}
219+
}
220+
221+
Ok(())
222+
}

0 commit comments

Comments
 (0)