From 013693160323c41f4c9113d5378cd3ade61f7a6e Mon Sep 17 00:00:00 2001 From: Viktor Alenkov Date: Sat, 24 Aug 2024 19:23:40 +0300 Subject: [PATCH] feat: support `time.Location` (#326) * 1. Added support `time.Location` 2. Added minimal config [EditorConfig](https://editorconfig.org/) * Added missing tests for parsing errors * Corrected the method names * I removed the editorconfig file so as not to confuse other developers with its presence. * I removed the editorconfig file so as not to confuse other developers with its presence. --- README.md | 1 + env.go | 41 +++++++++++++++++++++++++++-------------- env_test.go | 36 ++++++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6527dc1..3625727 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Complete list: - `uint8` - `uint` - `time.Duration` +- `time.Location` - `encoding.TextUnmarshaler` - `url.URL` diff --git a/env.go b/env.go index 27776b8..171013c 100644 --- a/env.go +++ b/env.go @@ -87,21 +87,34 @@ var ( func defaultTypeParsers() map[reflect.Type]ParserFunc { return map[reflect.Type]ParserFunc{ - reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) { - u, err := url.Parse(v) - if err != nil { - return nil, newParseValueError("unable to parse URL", err) - } - return *u, nil - }, - reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) { - s, err := time.ParseDuration(v) - if err != nil { - return nil, newParseValueError("unable to parse duration", err) - } - return s, err - }, + reflect.TypeOf(url.URL{}): parseURL, + reflect.TypeOf(time.Nanosecond): parseDuration, + reflect.TypeOf(time.Location{}): parseLocation, + } +} + +func parseURL(v string) (interface{}, error) { + u, err := url.Parse(v) + if err != nil { + return nil, newParseValueError("unable to parse URL", err) + } + return *u, nil +} + +func parseDuration(v string) (interface{}, error) { + d, err := time.ParseDuration(v) + if err != nil { + return nil, newParseValueError("unable to parse duration", err) + } + return d, err +} + +func parseLocation(v string) (interface{}, error) { + loc, err := time.LoadLocation(v) + if err != nil { + return nil, newParseValueError("unable to parse location", err) } + return *loc, nil } // ParserFunc defines the signature of a function that can be used within diff --git a/env_test.go b/env_test.go index 1a567b4..a45c97e 100644 --- a/env_test.go +++ b/env_test.go @@ -108,6 +108,11 @@ type Config struct { DurationPtr *time.Duration `env:"DURATION"` DurationPtrs []*time.Duration `env:"DURATIONS"` + Location time.Location `env:"LOCATION"` + Locations []time.Location `env:"LOCATIONS"` + LocationPtr *time.Location `env:"LOCATION"` + LocationPtrs []*time.Location `env:"LOCATIONS"` + Unmarshaler unmarshaler `env:"UNMARSHALER"` UnmarshalerPtr *unmarshaler `env:"UNMARSHALER"` Unmarshalers []unmarshaler `env:"UNMARSHALERS"` @@ -118,7 +123,7 @@ type Config struct { URLs []url.URL `env:"URLS"` URLPtrs []*url.URL `env:"URLS"` - StringWithdefault string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"` + StringWithDefault string `env:"DATABASE_URL" envDefault:"postgres://localhost:5432/db"` CustomSeparator []string `env:"SEPSTRINGS" envSeparator:":"` @@ -254,6 +259,12 @@ func TestParsesEnv(t *testing.T) { t.Setenv("DURATION", tos(duration1)) t.Setenv("DURATIONS", toss(duration1, duration2)) + location1 := time.UTC + location2, errLoadLocation := time.LoadLocation("Europe/Berlin") + isNoErr(t, errLoadLocation) + t.Setenv("LOCATION", tos(location1)) + t.Setenv("LOCATIONS", toss(location1, location2)) + unmarshaler1 := unmarshaler{time.Minute} unmarshaler2 := unmarshaler{time.Millisecond * 1232} t.Setenv("UNMARSHALER", tos(unmarshaler1.Duration)) @@ -377,6 +388,13 @@ func TestParsesEnv(t *testing.T) { isEqual(t, &duration1, cfg.DurationPtrs[0]) isEqual(t, &duration2, cfg.DurationPtrs[1]) + isEqual(t, *location1, cfg.Location) + isEqual(t, location1, cfg.LocationPtr) + isEqual(t, *location1, cfg.Locations[0]) + isEqual(t, *location2, cfg.Locations[1]) + isEqual(t, location1, cfg.LocationPtrs[0]) + isEqual(t, location2, cfg.LocationPtrs[1]) + isEqual(t, unmarshaler1, cfg.Unmarshaler) isEqual(t, &unmarshaler1, cfg.UnmarshalerPtr) isEqual(t, unmarshaler1, cfg.Unmarshalers[0]) @@ -391,7 +409,7 @@ func TestParsesEnv(t *testing.T) { isEqual(t, url1, cfg.URLPtrs[0].String()) isEqual(t, url2, cfg.URLPtrs[1].String()) - isEqual(t, "postgres://localhost:5432/db", cfg.StringWithdefault) + isEqual(t, "postgres://localhost:5432/db", cfg.StringWithDefault) isEqual(t, nonDefinedStr, cfg.NonDefined.String) isEqual(t, nonDefinedStr, cfg.NestedNonDefined.NonDefined.String) @@ -801,6 +819,20 @@ func TestInvalidDurations(t *testing.T) { isTrue(t, errors.Is(err, ParseError{})) } +func TestInvalidLocation(t *testing.T) { + t.Setenv("LOCATION", "should-be-a-valid-location") + err := Parse(&Config{}) + isErrorWithMessage(t, err, `env: parse error on field "Location" of type "time.Location": unable to parse location: unknown time zone should-be-a-valid-location; parse error on field "LocationPtr" of type "*time.Location": unable to parse location: unknown time zone should-be-a-valid-location`) + isTrue(t, errors.Is(err, ParseError{})) +} + +func TestInvalidLocations(t *testing.T) { + t.Setenv("LOCATIONS", "should-be-a-valid-location,UTC,Europe/Berlin") + err := Parse(&Config{}) + isErrorWithMessage(t, err, `env: parse error on field "Locations" of type "[]time.Location": unable to parse location: unknown time zone should-be-a-valid-location; parse error on field "LocationPtrs" of type "[]*time.Location": unable to parse location: unknown time zone should-be-a-valid-location`) + isTrue(t, errors.Is(err, ParseError{})) +} + func TestParseStructWithoutEnvTag(t *testing.T) { cfg := Config{} isNoErr(t, Parse(&cfg))