Skip to content

Commit

Permalink
feat: accept []*types (#95)
Browse files Browse the repository at this point in the history
* refactor: make it work on dep again

* fix: deps

* refactor: custom type

* refactor: custom type

* refactor: point to arrays

* test: 100% coverage

* docs: improved readme
  • Loading branch information
caarlos0 authored May 15, 2019
1 parent b2918ad commit 4bd75bf
Show file tree
Hide file tree
Showing 4 changed files with 381 additions and 217 deletions.
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
SOURCE_FILES?=./...
TEST_PATTERN?=.
TEST_OPTIONS?=

export GO111MODULE := on

Expand All @@ -14,7 +13,7 @@ build:
.PHONY: build

test:
go test $(TEST_OPTIONS) -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
go test -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=2m
.PHONY: test

cover: test
Expand Down
47 changes: 7 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,70 +51,37 @@ $ PRODUCTION=true HOSTS="host1:host2:host3" DURATION=1s go run main.go

## Supported types and defaults

The following types are supported out of the box:
Out of the box all built-in types are supported, plus a few others that
are commonly used.

Complete list:

- `string`
- `*string`
- `[]string`
- `bool`
- `*bool`
- `[]bool`
- `int`
- `*int`
- `[]int`
- `int8`
- `*int8`
- `[]int8`
- `int16`
- `[]int16`
- `*int16`
- `int32`
- `[]int32`
- `*int32`
- `int64`
- `[]int64`
- `*int64`
- `uint`
- `[]uint`
- `*uint`
- `uint8`
- `[]uint8`
- `*uint8`
- `uint16`
- `[]uint16`
- `*uint16`
- `uint32`
- `[]uint32`
- `*uint32`
- `uint64`
- `[]uint64`
- `*uint64`
- `float32`
- `*float32`
- `[]float32`
- `float64`
- `*float64`
- `[]float64`
- `string`
- `[]string`
- `time.Duration`
- `[]time.Duration`
- `encoding.TextUnmarshaler`
- `*encoding.TextUnmarshaler`
- `[]encoding.TextUnmarshaler`
- `[]*encoding.TextUnmarshaler`
- `url.URL`
- `[]url.URL`
- `*url.URL`

Pointers, slices and slices of pointers of those types are also supported.

You can also use/define a [custom parser func](#custom-parser-funcs) for any
other type you want.

If you set the `envDefault` tag for something, this value will be used in the
case of absence of it in the environment. If you don't do that AND the
environment variable is also not set, the zero-value
of the type will be used: empty for `string`s, `false` for `bool`s,
`0` for `int`s and so forth.
case of absence of it in the environment.

By default, slice types will split the environment value on `,`; you can change
this behavior by setting the `envSeparator` tag.
Expand Down
69 changes: 30 additions & 39 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,24 @@ var (
return float32(f), err
},
}
)

func defaultTypeParsers() map[reflect.Type]ParserFunc {
return map[reflect.Type]ParserFunc{
reflect.TypeOf(url.URL{}): URLFunc,
reflect.TypeOf(time.Nanosecond): DurationFunc,
defaultTypeParsers = map[reflect.Type]ParserFunc{
reflect.TypeOf(url.URL{}): func(v string) (interface{}, error) {
u, err := url.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable parse URL: %v", err)
}
return *u, nil
},
reflect.TypeOf(time.Nanosecond): func(v string) (interface{}, error) {
s, err := time.ParseDuration(v)
if err != nil {
return nil, fmt.Errorf("unable to parser duration: %v", err)
}
return s, err
},
}
}
)

// ParserFunc defines the signature of a function that can be used within `CustomParsers`
type ParserFunc func(v string) (interface{}, error)
Expand All @@ -101,15 +111,15 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc) error {
if ref.Kind() != reflect.Struct {
return ErrNotAStructPtr
}
var parsers = defaultTypeParsers()
var parsers = defaultTypeParsers
for k, v := range funcMap {
parsers[k] = v
}
return doParse(ref, parsers)
}

func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc) error {
refType := ref.Type()
var refType = ref.Type()

for i := 0; i < refType.NumField(); i++ {
refField := ref.Field(i)
Expand Down Expand Up @@ -226,13 +236,14 @@ func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[
return nil
}

parserFunc, ok = defaultBuiltInParsers[sf.Type.Kind()]
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
if ok {
val, err := parserFunc(value)
if err != nil {
return newParseError(sf, err)
}
field.Set(reflect.ValueOf(val).Convert(sf.Type))

fieldee.Set(reflect.ValueOf(val).Convert(typee))
return nil
}

Expand All @@ -246,18 +257,18 @@ func handleSlice(field reflect.Value, value string, sf reflect.StructField, func
}
var parts = strings.Split(value, separator)

var elemType = sf.Type.Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
var typee = sf.Type.Elem()
if typee.Kind() == reflect.Ptr {
typee = typee.Elem()
}

if _, ok := reflect.New(elemType).Interface().(encoding.TextUnmarshaler); ok {
if _, ok := reflect.New(typee).Interface().(encoding.TextUnmarshaler); ok {
return parseTextUnmarshalers(field, parts, sf)
}

parserFunc, ok := funcMap[elemType]
parserFunc, ok := funcMap[typee]
if !ok {
parserFunc, ok = defaultBuiltInParsers[elemType.Kind()]
parserFunc, ok = defaultBuiltInParsers[typee.Kind()]
if !ok {
return newNoParserError(sf)
}
Expand All @@ -269,10 +280,10 @@ func handleSlice(field reflect.Value, value string, sf reflect.StructField, func
if err != nil {
return newParseError(sf, err)
}
var v = reflect.ValueOf(r).Convert(elemType)
var v = reflect.ValueOf(r).Convert(typee)
if sf.Type.Elem().Kind() == reflect.Ptr {
// TODO: add this!
return fmt.Errorf("env: pointer slices of built-in and aliased types are not supported: %s %s", sf.Name, sf.Type)
v = reflect.New(typee)
v.Elem().Set(reflect.ValueOf(r).Convert(typee))
}
result = reflect.Append(result, v)
}
Expand Down Expand Up @@ -344,23 +355,3 @@ func (e parseError) Error() string {
func newNoParserError(sf reflect.StructField) error {
return fmt.Errorf(`env: no parser found for field "%s" of type "%s"`, sf.Name, sf.Type)
}

// Custom parsers

// URLFunc is a basic parser for the url.URL type that should be used with `env.ParseWithFuncs()`
func URLFunc(v string) (interface{}, error) {
u, err := url.Parse(v)
if err != nil {
return nil, fmt.Errorf("unable parse URL: %v", err)
}
return *u, nil
}

// DurationFunc is a basic parser for the time.Duration type that should be used with `env.ParseWithFuncs()`
func DurationFunc(v string) (interface{}, error) {
s, err := time.ParseDuration(v)
if err != nil {
return nil, fmt.Errorf("unable to parser duration: %v", err)
}
return s, err
}
Loading

0 comments on commit 4bd75bf

Please sign in to comment.