-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
282 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,6 @@ | ||
[package] | ||
name = "promkit" | ||
version = "0.4.0" | ||
authors = ["ynqa <[email protected]>"] | ||
edition = "2021" | ||
description = "A toolkit for building your own interactive command-line tools" | ||
repository = "https://github.com/ynqa/promkit" | ||
license = "MIT" | ||
readme = "README.md" | ||
|
||
[lib] | ||
name = "promkit" | ||
path = "src/lib.rs" | ||
|
||
[dependencies] | ||
crossterm = { version = "0.27.0", features = ["use-dev-tty"] } | ||
indexmap = "2.2.3" | ||
radix_trie = "0.2.1" | ||
serde = { version = "1.0.197" } | ||
serde_json = { version = "1.0.114", features = ["preserve_order"] } | ||
thiserror = "1.0.50" | ||
unicode-width = "0.1.8" | ||
|
||
[dev-dependencies] | ||
strip-ansi-escapes = "0.2.0" | ||
[workspace] | ||
resolver = "2" | ||
members = [ | ||
"promkit", | ||
"promkit-derive", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "promkit-derive" | ||
version = "0.1.0" | ||
authors = ["ynqa <[email protected]>"] | ||
edition = "2021" | ||
description = "A derive macro for promkit" | ||
repository = "https://github.com/ynqa/promkit" | ||
license = "MIT" | ||
readme = "README.md" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
syn = { version = "2.0.52", features = ["full"] } | ||
quote = "1.0" | ||
proc-macro2 = "1.0" | ||
promkit = { path = "../promkit", version = "0.4.0" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
use promkit::{crossterm::style::Color, style::StyleBuilder, Result}; | ||
use promkit_derive::Promkit; | ||
|
||
#[derive(Default, Debug, Promkit)] | ||
struct Profile { | ||
#[readline( | ||
prefix = "What is your name?", | ||
prefix_style = StyleBuilder::new().fgc(Color::DarkCyan).build(), | ||
)] | ||
name: String, | ||
|
||
#[readline(default)] | ||
hobby: Option<String>, | ||
|
||
#[readline(prefix = "How old are you?", ignore_invalid_attr = "nothing")] | ||
age: usize, | ||
} | ||
|
||
fn main() -> Result { | ||
let mut ret = Profile::default(); | ||
ret.readline_name()?; | ||
ret.readline_hobby()?; | ||
ret.readline_age()?; | ||
dbg!(ret); | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
extern crate proc_macro; | ||
|
||
use proc_macro2::TokenStream; | ||
use quote::quote; | ||
use syn::{parse::Error, parse_macro_input, spanned::Spanned, DeriveInput}; | ||
|
||
#[proc_macro_derive(Promkit, attributes(readline))] | ||
pub fn promkit_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { | ||
let ast = parse_macro_input!(input as DeriveInput); | ||
match impl_promkit_derive(&ast) { | ||
Ok(token) => token.into(), | ||
Err(e) => e.to_compile_error().into(), | ||
} | ||
} | ||
|
||
mod readline; | ||
|
||
fn impl_promkit_derive(ast: &DeriveInput) -> Result<TokenStream, Error> { | ||
let fields = match &ast.data { | ||
syn::Data::Struct(s) => match &s.fields { | ||
syn::Fields::Named(fields) => &fields.named, | ||
// tuple struct is like `struct Point(f32, f32);` | ||
syn::Fields::Unnamed(_) => { | ||
return Err(Error::new(ast.span(), "Not support tuple structs")) | ||
} | ||
// unit struct is like `struct Marker;` | ||
syn::Fields::Unit => return Err(Error::new(ast.span(), "Not support unit structs")), | ||
}, | ||
syn::Data::Enum(_) => return Err(Error::new(ast.span(), "Not support enums")), | ||
syn::Data::Union(_) => return Err(Error::new(ast.span(), "Not support unions")), | ||
}; | ||
|
||
let mut fns = quote! {}; | ||
|
||
for field in fields.iter() { | ||
for attr in field.attrs.iter() { | ||
#[allow(clippy::single_match)] | ||
match attr.path().get_ident().unwrap().to_string().as_str() { | ||
"readline" => { | ||
let expr = readline::impl_promkit_per_field(field, attr)?; | ||
fns = quote! { | ||
#fns | ||
#expr | ||
}; | ||
} | ||
_ => (), | ||
} | ||
} | ||
} | ||
|
||
let name = &ast.ident; | ||
Ok(quote! { | ||
impl #name { | ||
#fns | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
use proc_macro2::TokenStream; | ||
use quote::{quote, ToTokens}; | ||
use syn::{ | ||
parse::Error, punctuated::Punctuated, spanned::Spanned, Meta, MetaList, MetaNameValue, Token, | ||
}; | ||
|
||
pub fn impl_promkit_per_field( | ||
field: &syn::Field, | ||
attr: &syn::Attribute, | ||
) -> Result<TokenStream, Error> { | ||
let readline_preset: TokenStream = match &attr.meta { | ||
Meta::List(list) => { | ||
let results = [parse_default_meta(list), parse_kvs_meta(list)]; | ||
let errors: Vec<Error> = results | ||
.iter() | ||
.filter_map(|r| r.as_ref().err().cloned()) | ||
.collect(); | ||
|
||
if errors.len() == results.len() { | ||
let error_messages = errors | ||
.iter() | ||
.map(|e| e.to_string()) | ||
.collect::<Vec<_>>() | ||
.join(", "); | ||
Err(Error::new( | ||
list.span(), | ||
format!("Errors: {}", error_messages), | ||
)) | ||
} else { | ||
results | ||
.into_iter() | ||
.find_map(Result::ok) | ||
.ok_or_else(|| Error::new(list.span(), "Unexpected error")) | ||
} | ||
}?, | ||
others => { | ||
return Err(Error::new( | ||
others.span(), | ||
format!( | ||
"Support only readline(default), or readline(key=value, ...), but got {}", | ||
others.to_token_stream() | ||
), | ||
)) | ||
} | ||
}; | ||
|
||
let field_ident = field.ident.as_ref().unwrap(); | ||
let preset_fn = syn::Ident::new(&format!("readline_{}", field_ident), field_ident.span()); | ||
|
||
match &field.ty { | ||
syn::Type::Path(typ) => { | ||
let last_segment = typ.path.segments.last().unwrap(); | ||
match last_segment.ident.to_string().as_str() { | ||
"Option" => { | ||
if let syn::PathArguments::AngleBracketed(args) = &last_segment.arguments { | ||
if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first() { | ||
return Ok(quote! { | ||
pub fn #preset_fn(&mut self) -> promkit::Result { | ||
let value_str = #readline_preset?; | ||
let parsed_value = value_str.parse::<#inner_type>() | ||
.map_or_else(|_| None, Some); | ||
self.#field_ident = parsed_value; | ||
Ok(()) | ||
} | ||
}); | ||
} | ||
} | ||
Err(Error::new( | ||
last_segment.span(), | ||
format!("Support Option<T> but got {}", typ.to_token_stream()), | ||
)) | ||
} | ||
_ => { | ||
let ty = typ.to_token_stream(); | ||
Ok(quote! { | ||
pub fn #preset_fn(&mut self) -> promkit::Result { | ||
let value_str = #readline_preset?; | ||
let parsed_value = value_str.parse::<#ty>() | ||
.map_err(|e| promkit::Error::ParseError(e.to_string()))?; | ||
self.#field_ident = parsed_value; | ||
Ok(()) | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
ty => Err(Error::new( | ||
ty.span(), | ||
format!( | ||
"Support only Path for field type but got {}", | ||
ty.to_token_stream() | ||
), | ||
)), | ||
} | ||
} | ||
|
||
fn parse_default_meta(list: &MetaList) -> Result<TokenStream, Error> { | ||
match list.tokens.to_string().as_str() { | ||
"default" => Ok(quote! { | ||
promkit::preset::readline::Readline::default() | ||
.prompt()? | ||
.run() | ||
}), | ||
others => Err(Error::new( | ||
list.span(), | ||
format!("Support readline(default) but got {}", others), | ||
)), | ||
} | ||
} | ||
|
||
fn parse_kvs_meta(list: &MetaList) -> Result<TokenStream, Error> { | ||
let mut ret = quote! { | ||
promkit::preset::readline::Readline::default() | ||
}; | ||
|
||
list.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated) | ||
.map_err(|e| { | ||
Error::new( | ||
list.span(), | ||
format!( | ||
"Support readline(key=value, ...) but got {}, caused error: {}", | ||
list.tokens, e | ||
), | ||
) | ||
})? | ||
.into_iter() | ||
.for_each( | ||
|entry| match entry.path.get_ident().unwrap().to_string().as_str() { | ||
"prefix" => { | ||
let expr = entry.value; | ||
ret = quote! { | ||
#ret | ||
.prefix(format!("{} ", #expr)) | ||
}; | ||
} | ||
"mask" => { | ||
let expr = entry.value; | ||
ret = quote! { | ||
#ret | ||
.mask(#expr) | ||
}; | ||
} | ||
"prefix_style" => { | ||
let expr = entry.value; | ||
ret = quote! { | ||
#ret | ||
.prefix_style(#expr) | ||
}; | ||
} | ||
"active_char_style" => { | ||
let expr = entry.value; | ||
ret = quote! { | ||
#ret | ||
.active_char_style(#expr) | ||
}; | ||
} | ||
"inactive_char_style" => { | ||
let expr = entry.value; | ||
ret = quote! { | ||
#ret | ||
.inactive_char_style(#expr) | ||
}; | ||
} | ||
_ => (), | ||
}, | ||
); | ||
|
||
ret = quote! { | ||
#ret | ||
.prompt()? | ||
.run() | ||
}; | ||
|
||
Ok(ret) | ||
} |