@@ -7,53 +7,16 @@ use syn::{
7
7
} ;
8
8
9
9
/// A derive macro that enables structs to handle configuration from both CLI arguments and config files.
10
- ///
11
- /// This macro generates the necessary code to:
12
- /// 1. Parse command-line arguments using clap
13
- /// 2. Load and parse config files (YAML/JSON/TOML)
14
- /// 3. Merge the configurations with CLI taking precedence
15
- ///
16
- /// # Attributes
17
- /// - `#[cli_only]`: Field only appears as a CLI argument
18
- /// - `#[config_only]`: Field only appears in config file
19
- /// - `#[cli_and_config]`: Field appears in both (CLI overrides config)
20
- /// - `#[config_arg(name = "x", short = 'x', long = "xxx", default_value = "y")]`: Customize CLI argument
21
- /// - `#[multi_value_behavior(extend/overwrite)]`: For Vec fields, control merge behavior
22
- ///
23
- /// # Example
24
- /// ```rust
25
- /// use clap_config_file::ClapConfigFile;
26
- ///
27
- /// #[derive(ClapConfigFile)]
28
- /// struct Config {
29
- /// // CLI-only flag with custom name
30
- /// #[cli_only]
31
- /// #[config_arg(name = "verbose", short = 'v')]
32
- /// verbose: bool,
33
- ///
34
- /// // Config file only setting
35
- /// #[config_only]
36
- /// database_url: String,
37
- ///
38
- /// // Available in both with CLI override
39
- /// #[cli_and_config]
40
- /// #[config_arg(name = "port", default_value = "8080")]
41
- /// port: u16,
42
- ///
43
- /// // Vec field that extends values from both sources
44
- /// #[cli_and_config]
45
- /// #[multi_value_behavior(extend)]
46
- /// tags: Vec<String>,
47
- /// }
48
- /// ```
10
+ /// ...
49
11
#[ proc_macro_derive(
50
12
ClapConfigFile ,
51
13
attributes(
52
14
cli_only,
53
15
config_only,
54
16
cli_and_config,
55
17
config_arg,
56
- multi_value_behavior
18
+ multi_value_behavior,
19
+ positional
57
20
)
58
21
) ]
59
22
pub fn derive_clap_config_file ( input : TokenStream ) -> TokenStream {
@@ -64,35 +27,29 @@ pub fn derive_clap_config_file(input: TokenStream) -> TokenStream {
64
27
}
65
28
}
66
29
67
- /// Defines how a field's value can be provided, controlling whether it's available
68
- /// via CLI arguments, config file, or both.
69
30
#[ derive( Debug , Clone , Copy ) ]
70
31
enum FieldAvailability {
71
32
CliOnly ,
72
33
ConfigOnly ,
73
34
CliAndConfig ,
74
35
}
75
36
76
- /// Controls how Vec fields merge values when present in both CLI and config.
77
- /// - Extend: Append CLI values to config values
78
- /// - Overwrite: Replace config values with CLI values if present
79
37
#[ derive( Debug , Clone , Default ) ]
80
38
enum MultiValueBehavior {
81
39
#[ default]
82
40
Extend ,
83
41
Overwrite ,
84
42
}
85
43
86
- /// Holds CLI argument customization options specified via #[config_arg(...)]
87
44
#[ derive( Debug , Default , Clone ) ]
88
45
struct ArgAttributes {
89
46
name : Option < String > ,
90
47
short : Option < char > ,
91
48
long : Option < String > ,
92
49
default_value : Option < String > ,
50
+ is_positional : bool , // <--- new flag
93
51
}
94
52
95
- /// Aggregates all metadata about a struct field needed for code generation
96
53
#[ derive( Debug , Clone ) ]
97
54
struct FieldInfo {
98
55
availability : FieldAvailability ,
@@ -102,12 +59,6 @@ struct FieldInfo {
102
59
ty : syn:: Type ,
103
60
}
104
61
105
- /// Main implementation function that generates the expanded code for the derive macro.
106
- /// This includes:
107
- /// - Hidden CLI struct with clap attributes
108
- /// - Hidden config struct with serde attributes
109
- /// - Implementation of parsing and merging logic
110
- /// - Helper functions for config file discovery and loading
111
62
fn build_impl ( ast : & DeriveInput ) -> syn:: Result < TokenStream2 > {
112
63
let struct_name = & ast. ident ;
113
64
let generics = & ast. generics ;
@@ -131,13 +82,12 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
131
82
}
132
83
} ;
133
84
134
- // Parse user's field attributes
135
85
let parsed_fields: Vec < FieldInfo > = fields
136
86
. iter ( )
137
- . map ( |field | parse_one_field ( field ) )
87
+ . map ( |f | parse_one_field ( f ) )
138
88
. collect :: < Result < _ , _ > > ( ) ?;
139
89
140
- // For each field, generate the CLI struct token, the config struct token , and the final merge expression
90
+ // Build up the hidden CLI struct fields, config struct fields , and merge expressions
141
91
let mut cli_struct_fields = Vec :: new ( ) ;
142
92
let mut cfg_struct_fields = Vec :: new ( ) ;
143
93
let mut merge_stmts = Vec :: new ( ) ;
@@ -148,22 +98,20 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
148
98
let cfg_ts_opt = generate_config_field_tokens ( pf) ;
149
99
let merge_expr = generate_merge_expr ( pf) ;
150
100
151
- // If we got Some(...) token stream, push it into the vectors
152
101
if let Some ( ts) = cli_ts_opt {
153
102
cli_struct_fields. push ( ts) ;
154
103
}
155
104
if let Some ( ts) = cfg_ts_opt {
156
105
cfg_struct_fields. push ( ts) ;
157
106
}
158
107
159
- // Always push the final merge line (the user's struct field)
160
108
let field_name = & pf. ident ;
161
109
merge_stmts. push ( quote ! {
162
110
#field_name: #merge_expr
163
111
} ) ;
164
112
}
165
113
166
- // Add our special meta fields (for --no- config, etc.) to the CLI struct
114
+ // Add special config fields to the CLI struct
167
115
cli_struct_fields. push ( quote ! {
168
116
/// If true, skip reading any config file
169
117
#[ arg( long = "no-config" , default_value_t = false ) ]
@@ -178,22 +126,19 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
178
126
pub __raw_config: Option <String >,
179
127
} ) ;
180
128
181
- // Construct hidden struct names
182
129
let cli_struct_ident = syn:: Ident :: new ( & format ! ( "__{}_Cli" , struct_name) , Span :: call_site ( ) ) ;
183
130
let cfg_struct_ident = syn:: Ident :: new ( & format ! ( "__{}_Cfg" , struct_name) , Span :: call_site ( ) ) ;
184
-
185
131
let num_fields = parsed_fields. len ( ) ;
186
132
let field_names = parsed_fields. iter ( ) . map ( |pf| & pf. ident ) ;
187
133
188
- // Build final output
134
+ // We add #[command(name="advanced")] below. Adjust or remove as needed.
189
135
let expanded = quote ! {
190
- // Hidden CLI struct
191
136
#[ derive( :: clap:: Parser , Debug , Default ) ]
137
+ #[ command( name = "advanced" ) ]
192
138
struct #cli_struct_ident {
193
139
#( #cli_struct_fields) , *
194
140
}
195
141
196
- // Hidden config struct
197
142
#[ derive( serde:: Serialize , serde:: Deserialize , Debug , Default ) ]
198
143
struct #cfg_struct_ident {
199
144
#( #cfg_struct_fields) , *
@@ -389,13 +334,11 @@ fn build_impl(ast: &DeriveInput) -> syn::Result<TokenStream2> {
389
334
Ok ( expanded)
390
335
}
391
336
392
- /// Parses all attributes on a field to determine its configuration behavior
393
337
fn parse_one_field ( field : & Field ) -> syn:: Result < FieldInfo > {
394
338
let mut availability = None ;
395
339
let mut mv_behavior = MultiValueBehavior :: default ( ) ;
396
340
let mut arg_attrs = ArgAttributes :: default ( ) ;
397
341
398
- // Check each attribute for relevant markers
399
342
for attr in & field. attrs {
400
343
let path_ident = match attr. path ( ) . get_ident ( ) {
401
344
Some ( i) => i. to_string ( ) ,
@@ -415,6 +358,10 @@ fn parse_one_field(field: &Field) -> syn::Result<FieldInfo> {
415
358
ensure_avail_none ( & availability, attr) ?;
416
359
availability = Some ( FieldAvailability :: CliAndConfig ) ;
417
360
}
361
+ "positional" => {
362
+ // We'll mark this field as positional, so we skip generating `long/short`
363
+ arg_attrs. is_positional = true ;
364
+ }
418
365
"config_arg" => {
419
366
let meta = attr. meta . require_list ( ) ?;
420
367
for nested in meta. parse_args_with (
@@ -524,25 +471,17 @@ fn generate_cli_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
524
471
short,
525
472
long,
526
473
default_value,
474
+ is_positional,
527
475
} = & pf. arg_attrs ;
528
476
529
- // Create owned String here instead of referencing temporary
477
+ // name fallback for 'long' unless it's positional
530
478
let name_str = name. clone ( ) . unwrap_or_else ( || field_name. to_string ( ) ) ;
479
+ let default_attr = default_value
480
+ . as_ref ( )
481
+ . map ( |dv| quote ! ( default_value = #dv, ) )
482
+ . unwrap_or_default ( ) ;
531
483
532
- let short_attr = short. map ( |c| quote ! ( short = #c, ) ) ;
533
- let long_attr = if let Some ( ref l) = long {
534
- quote ! ( long = #l, )
535
- } else {
536
- quote ! ( long = #name_str, )
537
- } ;
538
-
539
- let default_attr = if let Some ( dv) = default_value {
540
- quote ! ( default_value = #dv, )
541
- } else {
542
- quote ! ( )
543
- } ;
544
-
545
- // If it's bool, use Option<bool> + ArgAction::SetTrue
484
+ // If bool, we parse it via Option<bool> + ArgAction::SetTrue
546
485
let ( final_ty, action) = if is_bool_type ( ty) {
547
486
(
548
487
quote ! ( Option <bool >) ,
@@ -552,26 +491,34 @@ fn generate_cli_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
552
491
( quote ! ( Option <#ty>) , quote ! ( ) )
553
492
} ;
554
493
555
- Some ( quote ! {
556
- #[ arg( #short_attr #long_attr #default_attr #action) ]
557
- pub #field_name: #final_ty
558
- } )
494
+ // If it's marked #[positional], skip long/short and let Clap treat it as a positional.
495
+ if * is_positional {
496
+ Some ( quote ! {
497
+ #[ arg( #action #default_attr) ]
498
+ pub #field_name: #final_ty
499
+ } )
500
+ } else {
501
+ let short_attr = short. map ( |c| quote ! ( short = #c, ) ) ;
502
+ let long_attr = if let Some ( ref l) = long {
503
+ quote ! ( long = #l, )
504
+ } else {
505
+ quote ! ( long = #name_str, )
506
+ } ;
507
+ Some ( quote ! {
508
+ #[ arg( #short_attr #long_attr #default_attr #action) ]
509
+ pub #field_name: #final_ty
510
+ } )
511
+ }
559
512
}
560
513
}
561
514
}
562
515
563
- /// Generates the field definition for the hidden config struct.
564
- /// All fields are marked with #[serde(default)] to handle missing values.
565
516
fn generate_config_field_tokens ( pf : & FieldInfo ) -> Option < TokenStream2 > {
566
517
match pf. availability {
567
- FieldAvailability :: CliOnly => {
568
- // No config field for CLI-only
569
- None
570
- }
518
+ FieldAvailability :: CliOnly => None ,
571
519
FieldAvailability :: ConfigOnly | FieldAvailability :: CliAndConfig => {
572
520
let field_name = & pf. ident ;
573
521
let ty = & pf. ty ;
574
- // We just do `#[serde(default)] pub field_name: T`
575
522
Some ( quote ! {
576
523
#[ serde( default ) ]
577
524
pub #field_name: #ty
@@ -580,22 +527,15 @@ fn generate_config_field_tokens(pf: &FieldInfo) -> Option<TokenStream2> {
580
527
}
581
528
}
582
529
583
- /// Generates the expression that combines CLI and config values for a field.
584
- /// The merge strategy depends on:
585
- /// - Field availability (cli_only, config_only, cli_and_config)
586
- /// - Whether the field is a Vec (uses multi_value_behavior)
587
- /// - For non-Vec fields, CLI takes precedence if present
588
530
fn generate_merge_expr ( pf : & FieldInfo ) -> TokenStream2 {
589
531
let field_name = & pf. ident ;
590
532
match pf. availability {
591
533
FieldAvailability :: CliOnly => {
592
- // final = cli.field.unwrap_or_default()
593
534
quote ! {
594
535
cli. #field_name. unwrap_or_default( )
595
536
}
596
537
}
597
538
FieldAvailability :: ConfigOnly => {
598
- // final = cfg.field
599
539
quote ! {
600
540
cfg. #field_name
601
541
}
@@ -622,7 +562,6 @@ fn generate_merge_expr(pf: &FieldInfo) -> TokenStream2 {
622
562
} ,
623
563
}
624
564
} else {
625
- // normal scalar
626
565
quote ! {
627
566
cli. #field_name. unwrap_or( cfg. #field_name)
628
567
}
@@ -631,7 +570,6 @@ fn generate_merge_expr(pf: &FieldInfo) -> TokenStream2 {
631
570
}
632
571
}
633
572
634
- /// Determines if a type is a bool by checking its path segments
635
573
fn is_bool_type ( ty : & syn:: Type ) -> bool {
636
574
if let syn:: Type :: Path ( tp) = ty {
637
575
if let Some ( seg) = tp. path . segments . last ( ) {
@@ -641,7 +579,6 @@ fn is_bool_type(ty: &syn::Type) -> bool {
641
579
false
642
580
}
643
581
644
- /// Determines if a type is a Vec by checking its path segments
645
582
fn is_vec_type ( ty : & syn:: Type ) -> bool {
646
583
if let syn:: Type :: Path ( tp) = ty {
647
584
if let Some ( seg) = tp. path . segments . last ( ) {
0 commit comments