From 20d65dc27be6d3a2c6b311f0b761821c8cb39231 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 19 Jun 2024 00:12:49 -0300 Subject: [PATCH 1/5] fix: retract v11.0.1, gate init nil pointers as it would automatically initialize nil pointers. this retracts that version, and gate this new feature behind an `init` tag option. closes #317 refs #306 --- README.md | 14 ++++++++++++++ env.go | 11 +++++++---- env_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++---- go.mod | 2 ++ 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9a693a9..4282b73 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,20 @@ $ PORT=8080 go run main.go {Host:localhost Port:8080 Address:localhost:8080} ``` +## Init `nil` pointers + +You can automatically initialize `nil` pointers regardless of if a variable is +set for them or not. +This behavior can be enabled by using the `init` tag option. + +Example: + +```go +type config struct { + URL *url.URL `env:"URL,init"` +} +``` + ## Not Empty fields While `required` demands the environment variable to be set, it doesn't check diff --git a/env.go b/env.go index d4bdf55..ee6e658 100644 --- a/env.go +++ b/env.go @@ -300,13 +300,13 @@ func doParseField(refField reflect.Value, refTypeField reflect.StructField, proc return err } - if isStructPtr(refField) && refField.IsNil() { + if isStructPtr(refField) && refField.IsNil() && params.Init { refField.Set(reflect.New(refField.Type().Elem())) refField = refField.Elem() - } - if _, ok := opts.FuncMap[refField.Type()]; ok { - return nil + if _, ok := opts.FuncMap[refField.Type()]; ok { + return nil + } } if reflect.Struct == refField.Kind() { @@ -361,6 +361,7 @@ type FieldParams struct { Unset bool NotEmpty bool Expand bool + Init bool } func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { @@ -393,6 +394,8 @@ func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, err result.NotEmpty = true case "expand": result.Expand = true + case "init": + result.Init = true default: return FieldParams{}, newNoSupportedTagOptionError(tag) } diff --git a/env_test.go b/env_test.go index f322e10..6d9c549 100644 --- a/env_test.go +++ b/env_test.go @@ -137,9 +137,10 @@ type Config struct { } type ParentStruct struct { - InnerStruct *InnerStruct - unexported *InnerStruct - Ignored *http.Client + InnerStruct *InnerStruct `env:",init"` + NilInnerStruct *InnerStruct + unexported *InnerStruct + Ignored *http.Client } type InnerStruct struct { @@ -2041,7 +2042,7 @@ func TestIssue234(t *testing.T) { Str string `env:"TEST"` } type ComplexConfig struct { - Foo *Test `envPrefix:"FOO_"` + Foo *Test `envPrefix:"FOO_" env:",init"` Bar Test `envPrefix:"BAR_"` Clean *Test } @@ -2077,3 +2078,43 @@ func TestIssue308(t *testing.T) { isNoErr(t, Parse(&cfg)) isEqual(t, Issue308Map{"FOO": []string{"BAR", "ZAZ"}}, cfg.Inner) } + +func TestIssue317(t *testing.T) { + type TestConfig struct { + U1 *url.URL `env:"U1"` + U2 *url.URL `env:"U2,init"` + } + cases := []struct { + desc string + environment map[string]string + expectedU1, expectedU2 *url.URL + }{ + { + desc: "unset", + environment: map[string]string{}, + expectedU1: nil, + expectedU2: &url.URL{}, + }, + { + desc: "empty", + environment: map[string]string{"U1": "", "U2": ""}, + expectedU1: nil, + expectedU2: &url.URL{}, + }, + { + desc: "set", + environment: map[string]string{"U1": "https://example.com/"}, + expectedU1: &url.URL{Scheme: "https", Host: "example.com", Path: "/"}, + expectedU2: &url.URL{}, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + cfg := TestConfig{} + err := ParseWithOptions(&cfg, Options{Environment: tc.environment}) + isNoErr(t, err) + isEqual(t, tc.expectedU1, cfg.U1) + isEqual(t, tc.expectedU2, cfg.U2) + }) + } +} diff --git a/go.mod b/go.mod index 135b454..de6a2b0 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/caarlos0/env/v11 +retract v11.0.1 // v11.0.1 accidentally introduced a breaking change regarding the behavior of uninitalized pointers. You can now chose to auto-innit nil pointers by setting the 'init' tag option. + go 1.18 From 57aa5b22b2f9044fbe099e17a5609f6fef857c42 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 19 Jun 2024 00:18:12 -0300 Subject: [PATCH 2/5] test: make sure #310 is covered too closes #310 --- env_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/env_test.go b/env_test.go index 6d9c549..533de62 100644 --- a/env_test.go +++ b/env_test.go @@ -2118,3 +2118,12 @@ func TestIssue317(t *testing.T) { }) } } + +func TestIssue310(t *testing.T) { + type TestConfig struct { + URL *url.URL + } + cfg, err := ParseAs[TestConfig]() + isNoErr(t, err) + isEqual(t, nil, cfg.URL) +} From ba8b0d27eb2e98244cc46245b1462df2241d0a43 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 19 Jun 2024 10:30:48 -0300 Subject: [PATCH 3/5] test: add test for multiple tag options Signed-off-by: Carlos Alexandro Becker --- env_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/env_test.go b/env_test.go index 533de62..2891fac 100644 --- a/env_test.go +++ b/env_test.go @@ -2127,3 +2127,27 @@ func TestIssue310(t *testing.T) { isNoErr(t, err) isEqual(t, nil, cfg.URL) } + +func TestMultipleTagOptions(t *testing.T) { + type TestConfig struct { + URL *url.URL `env:"URL,init,unset"` + } + t.Run("unset", func(t *testing.T) { + cfg, err := ParseAs[TestConfig]() + isNoErr(t, err) + isEqual(t, &url.URL{}, cfg.URL) + }) + t.Run("empty", func(t *testing.T) { + t.Setenv("URL", "") + cfg, err := ParseAs[TestConfig]() + isNoErr(t, err) + isEqual(t, &url.URL{}, cfg.URL) + }) + t.Run("set", func(t *testing.T) { + t.Setenv("URL", "https://github.com/caarlos0") + cfg, err := ParseAs[TestConfig]() + isNoErr(t, err) + isEqual(t, &url.URL{Scheme: "https", Host: "github.com", Path: "/caarlos0"}, cfg.URL) + isEqual(t, "", os.Getenv("URL")) + }) +} From 34aeedc4e68b7c01b5085eb4949cd29594c3e49f Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 19 Jun 2024 10:33:53 -0300 Subject: [PATCH 4/5] fix: typo --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index de6a2b0..215c28b 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/caarlos0/env/v11 -retract v11.0.1 // v11.0.1 accidentally introduced a breaking change regarding the behavior of uninitalized pointers. You can now chose to auto-innit nil pointers by setting the 'init' tag option. +retract v11.0.1 // v11.0.1 accidentally introduced a breaking change regarding the behavior of nil pointers. You can now chose to auto-initialize them by setting the `init` tag option. go 1.18 From 205b4b07f63d9fcec946cc97f9bc900cfd05407b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Wed, 19 Jun 2024 10:34:43 -0300 Subject: [PATCH 5/5] perf: cheap first Signed-off-by: Carlos Alexandro Becker --- env.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.go b/env.go index ee6e658..8b8b724 100644 --- a/env.go +++ b/env.go @@ -300,7 +300,7 @@ func doParseField(refField reflect.Value, refTypeField reflect.StructField, proc return err } - if isStructPtr(refField) && refField.IsNil() && params.Init { + if params.Init && isStructPtr(refField) && refField.IsNil() { refField.Set(reflect.New(refField.Type().Elem())) refField = refField.Elem()