darkwing_derive/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use proc_macro::TokenStream;
use quote::quote;
use syn::{
  parse_macro_input, Data, DeriveInput, Fields, Lit, Meta, NestedMeta,
};

/// Derives the `DarkwingArgs` trait for a struct, enabling conversion of struct
/// fields into command-line arguments.
///
/// This derive macro generates an implementation of the `stringify` method that
/// converts struct fields into a vector of (argument_name, value) pairs.
///
/// # Attributes
/// - `#[darkwing_args(internal)]`: Skip this field when generating arguments
/// - `#[darkwing_args(name = "custom-name")]`: Use a custom name for the
///   argument instead of the field name
///
/// # Examples
/// ```
/// use darkwing_derive::DarkwingArgs;
///
/// #[derive(DarkwingArgs)]
/// struct BrowserArgs {
///     #[darkwing_args(name = "user-data-dir")]
///     data_dir: String,
///     profile_name: String,
///     #[darkwing_args(internal)]
///     internal_field: String,
/// }
///
/// let args = BrowserArgs {
///     data_dir: "/path/to/data".to_string(),
///     profile_name: "default".to_string(),
///     internal_field: "hidden".to_string(),
/// };
///
/// // Will produce:
/// // vec![
/// //     ("--user-data-dir".to_string(), "/path/to/data".to_string()),
/// //     ("--profile_name".to_string(), "default".to_string())
/// // ]
/// // Note: internal_field is skipped
/// let stringified = args.stringify();
/// ```
///
/// Empty strings are automatically skipped:
/// ```
/// use darkwing_derive::DarkwingArgs;
///
/// #[derive(DarkwingArgs)]
/// struct BrowserArgs {
///     #[darkwing_args(name = "user-data-dir")]
///     data_dir: String,
///     profile_name: String,
///     #[darkwing_args(internal)]
///     internal_field: String,
/// }
/// let args = BrowserArgs {
///     data_dir: "/path/to/data".to_string(),
///     profile_name: "".to_string(), // This will be skipped
///     internal_field: "hidden".to_string(),
/// };
///
/// // Will produce:
/// // vec![
/// //     ("--user-data-dir".to_string(), "/path/to/data".to_string())
/// // ]
/// let stringified = args.stringify();
/// ```
#[proc_macro_derive(DarkwingArgs, attributes(darkwing_args))]
pub fn darkwing_args_derive(input: TokenStream) -> TokenStream {
  let input = parse_macro_input!(input as DeriveInput);
  let name = &input.ident;

  let fields = match &input.data {
    Data::Struct(data) => match &data.fields {
      Fields::Named(fields) => &fields.named,
      _ => panic!("DarkwingArgs only supports structs with named fields"),
    },
    _ => panic!("DarkwingArgs only supports structs"),
  };

  let stringify_fields = fields.iter().filter_map(|field| {
    let field_name = &field.ident;
    let darkwing_attr = field
      .attrs
      .iter()
      .find(|attr| attr.path.is_ident("darkwing_args"));

    if let Some(attr) = darkwing_attr {
      if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
        if meta_list.nested.iter().any(|nested| {
          matches!(nested, NestedMeta::Meta(Meta::Path(path)) if path.is_ident("internal"))
        }) {
          return None; // Skip internal fields
        }

        let arg_name = meta_list
          .nested
          .iter()
          .find_map(|nested| {
            if let NestedMeta::Meta(Meta::NameValue(name_value)) = nested {
              if name_value.path.is_ident("name") {
                if let Lit::Str(lit_str) = &name_value.lit {
                  return Some(lit_str.value());
                }
              }
            }
            None
          })
          .unwrap_or_else(|| field_name.as_ref().unwrap().to_string());

        return Some(quote! {
          if !self.#field_name.is_empty() {
            result.push((format!("--{}", #arg_name), self.#field_name.to_string()));
          }
        });
      }
    }

    Some(quote! {
      if !self.#field_name.is_empty() {
        result.push((format!("--{}", stringify!(#field_name)), self.#field_name.to_string()));
      }
    })
  });

  let expanded = quote! {
      impl #name {
          pub fn stringify(&self) -> Vec<(String, String)> {
              let mut result = Vec::new();
              #(#stringify_fields)*
              result
          }
      }
  };

  TokenStream::from(expanded)
}