Repository: pacak/bpaf
Pull request #176 (2023-03-03 15:55:29)

derive boxed and anywhere
Plus some documentation improvements


Patch:
diff --git a/Changelog.md b/Changelog.md
index 2a7d74f..05d4f23 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,11 @@
 # Change Log
 
+## bpaf [0.7.10], bpaf_derive [0.3.5] - unreleased
+- improve error messages for typos like `-llvm` instead of `--llvm`
+- improve error messages when a flag is accepted by a command but not directly
+- allow to derive position bool
+- derive anywhere and boxed
+
 ## bpaf [0.7.9], bpaf_derive [0.3.4] - 2023-02-14
 - `ParseFailure::exit_code`
 - A way to specify custom usage in derive macro
diff --git a/bpaf_derive/src/field/named_field.rs b/bpaf_derive/src/field/named_field.rs
index 090de16..2657cd1 100644
--- a/bpaf_derive/src/field/named_field.rs
+++ b/bpaf_derive/src/field/named_field.rs
@@ -380,13 +380,13 @@ impl Field {
                 ty
             }
             Shape::Bool => {
-                if self.naming.is_empty() {
-                    return Err(syn::Error::new(
-                        self.ty.span(),
-                        "Can't derive consumer for unnamed boolean field",
-                    ));
-                }
                 if self.consumer.is_none() {
+                    if self.naming.is_empty() {
+                        return Err(syn::Error::new(
+                            self.ty.span(),
+                            "Can't derive consumer for unnamed boolean field",
+                        ));
+                    }
                     self.consumer = Some(ConsumerAttr::Switch);
                 }
                 return Ok(());
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index da01ae5..42f2640 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -41,7 +41,13 @@ enum BParser {
     Command(CommandAttr, Box<OParser>, bool),
     CargoHelper(LitStr, Box<BParser>),
     CompStyle(Box<Expr>, Box<BParser>),
-    Constructor(ConstrName, Fields, bool),
+    Constructor {
+        name: ConstrName,
+        fields: Fields,
+        adjacent: bool,
+        anywhere: bool,
+        boxed: bool,
+    },
     Singleton(Box<ReqFlag>),
     Fold(Vec<BParser>, Option<Box<Expr>>),
 }
@@ -236,6 +242,8 @@ struct Outer {
     shorts: Vec<LitChar>,
     fallback: Option<Box<Expr>>,
     adjacent: bool,
+    anywhere: bool,
+    boxed: bool,
 }
 
 impl Outer {
@@ -251,6 +259,8 @@ impl Outer {
             shorts: Vec::new(),
             fallback: None,
             adjacent: false,
+            anywhere: false,
+            boxed: false,
         };
 
         let mut usage = None;
@@ -289,6 +299,10 @@ impl Outer {
                         res.version = Some(Box::new(ver));
                     } else if keyword == "adjacent" {
                         res.adjacent = true;
+                    } else if keyword == "anywhere" {
+                        res.anywhere = true;
+                    } else if keyword == "boxed" {
+                        res.boxed = true;
                     } else if keyword == "command" {
                         let name = parse_opt_arg::<LitStr>(input)?.unwrap_or_else(|| {
                             let n = to_snake_case(&outer_ty.to_string());
@@ -406,7 +420,13 @@ impl Top {
             constr: outer_ty.clone(),
             fallback: outer.fallback.clone(),
         };
-        let inner = BParser::Constructor(constr, fields, outer.adjacent);
+        let inner = BParser::Constructor {
+            name: constr,
+            fields,
+            adjacent: outer.adjacent,
+            anywhere: outer.anywhere,
+            boxed: outer.boxed,
+        };
         Ok(Top {
             name: outer
                 .generate
@@ -442,9 +462,21 @@ impl Top {
 
             let branch = if enum_contents.peek(token::Paren) || enum_contents.peek(token::Brace) {
                 let fields = Fields::parse(&enum_contents)?;
-                BParser::Constructor(constr, fields, outer.adjacent)
+                BParser::Constructor {
+                    name: constr,
+                    fields,
+                    adjacent: outer.adjacent,
+                    anywhere: outer.anywhere,
+                    boxed: outer.boxed,
+                }
             } else if let Some(_cmd) = &inner.command {
-                BParser::Constructor(constr, Fields::NoFields, false)
+                BParser::Constructor {
+                    name: constr,
+                    fields: Fields::NoFields,
+                    adjacent: outer.adjacent,
+                    anywhere: outer.anywhere,
+                    boxed: outer.boxed,
+                }
             } else {
                 let req_flag = ReqFlag::make(constr, inner.clone());
                 BParser::Singleton(Box::new(req_flag))
@@ -569,25 +601,35 @@ impl ToTokens for BParser {
                 ::bpaf::cargo_helper(#name, #inner)
             })
             .to_tokens(tokens),
-            BParser::Constructor(con, Fields::NoFields, _adj) => {
-                quote!(::bpaf::pure(#con)).to_tokens(tokens);
-                if let Some(fallback) = &con.fallback {
-                    quote!(.fallback(#fallback)).to_tokens(tokens);
+            BParser::Constructor {
+                name,
+                fields,
+                adjacent,
+                anywhere,
+                boxed,
+            } => {
+                if let Fields::NoFields = fields {
+                    quote!(::bpaf::pure(#name)).to_tokens(tokens);
+                } else {
+                    let parse_decls = fields.parser_decls();
+                    quote!({
+                        #(#parse_decls)*
+                        ::bpaf::construct!(#name #fields)
+                    })
+                    .to_tokens(tokens);
                 }
-            }
-            BParser::Constructor(con, bra, adj) => {
-                let parse_decls = bra.parser_decls();
-                quote!({
-                    #(#parse_decls)*
-                    ::bpaf::construct!(#con #bra)
-                })
-                .to_tokens(tokens);
-                if *adj {
+                if *adjacent {
                     quote!(.adjacent()).to_tokens(tokens);
                 }
-                if let Some(fallback) = &con.fallback {
+                if let Some(fallback) = &name.fallback {
                     quote!(.fallback(#fallback)).to_tokens(tokens);
                 }
+                if *anywhere {
+                    quote!(.anywhere()).to_tokens(tokens);
+                }
+                if *boxed {
+                    quote!(.boxed()).to_tokens(tokens);
+                }
             }
             BParser::Fold(xs, mfallback) => {
                 if xs.len() == 1 {
diff --git a/docs/src/anywhere_1/cases.txt b/docs/src/anywhere_1/cases.txt
new file mode 100644
index 0000000..fbbb945
--- /dev/null
+++ b/docs/src/anywhere_1/cases.txt
@@ -0,0 +1,15 @@
+? It's possible to implement multi argument options by using required flag followed by one or
+? more positional items
+> --turbo --set name Bob
+OK
+Options { multi_arg: Some(MultiArg { set: (), name: "name", value: "Bob" }), turbo: true }
+
+? Other flags can go on either side of items
+> --set name Bob --turbo
+OK
+Options { multi_arg: Some(MultiArg { set: (), name: "name", value: "Bob" }), turbo: true }
+
+? But not in between, with potentially confusing error messages - to be fixed :)
+> --set name --turbo Bob
+Stderr
+--set is not expected in this context
diff --git a/docs/src/anywhere_1/combine.rs b/docs/src/anywhere_1/combine.rs
new file mode 100644
index 0000000..25bd16f
--- /dev/null
+++ b/docs/src/anywhere_1/combine.rs
@@ -0,0 +1,30 @@
+//
+use bpaf::*;
+#[derive(Debug, Clone)]
+//
+#[allow(dead_code)]
+pub struct Options {
+    multi_arg: Option<MultiArg>,
+    turbo: bool,
+}
+
+#[derive(Debug, Clone)]
+//
+#[allow(dead_code)]
+pub struct MultiArg {
+    set: (),
+    name: String,
+    value: String,
+}
+
+pub fn options() -> OptionParser<Options> {
+    let set = long("set").req_flag(());
+    let name = positional("ARG");
+    let value = positional("ARG");
+    let multi_arg = construct!(MultiArg { set, name, value })
+        .anywhere()
+        .optional();
+
+    let turbo = long("turbo").switch();
+    construct!(Options { multi_arg, turbo }).to_options()
+}
diff --git a/docs/src/anywhere_1/derive.rs b/docs/src/anywhere_1/derive.rs
new file mode 100644
index 0000000..67c51e1
--- /dev/null
+++ b/docs/src/anywhere_1/derive.rs
@@ -0,0 +1,24 @@
+//
+use bpaf::*;
+#[derive(Debug, Clone, Bpaf)]
+//
+#[allow(dead_code)]
+#[bpaf(options)]
+pub struct Options {
+    #[bpaf(external, optional)]
+    multi_arg: Option<MultiArg>,
+    turbo: bool,
+}
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(anywhere)]
+//
+#[allow(dead_code)]
+pub struct MultiArg {
+    #[bpaf(long)]
+    set: (),
+    #[bpaf(positional)]
+    name: String,
+    #[bpaf(positional)]
+    value: String,
+}
diff --git a/src/args.rs b/src/args.rs
index 712286e..c5eb86d 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -409,7 +409,7 @@ mod inner {
         ///   ([-a] alpha) | beta
         /// and user passes "-a <TAB>" we should not suggest "beta"
         pub(crate) fn valid_complete_head(&self) -> bool {
-            self.len() == 0 || (self.len() == 1 && self.removed.last() == Some(&false))
+            self.is_empty() || (self.len() == 1 && self.removed.last() == Some(&false))
         }
 
         #[cfg(feature = "autocomplete")]
diff --git a/src/docs/anywhere_1.md b/src/docs/anywhere_1.md
new file mode 100644
index 0000000..989027b
--- /dev/null
+++ b/src/docs/anywhere_1.md
@@ -0,0 +1,86 @@
+<details>
+<summary style="display: list-item;">Combinatoric usage</summary>
+
+```no_run
+# use bpaf::*;
+#[derive(Debug, Clone)]
+# #[allow(dead_code)]
+pub struct Options {
+    multi_arg: Option<MultiArg>,
+    turbo: bool,
+}
+
+#[derive(Debug, Clone)]
+# #[allow(dead_code)]
+pub struct MultiArg {
+    set: (),
+    name: String,
+    value: String,
+}
+
+pub fn options() -> OptionParser<Options> {
+    let set = long("set").req_flag(());
+    let name = positional("ARG");
+    let value = positional("ARG");
+    let multi_arg = construct!(MultiArg { set, name, value })
+        .anywhere()
+        .optional();
+
+    let turbo = long("turbo").switch();
+    construct!(Options { multi_arg, turbo }).to_options()
+}
+```
+
+</details>
+<details>
+<summary style="display: list-item;">Derive usage</summary>
+
+```no_run
+# use bpaf::*;
+#[derive(Debug, Clone, Bpaf)]
+# #[allow(dead_code)]
+#[bpaf(options)]
+pub struct Options {
+    #[bpaf(external, optional)]
+    multi_arg: Option<MultiArg>,
+    turbo: bool,
+}
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(anywhere)]
+# #[allow(dead_code)]
+pub struct MultiArg {
+    #[bpaf(long)]
+    set: (),
+    #[bpaf(positional)]
+    name: String,
+    #[bpaf(positional)]
+    value: String,
+}
+```
+
+</details>
+<details>
+<summary style="display: list-item;">Examples</summary>
+
+
+It's possible to implement multi argument options by using required flag followed by one or
+more positional items
+```console
+% app --turbo --set name Bob
+Options { multi_arg: Some(MultiArg { set: (), name: "name", value: "Bob" }), turbo: true }
+```
+
+Other flags can go on either side of items
+```console
+% app --set name Bob --turbo
+Options { multi_arg: Some(MultiArg { set: (), name: "name", value: "Bob" }), turbo: true }
+```
+
+But not in between, with potentially confusing error messages - to be fixed :)
+```console
+% app --set name --turbo Bob
+--set is not expected in this context
+```
+
+</details>
diff --git a/src/lib.rs b/src/lib.rs
index 4d8992c..2b49b56 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1397,6 +1397,11 @@ pub trait Parser<T> {
     /// Can be expensive performance wise especially if parser contains complex logic.
     ///
     #[doc = include_str!("docs/anywhere.md")]
+    ///
+    /// When using parsers annotated with `anywhere` it's a good idea to place them before other
+    /// parsers so combinations they are looking for are not consumed by simplier parsers.
+    ///
+    #[doc = include_str!("docs/anywhere_1.md")]
     #[must_use]
     fn anywhere(self) -> ParseAnywhere<Self>
     where
@@ -1475,6 +1480,19 @@ pub trait Parser<T> {
     {
         self.to_options().run()
     }
+
+    #[doc(hidden)]
+    /// Create a boxed representation for a parser
+    ///
+    ///
+    fn boxed(self) -> ParseBox<T>
+    where
+        Self: Sized + Parser<T> + 'static,
+    {
+        ParseBox {
+            inner: Box::new(self),
+        }
+    }
 }
 
 #[non_exhaustive]
diff --git a/src/meta_youmean.rs b/src/meta_youmean.rs
index 764154b..bbedabd 100644
--- a/src/meta_youmean.rs
+++ b/src/meta_youmean.rs
@@ -90,9 +90,7 @@ impl std::fmt::Display for I<'_> {
             Self::ShortFlag(s) => write!(f, "-{}", s),
             Self::LongFlag(s) => write!(f, "--{}", s),
             Self::ShortCmd(s) => f.write_char(*s),
-            Self::LongCmd(s) => f.write_str(s),
-            Self::Ambiguity(s) => f.write_str(s),
-            Self::Nested(s) => f.write_str(s),
+            Self::LongCmd(s) | Self::Ambiguity(s) | Self::Nested(s) => f.write_str(s),
         }
     }
 }
diff --git a/src/structs.rs b/src/structs.rs
index 38c530b..4877b18 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -748,7 +748,7 @@ where
 /// Boxed parser doesn't expose internal representation in it's type and allows to return
 /// different parsers in different conditional branches
 ///
-/// You can create it with a single argument `construct` macro:
+/// You can create it with a single argument `construct` macro or by using `boxed` annotation
 #[doc = include_str!("docs/boxed.md")]
 pub struct ParseBox<T> {
     /// Boxed inner parser


Test patch:
diff --git a/bpaf_derive/src/field_tests.rs b/bpaf_derive/src/field_tests.rs
index 1cd6081..0f7fe6f 100644
--- a/bpaf_derive/src/field_tests.rs
+++ b/bpaf_derive/src/field_tests.rs
@@ -636,3 +636,15 @@ fn optional_external_strange() {
         "keyword `external` is at stage `external` can't follow previous stage (postprocessing)";
     field_trans_fail(input, msg);
 }
+
+#[test]
+fn positional_bool() {
+    let input: NamedField = parse_quote! {
+        #[bpaf(positional::<bool>("O_O"))]
+        flag: bool
+    };
+    let output = quote! {
+        ::bpaf::positional::<bool>("O_O")
+    };
+    assert_eq!(input.to_token_stream().to_string(), output.to_string());
+}
diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs
index bb84b4a..c8d5b03 100644
--- a/bpaf_derive/src/top_tests.rs
+++ b/bpaf_derive/src/top_tests.rs
@@ -700,6 +700,56 @@ fn adjacent_for_struct() {
     assert_eq!(top.to_token_stream().to_string(), expected.to_string());
 }
 
+#[test]
+fn anywhere_for_struct() {
+    let top: Top = parse_quote! {
+        #[bpaf(adjacent, anywhere)]
+        struct Opts {
+            a: String,
+            b: String,
+        }
+    };
+
+    let expected = quote! {
+        fn opts() -> impl ::bpaf::Parser<Opts> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let a = ::bpaf::short('a').argument::<String>("ARG");
+                let b = ::bpaf::short('b').argument::<String>("ARG");
+                ::bpaf::construct!(Opts { a, b })
+            }.adjacent().anywhere()
+        }
+    };
+
+    assert_eq!(top.to_token_stream().to_string(), expected.to_string());
+}
+
+#[test]
+fn box_for_struct() {
+    let top: Top = parse_quote! {
+        #[bpaf(boxed)]
+        struct Opts {
+            a: String,
+            b: String,
+        }
+    };
+
+    let expected = quote! {
+        fn opts() -> impl ::bpaf::Parser<Opts> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let a = ::bpaf::short('a').argument::<String>("ARG");
+                let b = ::bpaf::short('b').argument::<String>("ARG");
+                ::bpaf::construct!(Opts { a, b })
+            }.boxed()
+        }
+    };
+
+    assert_eq!(top.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn no_fields_declaration() {
     let top: Top = parse_quote! {