@@ -335,27 +335,27 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
335335 // Note: flatten feature is not yet fully implemented
336336 // For now, treat all fields as regular fields
337337 {
338- // Generate expected environment variable name
339- let env_key = if let Some ( custom_name) = & f. env_name {
340- // Use custom name directly if provided
341- custom_name. clone ( )
342- } else if !env_prefix. is_empty ( ) {
343- // Use prefix + field name pattern
344- format ! ( "{}_{}" , env_prefix, field_str. to_uppercase( ) )
345- } else {
346- // Just field name in uppercase
347- field_str. to_uppercase ( )
348- } ;
349-
350338 // Generate CLI argument name (kebab-case)
351339 let cli_key = if let Some ( custom_name) = & f. cli_name {
352340 custom_name. clone ( )
353341 } else {
354342 field_str. replace ( '_' , "-" )
355343 } ;
356344
345+ // Store field info for runtime env key computation
346+ // We can't pre-compute env_key because it depends on composed_prefix
347+ let custom_env_opt = if let Some ( custom) = & f. env_name {
348+ quote ! { Some ( #custom. to_string( ) ) }
349+ } else {
350+ quote ! { None }
351+ } ;
352+
357353 regular_mappings. push ( quote ! {
358- ( #field_str. to_string( ) , #env_key. to_string( ) , #cli_key. to_string( ) )
354+ (
355+ #field_str. to_string( ) ,
356+ #custom_env_opt,
357+ #cli_key. to_string( )
358+ )
359359 } ) ;
360360
361361 // Handle default values
@@ -375,12 +375,32 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
375375 quote ! {
376376 impl #impl_generics #name #ty_generics #where_clause {
377377 pub fn from_gonfig( ) -> :: gonfig:: Result <Self > {
378- Self :: from_gonfig_with_builder( :: gonfig:: ConfigBuilder :: new( ) )
378+ Self :: from_gonfig_with_parent_prefix( "" )
379+ }
380+
381+ /// Load configuration with a parent prefix for hierarchical composition.
382+ /// When used as a nested config, the parent prefix is automatically prepended.
383+ pub fn from_gonfig_with_parent_prefix( parent_prefix: & str ) -> :: gonfig:: Result <Self > {
384+ Self :: from_gonfig_with_builder_and_parent( :: gonfig:: ConfigBuilder :: new( ) , parent_prefix)
385+ }
386+
387+ pub fn from_gonfig_with_builder( builder: :: gonfig:: ConfigBuilder ) -> :: gonfig:: Result <Self > {
388+ Self :: from_gonfig_with_builder_and_parent( builder, "" )
379389 }
380390
381- pub fn from_gonfig_with_builder( mut builder: :: gonfig:: ConfigBuilder ) -> :: gonfig:: Result <Self > {
382- // Regular field mappings: (field_name, env_key, cli_key)
383- let field_mappings: Vec <( String , String , String ) > = vec![ #( #regular_mappings) , * ] ;
391+ fn from_gonfig_with_builder_and_parent( mut builder: :: gonfig:: ConfigBuilder , parent_prefix: & str ) -> :: gonfig:: Result <Self > {
392+ // Compose prefix: parent_prefix + current env_prefix
393+ let composed_prefix = if parent_prefix. is_empty( ) {
394+ #env_prefix. to_string( )
395+ } else if #env_prefix. is_empty( ) {
396+ parent_prefix. to_string( )
397+ } else {
398+ format!( "{}_{}" , parent_prefix, #env_prefix)
399+ } ;
400+
401+ // Regular field mappings: (field_name, custom_env_name, cli_key)
402+ // env_key will be computed at runtime using composed_prefix
403+ let field_mappings: Vec <( String , Option <String >, String ) > = vec![ #( #regular_mappings) , * ] ;
384404
385405 // Default value mappings: (field_name, default_value)
386406 let default_values: Vec <( String , String ) > = vec![ #( #default_mappings) , * ] ;
@@ -389,13 +409,21 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
389409 // Create custom environment source with field mappings
390410 let mut env = :: gonfig:: Environment :: new( ) ;
391411
392- if !#env_prefix . is_empty( ) {
393- env = env. with_prefix( #env_prefix ) ;
412+ if !composed_prefix . is_empty( ) {
413+ env = env. with_prefix( & composed_prefix ) ;
394414 }
395415
396416 // Apply field-level mappings for regular fields
397- for ( field_name, env_key, _cli_key) in & field_mappings {
398- env = env. with_field_mapping( field_name, env_key) ;
417+ // Compute env_key at runtime using composed_prefix
418+ for ( field_name, custom_env_name, _cli_key) in & field_mappings {
419+ let env_key = if let Some ( custom) = custom_env_name {
420+ custom. clone( )
421+ } else if !composed_prefix. is_empty( ) {
422+ format!( "{}_{}" , composed_prefix, field_name. to_uppercase( ) )
423+ } else {
424+ field_name. to_uppercase( )
425+ } ;
426+ env = env. with_field_mapping( field_name, & env_key) ;
399427 }
400428
401429 builder = builder. with_env_custom( env) ;
@@ -406,7 +434,7 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
406434 let mut cli = :: gonfig:: Cli :: from_args( ) ;
407435
408436 // Apply field-level CLI mappings for regular fields
409- for ( field_name, _env_key , cli_key) in & field_mappings {
437+ for ( field_name, _custom_env_name , cli_key) in & field_mappings {
410438 cli = cli. with_field_mapping( field_name, cli_key) ;
411439 }
412440
@@ -450,10 +478,10 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
450478
451479 // Build the final configuration
452480 if #has_nested {
453- // Struct has nested fields - load them automatically
454- // Note: Nested fields must have #[serde(default)] or fields must be Option<T>
481+ // Struct has nested fields - load them automatically with composed prefix
482+ // Each nested struct inherits and composes the parent's prefix
455483 #(
456- let #nested_field_names = <#nested_field_types>:: from_gonfig ( ) ?;
484+ let #nested_field_names = <#nested_field_types>:: from_gonfig_with_parent_prefix ( & composed_prefix ) ?;
457485 ) *
458486
459487 // Build config value for regular fields
@@ -483,20 +511,30 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
483511 pub fn gonfig_builder( ) -> :: gonfig:: ConfigBuilder {
484512 let mut builder = :: gonfig:: ConfigBuilder :: new( ) ;
485513
486- // Regular field mappings: (field_name, env_key, cli_key)
487- let field_mappings: Vec <( String , String , String ) > = vec![ #( #regular_mappings) , * ] ;
514+ // Regular field mappings: (field_name, custom_env_name, cli_key)
515+ let field_mappings: Vec <( String , Option <String >, String ) > = vec![ #( #regular_mappings) , * ] ;
516+
517+ // Use env_prefix directly (no parent composition in builder method)
518+ let prefix = #env_prefix;
488519
489520 if #allow_env {
490521 // Create custom environment source with field mappings
491522 let mut env = :: gonfig:: Environment :: new( ) ;
492523
493- if !#env_prefix . is_empty( ) {
494- env = env. with_prefix( #env_prefix ) ;
524+ if !prefix . is_empty( ) {
525+ env = env. with_prefix( prefix ) ;
495526 }
496527
497528 // Apply field-level mappings for regular fields
498- for ( field_name, env_key, _cli_key) in & field_mappings {
499- env = env. with_field_mapping( field_name, env_key) ;
529+ for ( field_name, custom_env_name, _cli_key) in & field_mappings {
530+ let env_key = if let Some ( custom) = custom_env_name {
531+ custom. clone( )
532+ } else if !prefix. is_empty( ) {
533+ format!( "{}_{}" , prefix, field_name. to_uppercase( ) )
534+ } else {
535+ field_name. to_uppercase( )
536+ } ;
537+ env = env. with_field_mapping( field_name, & env_key) ;
500538 }
501539
502540 builder = builder. with_env_custom( env) ;
@@ -507,7 +545,7 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
507545 let mut cli = :: gonfig:: Cli :: from_args( ) ;
508546
509547 // Apply field-level CLI mappings for regular fields
510- for ( field_name, _env_key , cli_key) in & field_mappings {
548+ for ( field_name, _custom_env_name , cli_key) in & field_mappings {
511549 cli = cli. with_field_mapping( field_name, cli_key) ;
512550 }
513551
0 commit comments