bpaf_derive/
td.rs

1use crate::{
2    attrs::PostDecor,
3    help::Help,
4    utils::{parse_arg, parse_opt_arg},
5};
6use quote::{quote, ToTokens};
7use syn::{
8    parse::{Parse, ParseStream},
9    parse_quote, token, Error, Expr, Ident, LitChar, LitStr, Result,
10};
11
12// 1. options[("name")] and command[("name")] must be first in line and change parsing mode
13//  some flags are valid in different modes but other than that order matters and structure does
14//  not
15
16#[derive(Debug, Default)]
17pub(crate) struct CommandCfg {
18    pub(crate) name: Option<LitStr>,
19    pub(crate) long: Vec<LitStr>,
20    pub(crate) short: Vec<LitChar>,
21    pub(crate) help: Option<Help>,
22}
23
24#[derive(Debug, Default)]
25pub(crate) struct OptionsCfg {
26    pub(crate) cargo_helper: Option<LitStr>,
27    pub(crate) descr: Option<Help>,
28    pub(crate) footer: Option<Help>,
29    pub(crate) header: Option<Help>,
30    pub(crate) usage: Option<Box<Expr>>,
31    pub(crate) version: Option<Box<Expr>>,
32    pub(crate) max_width: Option<Box<Expr>>,
33    pub(crate) fallback_usage: bool,
34}
35
36#[derive(Debug, Default)]
37pub(crate) struct ParserCfg {
38    pub(crate) group_help: Option<Help>,
39}
40
41#[derive(Debug)]
42pub(crate) enum Mode {
43    Command {
44        command: CommandCfg,
45        options: OptionsCfg,
46    },
47    Options {
48        options: OptionsCfg,
49    },
50    Parser {
51        parser: ParserCfg,
52    },
53}
54
55#[derive(Debug)]
56pub(crate) enum HelpMsg {
57    Lit(String),
58    Custom(Box<Expr>),
59}
60
61impl From<String> for HelpMsg {
62    fn from(value: String) -> Self {
63        Self::Lit(value)
64    }
65}
66
67impl From<Box<Expr>> for HelpMsg {
68    fn from(value: Box<Expr>) -> Self {
69        Self::Custom(value)
70    }
71}
72
73impl ToTokens for HelpMsg {
74    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
75        match self {
76            HelpMsg::Lit(l) => l.to_tokens(tokens),
77            HelpMsg::Custom(l) => l.to_tokens(tokens),
78        }
79    }
80}
81
82#[derive(Debug)]
83pub(crate) struct TopInfo {
84    /// Should visibility for generated function to be inherited?
85    pub(crate) private: bool,
86    /// Should parser be generated with a custom name?
87    pub(crate) custom_name: Option<Ident>,
88    /// add .boxed() at the end
89    pub(crate) boxed: bool,
90    /// don't convert rustdoc to group_help, help, etc.
91    pub(crate) ignore_rustdoc: bool,
92
93    pub(crate) adjacent: bool,
94    pub(crate) mode: Mode,
95    pub(crate) attrs: Vec<PostDecor>,
96
97    /// Custom absolute path to the `bpaf` crate.
98    pub(crate) bpaf_path: Option<syn::Path>,
99}
100
101impl Default for TopInfo {
102    fn default() -> Self {
103        Self {
104            private: false,
105            custom_name: None,
106            boxed: false,
107            adjacent: false,
108            mode: Mode::Parser {
109                parser: Default::default(),
110            },
111            attrs: Vec::new(),
112            ignore_rustdoc: false,
113            bpaf_path: None,
114        }
115    }
116}
117
118const TOP_NEED_OPTIONS: &str =
119    "You need to add `options` annotation at the beginning to use this one";
120
121const TOP_NEED_COMMAND: &str =
122    "You need to add `command` annotation at the beginning to use this one";
123
124const TOP_NEED_PARSER: &str = "This annotation can't be used with either `options` or `command`";
125
126fn with_options(
127    kw: &Ident,
128    cfg: Option<&mut OptionsCfg>,
129    f: impl FnOnce(&mut OptionsCfg),
130) -> Result<()> {
131    match cfg {
132        Some(cfg) => {
133            f(cfg);
134            Ok(())
135        }
136        None => Err(Error::new_spanned(kw, TOP_NEED_OPTIONS)),
137    }
138}
139
140fn with_command(
141    kw: &Ident,
142    cfg: Option<&mut CommandCfg>,
143    f: impl FnOnce(&mut CommandCfg),
144) -> Result<()> {
145    match cfg {
146        Some(cfg) => {
147            f(cfg);
148            Ok(())
149        }
150        None => Err(Error::new_spanned(kw, TOP_NEED_COMMAND)),
151    }
152}
153
154fn with_parser(
155    kw: &Ident,
156    cfg: Option<&mut ParserCfg>,
157    f: impl FnOnce(&mut ParserCfg),
158) -> Result<()> {
159    match cfg {
160        Some(cfg) => {
161            f(cfg);
162            Ok(())
163        }
164        None => Err(Error::new_spanned(kw, TOP_NEED_PARSER)),
165    }
166}
167
168impl Parse for TopInfo {
169    fn parse(input: ParseStream) -> Result<Self> {
170        let mut private = false;
171        let mut custom_name = None;
172        let mut boxed = false;
173        let mut ignore_rustdoc = false;
174        let mut command = None;
175        let mut options = None;
176        let mut parser = Some(ParserCfg::default());
177        let mut adjacent = false;
178        let mut attrs = Vec::new();
179        let mut first = true;
180        let mut bpaf_path = None;
181        loop {
182            let kw = input.parse::<Ident>()?;
183
184            if first && kw == "options" {
185                let mut cfg = OptionsCfg::default();
186                if let Some(helper) = parse_opt_arg(input)? {
187                    cfg.cargo_helper = Some(helper);
188                }
189                options = Some(cfg);
190                parser = None;
191            } else if first && kw == "command" {
192                let mut cfg = CommandCfg::default();
193                if let Some(name) = parse_opt_arg(input)? {
194                    cfg.name = Some(name);
195                }
196                options = Some(OptionsCfg::default());
197                command = Some(cfg);
198                parser = None;
199            } else if kw == "private" {
200                private = true;
201            } else if kw == "generate" {
202                custom_name = parse_arg(input)?;
203            } else if kw == "options" {
204                return Err(Error::new_spanned(
205                    kw,
206                    "This annotation must be first and used only once: try `#[bpaf(options, ...`",
207                ));
208            } else if kw == "command" {
209                return Err(Error::new_spanned(
210                    kw,
211                    "This annotation must be first: try `#[bpaf(command, ...`",
212                ));
213            } else if kw == "version" {
214                let version = parse_opt_arg(input)?
215                    .unwrap_or_else(|| parse_quote!(env!("CARGO_PKG_VERSION")));
216                with_options(&kw, options.as_mut(), |cfg| cfg.version = Some(version))?;
217            } else if kw == "boxed" {
218                boxed = true;
219            } else if kw == "adjacent" {
220                adjacent = true;
221            } else if kw == "fallback_to_usage" {
222                if let Some(opts) = options.as_mut() {
223                    opts.fallback_usage = true;
224                } else {
225                    return Err(Error::new_spanned(
226                    kw,
227                    "This annotation only makes sense in combination with `options` or `command`",
228                ));
229                }
230            } else if kw == "short" {
231                let short = parse_arg(input)?;
232                with_command(&kw, command.as_mut(), |cfg| cfg.short.push(short))?;
233            } else if kw == "long" {
234                let long = parse_arg(input)?;
235                with_command(&kw, command.as_mut(), |cfg| cfg.long.push(long))?;
236            } else if kw == "header" {
237                let header = parse_arg(input)?;
238                with_options(&kw, options.as_mut(), |cfg| cfg.header = Some(header))?;
239            } else if kw == "footer" {
240                let footer = parse_arg(input)?;
241                with_options(&kw, options.as_mut(), |opt| opt.footer = Some(footer))?;
242            } else if kw == "usage" {
243                let usage = parse_arg(input)?;
244                with_options(&kw, options.as_mut(), |opt| opt.usage = Some(usage))?;
245            } else if kw == "group_help" {
246                let group_help = parse_arg(input)?;
247                with_parser(&kw, parser.as_mut(), |opt| {
248                    opt.group_help = Some(group_help)
249                })?;
250            } else if kw == "ignore_rustdoc" {
251                ignore_rustdoc = true;
252            } else if kw == "descr" {
253                let descr = parse_arg(input)?;
254                with_options(&kw, options.as_mut(), |opt| opt.descr = Some(descr))?;
255            } else if kw == "help" {
256                let help = parse_arg(input)?;
257                with_command(&kw, command.as_mut(), |cfg| cfg.help = Some(help))?;
258            } else if kw == "path" {
259                bpaf_path.replace(parse_arg::<syn::Path>(input)?);
260            } else if kw == "max_width" {
261                let max_width = parse_arg(input)?;
262                with_options(&kw, options.as_mut(), |opt| opt.max_width = Some(max_width))?;
263            } else if let Some(pd) = PostDecor::parse(input, &kw)? {
264                attrs.push(pd);
265            } else {
266                return Err(Error::new_spanned(
267                    kw,
268                    "Unexpected attribute for top level annotation",
269                ));
270            }
271
272            if input.is_empty() {
273                break;
274            }
275            input.parse::<token::Comma>()?;
276            if input.is_empty() {
277                break;
278            }
279            first = false;
280        }
281
282        let mode = match (options, command) {
283            (Some(options), Some(command)) => Mode::Command { command, options },
284            (Some(options), None) => Mode::Options { options },
285            _ => Mode::Parser {
286                parser: parser.unwrap_or_default(),
287            },
288        };
289
290        Ok(TopInfo {
291            ignore_rustdoc,
292            private,
293            custom_name,
294            boxed,
295            adjacent,
296            mode,
297            attrs,
298            bpaf_path,
299        })
300    }
301}
302
303#[derive(Debug, Default)]
304pub(crate) struct Ed {
305    pub(crate) skip: bool,
306    pub(crate) attrs: Vec<EAttr>,
307}
308
309pub(crate) enum VariantMode {
310    Command,
311    Parser,
312}
313
314impl Parse for Ed {
315    fn parse(input: ParseStream) -> Result<Self> {
316        let mut attrs = Vec::new();
317        let mut skip = false;
318
319        let mode = {
320            let first = input.fork().parse::<Ident>()?;
321            if first == "command" {
322                VariantMode::Command
323            } else {
324                VariantMode::Parser
325            }
326        };
327
328        loop {
329            let kw = input.parse::<Ident>()?;
330
331            if kw == "command" {
332                attrs.push(if let Some(name) = parse_opt_arg(input)? {
333                    EAttr::NamedCommand(name)
334                } else {
335                    EAttr::UnnamedCommand
336                });
337            } else if kw == "short" {
338                if matches!(mode, VariantMode::Command) {
339                    attrs.push(EAttr::CommandShort(parse_arg(input)?));
340                } else {
341                    attrs.push(EAttr::UnitShort(parse_opt_arg(input)?));
342                }
343            } else if kw == "hide" {
344                attrs.push(EAttr::Hide);
345            } else if kw == "long" {
346                if matches!(mode, VariantMode::Command) {
347                    attrs.push(EAttr::CommandLong(parse_arg(input)?));
348                } else {
349                    attrs.push(EAttr::UnitLong(parse_opt_arg(input)?));
350                }
351            } else if kw == "fallback_to_usage" {
352                if matches!(mode, VariantMode::Command) {
353                    attrs.push(EAttr::FallbackUsage);
354                } else {
355                    return Err(Error::new_spanned(
356                        kw,
357                        "In this context this attribute requires \"command\" annotation",
358                    ));
359                }
360            } else if kw == "skip" {
361                skip = true;
362            } else if kw == "adjacent" {
363                attrs.push(EAttr::Adjacent);
364            } else if kw == "usage" {
365                attrs.push(EAttr::Usage(parse_arg(input)?));
366            } else if kw == "header" {
367                attrs.push(EAttr::Header(parse_arg(input)?));
368            } else if kw == "footer" {
369                attrs.push(EAttr::Footer(parse_arg(input)?));
370            } else if kw == "env" {
371                attrs.push(EAttr::Env(parse_arg(input)?));
372            } else {
373                return Err(Error::new_spanned(
374                    kw,
375                    "Unexpected attribute for enum variant annotation",
376                ));
377            }
378
379            if input.is_empty() {
380                break;
381            }
382            input.parse::<token::Comma>()?;
383            if input.is_empty() {
384                break;
385            }
386        }
387
388        Ok(Ed { skip, attrs })
389    }
390}
391
392#[derive(Debug, Clone)]
393pub(crate) enum EAttr {
394    NamedCommand(LitStr),
395    UnnamedCommand,
396
397    FallbackUsage,
398    CommandShort(LitChar),
399    CommandLong(LitStr),
400    Adjacent,
401    Hide,
402    UnitShort(Option<LitChar>),
403    UnitLong(Option<LitStr>),
404    Descr(Help),
405    Header(Help),
406    Footer(Help),
407    Usage(Box<Expr>),
408    Env(Box<Expr>),
409    ToOptions,
410}
411
412impl ToTokens for EAttr {
413    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
414        match self {
415            Self::ToOptions => quote!(to_options()),
416            Self::NamedCommand(n) => quote!(command(#n)),
417            Self::CommandShort(n) => quote!(short(#n)),
418            Self::CommandLong(n) => quote!(long(#n)),
419            Self::Adjacent => quote!(adjacent()),
420            Self::Descr(d) => quote!(descr(#d)),
421            Self::Header(d) => quote!(header(#d)),
422            Self::Footer(d) => quote!(footer(#d)),
423            Self::Usage(u) => quote!(usage(#u)),
424            Self::Env(e) => quote!(env(#e)),
425            Self::Hide => quote!(hide()),
426            Self::FallbackUsage => quote!(fallback_to_usage()),
427
428            Self::UnnamedCommand | Self::UnitShort(_) | Self::UnitLong(_) => unreachable!(),
429        }
430        .to_tokens(tokens);
431    }
432}