Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.1.0: promkit-derive #17

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
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 <un.pensiero.vano@gmail.com>"]
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.5.1" }
24 changes: 24 additions & 0 deletions promkit-derive/examples/derive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use promkit::{crossterm::style::Color, style::StyleBuilder};
use promkit_derive::Promkit;

#[derive(Default, Debug, Promkit)]
struct Profile {
#[form(
label = "What is your name?",
label_style = StyleBuilder::new().fgc(Color::DarkCyan).build(),
)]
name: String,

#[form(default)]
hobby: Option<String>,

#[form(label = "How old are you?", ignore_invalid_attr = "nothing")]
age: usize,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut ret = Profile::default();
ret.build()?;
dbg!(ret);
Ok(())
}
109 changes: 109 additions & 0 deletions promkit-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
extern crate proc_macro;

use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Error, parse_macro_input, spanned::Spanned, DeriveInput};

#[proc_macro_derive(Promkit, attributes(form))]
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 text_editor;

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,
syn::Fields::Unnamed(_) => {
return Err(Error::new(ast.span(), "Not support tuple structs"))
}
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 text_editor_states = Vec::new();
let mut field_assignments = Vec::new();
let mut field_types = Vec::new();

for (idx, field) in fields.iter().enumerate() {
for attr in field.attrs.iter() {
#[allow(clippy::single_match)]
match attr.path().get_ident().unwrap().to_string().as_str() {
"form" => {
let state = text_editor::create_state(attr)?;
text_editor_states.push(state);

let field_ident = field.ident.as_ref().unwrap();
let idx_lit = syn::Index::from(idx);

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()
{
field_assignments.push(quote! {
self.#field_ident = results[#idx_lit].parse::<#inner_type>().ok();
});
field_types.push(quote! { Option<#inner_type> });
}
}
}
_ => {
let ty = &field.ty;
field_assignments.push(quote! {
self.#field_ident = results[#idx_lit].parse::<#ty>()?;
});
field_types.push(quote! { #ty });
}
}
}
ty => {
return Err(Error::new(
ty.span(),
format!(
"Support only Path for field type but got {}",
ty.to_token_stream(),
),
))
}
}
}
_ => (),
}
}
}

let name = &ast.ident;
let combined_states = quote! {
vec![
#(#text_editor_states),*
]
};

Ok(quote! {
impl #name {
pub fn build(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let states = #combined_states;
let mut form = promkit::preset::form::Form::new(states);
let results = form.prompt()?.run()?;

#(#field_assignments)*

Ok(())
}
}
})
}
98 changes: 98 additions & 0 deletions promkit-derive/src/text_editor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Error, punctuated::Punctuated, spanned::Spanned, Meta, MetaNameValue, Token};

pub fn create_state(attr: &syn::Attribute) -> Result<TokenStream, Error> {
let mut prefix = quote! { String::from("❯❯ ") };
let mut prefix_style = quote! {
promkit::style::StyleBuilder::new().attrs(
promkit::crossterm::style::Attributes::from(
promkit::crossterm::style::Attribute::Bold,
)
).build()
};
let mut active_char_style = quote! {
promkit::style::StyleBuilder::new().bgc(promkit::crossterm::style::Color::DarkCyan).build()
};
let mut inactive_char_style = quote! {
promkit::style::StyleBuilder::new().build()
};
let mut mask = quote! { None::<char> };
let mut edit_mode = quote! { promkit::text_editor::Mode::default() };
let mut word_break_chars = quote! { std::collections::HashSet::from([' ']) };

match &attr.meta {
Meta::List(list) => {
if list.tokens.to_string() != "default" {
list.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)
.map_err(|e| {
Error::new(
list.span(),
format!(
"Support form(key=value, ...) but got {}, caused error: {}",
list.tokens, e
),
)
})?
.into_iter()
.for_each(
|entry| match entry.path.get_ident().unwrap().to_string().as_str() {
"label" => {
let expr = entry.value;
prefix = quote! { format!("{} ", #expr) };
}
"label_style" => {
let expr = entry.value;
prefix_style = quote! { #expr };
}
"active_char_style" => {
let expr = entry.value;
active_char_style = quote! { #expr };
}
"inactive_char_style" => {
let expr = entry.value;
inactive_char_style = quote! { #expr };
}
"mask" => {
let expr = entry.value;
mask = quote! { #expr };
}
"edit_mode" => {
let expr = entry.value;
edit_mode = quote! { #expr };
}
"word_break_chars" => {
let expr = entry.value;
word_break_chars = quote! { #expr };
}
_ => (),
},
);
}
}
others => {
return Err(Error::new(
others.span(),
format!(
"Support only form, form(default), or form(key=value, ...), but got {}",
others.to_token_stream()
),
))
}
};

Ok(quote! {
promkit::text_editor::State {
texteditor: Default::default(),
history: Default::default(),
prefix: #prefix,
prefix_style: #prefix_style,
active_char_style: #active_char_style,
inactive_char_style: #inactive_char_style,
mask: #mask,
edit_mode: #edit_mode,
word_break_chars: #word_break_chars,
lines: Default::default()
}
})
}
Loading