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)
}