Skip to content

Commit

Permalink
init promkit-derive crate
Browse files Browse the repository at this point in the history
  • Loading branch information
ynqa committed Mar 19, 2024
1 parent ff05682 commit 5b43b71
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 25 deletions.
31 changes: 6 additions & 25 deletions Cargo.toml
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",
]
18 changes: 18 additions & 0 deletions promkit-derive/Cargo.toml
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" }
26 changes: 26 additions & 0 deletions promkit-derive/examples/example.rs
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(())
}
57 changes: 57 additions & 0 deletions promkit-derive/src/lib.rs
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
}
})
}
175 changes: 175 additions & 0 deletions promkit-derive/src/readline.rs
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)
}

0 comments on commit 5b43b71

Please sign in to comment.