@@ -154,34 +154,60 @@ struct GonfigField {
154154/// ```
155155///
156156/// ## `#[gonfig(nested)]`
157- /// **[Experimental]** Marks a field as a nested configuration struct.
157+ /// Marks a field as a nested configuration struct that should be loaded automatically .
158158///
159- /// Currently, nested fields must be manually constructed. This attribute prevents
160- /// the field from being loaded with the parent config.
159+ /// When a field is marked as nested, the macro automatically calls that field type's
160+ /// `from_gonfig()` method, allowing you to compose configuration from multiple structs
161+ /// with their own prefixes and loading logic.
162+ ///
163+ /// **Requirements:**
164+ /// - Nested field types must derive `Gonfig`
165+ /// - Nested field types must implement `Default` or have `#[serde(default)]`
166+ /// - Parent struct must mark nested fields with `#[serde(default)]`
161167///
162168/// **Example:**
163169/// ```rust,ignore
164- /// #[derive(Gonfig, Deserialize)]
165- /// #[Gonfig(env_prefix = "APP")]
166- /// struct Config {
167- /// #[gonfig(nested)]
168- /// server: ServerConfig,
169- /// }
170+ /// use gonfig::Gonfig;
171+ /// use serde::{Deserialize, Serialize};
170172///
171- /// #[derive(Gonfig , Deserialize)]
173+ /// #[derive(Debug , Deserialize, Gonfig )]
172174/// #[Gonfig(env_prefix = "SERVER")]
175+ /// #[serde(default)]
173176/// struct ServerConfig {
177+ /// #[gonfig(default = "127.0.0.1")]
174178/// host: String,
179+ ///
180+ /// #[gonfig(default = "8080")]
181+ /// port: u16,
175182/// }
176183///
177- /// // Manual construction (automatic loading coming in future version):
178- /// fn load_config() -> Result<Config> {
179- /// let server = ServerConfig::from_gonfig()?;
180- /// // Manually combine with parent struct
181- /// Ok(Config { server })
184+ /// impl Default for ServerConfig {
185+ /// fn default() -> Self {
186+ /// Self { host: String::new(), port: 0 }
187+ /// }
182188/// }
189+ ///
190+ /// #[derive(Debug, Deserialize, Gonfig)]
191+ /// #[Gonfig(env_prefix = "APP")]
192+ /// struct AppConfig {
193+ /// #[gonfig(nested)]
194+ /// #[serde(default)] // Required for nested fields
195+ /// server: ServerConfig,
196+ ///
197+ /// #[gonfig(default = "production")]
198+ /// environment: String,
199+ /// }
200+ ///
201+ /// // Automatic loading - ServerConfig loads with SERVER_ prefix
202+ /// let config = AppConfig::from_gonfig()?;
203+ /// println!("Server: {}:{}", config.server.host, config.server.port);
183204/// ```
184205///
206+ /// **Environment Variables:**
207+ /// - `APP_ENVIRONMENT` → AppConfig.environment
208+ /// - `SERVER_HOST` → ServerConfig.host (nested struct uses its own prefix)
209+ /// - `SERVER_PORT` → ServerConfig.port
210+ ///
185211/// ## `#[skip]` or `#[skip_gonfig]`
186212/// Exclude a field from configuration loading. Useful for non-serializable fields or
187213/// fields that should only be set at runtime.
@@ -286,44 +312,50 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
286312 . expect ( "Only structs are supported" )
287313 . fields ;
288314
289- // Separate regular fields from flattened fields
315+ // Separate regular fields from nested fields
290316 let mut regular_mappings = Vec :: new ( ) ;
291317 let mut default_mappings = Vec :: new ( ) ;
318+ let mut nested_fields = Vec :: new ( ) ;
319+ let mut all_fields = Vec :: new ( ) ; // Track all fields for manual construction
292320
293321 for f in fields. iter ( ) . filter ( |f| !f. skip_gonfig && !f. skip ) {
294322 let field_name = f. ident . as_ref ( ) . unwrap ( ) ;
295323 let field_str = field_name. to_string ( ) ;
324+ let field_type = & f. ty ;
296325
297- // Skip nested fields - they should be loaded separately
298- // TODO: Implement automatic nested struct loading with prefix composition
326+ // Collect nested fields for automatic loading
299327 if f. nested {
328+ nested_fields. push ( ( field_name. clone ( ) , field_type. clone ( ) ) ) ;
329+ all_fields. push ( ( field_name. clone ( ) , true ) ) ; // Mark as nested
300330 continue ;
301331 }
302332
333+ all_fields. push ( ( field_name. clone ( ) , false ) ) ; // Mark as regular
334+
303335 // Note: flatten feature is not yet fully implemented
304336 // For now, treat all fields as regular fields
305337 {
306- // Generate expected environment variable name
307- let env_key = if let Some ( custom_name) = & f. env_name {
308- // Use custom name directly if provided
309- custom_name. clone ( )
310- } else if !env_prefix. is_empty ( ) {
311- // Use prefix + field name pattern
312- format ! ( "{}_{}" , env_prefix, field_str. to_uppercase( ) )
313- } else {
314- // Just field name in uppercase
315- field_str. to_uppercase ( )
316- } ;
317-
318338 // Generate CLI argument name (kebab-case)
319339 let cli_key = if let Some ( custom_name) = & f. cli_name {
320340 custom_name. clone ( )
321341 } else {
322342 field_str. replace ( '_' , "-" )
323343 } ;
324344
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+
325353 regular_mappings. push ( quote ! {
326- ( #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+ )
327359 } ) ;
328360
329361 // Handle default values
@@ -335,15 +367,40 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
335367 }
336368 }
337369
370+ // Prepare nested field names and types for code generation
371+ let has_nested = !nested_fields. is_empty ( ) ;
372+ let nested_field_names: Vec < _ > = nested_fields. iter ( ) . map ( |( name, _) | name) . collect ( ) ;
373+ let nested_field_types: Vec < _ > = nested_fields. iter ( ) . map ( |( _, ty) | ty) . collect ( ) ;
374+
338375 quote ! {
339376 impl #impl_generics #name #ty_generics #where_clause {
340377 pub fn from_gonfig( ) -> :: gonfig:: Result <Self > {
341- Self :: from_gonfig_with_builder ( :: gonfig :: ConfigBuilder :: new ( ) )
378+ Self :: from_gonfig_with_parent_prefix ( "" )
342379 }
343380
344- pub fn from_gonfig_with_builder( mut builder: :: gonfig:: ConfigBuilder ) -> :: gonfig:: Result <Self > {
345- // Regular field mappings: (field_name, env_key, cli_key)
346- let field_mappings: Vec <( String , String , String ) > = vec![ #( #regular_mappings) , * ] ;
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, "" )
389+ }
390+
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) , * ] ;
347404
348405 // Default value mappings: (field_name, default_value)
349406 let default_values: Vec <( String , String ) > = vec![ #( #default_mappings) , * ] ;
@@ -352,13 +409,21 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
352409 // Create custom environment source with field mappings
353410 let mut env = :: gonfig:: Environment :: new( ) ;
354411
355- if !#env_prefix . is_empty( ) {
356- env = env. with_prefix( #env_prefix ) ;
412+ if !composed_prefix . is_empty( ) {
413+ env = env. with_prefix( & composed_prefix ) ;
357414 }
358415
359416 // Apply field-level mappings for regular fields
360- for ( field_name, env_key, _cli_key) in & field_mappings {
361- 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) ;
362427 }
363428
364429 builder = builder. with_env_custom( env) ;
@@ -369,7 +434,7 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
369434 let mut cli = :: gonfig:: Cli :: from_args( ) ;
370435
371436 // Apply field-level CLI mappings for regular fields
372- for ( field_name, _env_key , cli_key) in & field_mappings {
437+ for ( field_name, _custom_env_name , cli_key) in & field_mappings {
373438 cli = cli. with_field_mapping( field_name, cli_key) ;
374439 }
375440
@@ -411,27 +476,69 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
411476 builder = builder. with_defaults( :: serde_json:: Value :: Object ( defaults_json) ) ?;
412477 }
413478
414- // Build the final configuration with explicit type
415- builder. build:: <Self >( )
479+ // Build the final configuration
480+ if #has_nested {
481+ // Struct has nested fields - load them automatically with composed prefix
482+ // Each nested struct inherits and composes the parent's prefix
483+ #(
484+ let #nested_field_names = <#nested_field_types>:: from_gonfig_with_parent_prefix( & composed_prefix) ?;
485+ ) *
486+
487+ // Build config value for regular fields (excluding nested fields to avoid conflicts)
488+ let mut config_value = builder. build_value( ) ?;
489+
490+ // Remove nested fields from config_value to avoid conflicts with regular field mapping
491+ if let :: serde_json:: Value :: Object ( ref mut map) = config_value {
492+ #(
493+ map. remove( stringify!( #nested_field_names) ) ;
494+ ) *
495+ }
496+
497+ // Deserialize into Self with nested fields temporarily set to default
498+ let mut result: Self = :: serde_json:: from_value( config_value)
499+ . map_err( |e| :: gonfig:: Error :: Serialization (
500+ format!( "Failed to deserialize config: {}" , e)
501+ ) ) ?;
502+
503+ // Replace nested fields with loaded values
504+ #(
505+ result. #nested_field_names = #nested_field_names;
506+ ) *
507+
508+ Ok ( result)
509+ } else {
510+ // No nested fields - use simple deserialization
511+ builder. build:: <Self >( )
512+ }
416513 }
417514
418515 pub fn gonfig_builder( ) -> :: gonfig:: ConfigBuilder {
419516 let mut builder = :: gonfig:: ConfigBuilder :: new( ) ;
420517
421- // Regular field mappings: (field_name, env_key, cli_key)
422- let field_mappings: Vec <( String , String , String ) > = vec![ #( #regular_mappings) , * ] ;
518+ // Regular field mappings: (field_name, custom_env_name, cli_key)
519+ let field_mappings: Vec <( String , Option <String >, String ) > = vec![ #( #regular_mappings) , * ] ;
520+
521+ // Use env_prefix directly (no parent composition in builder method)
522+ let prefix = #env_prefix;
423523
424524 if #allow_env {
425525 // Create custom environment source with field mappings
426526 let mut env = :: gonfig:: Environment :: new( ) ;
427527
428- if !#env_prefix . is_empty( ) {
429- env = env. with_prefix( #env_prefix ) ;
528+ if !prefix . is_empty( ) {
529+ env = env. with_prefix( prefix ) ;
430530 }
431531
432532 // Apply field-level mappings for regular fields
433- for ( field_name, env_key, _cli_key) in & field_mappings {
434- env = env. with_field_mapping( field_name, env_key) ;
533+ for ( field_name, custom_env_name, _cli_key) in & field_mappings {
534+ let env_key = if let Some ( custom) = custom_env_name {
535+ custom. clone( )
536+ } else if !prefix. is_empty( ) {
537+ format!( "{}_{}" , prefix, field_name. to_uppercase( ) )
538+ } else {
539+ field_name. to_uppercase( )
540+ } ;
541+ env = env. with_field_mapping( field_name, & env_key) ;
435542 }
436543
437544 builder = builder. with_env_custom( env) ;
@@ -442,7 +549,7 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
442549 let mut cli = :: gonfig:: Cli :: from_args( ) ;
443550
444551 // Apply field-level CLI mappings for regular fields
445- for ( field_name, _env_key , cli_key) in & field_mappings {
552+ for ( field_name, _custom_env_name , cli_key) in & field_mappings {
446553 cli = cli. with_field_mapping( field_name, cli_key) ;
447554 }
448555
0 commit comments