Skip to content

Commit e104cb7

Browse files
committed
feat: Implement automatic nested struct loading
Implements automatic loading for nested configuration structs marked with #[gonfig(nested)] attribute. Nested structs now load automatically by calling their own from_gonfig() method, enabling clean hierarchical configuration. How it works: - When from_gonfig() is called on a parent struct, it automatically loads all nested fields by calling their from_gonfig() methods - Each nested struct uses its own env_prefix for loading - Regular fields load from parent's prefix - Nested fields load from their own prefix Requirements: - Nested field types must derive Gonfig - Nested field types must implement Default - Parent struct must mark nested fields with #[serde(default)] Example: #[derive(Gonfig, Default)] #[Gonfig(env_prefix = "APP")] #[serde(default)] struct Config { #[gonfig(nested)] #[serde(default)] server: ServerConfig, } #[derive(Gonfig, Default)] #[Gonfig(env_prefix = "SERVER")] #[serde(default)] struct ServerConfig { host: String, } // Automatic loading - just works! let config = Config::from_gonfig()?; Environment variable mapping: - APP_ENVIRONMENT -> Config.environment - SERVER_HOST -> ServerConfig.host (loaded via nested from_gonfig) - SERVER_PORT -> ServerConfig.port Implementation details: - Detect nested fields during macro expansion - Generate code to call from_gonfig() for each nested field - Deserialize regular fields from config builder - Replace nested field defaults with loaded values Testing: - Added 2 test files with 6 new tests - Updated issue #23 test to demonstrate automatic loading - Tests basic nesting, deep nesting (3 levels), multiple nested fields - All existing tests pass (63 total) Fixes #25
1 parent 36dc786 commit e104cb7

4 files changed

Lines changed: 311 additions & 29 deletions

File tree

gonfig_derive/src/lib.rs

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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,20 +312,26 @@ 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
{
@@ -335,6 +367,11 @@ 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> {
@@ -411,8 +448,36 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
411448
builder = builder.with_defaults(::serde_json::Value::Object(defaults_json))?;
412449
}
413450

414-
// Build the final configuration with explicit type
415-
builder.build::<Self>()
451+
// Build the final configuration
452+
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>
455+
#(
456+
let #nested_field_names = <#nested_field_types>::from_gonfig()?;
457+
)*
458+
459+
// Build config value for regular fields
460+
let mut config_value = builder.build_value()?;
461+
462+
// Don't add nested fields to the config_value - let serde use Default for them
463+
// This requires nested field types to have #[serde(default)] at struct or field level
464+
465+
// Deserialize into Self, nested fields will use Default temporarily
466+
let mut result: Self = ::serde_json::from_value(config_value)
467+
.map_err(|e| ::gonfig::Error::Serialization(
468+
format!("Failed to deserialize config: {}", e)
469+
))?;
470+
471+
// Replace nested fields with loaded values
472+
#(
473+
result.#nested_field_names = #nested_field_names;
474+
)*
475+
476+
Ok(result)
477+
} else {
478+
// No nested fields - use simple deserialization
479+
builder.build::<Self>()
480+
}
416481
}
417482

418483
pub fn gonfig_builder() -> ::gonfig::ConfigBuilder {

tests/issue_23_nested_attr.rs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@ use serde::{Deserialize, Serialize};
1111
#[gonfig(env_prefix = "APP")]
1212
pub struct Config {
1313
#[gonfig(nested)]
14+
#[serde(default)] // Required for automatic nested loading
1415
pub server: ServerConfig,
1516

1617
#[gonfig(default = "production")]
1718
pub environment: String,
1819
}
1920

20-
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
21+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig, Default)]
2122
#[gonfig(env_prefix = "SERVER")]
23+
#[serde(default)] // Allows automatic nested loading
2224
pub struct ServerConfig {
2325
#[gonfig(default = "127.0.0.1")]
2426
pub host: String,
@@ -51,17 +53,22 @@ mod tests {
5153
}
5254

5355
#[test]
54-
fn test_manual_nested_composition() {
55-
// Demonstrate the current recommended pattern for nested configs
56-
// (Automatic composition will be added in a future version)
56+
fn test_automatic_nested_loading_with_core_alias() {
57+
// **Enhancement**: As of v0.1.12, nested fields are automatically loaded!
58+
// No manual composition needed anymore
59+
60+
let config = Config::from_gonfig();
61+
assert!(
62+
config.is_ok(),
63+
"Config with nested fields should load automatically: {:?}",
64+
config.err()
65+
);
5766

58-
let server = ServerConfig::from_gonfig().expect("Server config should load");
59-
let config = Config {
60-
server,
61-
environment: "production".to_string(),
62-
};
67+
let config = config.unwrap();
6368

69+
// Nested struct was loaded automatically via ServerConfig::from_gonfig()
6470
assert_eq!(config.server.host, "127.0.0.1");
71+
assert_eq!(config.server.port, 3000);
6572
assert_eq!(config.environment, "production");
6673
}
6774
}

tests/nested_auto_load_basic.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Test automatic nested struct loading - basic scenario
2+
// Uses unique env vars to avoid test interference
3+
4+
use gonfig::Gonfig;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig, Default)]
8+
#[gonfig(env_prefix = "BASIC_SERVER")]
9+
#[serde(default)]
10+
pub struct BasicServerConfig {
11+
#[gonfig(default = "127.0.0.1")]
12+
pub host: String,
13+
14+
#[gonfig(default = "3000")]
15+
pub port: u16,
16+
}
17+
18+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
19+
#[gonfig(env_prefix = "BASIC_APP")]
20+
pub struct BasicAppConfig {
21+
#[gonfig(nested)]
22+
#[serde(default)]
23+
pub server: BasicServerConfig,
24+
25+
#[gonfig(default = "production")]
26+
pub environment: String,
27+
}
28+
29+
#[cfg(test)]
30+
mod tests {
31+
use super::*;
32+
33+
#[test]
34+
fn test_basic_automatic_nested_loading() {
35+
let config = BasicAppConfig::from_gonfig();
36+
37+
assert!(
38+
config.is_ok(),
39+
"Should automatically load nested struct: {:?}",
40+
config.err()
41+
);
42+
43+
let config = config.unwrap();
44+
45+
// Verify nested struct was loaded automatically
46+
assert_eq!(config.server.host, "127.0.0.1");
47+
assert_eq!(config.server.port, 3000);
48+
assert_eq!(config.environment, "production");
49+
}
50+
}

0 commit comments

Comments
 (0)