aboutsummaryrefslogtreecommitdiffstats
path: root/pish_derive/src/lib.rs
diff options
context:
space:
mode:
authorJonas Maier <>2026-03-08 08:28:57 +0100
committerJonas Maier <>2026-03-08 08:28:57 +0100
commitdeafab9c930ab092c4ee8abd4cbe09c7eb34aa22 (patch)
tree8cd3b19f41eb37d7fef0ad45e707020b0e9a0fc6 /pish_derive/src/lib.rs
parent46a1a298af63c82e48fd0aa805f32c691eb7ff97 (diff)
downloadpish-deafab9c930ab092c4ee8abd4cbe09c7eb34aa22.tar.gz
argument parsing
Diffstat (limited to 'pish_derive/src/lib.rs')
-rw-r--r--pish_derive/src/lib.rs96
1 files changed, 96 insertions, 0 deletions
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)
+}