Skip to content

Commit 6388feb

Browse files
authored
Merge pull request #24 from 0xvasanth/fix/core-alias-qualified-paths
fix: Support nested attribute and core/std aliasing in derive macro
2 parents ceefdea + d26420b commit 6388feb

6 files changed

Lines changed: 442 additions & 4 deletions

File tree

ISSUE_23_CURRENT_STATUS.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Issue #23 - Current Status & Analysis
2+
3+
## Summary
4+
5+
Issue #23 is more complex than initially described. The actual problem is:
6+
7+
1. User tries to use `#[gonfig(nested)]` attribute - **which doesn't exist**
8+
2. When darling (attribute parser) encounters unknown attribute, it generates error with unqualified `core::compile_error!()`
9+
3. When user has `use core as tradesmith_core;`, the compiler can't resolve the error message
10+
11+
## What We've Done So Far
12+
13+
### Commit 1: `9754b21` (Initial Fix - Incomplete)
14+
- Fixed `std::path::Path` to use fully qualified paths (::std::path::Path)
15+
- Added comprehensive regression tests for std/core aliasing
16+
- **BUT**: This didn't fix the user's actual issue
17+
18+
### Current Changes (Uncommitted)
19+
- Added `nested` field to `GonfigField` struct
20+
- Marked as `#[allow(dead_code)]` and reserved for future use
21+
- Now the `#[gonfig(nested)]` attribute is **accepted** (prevents darling error)
22+
- Test confirms it compiles with core alias
23+
24+
## The Real Question
25+
26+
**We need user clarification on what `nested` should do:**
27+
28+
### Option A: Accept But Don't Implement (Current State)
29+
- ✅ Fixes compilation error
30+
- ✅ Allows code to compile with core alias
31+
-`nested` attribute does nothing functionally
32+
- ❌ Misleading to users who expect it to work
33+
34+
### Option B: Implement Full Nested Support
35+
- ✅ Provides actual functionality
36+
- ✅ Useful feature for users
37+
- ❌ More complex implementation
38+
- ❌ Need to define exact behavior:
39+
- How do prefixes work?
40+
- Do fields flatten or stay nested?
41+
- How do defaults propagate?
42+
43+
### Option C: Remove from Issue Example
44+
- Ask user if they actually need `nested` feature
45+
- Maybe they just used it as an example
46+
- They might only need the std::path::Path fix
47+
48+
## Waiting For
49+
50+
GitHub comment posted: https://github.com/0xvasanth/gonfig/issues/23#issuecomment-3733426020
51+
52+
**Questions asked:**
53+
1. What should `nested` do functionally?
54+
2. How should environment variable prefixes work with nested structs?
55+
3. Is this the same as the existing `flatten` attribute?
56+
4. Do they prefer Option A, B, or C?
57+
58+
## Test Results
59+
60+
All new tests pass:
61+
- ✅ issue_23_core_alias (2 tests)
62+
- ✅ issue_23_std_alias (2 tests)
63+
- ✅ issue_23_both_aliases (4 tests)
64+
- ✅ issue_23_nested_attr (1 test) - **New: verifies nested compiles**
65+
66+
**Total: 9 passing regression tests for issue #23**
67+
68+
## Files Changed
69+
70+
```
71+
gonfig_derive/src/lib.rs - Added nested field
72+
tests/issue_23_nested_attr.rs - New test for nested attribute
73+
```
74+
75+
## Next Steps
76+
77+
1. **Wait for user response** on GitHub issue
78+
2. Based on response:
79+
- If Option A: Amend commit, add docs, done
80+
- If Option B: Implement full nested feature
81+
- If Option C: Revert nested, focus on other fixes
82+
83+
## Technical Notes
84+
85+
### Why the Original Fix Wasn't Enough
86+
87+
The std::path::Path fix (commit `9754b21`) solved ONE potential issue but not THE issue in the user's reproduction case. Their example specifically uses `#[gonfig(nested)]` which triggers a different code path in darling.
88+
89+
### The Darling Issue
90+
91+
Darling generates compile errors like this:
92+
```rust
93+
::core::compile_error!("Unknown field: `nested`")
94+
```
95+
96+
When user has `use core as my_core;`, this becomes:
97+
```rust
98+
my_core::compile_error!("Unknown field: `nested`") // ERROR: compile_error not in my_core
99+
```
100+
101+
By adding `nested` to GonfigField, darling accepts it and doesn't generate the error.
102+
103+
### Complete Fix Requires
104+
105+
1. ✅ Fully qualified std::path::Path (done in `9754b21`)
106+
2. ✅ Accept `nested` attribute (done, uncommitted)
107+
3. ❓ Implement `nested` functionality (pending user clarification)

gonfig_derive/src/lib.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ struct GonfigField {
4646
#[darling(default)]
4747
flatten: bool,
4848

49+
// Reserved for future use (nested configuration feature)
50+
#[allow(dead_code)]
51+
#[darling(default)]
52+
nested: bool,
53+
4954
#[darling(default)]
5055
default: Option<String>,
5156
}
@@ -148,6 +153,35 @@ struct GonfigField {
148153
/// }
149154
/// ```
150155
///
156+
/// ## `#[gonfig(nested)]`
157+
/// **[Experimental]** Marks a field as a nested configuration struct.
158+
///
159+
/// Currently, nested fields must be manually constructed. This attribute prevents
160+
/// the field from being loaded with the parent config.
161+
///
162+
/// **Example:**
163+
/// ```rust,ignore
164+
/// #[derive(Gonfig, Deserialize)]
165+
/// #[Gonfig(env_prefix = "APP")]
166+
/// struct Config {
167+
/// #[gonfig(nested)]
168+
/// server: ServerConfig,
169+
/// }
170+
///
171+
/// #[derive(Gonfig, Deserialize)]
172+
/// #[Gonfig(env_prefix = "SERVER")]
173+
/// struct ServerConfig {
174+
/// host: String,
175+
/// }
176+
///
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 })
182+
/// }
183+
/// ```
184+
///
151185
/// ## `#[skip]` or `#[skip_gonfig]`
152186
/// Exclude a field from configuration loading. Useful for non-serializable fields or
153187
/// fields that should only be set at runtime.
@@ -260,6 +294,12 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
260294
let field_name = f.ident.as_ref().unwrap();
261295
let field_str = field_name.to_string();
262296

297+
// Skip nested fields - they should be loaded separately
298+
// TODO: Implement automatic nested struct loading with prefix composition
299+
if f.nested {
300+
continue;
301+
}
302+
263303
// Note: flatten feature is not yet fully implemented
264304
// For now, treat all fields as regular fields
265305
{
@@ -338,19 +378,20 @@ fn generate_gonfig_impl(opts: &GonfigOpts) -> proc_macro2::TokenStream {
338378

339379
if #allow_config {
340380
// Config file support - check for default config files
341-
use std::path::Path;
381+
// Note: Using fully qualified paths to avoid conflicts with user's std/core aliases
382+
// See: https://github.com/0xvasanth/gonfig/issues/23
342383

343-
if Path::new("config.toml").exists() {
384+
if ::std::path::Path::new("config.toml").exists() {
344385
builder = match builder.with_file("config.toml") {
345386
Ok(b) => b,
346387
Err(e) => return Err(e),
347388
};
348-
} else if Path::new("config.yaml").exists() {
389+
} else if ::std::path::Path::new("config.yaml").exists() {
349390
builder = match builder.with_file("config.yaml") {
350391
Ok(b) => b,
351392
Err(e) => return Err(e),
352393
};
353-
} else if Path::new("config.json").exists() {
394+
} else if ::std::path::Path::new("config.json").exists() {
354395
builder = match builder.with_file("config.json") {
355396
Ok(b) => b,
356397
Err(e) => return Err(e),

tests/issue_23_both_aliases.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Regression test for issue #23: https://github.com/0xvasanth/gonfig/issues/23
2+
// Comprehensive test with both core and std aliased and all features enabled
3+
4+
#![allow(unused_imports)]
5+
6+
use core as my_core;
7+
use gonfig::Gonfig;
8+
use serde::{Deserialize, Serialize};
9+
use std as my_std;
10+
11+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
12+
#[gonfig(env_prefix = "BOTH_ALIAS", allow_config, allow_cli)]
13+
pub struct FullFeaturedConfig {
14+
#[gonfig(default = "localhost")]
15+
pub hostname: String,
16+
17+
#[gonfig(default = "8080")]
18+
pub port: u16,
19+
20+
#[gonfig(default = "info")]
21+
#[gonfig(env_name = "LOG_LEVEL")]
22+
pub log_level: String,
23+
}
24+
25+
// Test nested configuration
26+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
27+
#[gonfig(env_prefix = "DATABASE", allow_config)]
28+
pub struct DatabaseConfig {
29+
#[gonfig(default = "postgres://localhost/mydb")]
30+
pub url: String,
31+
32+
#[gonfig(default = "10")]
33+
pub max_connections: u32,
34+
35+
#[gonfig(default = "30")]
36+
pub timeout_seconds: u64,
37+
}
38+
39+
// Test with skip attribute
40+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
41+
#[gonfig(env_prefix = "APP", allow_config, allow_cli)]
42+
pub struct AppConfigWithSkip {
43+
#[gonfig(default = "production")]
44+
pub environment: String,
45+
46+
#[skip]
47+
#[serde(skip)]
48+
pub runtime_data: Option<String>,
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use super::*;
54+
55+
#[test]
56+
fn test_both_aliases_all_features() {
57+
// Most comprehensive test: both std and core aliased, all features enabled
58+
let config = FullFeaturedConfig::from_gonfig();
59+
assert!(
60+
config.is_ok(),
61+
"Should compile and run with both std and core aliased"
62+
);
63+
64+
let config = config.unwrap();
65+
assert_eq!(config.hostname, "localhost");
66+
assert_eq!(config.port, 8080);
67+
assert_eq!(config.log_level, "info");
68+
}
69+
70+
#[test]
71+
fn test_both_aliases_nested_config() {
72+
// Test with database configuration
73+
let config = DatabaseConfig::from_gonfig();
74+
assert!(
75+
config.is_ok(),
76+
"Nested config should work with both aliases"
77+
);
78+
79+
let config = config.unwrap();
80+
assert_eq!(config.url, "postgres://localhost/mydb");
81+
assert_eq!(config.max_connections, 10);
82+
assert_eq!(config.timeout_seconds, 30);
83+
}
84+
85+
#[test]
86+
fn test_both_aliases_with_skip() {
87+
// Test with skip attribute
88+
let config = AppConfigWithSkip::from_gonfig();
89+
assert!(config.is_ok(), "Config with skip should work with aliases");
90+
91+
let config = config.unwrap();
92+
assert_eq!(config.environment, "production");
93+
assert_eq!(config.runtime_data, None);
94+
}
95+
96+
#[test]
97+
fn test_builder_pattern_with_aliases() {
98+
// Test the builder pattern also works with aliases
99+
let builder = FullFeaturedConfig::gonfig_builder();
100+
let config = FullFeaturedConfig::from_gonfig_with_builder(builder);
101+
assert!(config.is_ok(), "Builder pattern should work with aliases");
102+
}
103+
}

tests/issue_23_core_alias.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Regression test for issue #23: https://github.com/0xvasanth/gonfig/issues/23
2+
// Tests that the gonfig derive macro works correctly when core crate is aliased
3+
4+
#![allow(unused_imports)]
5+
6+
use core as tradesmith_core; // Alias core to simulate user's environment
7+
use gonfig::Gonfig;
8+
use serde::{Deserialize, Serialize};
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
11+
#[gonfig(env_prefix = "CORE_ALIAS_TEST", allow_config)]
12+
pub struct ConfigWithCoreAlias {
13+
#[gonfig(default = "127.0.0.1")]
14+
pub host: String,
15+
16+
#[gonfig(default = "8080")]
17+
pub port: u16,
18+
}
19+
20+
#[derive(Debug, Clone, Serialize, Deserialize, Gonfig)]
21+
#[gonfig(env_prefix = "SERVER", allow_config, allow_cli)]
22+
pub struct ServerConfigWithCoreAlias {
23+
#[gonfig(default = "localhost")]
24+
pub address: String,
25+
26+
#[gonfig(default = "3000")]
27+
pub server_port: u16,
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
34+
#[test]
35+
fn test_core_alias_basic_with_config() {
36+
// This test verifies that the macro-generated code compiles
37+
// when core is aliased and allow_config is enabled
38+
let config = ConfigWithCoreAlias::from_gonfig();
39+
assert!(config.is_ok(), "Should compile with core alias");
40+
41+
let config = config.unwrap();
42+
assert_eq!(config.host, "127.0.0.1");
43+
assert_eq!(config.port, 8080);
44+
}
45+
46+
#[test]
47+
fn test_core_alias_with_all_features() {
48+
// Test with both allow_config and allow_cli enabled
49+
let config = ServerConfigWithCoreAlias::from_gonfig();
50+
assert!(
51+
config.is_ok(),
52+
"Should compile with core alias and all features"
53+
);
54+
55+
let config = config.unwrap();
56+
assert_eq!(config.address, "localhost");
57+
assert_eq!(config.server_port, 3000);
58+
}
59+
}

0 commit comments

Comments
 (0)