Skip to content

Commit 5b43b71

Browse files
committed
init promkit-derive crate
1 parent ff05682 commit 5b43b71

File tree

5 files changed

+282
-25
lines changed

5 files changed

+282
-25
lines changed

Cargo.toml

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,6 @@
1-
[package]
2-
name = "promkit"
3-
version = "0.4.0"
4-
authors = ["ynqa <[email protected]>"]
5-
edition = "2021"
6-
description = "A toolkit for building your own interactive command-line tools"
7-
repository = "https://github.com/ynqa/promkit"
8-
license = "MIT"
9-
readme = "README.md"
10-
11-
[lib]
12-
name = "promkit"
13-
path = "src/lib.rs"
14-
15-
[dependencies]
16-
crossterm = { version = "0.27.0", features = ["use-dev-tty"] }
17-
indexmap = "2.2.3"
18-
radix_trie = "0.2.1"
19-
serde = { version = "1.0.197" }
20-
serde_json = { version = "1.0.114", features = ["preserve_order"] }
21-
thiserror = "1.0.50"
22-
unicode-width = "0.1.8"
23-
24-
[dev-dependencies]
25-
strip-ansi-escapes = "0.2.0"
1+
[workspace]
2+
resolver = "2"
3+
members = [
4+
"promkit",
5+
"promkit-derive",
6+
]

promkit-derive/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "promkit-derive"
3+
version = "0.1.0"
4+
authors = ["ynqa <[email protected]>"]
5+
edition = "2021"
6+
description = "A derive macro for promkit"
7+
repository = "https://github.com/ynqa/promkit"
8+
license = "MIT"
9+
readme = "README.md"
10+
11+
[lib]
12+
proc-macro = true
13+
14+
[dependencies]
15+
syn = { version = "2.0.52", features = ["full"] }
16+
quote = "1.0"
17+
proc-macro2 = "1.0"
18+
promkit = { path = "../promkit", version = "0.4.0" }

promkit-derive/examples/example.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use promkit::{crossterm::style::Color, style::StyleBuilder, Result};
2+
use promkit_derive::Promkit;
3+
4+
#[derive(Default, Debug, Promkit)]
5+
struct Profile {
6+
#[readline(
7+
prefix = "What is your name?",
8+
prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
9+
)]
10+
name: String,
11+
12+
#[readline(default)]
13+
hobby: Option<String>,
14+
15+
#[readline(prefix = "How old are you?", ignore_invalid_attr = "nothing")]
16+
age: usize,
17+
}
18+
19+
fn main() -> Result {
20+
let mut ret = Profile::default();
21+
ret.readline_name()?;
22+
ret.readline_hobby()?;
23+
ret.readline_age()?;
24+
dbg!(ret);
25+
Ok(())
26+
}

promkit-derive/src/lib.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
extern crate proc_macro;
2+
3+
use proc_macro2::TokenStream;
4+
use quote::quote;
5+
use syn::{parse::Error, parse_macro_input, spanned::Spanned, DeriveInput};
6+
7+
#[proc_macro_derive(Promkit, attributes(readline))]
8+
pub fn promkit_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
9+
let ast = parse_macro_input!(input as DeriveInput);
10+
match impl_promkit_derive(&ast) {
11+
Ok(token) => token.into(),
12+
Err(e) => e.to_compile_error().into(),
13+
}
14+
}
15+
16+
mod readline;
17+
18+
fn impl_promkit_derive(ast: &DeriveInput) -> Result<TokenStream, Error> {
19+
let fields = match &ast.data {
20+
syn::Data::Struct(s) => match &s.fields {
21+
syn::Fields::Named(fields) => &fields.named,
22+
// tuple struct is like `struct Point(f32, f32);`
23+
syn::Fields::Unnamed(_) => {
24+
return Err(Error::new(ast.span(), "Not support tuple structs"))
25+
}
26+
// unit struct is like `struct Marker;`
27+
syn::Fields::Unit => return Err(Error::new(ast.span(), "Not support unit structs")),
28+
},
29+
syn::Data::Enum(_) => return Err(Error::new(ast.span(), "Not support enums")),
30+
syn::Data::Union(_) => return Err(Error::new(ast.span(), "Not support unions")),
31+
};
32+
33+
let mut fns = quote! {};
34+
35+
for field in fields.iter() {
36+
for attr in field.attrs.iter() {
37+
#[allow(clippy::single_match)]
38+
match attr.path().get_ident().unwrap().to_string().as_str() {
39+
"readline" => {
40+
let expr = readline::impl_promkit_per_field(field, attr)?;
41+
fns = quote! {
42+
#fns
43+
#expr
44+
};
45+
}
46+
_ => (),
47+
}
48+
}
49+
}
50+
51+
let name = &ast.ident;
52+
Ok(quote! {
53+
impl #name {
54+
#fns
55+
}
56+
})
57+
}

promkit-derive/src/readline.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{quote, ToTokens};
3+
use syn::{
4+
parse::Error, punctuated::Punctuated, spanned::Spanned, Meta, MetaList, MetaNameValue, Token,
5+
};
6+
7+
pub fn impl_promkit_per_field(
8+
field: &syn::Field,
9+
attr: &syn::Attribute,
10+
) -> Result<TokenStream, Error> {
11+
let readline_preset: TokenStream = match &attr.meta {
12+
Meta::List(list) => {
13+
let results = [parse_default_meta(list), parse_kvs_meta(list)];
14+
let errors: Vec<Error> = results
15+
.iter()
16+
.filter_map(|r| r.as_ref().err().cloned())
17+
.collect();
18+
19+
if errors.len() == results.len() {
20+
let error_messages = errors
21+
.iter()
22+
.map(|e| e.to_string())
23+
.collect::<Vec<_>>()
24+
.join(", ");
25+
Err(Error::new(
26+
list.span(),
27+
format!("Errors: {}", error_messages),
28+
))
29+
} else {
30+
results
31+
.into_iter()
32+
.find_map(Result::ok)
33+
.ok_or_else(|| Error::new(list.span(), "Unexpected error"))
34+
}
35+
}?,
36+
others => {
37+
return Err(Error::new(
38+
others.span(),
39+
format!(
40+
"Support only readline(default), or readline(key=value, ...), but got {}",
41+
others.to_token_stream()
42+
),
43+
))
44+
}
45+
};
46+
47+
let field_ident = field.ident.as_ref().unwrap();
48+
let preset_fn = syn::Ident::new(&format!("readline_{}", field_ident), field_ident.span());
49+
50+
match &field.ty {
51+
syn::Type::Path(typ) => {
52+
let last_segment = typ.path.segments.last().unwrap();
53+
match last_segment.ident.to_string().as_str() {
54+
"Option" => {
55+
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments {
56+
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() {
57+
return Ok(quote! {
58+
pub fn #preset_fn(&mut self) -> promkit::Result {
59+
let value_str = #readline_preset?;
60+
let parsed_value = value_str.parse::<#inner_type>()
61+
.map_or_else(|_| None, Some);
62+
self.#field_ident = parsed_value;
63+
Ok(())
64+
}
65+
});
66+
}
67+
}
68+
Err(Error::new(
69+
last_segment.span(),
70+
format!("Support Option<T> but got {}", typ.to_token_stream()),
71+
))
72+
}
73+
_ => {
74+
let ty = typ.to_token_stream();
75+
Ok(quote! {
76+
pub fn #preset_fn(&mut self) -> promkit::Result {
77+
let value_str = #readline_preset?;
78+
let parsed_value = value_str.parse::<#ty>()
79+
.map_err(|e| promkit::Error::ParseError(e.to_string()))?;
80+
self.#field_ident = parsed_value;
81+
Ok(())
82+
}
83+
})
84+
}
85+
}
86+
}
87+
ty => Err(Error::new(
88+
ty.span(),
89+
format!(
90+
"Support only Path for field type but got {}",
91+
ty.to_token_stream()
92+
),
93+
)),
94+
}
95+
}
96+
97+
fn parse_default_meta(list: &MetaList) -> Result<TokenStream, Error> {
98+
match list.tokens.to_string().as_str() {
99+
"default" => Ok(quote! {
100+
promkit::preset::readline::Readline::default()
101+
.prompt()?
102+
.run()
103+
}),
104+
others => Err(Error::new(
105+
list.span(),
106+
format!("Support readline(default) but got {}", others),
107+
)),
108+
}
109+
}
110+
111+
fn parse_kvs_meta(list: &MetaList) -> Result<TokenStream, Error> {
112+
let mut ret = quote! {
113+
promkit::preset::readline::Readline::default()
114+
};
115+
116+
list.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)
117+
.map_err(|e| {
118+
Error::new(
119+
list.span(),
120+
format!(
121+
"Support readline(key=value, ...) but got {}, caused error: {}",
122+
list.tokens, e
123+
),
124+
)
125+
})?
126+
.into_iter()
127+
.for_each(
128+
|entry| match entry.path.get_ident().unwrap().to_string().as_str() {
129+
"prefix" => {
130+
let expr = entry.value;
131+
ret = quote! {
132+
#ret
133+
.prefix(format!("{} ", #expr))
134+
};
135+
}
136+
"mask" => {
137+
let expr = entry.value;
138+
ret = quote! {
139+
#ret
140+
.mask(#expr)
141+
};
142+
}
143+
"prefix_style" => {
144+
let expr = entry.value;
145+
ret = quote! {
146+
#ret
147+
.prefix_style(#expr)
148+
};
149+
}
150+
"active_char_style" => {
151+
let expr = entry.value;
152+
ret = quote! {
153+
#ret
154+
.active_char_style(#expr)
155+
};
156+
}
157+
"inactive_char_style" => {
158+
let expr = entry.value;
159+
ret = quote! {
160+
#ret
161+
.inactive_char_style(#expr)
162+
};
163+
}
164+
_ => (),
165+
},
166+
);
167+
168+
ret = quote! {
169+
#ret
170+
.prompt()?
171+
.run()
172+
};
173+
174+
Ok(ret)
175+
}

0 commit comments

Comments
 (0)