gflags_derive/lib.rs
1//! Derive command line arguments from `struct` fields using
2//! [`gflags`][gflags].
3//!
4//! This is an alternative to the "Defining flags" section of the
5//! [`gflags`][gflags] manual.
6//!
7//! [gflags]: https://docs.rs/gflags
8//!
9//! # Defining flags
10//!
11//! Create a struct to contain the configuration data for your library or
12//! binary.
13//!
14//! For example, this hypothetical logging library that defines two
15//! configuration options.
16//!
17//! ```ignore
18//! struct Config {
19//! /// True if log messages should also be sent to STDERR
20//! to_stderr: bool,
21//!
22//! /// The directory to write log files to
23//! dir: String,
24//! }
25//! ```
26//!
27//! Flags are added to the registry by deriving `gflags_derive::Gflags` on the
28//! struct.
29//!
30//! ```ignore
31//! use gflags_derive::GFlags;
32//!
33//! #[derive(GFlags)]
34//! struct Config {
35//! /// True if log messages should also be sent to STDERR
36//! to_stderr: bool,
37//!
38//! /// The directory to write log files to
39//! dir: String,
40//! }
41//! ```
42//!
43//! You now have two new flags, as if you had written:
44//!
45//! ```ignore
46//! gflags::define! {
47//! /// True if log messages should also be sent to STDERR
48//! --to_stderr: bool
49//! }
50//!
51//! gflags::define! {
52//! /// The directory to write log files to
53//! --dir: &str
54//! }
55//! ```
56//!
57//! Note that:
58//!
59//! - The comment on each struct field is also the documentation comment for
60//! the flag, which becomes its help text.
61//! - The type for the `--dir` flag has been converted from `String` to `&str`.
62//!
63//! # Defining a flag prefix
64//!
65//! You might want all the flag names to have the same prefix, without needing
66//! to use that prefix on the field names. For example, a logging module might
67//! want all the flags to start `log-` or `log_`.
68//!
69//! To support this, use the `#[gflags(prefix = "...")]` attribute on the
70//! struct.
71//!
72//! ```ignore
73//! use gflags_derive::GFlags;
74//!
75//! #[derive(GFlags)]
76//! #[gflags(prefix = "log_")]
77//! struct Config {
78//! /// True if log messages should also be sent to STDERR
79//! to_stderr: bool,
80//!
81//! /// The directory to write log files to
82//! dir: String,
83//! }
84//! ```
85//!
86//! The flag definitions now include the prefix, as if you had written:
87//!
88//! ```ignore
89//! gflags::define! {
90//! /// True if log messages should also be sent to STDERR
91//! --log_to_stderr: bool
92//! }
93//!
94//! gflags::define! {
95//! /// The directory to write log files to
96//! --log_dir: &str
97//! }
98//! ```
99//!
100//! If the flag prefix ends with `-` then the macro converts the flag names to
101//! kebab-case instead of snake_case. So writing:
102//!
103//! ```ignore
104//! use gflags_derive::GFlags;
105//!
106//! #[derive(GFlags)]
107//! #[gflags(prefix = "log-")]
108//! struct Config {
109//! /// True if log messages should also be sent to STDERR
110//! to_stderr: bool,
111//!
112//! /// The directory to write log files to
113//! dir: String,
114//! }
115//! ```
116//!
117//! generates the following flags:
118//!
119//! ```ignore
120//! gflags::define! {
121//! /// True if log messages should also be sent to STDERR
122//! --log-to-stderr: bool
123//! }
124//!
125//! gflags::define! {
126//! /// The directory to write log files to
127//! --log-dir: &str
128//! }
129//! ```
130//!
131//! # Handling `Option<T>`
132//!
133//! Your configuration `struct` may have fields that have `Option<T>` types.
134//! For these fields `gflags_derive` creates a flag of the inner type `T`.
135//!
136//! # Customising the default value
137//!
138//! To specify a default value for the flag add a `#[gflags(default = ...)]`
139//! attribute to the field.
140//!
141//! The value for the attribute is the literal value, not a quoted value.
142//! Only quote the value if the type of the field is a string or can be
143//! created from a string.
144//!
145//! For example, to set the default value of the `--log-to-stderr` flag to
146//! `true`:
147//!
148//! ```ignore
149//! use gflags_derive::GFlags;
150//!
151//! #[derive(GFlags)]
152//! #[gflags(prefix = "log-")]
153//! struct Config {
154//! /// True if log messages should also be sent to STDERR
155//! #[gflags(default = true)]
156//! to_stderr: bool,
157//!
158//! /// The directory to write log files to
159//! dir: String,
160//! }
161//! ```
162//!
163//! Specifying this with quotes, `#[gflags(default = "true")]` will give a
164//! compile time error:
165//!
166//! ```text
167//! expected `bool`, found `&str`
168//! ```
169//!
170//! > **Important**: This does *not* change the default value when an instance
171//! of the `Config` struct is created. It only changes the default value of
172//! the `LOG_TO_STDERR.flag` variable.
173//!
174//! # Customising the type
175//!
176//! To use a different type for the field and the command line flag add a
177//! `#[gflags(type = "...")]` attribute to the field. For example, to store
178//! the log directory as a `PathBuf` but accept a string on the command line:
179//!
180//! ```ignore
181//! use gflags_derive::GFlags;
182//! use std::path::PathBuf;
183//!
184//! #[derive(GFlags)]
185//! #[gflags(prefix = "log-")]
186//! struct Config {
187//! /// True if log messages should also be sent to STDERR
188//! to_stderr: bool,
189//!
190//! /// The directory to write log files to
191//! #[gflags(type = "&str")]
192//! dir: PathBuf,
193//! }
194//! ```
195//!
196//! # Customising the visibility
197//!
198//! To use a different visibility for the flags add a
199//! `#[gflags(visibility = "...")]` attribute to the field and give a Rust
200//! visibility specifier.
201//!
202//! In this example the `LOG_DIR` flag variable will be visible in the parent
203//! module.
204//!
205//! ```ignore
206//! use gflags_derive::GFlags;
207//! use std::path::PathBuf;
208//!
209//! #[derive(GFlags)]
210//! #[gflags(prefix = "log-")]
211//! struct Config {
212//! /// True if log messages should also be sent to STDERR
213//! to_stderr: bool,
214//!
215//! /// The directory to write log files to
216//! #[gflags(visibility = "pub(super)")]
217//! #[gflags(type = "&str")]
218//! dir: PathBuf,
219//! }
220//! ```
221//!
222//! # Specifying a placeholder
223//!
224//! To give a placeholder that will appear in the flag's `help` output add a
225//! `#[gflags(placeholder = "...")]` attribute to the field. This will be
226//! wrapped in `<...>` for display.
227//!
228//! ```ignore
229//! use gflags_derive::GFlags;
230//! use std::path::PathBuf;
231//!
232//! #[derive(GFlags)]
233//! #[gflags(prefix = "log-")]
234//! struct Config {
235//! /// True if log messages should also be sent to STDERR
236//! to_stderr: bool,
237//!
238//! /// The directory to write log files to
239//! #[gflags(placeholder = "DIR")]
240//! #[gflags(type = "&str")]
241//! dir: PathBuf,
242//! }
243//! ```
244//!
245//! In the help output the `--log-dir` flag will appear as:
246//!
247//! ```text
248//! --log-dir <DIR>
249//! The directory to write log files to
250//! ```
251//!
252//! # Skipping flags
253//!
254//! To skip flag generation for a field add a `#[gflags(skip)]` attribute to
255//! the field.
256//!
257//! ```ignore
258//! use gflags_derive::GFlags;
259//! use std::path::PathBuf;
260//!
261//! #[derive(GFlags)]
262//! #[gflags(prefix = "log-")]
263//! struct Config {
264//! /// True if log messages should also be sent to STDERR
265//! to_stderr: bool,
266//!
267//! /// The directory to write log files to
268//! #[gflags(skip)]
269//! dir: PathBuf,
270//! }
271//! ```
272//!
273//! No `--log-dir` flag will be generated.
274//!
275//! # Providing multiple attributes
276//!
277//! If you want to provide multiple attributes on a field then you can mix
278//! and match specifing multiple options in a single `#[gflags(...)]` attribute
279//! and specifying multiple `#[gflags(...)]` attributes. The following examples
280//! are identical.
281//!
282//! ```ignore
283//! ...
284//! /// The directory to write log files to
285//! #[gflags(type = "&str", visibility = "pub(super)")]
286//! dir: PathBuf,
287//! ...
288//! ```
289//!
290//! ```ignore
291//! ...
292//! /// The directory to write log files to
293//! #[gflags(type = "&str")]
294//! #[gflags(visibility = "pub(super)")]
295//! dir: PathBuf,
296//! ...
297//! ```
298//!
299//! # Deserializing and merging flags
300//!
301//! This supports a powerful pattern for configuring an application that is
302//! composed of multiple crates, where each crate exports a configuration and
303//! supports multiple flags, and the application crate defines a configuration
304//! that imports the configuration structs from the component crates.
305//!
306//! This master configuration can be deserialized from e.g. a JSON file, and
307//! then each component crate can have the opportunity to override the loaded
308//! configuration with information from the command line flags that are specific
309//! to that crate.
310//!
311//! See the `examples/json` directory for a complete application that does
312//! this.
313//!
314//! # Use with `prost`
315//!
316//! This macro can be used to derive flags for `structs` generated from
317//! Protobuffer schemas using `prost` and `prost-build`.
318//!
319//! Given this `.proto` file
320//!
321//! ```proto
322//! syntax = "proto3"
323//!
324//! package log.config.v1;
325//!
326//! message Config {
327//! // True if log messages should also be sent to STDERR
328//! bool to_stderr = 1;
329//!
330//! // The directory to write log files to
331//! string dir = 2;
332//! }
333//! ```
334//!
335//! This `build.rs` file will add the relevant attributes to add the `log-`
336//! prefix and skip the `dir` field.
337//!
338//! ```ignore
339//! fn main() {
340//! let mut config = prost_build::Config::new();
341//!
342//! config.type_attribute(".log.config.v1.Config", "#[derive(gflags_derive::GFlags)]");
343//! config.type_attribute(".log.config.v1.Config", "#[gflags(prefix=\"log-\")]");
344//!
345//! config.field_attribute(".log.config.v1.Config.dir", "#[gflags(skip)]");
346//!
347//! config
348//! .compile_protos(&["proto/log/config/v1/config.proto"], &["proto"])
349//! .unwrap();
350//! }
351//! ```
352//!
353//! See the `examples/protobuf` directory for a complete application that
354//! does this.
355
356#![doc(html_root_url = "https://docs.rs/gflags-derive/0.1.0")]
357
358extern crate proc_macro;
359
360use crate::FlagCase::{KebabCase, SnakeCase};
361use proc_macro2::{Ident, Literal, Span, TokenStream, TokenTree};
362use proc_macro_error::{abort, abort_call_site, proc_macro_error};
363use quote::{format_ident, quote};
364use std::collections::HashSet;
365use syn::{
366 punctuated::Punctuated, Attribute, Data, DataStruct, Field, Fields, FieldsNamed,
367 GenericArgument, Lit, Meta, NestedMeta, Path, PathArguments, PathSegment, Token, Type,
368};
369
370#[derive(Debug, PartialEq)]
371enum FlagCase {
372 SnakeCase,
373 KebabCase,
374}
375
376#[derive(Debug)]
377struct Config {
378 /// Prefix to apply to flag names
379 prefix: String,
380
381 flag_case: FlagCase,
382}
383
384impl Default for Config {
385 fn default() -> Self {
386 Config {
387 prefix: "".to_string(),
388 flag_case: KebabCase,
389 }
390 }
391}
392
393fn impl_gflags_macro(ast: &syn::DeriveInput) -> proc_macro::TokenStream {
394 let fields: Vec<&Field> = match &ast.data {
395 Data::Struct(DataStruct {
396 fields: Fields::Named(FieldsNamed { named: fields, .. }),
397 ..
398 }) => fields.into_iter().collect(),
399 _ => abort_call_site!("expected a struct with named fields"),
400 };
401
402 let config = config_from_attributes(&ast.attrs);
403
404 let mut flags: Vec<TokenStream> = vec![];
405
406 for field in fields {
407 let flag = flag_from_field(&config, field);
408 flags.push(flag);
409 }
410
411 let gen = quote! {
412 #(#flags)*
413 };
414
415 gen.into()
416}
417
418/// Represents a `#[gflags(...)]` attribute on a struct or field.
419#[derive(Debug, Default)]
420struct GFlagsAttribute {
421 /// True if this field should be skipped (do not generate a flag for it)
422 skip: bool,
423
424 /// Prefix to apply to this flag (or global)
425 prefix: Option<String>,
426
427 /// Casing for this flag
428 flag_case: Option<FlagCase>,
429
430 /// Tokens that define the type to use for this flag
431 ty: Option<TokenStream>,
432
433 /// Visibility for the flag
434 visibility: Option<TokenStream>,
435
436 /// Placeholder to display in the help
437 placeholder: Option<TokenStream>,
438
439 /// Default value if the flag is not set
440 default: Option<TokenStream>,
441}
442
443impl From<Meta> for GFlagsAttribute {
444 fn from(meta: Meta) -> Self {
445 let meta = match meta {
446 Meta::List(meta) => meta,
447 _ => abort!(meta, "`#[gflags(...)]` expects a parameter list"),
448 };
449
450 if meta.nested.is_empty() {
451 abort!(meta, "`#[gflags(...)]` expects a non-empty parameter list");
452 }
453
454 let mut config = GFlagsAttribute::default();
455
456 let keywords: HashSet<&'static str> = [
457 "default",
458 "placeholder",
459 "prefix",
460 "skip",
461 "type",
462 "visibility",
463 ]
464 .iter()
465 .cloned()
466 .collect();
467
468 for kv in meta.nested {
469 let kv = match kv {
470 NestedMeta::Meta(Meta::Path(path)) => {
471 let keyword = path.get_ident().expect("No ident found");
472 if !keywords.contains(&keyword.to_string().as_ref()) {
473 abort!(path, "Invalid keyword `{}`", keyword);
474 }
475
476 if path.is_ident("skip") {
477 config.skip = true;
478 break;
479 }
480
481 abort!(path, "Keyword `{}` requires a value", keyword);
482 }
483 NestedMeta::Meta(Meta::NameValue(kv)) => kv,
484 _ => abort!(kv, "`#[gflags(...)]` expects key=value pairs"),
485 };
486
487 if kv.path.is_ident("default") {
488 let lit = kv.lit;
489 config.default = Some(quote! { = #lit });
490 continue;
491 }
492
493 if kv.path.is_ident("placeholder") {
494 config.placeholder = match kv.lit {
495 Lit::Str(lit) => {
496 if lit.value().is_empty() {
497 abort!(
498 lit,
499 "`#[gflags(placeholder=...)]` expects a non-empty quoted string"
500 )
501 }
502 let tokens = lit.parse::<TokenStream>().unwrap();
503 Some(quote! { < #tokens > })
504 }
505 _ => abort!(
506 kv.lit,
507 "`#[gflags(placeholder=...)]` expects a quoted string"
508 ),
509 };
510 continue;
511 }
512
513 if kv.path.is_ident("prefix") {
514 let mut prefix = match kv.lit {
515 Lit::Str(lit) => {
516 if lit.value().is_empty() {
517 abort!(
518 lit,
519 "`#[gflags(prefix=...)]` expects a non-empty quoted string"
520 );
521 }
522
523 lit.value()
524 }
525 _ => abort!(kv.lit, "`#[gflags(prefix=...)]` expects a quoted string"),
526 };
527
528 if prefix.ends_with('_') {
529 config.flag_case = Some(SnakeCase);
530 prefix.pop();
531 }
532
533 if prefix.ends_with('-') {
534 config.flag_case = Some(KebabCase);
535 prefix.pop();
536 }
537
538 config.prefix = Some(prefix);
539 continue;
540 }
541
542 if kv.path.is_ident("skip") {
543 abort!(kv.lit, "`#[gflags(skip)]` does not take a value");
544 }
545
546 if kv.path.is_ident("type") {
547 config.ty = match kv.lit {
548 Lit::Str(lit) => {
549 if lit.value().is_empty() {
550 abort!(
551 lit,
552 "`#[gflags(type=...)]` expects a non-empty quoted string"
553 );
554 }
555
556 Some(lit.parse().unwrap())
557 }
558 _ => abort!(kv.lit, "`#[gflags(type=...)]` expects a quoted string"),
559 };
560
561 continue;
562 }
563
564 if kv.path.is_ident("visibility") {
565 config.visibility = match kv.lit {
566 Lit::Str(lit) => {
567 if lit.value().is_empty() {
568 abort!(
569 lit,
570 "`#[gflags(visibility=...)]` expects a non-empty quoted string"
571 )
572 }
573 Some(lit.parse().unwrap())
574 }
575 _ => abort!(
576 kv.lit,
577 "`#[gflags(visibility=...)]` expects a quoted string"
578 ),
579 };
580 continue;
581 }
582
583 abort!(
584 kv.path,
585 "Invalid keyword `{}`",
586 kv.path.get_ident().unwrap()
587 );
588 }
589
590 config
591 }
592}
593
594impl From<&[Attribute]> for GFlagsAttribute {
595 fn from(attrs: &[Attribute]) -> Self {
596 let mut config: Self = Default::default();
597 for attr in attrs {
598 match attr.parse_meta() {
599 Ok(meta) => {
600 if !meta.path().is_ident("gflags") {
601 continue;
602 }
603 let parsed_config = GFlagsAttribute::from(meta);
604
605 // Any results in the parsed config overwrite any existing values.
606 // This allows multiple #[gflags(...)] attributes to exist on
607 // a single field
608 if parsed_config.skip {
609 config.skip = true
610 };
611
612 if parsed_config.default.is_some() {
613 config.default = parsed_config.default;
614 }
615
616 if parsed_config.placeholder.is_some() {
617 config.placeholder = parsed_config.placeholder;
618 }
619
620 if parsed_config.prefix.is_some() {
621 config.prefix = parsed_config.prefix;
622 }
623
624 if parsed_config.flag_case.is_some() {
625 config.flag_case = parsed_config.flag_case;
626 }
627
628 if parsed_config.ty.is_some() {
629 config.ty = parsed_config.ty;
630 }
631
632 if parsed_config.visibility.is_some() {
633 config.visibility = parsed_config.visibility;
634 }
635 }
636 Err(e) => abort!(attr, e),
637 }
638 }
639
640 config
641 }
642}
643
644/// Generate a configuration based on `#[gflags(...)` attribute values
645fn config_from_attributes(attrs: &[Attribute]) -> Config {
646 let mut config: Config = Default::default();
647
648 let gfa = GFlagsAttribute::from(attrs);
649
650 if gfa.prefix.is_some() {
651 config.prefix = gfa.prefix.unwrap();
652 }
653
654 if gfa.flag_case.is_some() {
655 config.flag_case = gfa.flag_case.unwrap();
656 }
657
658 config
659}
660
661fn flag_from_field(config: &Config, field: &Field) -> TokenStream {
662 let gfa = GFlagsAttribute::from(field.attrs.as_ref());
663 if gfa.skip {
664 return TokenStream::new();
665 }
666
667 // Figure out the flag name
668 let flag_name = if config.flag_case == SnakeCase {
669 let ident = if !config.prefix.is_empty() {
670 format_ident!(
671 "{}_{}",
672 config.prefix,
673 field
674 .ident
675 .as_ref()
676 .expect("Unwrapping field.ident (prefix) failed")
677 )
678 } else {
679 field
680 .ident
681 .as_ref()
682 .expect("Unwrapping field.ident (no-prefix) failed")
683 .clone()
684 };
685 quote! {--#ident}
686 } else {
687 let span = Span::call_site();
688 let mut segments: Punctuated<Ident, Token![-]> = Punctuated::new();
689 if !config.prefix.is_empty() {
690 segments.push(Ident::new(&config.prefix, span));
691 }
692
693 let field = field.ident.as_ref().unwrap().to_string();
694 for part in field.split('_') {
695 segments.push(Ident::new(part, span));
696 }
697 quote! {--#segments}
698 };
699
700 // Figure out the default value
701 let default = match gfa.default {
702 Some(default) => default,
703 _ => TokenStream::new(),
704 };
705
706 // Figure out the placeholder
707 let placeholder = match gfa.placeholder {
708 Some(placeholder) => placeholder,
709 _ => TokenStream::new(),
710 };
711
712 // Figure out the visibility
713 let visibility = match gfa.visibility {
714 Some(visibility) => visibility,
715 _ => TokenStream::new(),
716 };
717
718 // Figure out the type
719 let ty = match gfa.ty {
720 Some(ty) => ty,
721 _ => match &field.ty {
722 Type::Path(ty) => {
723 let mut last = ty.path.segments.last().unwrap();
724 let mut ident = &last.ident;
725
726 let mut final_type = ty.clone();
727
728 // Replace `Option<T>` with `T` before proceeding
729 if *ident == "Option" {
730 let option_type = syn::Type::from(final_type);
731
732 let new_ty = extract_type_from_option(&option_type);
733 match new_ty {
734 Some(Type::Path(new_ty)) => {
735 final_type = new_ty.clone();
736 last = final_type.path.segments.last().unwrap();
737 ident = &last.ident;
738 }
739 _ => abort!(&field.ty, "Unexpected type"),
740 }
741 }
742
743 if *ident == "String" {
744 quote! { &str }
745 } else {
746 quote! { #final_type }
747 }
748 }
749 _ => abort!(&field.ty, "Unexpected type"),
750 },
751 };
752
753 // Figure out the doc string, if there is one
754 let mut docs: Vec<Literal> = vec![];
755
756 for attr in &field.attrs {
757 if !attr.path.is_ident("doc") {
758 continue;
759 }
760 let tokens = attr.tokens.clone();
761 for token in tokens {
762 if let TokenTree::Literal(l) = token {
763 docs.push(l);
764 }
765 }
766 }
767
768 // Construct the macro call
769 let gen = quote! {
770 gflags::define! {
771 #( #[doc = #docs])*
772 #visibility #flag_name #placeholder: #ty #default
773 }
774 };
775
776 gen
777}
778
779/// Given a `syn::Type` that is an `Option<T>`, return the `syn::Type` for the
780/// `T`, or `None` if it's not a `syn::Type::Path`.
781///
782/// https://stackoverflow.com/questions/55271857/how-can-i-get-the-t-from-an-optiont-when-using-syn
783fn extract_type_from_option(ty: &syn::Type) -> Option<&syn::Type> {
784 fn extract_type_path(ty: &syn::Type) -> Option<&Path> {
785 match *ty {
786 syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
787 _ => None,
788 }
789 }
790
791 fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
792 let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
793 acc.push_str(&v.ident.to_string());
794 acc.push('|');
795 acc
796 });
797 vec!["Option|", "std|option|Option|", "core|option|Option|"]
798 .into_iter()
799 .find(|s| idents_of_path == *s)
800 .and_then(|_| path.segments.last())
801 }
802
803 extract_type_path(ty)
804 .and_then(|path| extract_option_segment(path))
805 .and_then(|pair_path_segment| {
806 let type_params = &pair_path_segment.arguments;
807 // It should have only one angle-bracketed param ("<String>"):
808 match *type_params {
809 PathArguments::AngleBracketed(ref params) => params.args.first(),
810 _ => None,
811 }
812 })
813 .and_then(|generic_arg| match *generic_arg {
814 GenericArgument::Type(ref ty) => Some(ty),
815 _ => None,
816 })
817}
818
819/// # Struct level attributes
820///
821/// `#[gflags(prefix = "...")]` -- apply this prefix to flag names
822///
823/// # Field level attributes
824///
825/// `#[gflags(default = ...)]` -- default value for this flag
826///
827/// `#[gflags(placeholder= "...")]` -- placeholder to display in help
828///
829/// `#[gflags(skip)]` -- do not generate a flag for this field
830///
831/// `#[gflags(type = "...")]` -- generate a flag with this type
832///
833/// `#[gflags(visibility = "...")]` -- generate a flag with this visibility
834///
835/// Refer to the [crate level documentation](index.html) for a complete example.
836#[proc_macro_derive(GFlags, attributes(gflags))]
837#[proc_macro_error]
838pub fn gflags_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
839 let ast = syn::parse(input).unwrap();
840 impl_gflags_macro(&ast)
841}