Skip to content

feat(config): support nested environment variables#27

Merged
ren0503 merged 1 commit into
masterfrom
feat/ren/26-support-nested-env
Jan 25, 2026
Merged

feat(config): support nested environment variables#27
ren0503 merged 1 commit into
masterfrom
feat/ren/26-support-nested-env

Conversation

@ren0503

@ren0503 ren0503 commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

Support nested struct in configuration with hybrid loading from YAML and Env files.

@coderabbitai

coderabbitai Bot commented Jan 25, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added new DB environment variables (DB_USER, DB_PASS, DB_NAME) and a default database section in config YAML.
  • Refactor

    • Switched to a standardized environment parsing library for more robust config loading and validation.
  • Stability

    • Parsing failures now surface decisively (fail-fast) to avoid running with invalid configs.
  • Tests

    • Added tests covering merged config behavior and parse-error handling.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Replaced the project's custom reflection-based env parsing with github.com/caarlos0/env/v11, added an internal generic deep-merge function for structs, updated environment/YAML samples and go.mod, and adjusted tests to use the new parser and validate nested-struct merging and parse errors.

Changes

Cohort / File(s) Summary
Env parsing integration
env.go, namespace.go, env_test.go
Replaced custom Scan/reflect logic with env.Parse/env.ParseWithOptions from github.com/caarlos0/env/v11; updated tests to call env.Parse; added fatal log on parse failure in ForFeature; removed manual reflection code.
Configuration files
.env.example, env.yaml
Added DB_USER, DB_PASS, DB_NAME to .env.example; adjusted YAML formatting (unquoted node_env) and added database section (host, port).
Dependency
go.mod
Added dependency github.com/caarlos0/env/v11 v11.3.1.
Struct merging logic
module.go
Replaced common.MergeStruct usage with an internal generic mergeStruct[T any] and mergeRecursive helper implementing deep, recursive field merging.
Module tests
module_test.go
Added tests: Test_Hybrid_Nested (nested struct merge/precedence) and Test_ForRoot_ParseError (parse failure handling).
Test adjustments
env_test.go (also referenced above)
Converted tests from custom config.Scan behavior to env.Parse semantics; changed Special field tag to env:"-"; removed panic-based tests.

Sequence Diagram(s)

sequenceDiagram
  participant ForRoot
  participant YAML_Loader
  participant EnvLoader as env.Parse
  participant Merger as mergeStruct
  participant App

  ForRoot->>YAML_Loader: load YAML config (env.yaml)
  YAML_Loader-->>ForRoot: YAML struct
  ForRoot->>EnvLoader: parse .env/.env.example into struct
  EnvLoader-->>ForRoot: env struct (or error)
  ForRoot->>Merger: merge YAML struct + env struct
  Merger-->>ForRoot: merged config
  ForRoot->>App: provide merged config to feature/provider
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

  • Support nested struct env #26 — Matches replacement of custom Scan with caarlos0/env and the nested-struct merge behavior validated by Test_Hybrid_Nested.

Possibly related PRs

Poem

🐰 I hopped through vars and tags today,
Replaced my mirrors with a tidy way,
I merge the nests where secrets hide,
And parse the fields with gentle pride,
Config carrots ready — hip hooray! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature: adding support for nested environment variables in configuration.
Description check ✅ Passed The description is related to the changeset, explaining the addition of nested struct support with hybrid YAML and env file loading.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ren0503 ren0503 added this to the Config Release v2.2.0 milestone Jan 25, 2026
@ren0503 ren0503 linked an issue Jan 25, 2026 that may be closed by this pull request
@codecov

codecov Bot commented Jan 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.09677% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
module.go 89.47% 1 Missing and 1 partial ⚠️
namespace.go 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Support nested struct in configuration with hybrid loading from YAML and Env files.
@ren0503
ren0503 force-pushed the feat/ren/26-support-nested-env branch from 4ffe9d2 to 15f6cc2 Compare January 25, 2026 07:43
@ren0503
ren0503 merged commit 3a840ee into master Jan 25, 2026
1 of 3 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@namespace.go`:
- Around line 29-34: The ForFeature function currently calls log.Fatalf on
env.ParseWithOptions failure (using env.ParseWithOptions(&value, opts)), which
force-exits instead of returning an error like NewEnv; change this to return the
parse error (or wrap it with context) to match NewEnv's error-returning behavior
and be consistent with how ForRoot handles errors, and remove the call to
log.Fatalf so callers can handle the error.
🧹 Nitpick comments (4)
.env.example (1)

19-23: Add trailing newline and consider consistent formatting.

The static analysis correctly flags that line 23 is missing a trailing newline. Additionally, while this is an example file, consider maintaining consistent formatting with or without spaces around = throughout.

Suggested fix
 MONGO_DBNAME = db
 
 DB_USER = root
 DB_PASS = root
-DB_NAME = db
+DB_NAME = db
+
module.go (2)

96-113: Pointer, slice, and map fields use shallow IsZero semantics.

The current implementation treats pointer, slice, and map fields as primitives—they're overwritten only if non-zero. This means:

  • A non-nil pointer in source replaces target's pointer entirely (no deep merge of pointed-to structs)
  • Slices/maps are replaced wholesale, not merged element-by-element

If nested pointer-to-struct fields need deep merging, additional handling is required. If shallow replacement is intentional for these types, consider adding a comment to document this behavior.


84-94: Potential panic if T is not a struct type.

mergeRecursive calls target.NumField() which panics if the value is not a struct. Since the generic constraint is T any, callers could theoretically pass non-struct types.

Consider either:

  1. Adding a runtime check at the start of mergeStruct
  2. Documenting that T must be a struct type
Optional defensive check
 func mergeStruct[T any](input ...T) T {
 	var result T
 	resultVal := reflect.ValueOf(&result).Elem()
+	if resultVal.Kind() != reflect.Struct {
+		return result
+	}

 	for _, item := range input {
 		inputVal := reflect.ValueOf(item)
 		mergeRecursive(resultVal, inputVal)
 	}

 	return result
 }
env.go (1)

15-22: Consider simplifying this comment.

The comment expresses uncertainty but the code clearly returns an error on godotenv.Load failure. A simpler explanation would suffice:

Suggested simplification
-	// We don't check for error here because it's possible that the user
-	// provides environment variables directly without a .env file.
-	// However, to maintain backward compatibility with previous strict behavior (as enforced by tests),
-	// we will return error if godotenv fails, unless we decide to relax it.
-	// The previous implementation returned error.
+	// Return error if .env file cannot be loaded to maintain backward compatibility.
+	// Consider relaxing this if users need to provide env vars without a file.
 	if err := godotenv.Load(path); err != nil {
 		return nil, err
 	}

Comment thread namespace.go
Comment on lines +29 to +34
opts := env.Options{
DefaultValueTagName: "default",
}
if err := env.ParseWithOptions(&value, opts); err != nil {
log.Fatalf("failed to parse env: %v", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inconsistent error handling: log.Fatalf vs returning error.

ForFeature calls log.Fatalf on parse failure, terminating the program immediately. However, NewEnv in env.go returns errors to the caller. This inconsistency could surprise users:

  • config.NewEnv[T]() → returns error, caller handles it
  • config.ForFeature[T]() → crashes the program on parse error

Consider returning an error or logging without terminating, similar to how ForRoot handles errors (lines 39-41 in module.go continue on error).

🤖 Prompt for AI Agents
In `@namespace.go` around lines 29 - 34, The ForFeature function currently calls
log.Fatalf on env.ParseWithOptions failure (using env.ParseWithOptions(&value,
opts)), which force-exits instead of returning an error like NewEnv; change this
to return the parse error (or wrap it with context) to match NewEnv's
error-returning behavior and be consistent with how ForRoot handles errors, and
remove the call to log.Fatalf so callers can handle the error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support nested struct env

1 participant