Skip to content

Commit

Permalink
refactor: Add error handling to env parsers (#111)
Browse files Browse the repository at this point in the history
* refactor: Add error handling to env parsers

* refactor: Get rid of internal default value parsers

* style: Fix linter warnings
  • Loading branch information
obalunenko authored Jul 27, 2023
1 parent 1887d16 commit 011a926
Show file tree
Hide file tree
Showing 9 changed files with 1,689 additions and 1,269 deletions.
40 changes: 20 additions & 20 deletions getenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ package getenv
import (
"errors"
"fmt"
"os"
"reflect"

"github.com/obalunenko/getenv/internal"
"github.com/obalunenko/getenv/option"
Expand All @@ -64,40 +62,42 @@ var (
)

// Env retrieves the value of the environment variable named by the key.
// If the variable is present in the environment the value will be parsed and returned.
// If the variable is present in the environment, the value will be parsed and returned.
// Otherwise, an error will be returned.
func Env[T internal.EnvParsable](key string, options ...option.Option) (T, error) {
// Create a default value of the same type as the value that we want to get.
var defVal T
var t T

w := internal.NewEnvParser(t)

val := EnvOrDefault(key, defVal, options...)
params := newParseParams(options)

val, err := w.ParseEnv(key, params)
if err != nil {
if errors.Is(err, internal.ErrNotSet) {
return t, fmt.Errorf("failed to get environment variable[%s]: %w", key, ErrNotSet)
}

// If the value is equal to the default value, it means that the value was not parsed.
// This means that the environment variable was not set, or it was set to an invalid value.
if reflect.DeepEqual(val, defVal) {
v, ok := os.LookupEnv(key)
if !ok {
return val, fmt.Errorf("could not get variable[%s]: %w", key, ErrNotSet)
if errors.Is(err, internal.ErrInvalidValue) {
return t, fmt.Errorf("failed to parse environment variable[%s]: %w", key, ErrInvalidValue)
}

return val, fmt.Errorf("could not parse variable[%s] value[%v] to type[%T]: %w", key, v, defVal, ErrInvalidValue)
return t, fmt.Errorf("failed to parse environment variable[%s]: %w", key, err)
}

return val, nil
return val.(T), nil
}

// EnvOrDefault retrieves the value of the environment variable named by the key.
// If the variable is present in the environment the value will be parsed and returned.
// Otherwise, the default value will be returned.
// The value returned will be of the same type as the default value.
func EnvOrDefault[T internal.EnvParsable](key string, defaultVal T, options ...option.Option) T {
w := internal.NewEnvParser(defaultVal)

params := newParseParams(options)

val := w.ParseEnv(key, defaultVal, params)
val, err := Env[T](key, options...)
if err != nil {
return defaultVal
}

return val.(T)
return val
}

// newParseParams creates new parameters from options.
Expand Down
2 changes: 1 addition & 1 deletion getenv_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func ExampleEnv() {
// Output:
// [string]: golly; err: <nil>
// [int]: 123; err: <nil>
// [int]: 0; err: could not parse variable[GH_GETENV_TEST] value[123s4] to type[int]: invalid value
// [int]: 0; err: failed to parse environment variable[GH_GETENV_TEST]: invalid value
// [time.Time]: 2022-01-20 00:00:00 +0000 UTC; err: <nil>
// [[]float64]: [26.89 0.67]; err: <nil>
// [time.Duration]: 2h35m0s; err: <nil>
Expand Down
10 changes: 5 additions & 5 deletions getenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3858,7 +3858,7 @@ func TestEnvInt(t *testing.T) {
expected: expected{
val: 0,
wantError: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Error(t, err) && assert.ErrorIs(t, err, getenv.ErrNotSet)
return assert.Error(t, err) && assert.ErrorContains(t, err, getenv.ErrNotSet.Error())
},
},
},
Expand Down Expand Up @@ -3892,7 +3892,7 @@ func TestEnvInt(t *testing.T) {
expected: expected{
val: 0,
wantError: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Error(t, err) && assert.ErrorIs(t, err, getenv.ErrInvalidValue)
return assert.Error(t, err) && assert.ErrorContains(t, err, getenv.ErrInvalidValue.Error())
},
},
},
Expand Down Expand Up @@ -3945,7 +3945,7 @@ func TestEnvIntSlice(t *testing.T) {
expected: expected{
val: nil,
wantError: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Error(t, err) && assert.ErrorIs(t, err, getenv.ErrNotSet)
return assert.Error(t, err) && assert.ErrorContains(t, err, getenv.ErrNotSet.Error())
},
},
},
Expand Down Expand Up @@ -3981,7 +3981,7 @@ func TestEnvIntSlice(t *testing.T) {
expected: expected{
val: nil,
wantError: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Error(t, err) && assert.ErrorIs(t, err, getenv.ErrInvalidValue)
return assert.Error(t, err) && assert.ErrorContains(t, err, getenv.ErrInvalidValue.Error())
},
},
},
Expand All @@ -4000,7 +4000,7 @@ func TestEnvIntSlice(t *testing.T) {
expected: expected{
val: nil,
wantError: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.Error(t, err) && assert.ErrorIs(t, err, getenv.ErrInvalidValue)
return assert.Error(t, err) && assert.ErrorContains(t, err, getenv.ErrNotSet.Error())
},
},
},
Expand Down
8 changes: 4 additions & 4 deletions internal/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ func (p precondition) maybeSetEnv(tb testing.TB, key string) {
}
}

// getURL is a helper function for getting url.URL from string.
func getURL(tb testing.TB, rawURL string) url.URL {
// getTestURL is a helper function for getting url.URL from string.
func getTestURL(tb testing.TB, rawURL string) url.URL {
tb.Helper()

val, err := url.Parse(rawURL)
Expand All @@ -43,8 +43,8 @@ func getURL(tb testing.TB, rawURL string) url.URL {
return *val
}

// getIP is a helper function for getting net.IP from string.
func getIP(tb testing.TB, raw string) net.IP {
// getTestIP is a helper function for getting net.IP from string.
func getTestIP(tb testing.TB, raw string) net.IP {
tb.Helper()

return net.ParseIP(raw)
Expand Down
17 changes: 16 additions & 1 deletion internal/errors.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
package internal

import "errors"
import (
"errors"
"fmt"
)

var (
// ErrNotSet is an error that is returned when the environment variable is not set.
ErrNotSet = errors.New("not set")
// ErrInvalidValue is an error that is returned when the environment variable is not valid.
ErrInvalidValue = errors.New("invalid value")
)

func newErrInvalidValue(msg string) error {
return newWrapErr(msg, ErrInvalidValue)
}

func newErrNotSet(msg string) error {
return newWrapErr(msg, ErrNotSet)
}

func newWrapErr(msg string, wrapErr error) error {
return fmt.Errorf("%s: %w", msg, wrapErr)
}
98 changes: 33 additions & 65 deletions internal/iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,158 +200,126 @@ func newBoolParser(v any) EnvParser {
// EnvParser interface for parsing environment variables.
type EnvParser interface {
// ParseEnv parses environment variable by key and returns value.
ParseEnv(key string, defaltVal any, options Parameters) any
ParseEnv(key string, options Parameters) (any, error)
}

// stringParser is a parser for string type.
type stringParser string

func (s stringParser) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := stringOrDefault(key, defaltVal.(string))

return val
func (s stringParser) ParseEnv(key string, _ Parameters) (any, error) {
return getString(key)
}

type stringSliceParser []string

func (s stringSliceParser) ParseEnv(key string, defaltVal any, options Parameters) any {
func (s stringSliceParser) ParseEnv(key string, options Parameters) (any, error) {
sep := options.Separator

val := stringSliceOrDefault(key, defaltVal.([]string), sep)

return val
return getStringSlice(key, sep)
}

type numberParser[T Number] struct{}

func (n numberParser[T]) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := numberOrDefaultGen[T](key, defaltVal.(T))

return val
func (n numberParser[T]) ParseEnv(key string, _ Parameters) (any, error) {
return getNumberGen[T](key)
}

type numberSliceParser[S []T, T Number] struct{}

func (i numberSliceParser[S, T]) ParseEnv(key string, defaltVal any, options Parameters) any {
func (i numberSliceParser[S, T]) ParseEnv(key string, options Parameters) (any, error) {
sep := options.Separator

val := numberSliceOrDefaultGen(key, defaltVal.(S), sep)

return val
return getNumberSliceGen[S, T](key, sep)
}

type boolParser bool

func (b boolParser) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := boolOrDefault(key, defaltVal.(bool))

return val
func (b boolParser) ParseEnv(key string, _ Parameters) (any, error) {
return getBool(key)
}

type timeParser time.Time

func (t timeParser) ParseEnv(key string, defaltVal any, options Parameters) any {
func (t timeParser) ParseEnv(key string, options Parameters) (any, error) {
layout := options.Layout

val := timeOrDefault(key, defaltVal.(time.Time), layout)

return val
return getTime(key, layout)
}

type timeSliceParser []time.Time

func (t timeSliceParser) ParseEnv(key string, defaltVal any, options Parameters) any {
func (t timeSliceParser) ParseEnv(key string, options Parameters) (any, error) {
layout := options.Layout
sep := options.Separator

val := timeSliceOrDefault(key, defaltVal.([]time.Time), layout, sep)

return val
return getTimeSlice(key, layout, sep)
}

type durationSliceParser []time.Duration

func (t durationSliceParser) ParseEnv(key string, defaltVal any, options Parameters) any {
func (t durationSliceParser) ParseEnv(key string, options Parameters) (any, error) {
sep := options.Separator

val := durationSliceOrDefault(key, defaltVal.([]time.Duration), sep)

return val
return getDurationSlice(key, sep)
}

type durationParser time.Duration

func (d durationParser) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := durationOrDefault(key, defaltVal.(time.Duration))

return val
func (d durationParser) ParseEnv(key string, _ Parameters) (any, error) {
return getDuration(key)
}

// stringSliceParser is a parser for []string
type urlParser url.URL

func (t urlParser) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := urlOrDefault(key, defaltVal.(url.URL))

return val
func (t urlParser) ParseEnv(key string, _ Parameters) (any, error) {
return getURL(key)
}

// urlSliceParser is a parser for []url.URL
type urlSliceParser []url.URL

func (t urlSliceParser) ParseEnv(key string, defaltVal any, opts Parameters) any {
func (t urlSliceParser) ParseEnv(key string, opts Parameters) (any, error) {
separator := opts.Separator

val := urlSliceOrDefault(key, defaltVal.([]url.URL), separator)

return val
return getURLSlice(key, separator)
}

// ipParser is a parser for net.IP
type ipParser net.IP

func (t ipParser) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := ipOrDefault(key, defaltVal.(net.IP))

return val
func (t ipParser) ParseEnv(key string, _ Parameters) (any, error) {
return getIP(key)
}

// ipSliceParser is a parser for []net.IP
type ipSliceParser []net.IP

func (t ipSliceParser) ParseEnv(key string, defaltVal any, opts Parameters) any {
func (t ipSliceParser) ParseEnv(key string, opts Parameters) (any, error) {
separator := opts.Separator

val := ipSliceOrDefault(key, defaltVal.([]net.IP), separator)

return val
return getIPSlice(key, separator)
}

// boolSliceParser is a parser for []bool
type boolSliceParser []bool

func (b boolSliceParser) ParseEnv(key string, defaltVal any, options Parameters) any {
func (b boolSliceParser) ParseEnv(key string, options Parameters) (any, error) {
sep := options.Separator

val := boolSliceOrDefault(key, defaltVal.([]bool), sep)

return val
return getBoolSlice(key, sep)
}

type complexParser[T Complex] struct{}

func (n complexParser[T]) ParseEnv(key string, defaltVal any, _ Parameters) any {
val := complexOrDefaultGen[T](key, defaltVal.(T))

return val
func (n complexParser[T]) ParseEnv(key string, _ Parameters) (any, error) {
return getComplexGen[T](key)
}

type complexSliceParser[S []T, T Complex] struct{}

func (i complexSliceParser[S, T]) ParseEnv(key string, defaltVal any, options Parameters) any {
func (i complexSliceParser[S, T]) ParseEnv(key string, options Parameters) (any, error) {
sep := options.Separator

val := complexSliceOrDefaultGen(key, defaltVal.(S), sep)

return val
return getComplexSliceGen[S, T](key, sep)
}
16 changes: 8 additions & 8 deletions internal/iface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,24 +200,24 @@ func TestNewEnvParser(t *testing.T) {
want: durationSliceParser([]time.Duration{time.Minute}),
},
{
v: getURL(t, "http://example.com"),
v: getTestURL(t, "http://example.com"),
wantPanic: assert.NotPanics,
want: urlParser(getURL(t, "http://example.com")),
want: urlParser(getTestURL(t, "http://example.com")),
},
{
v: []url.URL{getURL(t, "http://example.com")},
v: []url.URL{getTestURL(t, "http://example.com")},
wantPanic: assert.NotPanics,
want: urlSliceParser([]url.URL{getURL(t, "http://example.com")}),
want: urlSliceParser([]url.URL{getTestURL(t, "http://example.com")}),
},
{
v: getIP(t, "0.0.0.0"),
v: getTestIP(t, "0.0.0.0"),
wantPanic: assert.NotPanics,
want: ipParser(getIP(t, "0.0.0.0")),
want: ipParser(getTestIP(t, "0.0.0.0")),
},
{
v: []net.IP{getIP(t, "0.0.0.0")},
v: []net.IP{getTestIP(t, "0.0.0.0")},
wantPanic: assert.NotPanics,
want: ipSliceParser([]net.IP{getIP(t, "0.0.0.0")}),
want: ipSliceParser([]net.IP{getTestIP(t, "0.0.0.0")}),
},
{
v: uintptr(2),
Expand Down
Loading

0 comments on commit 011a926

Please sign in to comment.