diff options
Diffstat (limited to 'pish_derive')
| -rw-r--r-- | pish_derive/.gitignore | 1 | ||||
| -rw-r--r-- | pish_derive/Cargo.lock | 47 | ||||
| -rw-r--r-- | pish_derive/Cargo.toml | 12 | ||||
| -rw-r--r-- | pish_derive/src/lib.rs | 96 |
4 files changed, 156 insertions, 0 deletions
diff --git a/pish_derive/.gitignore b/pish_derive/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/pish_derive/.gitignore @@ -0,0 +1 @@ +/target
\ No newline at end of file diff --git a/pish_derive/Cargo.lock b/pish_derive/Cargo.lock new file mode 100644 index 0000000..af838d6 --- /dev/null +++ b/pish_derive/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "pish_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/pish_derive/Cargo.toml b/pish_derive/Cargo.toml new file mode 100644 index 0000000..11fea7e --- /dev/null +++ b/pish_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pish_derive" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.106" +quote = "1.0.45" +syn = "2.0.117" diff --git a/pish_derive/src/lib.rs b/pish_derive/src/lib.rs new file mode 100644 index 0000000..2ac98f7 --- /dev/null +++ b/pish_derive/src/lib.rs @@ -0,0 +1,96 @@ +use proc_macro::{Literal, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; + +#[proc_macro_derive(FromArgs)] +pub fn derive_cli(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + + let fields = match input.data { + Data::Struct(data) => data.fields, + _ => panic!("Cli can only be derived for structs"), + }; + + let mut field_parsers_init = Vec::new(); + let mut field_parsers = Vec::new(); + let mut field_parsers_post = Vec::new(); + let mut field_names = Vec::new(); + + if let Fields::Named(fields_named) = fields { + for field in fields_named.named { + let ident = field.ident.unwrap(); + let name_str = ident.to_string(); + + let mut long_name = b"--".to_vec(); + long_name.extend_from_slice(name_str.as_bytes()); + let long_name = proc_macro2::Literal::byte_string(&long_name); + + field_names.push(ident.clone()); + + // initialization + field_parsers_init.push(quote! { + let mut #ident = None; + }); + + let is_bool = field.ty.to_token_stream().to_string() == String::from("bool"); + let is_option = field.ty.to_token_stream().to_string().starts_with("Option"); // bad bad detection + + // in the loop + if is_bool { + field_parsers.push(quote! { + if arg == #long_name { + #ident = Some(true); + continue; + } + }); + } else { + field_parsers.push(quote! { + if arg == #long_name { + let Some(val) = iter.next() else { + return Err(ArgParseError::MissingArgValue(#name_str)); + }; + match String::from_utf8_lossy(val).parse() { + Ok(parsed) => { + #ident = Some(parsed); + continue; + } + Err(err) => { + return Err(ArgParseError::ArgValueParseError(#name_str, format!("{err:?}"))); + } + } + } + }); + } + + // after loop + if is_bool { + field_parsers_post.push(quote!{ let #ident = #ident.unwrap_or(false); }); + } else if !is_option { + field_parsers_post.push(quote!{ let Some(#ident) = #ident else { return Err(ArgParseError::MissingArg(#name_str)) }; }); + } + } + } + + let expanded = quote! { + impl ArgParse for #name { + fn parse<'a>(args: &'a [BString]) -> std::result::Result<Self, ArgParseError<'a>> { + let mut iter = args.iter().skip(1); + + #(#field_parsers_init)* + + while let Some(arg) = iter.next() { + #(#field_parsers)*; + return Err(ArgParseError::LeftoverArg(arg)); + } + + #(#field_parsers_post)* + + Ok(Self { #( #field_names ),* }) + } + } + }; + + TokenStream::from(expanded) +} |
