Skip to content

Commit

Permalink
feat: support time.Location (#326)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
BorzdeG authored Aug 24, 2024
1 parent aa50469 commit 0136931
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 16 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Complete list:
- `uint8`
- `uint`
- `time.Duration`
- `time.Location`
- `encoding.TextUnmarshaler`
- `url.URL`

Expand Down
41 changes: 27 additions & 14 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 34 additions & 2 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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:":"`

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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])
Expand All @@ -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)

Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 0136931

Please sign in to comment.