Skip to content

Commit 3ee3649

Browse files
authored
Merge pull request #17 from ynqa/dev-0.4.0/promkit-derive
v0.1.0: `promkit-derive`
2 parents dded8f2 + c6a9593 commit 3ee3649

File tree

5 files changed

+250
-0
lines changed

5 files changed

+250
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
resolver = "2"
33
members = [
44
"promkit",
5+
"promkit-derive",
56
]

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.5.1" }

promkit-derive/examples/derive.rs

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

promkit-derive/src/lib.rs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
extern crate proc_macro;
2+
3+
use proc_macro2::TokenStream;
4+
use quote::{quote, ToTokens};
5+
use syn::{parse::Error, parse_macro_input, spanned::Spanned, DeriveInput};
6+
7+
#[proc_macro_derive(Promkit, attributes(form))]
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 text_editor;
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+
syn::Fields::Unnamed(_) => {
23+
return Err(Error::new(ast.span(), "Not support tuple structs"))
24+
}
25+
syn::Fields::Unit => return Err(Error::new(ast.span(), "Not support unit structs")),
26+
},
27+
syn::Data::Enum(_) => return Err(Error::new(ast.span(), "Not support enums")),
28+
syn::Data::Union(_) => return Err(Error::new(ast.span(), "Not support unions")),
29+
};
30+
31+
let mut text_editor_states = Vec::new();
32+
let mut field_assignments = Vec::new();
33+
let mut field_types = Vec::new();
34+
35+
for (idx, field) in fields.iter().enumerate() {
36+
for attr in field.attrs.iter() {
37+
#[allow(clippy::single_match)]
38+
match attr.path().get_ident().unwrap().to_string().as_str() {
39+
"form" => {
40+
let state = text_editor::create_state(attr)?;
41+
text_editor_states.push(state);
42+
43+
let field_ident = field.ident.as_ref().unwrap();
44+
let idx_lit = syn::Index::from(idx);
45+
46+
match &field.ty {
47+
syn::Type::Path(typ) => {
48+
let last_segment = typ.path.segments.last().unwrap();
49+
match last_segment.ident.to_string().as_str() {
50+
"Option" => {
51+
if let syn::PathArguments::AngleBracketed(args) =
52+
&last_segment.arguments
53+
{
54+
if let Some(syn::GenericArgument::Type(inner_type)) =
55+
args.args.first()
56+
{
57+
field_assignments.push(quote! {
58+
self.#field_ident = results[#idx_lit].parse::<#inner_type>().ok();
59+
});
60+
field_types.push(quote! { Option<#inner_type> });
61+
}
62+
}
63+
}
64+
_ => {
65+
let ty = &field.ty;
66+
field_assignments.push(quote! {
67+
self.#field_ident = results[#idx_lit].parse::<#ty>()?;
68+
});
69+
field_types.push(quote! { #ty });
70+
}
71+
}
72+
}
73+
ty => {
74+
return Err(Error::new(
75+
ty.span(),
76+
format!(
77+
"Support only Path for field type but got {}",
78+
ty.to_token_stream(),
79+
),
80+
))
81+
}
82+
}
83+
}
84+
_ => (),
85+
}
86+
}
87+
}
88+
89+
let name = &ast.ident;
90+
let combined_states = quote! {
91+
vec![
92+
#(#text_editor_states),*
93+
]
94+
};
95+
96+
Ok(quote! {
97+
impl #name {
98+
pub fn build(&mut self) -> Result<(), Box<dyn std::error::Error>> {
99+
let states = #combined_states;
100+
let mut form = promkit::preset::form::Form::new(states);
101+
let results = form.prompt()?.run()?;
102+
103+
#(#field_assignments)*
104+
105+
Ok(())
106+
}
107+
}
108+
})
109+
}

promkit-derive/src/text_editor.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{quote, ToTokens};
3+
use syn::{parse::Error, punctuated::Punctuated, spanned::Spanned, Meta, MetaNameValue, Token};
4+
5+
pub fn create_state(attr: &syn::Attribute) -> Result<TokenStream, Error> {
6+
let mut prefix = quote! { String::from("❯❯ ") };
7+
let mut prefix_style = quote! {
8+
promkit::style::StyleBuilder::new().attrs(
9+
promkit::crossterm::style::Attributes::from(
10+
promkit::crossterm::style::Attribute::Bold,
11+
)
12+
).build()
13+
};
14+
let mut active_char_style = quote! {
15+
promkit::style::StyleBuilder::new().bgc(promkit::crossterm::style::Color::DarkCyan).build()
16+
};
17+
let mut inactive_char_style = quote! {
18+
promkit::style::StyleBuilder::new().build()
19+
};
20+
let mut mask = quote! { None::<char> };
21+
let mut edit_mode = quote! { promkit::text_editor::Mode::default() };
22+
let mut word_break_chars = quote! { std::collections::HashSet::from([' ']) };
23+
24+
match &attr.meta {
25+
Meta::List(list) => {
26+
if list.tokens.to_string() != "default" {
27+
list.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)
28+
.map_err(|e| {
29+
Error::new(
30+
list.span(),
31+
format!(
32+
"Support form(key=value, ...) but got {}, caused error: {}",
33+
list.tokens, e
34+
),
35+
)
36+
})?
37+
.into_iter()
38+
.for_each(
39+
|entry| match entry.path.get_ident().unwrap().to_string().as_str() {
40+
"label" => {
41+
let expr = entry.value;
42+
prefix = quote! { format!("{} ", #expr) };
43+
}
44+
"label_style" => {
45+
let expr = entry.value;
46+
prefix_style = quote! { #expr };
47+
}
48+
"active_char_style" => {
49+
let expr = entry.value;
50+
active_char_style = quote! { #expr };
51+
}
52+
"inactive_char_style" => {
53+
let expr = entry.value;
54+
inactive_char_style = quote! { #expr };
55+
}
56+
"mask" => {
57+
let expr = entry.value;
58+
mask = quote! { #expr };
59+
}
60+
"edit_mode" => {
61+
let expr = entry.value;
62+
edit_mode = quote! { #expr };
63+
}
64+
"word_break_chars" => {
65+
let expr = entry.value;
66+
word_break_chars = quote! { #expr };
67+
}
68+
_ => (),
69+
},
70+
);
71+
}
72+
}
73+
others => {
74+
return Err(Error::new(
75+
others.span(),
76+
format!(
77+
"Support only form, form(default), or form(key=value, ...), but got {}",
78+
others.to_token_stream()
79+
),
80+
))
81+
}
82+
};
83+
84+
Ok(quote! {
85+
promkit::text_editor::State {
86+
texteditor: Default::default(),
87+
history: Default::default(),
88+
prefix: #prefix,
89+
prefix_style: #prefix_style,
90+
active_char_style: #active_char_style,
91+
inactive_char_style: #inactive_char_style,
92+
mask: #mask,
93+
edit_mode: #edit_mode,
94+
word_break_chars: #word_break_chars,
95+
lines: Default::default()
96+
}
97+
})
98+
}

0 commit comments

Comments
 (0)