Repository: google/argh
Pull request #142 (2022-09-20 22:39:45)

Support generic structs and enums in derive(FromArgs)
This removes the restriction against deriving a struct with FromArgs that takes generic arguments, and adds a test to make sure it works correctly. There isn't really a good reason for this constraint as far as I can tell, and it's surprising if you run into it.


Patch:
diff --git a/argh/src/lib.rs b/argh/src/lib.rs
index 49ab386..7ca94a3 100644
--- a/argh/src/lib.rs
+++ b/argh/src/lib.rs
@@ -810,7 +810,7 @@ pub fn parse_struct_args(
     let mut positional_index = 0;
     let mut options_ended = false;
 
-    'parse_args: while let Some(&next_arg) = remaining_args.get(0) {
+    'parse_args: while let Some(&next_arg) = remaining_args.first() {
         remaining_args = &remaining_args[1..];
         if (next_arg == "--help" || next_arg == "help") && !options_ended {
             help = true;
@@ -878,7 +878,7 @@ impl<'a> ParseStructOptions<'a> {
             ParseStructOption::Flag(ref mut b) => b.set_flag(arg),
             ParseStructOption::Value(ref mut pvs) => {
                 let value = remaining_args
-                    .get(0)
+                    .first()
                     .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?;
                 *remaining_args = &remaining_args[1..];
                 pvs.fill_slot(arg, value).map_err(|s| {
@@ -977,6 +977,7 @@ pub struct ParseStructSubCommand<'a> {
     pub dynamic_subcommands: &'a [&'static CommandInfo],
 
     // The function to parse the subcommand arguments.
+    #[allow(clippy::type_complexity)]
     pub parse_func: &'a mut dyn FnMut(&[&str], &[&str]) -> Result<(), EarlyExit>,
 }
 
diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs
index 7dc5f32..0a190f5 100644
--- a/argh_derive/src/lib.rs
+++ b/argh_derive/src/lib.rs
@@ -36,16 +36,14 @@ pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 /// as well as all errors that occurred.
 fn impl_from_args(input: &syn::DeriveInput) -> TokenStream {
     let errors = &Errors::default();
-    if !input.generics.params.is_empty() {
-        errors.err(
-            &input.generics,
-            "`#![derive(FromArgs)]` cannot be applied to types with generic parameters",
-        );
-    }
     let type_attrs = &TypeAttrs::parse(errors, input);
     let mut output_tokens = match &input.data {
-        syn::Data::Struct(ds) => impl_from_args_struct(errors, &input.ident, type_attrs, ds),
-        syn::Data::Enum(de) => impl_from_args_enum(errors, &input.ident, type_attrs, de),
+        syn::Data::Struct(ds) => {
+            impl_from_args_struct(errors, &input.ident, type_attrs, &input.generics, ds)
+        }
+        syn::Data::Enum(de) => {
+            impl_from_args_enum(errors, &input.ident, type_attrs, &input.generics, de)
+        }
         syn::Data::Union(_) => {
             errors.err(input, "`#[derive(FromArgs)]` cannot be applied to unions");
             TokenStream::new()
@@ -208,6 +206,7 @@ fn impl_from_args_struct(
     errors: &Errors,
     name: &syn::Ident,
     type_attrs: &TypeAttrs,
+    generic_args: &syn::Generics,
     ds: &syn::DataStruct,
 ) -> TokenStream {
     let fields = match &ds.fields {
@@ -244,11 +243,12 @@ fn impl_from_args_struct(
     let redact_arg_values_method =
         impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields);
 
-    let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs);
+    let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs, generic_args);
 
+    let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
     let trait_impl = quote_spanned! { impl_span =>
         #[automatically_derived]
-        impl argh::FromArgs for #name {
+        impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause {
             #from_args_method
 
             #redact_arg_values_method
@@ -541,14 +541,20 @@ fn ensure_unique_names(errors: &Errors, fields: &[StructField<'_>]) {
 }
 
 /// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate.
-fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs) -> TokenStream {
+fn top_or_sub_cmd_impl(
+    errors: &Errors,
+    name: &syn::Ident,
+    type_attrs: &TypeAttrs,
+    generic_args: &syn::Generics,
+) -> TokenStream {
     let description =
         help::require_description(errors, name.span(), &type_attrs.description, "type");
+    let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
     if type_attrs.is_subcommand.is_none() {
         // Not a subcommand
         quote! {
             #[automatically_derived]
-            impl argh::TopLevelCommand for #name {}
+            impl #impl_generics argh::TopLevelCommand for #name #ty_generics #where_clause {}
         }
     } else {
         let empty_str = syn::LitStr::new("", Span::call_site());
@@ -558,7 +564,7 @@ fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttr
         });
         quote! {
             #[automatically_derived]
-            impl argh::SubCommand for #name {
+            impl #impl_generics argh::SubCommand for #name #ty_generics #where_clause {
                 const COMMAND: &'static argh::CommandInfo = &argh::CommandInfo {
                     name: #subcommand_name,
                     description: #description,
@@ -879,6 +885,7 @@ fn impl_from_args_enum(
     errors: &Errors,
     name: &syn::Ident,
     type_attrs: &TypeAttrs,
+    generic_args: &syn::Generics,
     de: &syn::DataEnum,
 ) -> TokenStream {
     parse_attrs::check_enum_type_attrs(errors, type_attrs, &de.enum_token.span);
@@ -937,8 +944,9 @@ fn impl_from_args_enum(
         }
     });
 
+    let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
     quote! {
-        impl argh::FromArgs for #name {
+        impl #impl_generics argh::FromArgs for #name #ty_generics #where_clause {
             fn from_args(command_name: &[&str], args: &[&str])
                 -> std::result::Result<Self, argh::EarlyExit>
             {
@@ -980,7 +988,7 @@ fn impl_from_args_enum(
             }
         }
 
-        impl argh::SubCommands for #name {
+        impl #impl_generics argh::SubCommands for #name #ty_generics #where_clause {
             const COMMANDS: &'static [&'static argh::CommandInfo] = &[#(
                 <#variant_ty as argh::SubCommand>::COMMAND,
             )*];


Test patch:
diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs
index 9005a09..b3bc5ed 100644
--- a/argh/tests/lib.rs
+++ b/argh/tests/lib.rs
@@ -35,6 +35,34 @@ fn basic_example() {
     assert_eq!(up, GoUp { jump: false, height: 5, pilot_nickname: None });
 }
 
+#[test]
+fn generic_example() {
+    use std::fmt::Display;
+    use std::str::FromStr;
+
+    #[derive(FromArgs, PartialEq, Debug)]
+    /// Reach new heights.
+    struct GoUp<S: FromStr>
+    where
+        <S as FromStr>::Err: Display,
+    {
+        /// whether or not to jump
+        #[argh(switch, short = 'j')]
+        jump: bool,
+
+        /// how high to go
+        #[argh(option)]
+        height: usize,
+
+        /// an optional nickname for the pilot
+        #[argh(option)]
+        pilot_nickname: Option<S>,
+    }
+
+    let up = GoUp::<String>::from_args(&["cmdname"], &["--height", "5"]).expect("failed go_up");
+    assert_eq!(up, GoUp::<String> { jump: false, height: 5, pilot_nickname: None });
+}
+
 #[test]
 fn custom_from_str_example() {
     #[derive(FromArgs)]
@@ -128,7 +156,7 @@ fn dynamic_subcommand_example() {
             let description = Self::commands().iter().find(|x| x.name == command_name)?.description;
             if args.len() > 1 {
                 Some(Err(argh::EarlyExit::from("Too many arguments".to_owned())))
-            } else if let Some(arg) = args.get(0) {
+            } else if let Some(arg) = args.first() {
                 Some(Ok(DynamicSubCommandImpl { got: format!("{} got {:?}", description, arg) }))
             } else {
                 Some(Err(argh::EarlyExit::from("Not enough arguments".to_owned())))
@@ -957,7 +985,7 @@ Options:
                 None
             } else if args.len() > 1 {
                 Some(Err(argh::EarlyExit::from("Too many arguments".to_owned())))
-            } else if let Some(arg) = args.get(0) {
+            } else if let Some(arg) = args.first() {
                 Some(Ok(HelpExamplePlugin { got: format!("plugin got {:?}", arg) }))
             } else {
                 Some(Ok(HelpExamplePlugin { got: "plugin got no argument".to_owned() }))