Skip to content

Commit

Permalink
feat!: refactor parse and parse with options (#256)
Browse files Browse the repository at this point in the history
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
  • Loading branch information
caarlos0 committed Apr 6, 2023
1 parent dc45fbb commit 4e0f915
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 145 deletions.
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://img.shields.io/github/actions/workflow/status/caarlos0/env/build.yml?branch=main&style=for-the-badge)](https://github.com/caarlos0/env/actions?workflow=build)
[![Coverage Status](https://img.shields.io/codecov/c/gh/caarlos0/env.svg?logo=codecov&style=for-the-badge)](https://codecov.io/gh/caarlos0/env)
[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v7)
[![](http://img.shields.io/badge/godoc-reference-5272B4.svg?style=for-the-badge)](https://pkg.go.dev/github.com/caarlos0/env/v8)

A simple and zero-dependencies library to parse environment variables into structs.

Expand All @@ -11,7 +11,7 @@ A simple and zero-dependencies library to parse environment variables into struc
Get the module with:

```sh
go get github.com/caarlos0/env/v7
go get github.com/caarlos0/env/v8
```

The usage looks like this:
Expand All @@ -23,7 +23,7 @@ import (
"fmt"
"time"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type config struct {
Expand Down Expand Up @@ -110,15 +110,16 @@ of the variable.

If you have a type that is not supported out of the box by the lib, you are able
to use (or define) and pass custom parsers (and their associated `reflect.Type`)
to the `env.ParseWithFuncs()` function.
to the `env.ParseWithOptions()` function.

In addition to accepting a struct pointer (same as `Parse()`), this function
also accepts a `map[reflect.Type]env.ParserFunc`.
also accepts a `Options{}`, and you can set your custom parsers in the `FuncMap`
field.

If you add a custom parser for, say `Foo`, it will also be used to parse
`*Foo` and `[]Foo` types.

Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v7)
Check the examples in the [go doc](http://pkg.go.dev/github.com/caarlos0/env/v8)
for more info.

### A note about `TextUnmarshaler` and `time.Time`
Expand Down Expand Up @@ -196,7 +197,7 @@ package main
import (
"fmt"
"time"
"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type config struct {
Expand Down Expand Up @@ -245,7 +246,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -283,7 +284,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -319,7 +320,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -353,7 +354,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -403,7 +404,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -442,7 +443,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -476,7 +477,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down Expand Up @@ -509,7 +510,7 @@ import (
"fmt"
"log"

"github.com/caarlos0/env/v7"
"github.com/caarlos0/env/v8"
)

type Config struct {
Expand Down
136 changes: 56 additions & 80 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,72 +117,60 @@ type Options struct {
// name by default if the `env` key is missing.
UseFieldNameByDefault bool

// Sets to true if we have already configured once.
configured bool
// Custom parse functions for different types.
FuncMap map[reflect.Type]ParserFunc
}

// configure will do the basic configurations and defaults.
func configure(opts []Options) []Options {
// If we have already configured the first item
// of options will have been configured set to true.
if len(opts) > 0 && opts[0].configured {
return opts
}

// Created options with defaults.
opt := Options{
func defaultOptions() Options {
return Options{
TagName: "env",
Environment: toMap(os.Environ()),
configured: true,
FuncMap: defaultTypeParsers(),
}

// Loop over all opts structs and set
// to opt if value is not default/empty.
for _, item := range opts {
if item.Environment != nil {
opt.Environment = item.Environment
}
if item.TagName != "" {
opt.TagName = item.TagName
}
if item.OnSet != nil {
opt.OnSet = item.OnSet
}
if item.Prefix != "" {
opt.Prefix = item.Prefix
}
opt.UseFieldNameByDefault = item.UseFieldNameByDefault
opt.RequiredIfNoDef = item.RequiredIfNoDef
}

return []Options{opt}
}

func getOnSetFn(opts []Options) OnSetFn {
return opts[0].OnSet
func customOptions(opt Options) Options {
defOpts := defaultOptions()
if opt.TagName == "" {
opt.TagName = defOpts.TagName
}
if opt.Environment == nil {
opt.Environment = defOpts.Environment
}
if opt.FuncMap == nil {
opt.FuncMap = map[reflect.Type]ParserFunc{}
}
for k, v := range defOpts.FuncMap {
opt.FuncMap[k] = v
}
return opt
}

// getTagName returns the tag name.
func getTagName(opts []Options) string {
return opts[0].TagName
func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options {
return Options{
Environment: opts.Environment,
TagName: opts.TagName,
RequiredIfNoDef: opts.RequiredIfNoDef,
OnSet: opts.OnSet,
Prefix: opts.Prefix + field.Tag.Get("envPrefix"),
UseFieldNameByDefault: opts.UseFieldNameByDefault,
FuncMap: opts.FuncMap,
}
}

// getEnvironment returns the environment map.
func getEnvironment(opts []Options) map[string]string {
return opts[0].Environment
// Parse parses a struct containing `env` tags and loads its values from
// environment variables.
func Parse(v interface{}) error {
return parseInternal(v, defaultOptions())
}

// Parse parses a struct containing `env` tags and loads its values from
// environment variables.
func Parse(v interface{}, opts ...Options) error {
return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, opts...)
func ParseWithOptions(v interface{}, opts Options) error {
return parseInternal(v, customOptions(opts))
}

// ParseWithFuncs is the same as `Parse` except it also allows the user to pass
// in custom parsers.
func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error {
opts = configure(opts)

func parseInternal(v interface{}, opts Options) error {
ptrRef := reflect.ValueOf(v)
if ptrRef.Kind() != reflect.Ptr {
return newAggregateError(NotStructPtrError{})
Expand All @@ -191,15 +179,10 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...
if ref.Kind() != reflect.Struct {
return newAggregateError(NotStructPtrError{})
}
parsers := defaultTypeParsers()
for k, v := range funcMap {
parsers[k] = v
}

return doParse(ref, parsers, opts)
return doParse(ref, opts)
}

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

var agrErr AggregateError
Expand All @@ -208,7 +191,7 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Opti
refField := ref.Field(i)
refTypeField := refType.Field(i)

if err := doParseField(refField, refTypeField, funcMap, opts); err != nil {
if err := doParseField(refField, refTypeField, opts); err != nil {
if val, ok := err.(AggregateError); ok {
agrErr.Errors = append(agrErr.Errors, val.Errors...)
} else {
Expand All @@ -224,27 +207,27 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc, opts []Opti
return agrErr
}

func doParseField(refField reflect.Value, refTypeField reflect.StructField, funcMap map[reflect.Type]ParserFunc, opts []Options) error {
func doParseField(refField reflect.Value, refTypeField reflect.StructField, opts Options) error {
if !refField.CanSet() {
return nil
}
if reflect.Ptr == refField.Kind() && !refField.IsNil() {
return ParseWithFuncs(refField.Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
return parseInternal(refField.Interface(), optionsWithEnvPrefix(refTypeField, opts))
}
if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" {
return ParseWithFuncs(refField.Addr().Interface(), funcMap, optsWithPrefix(refTypeField, opts)...)
return parseInternal(refField.Addr().Interface(), optionsWithEnvPrefix(refTypeField, opts))
}
value, err := get(refTypeField, opts)
if err != nil {
return err
}

if value != "" {
return set(refField, refTypeField, value, funcMap)
return set(refField, refTypeField, value, opts.FuncMap)
}

if reflect.Struct == refField.Kind() {
return doParse(refField, funcMap, optsWithPrefix(refTypeField, opts))
return doParse(refField, optionsWithEnvPrefix(refTypeField, opts))
}

return nil
Expand All @@ -263,20 +246,19 @@ func toEnvName(input string) string {
return string(output)
}

func get(field reflect.StructField, opts []Options) (val string, err error) {
func get(field reflect.StructField, opts Options) (val string, err error) {
var exists bool
var isDefault bool
var loadFile bool
var unset bool
var notEmpty bool

required := opts[0].RequiredIfNoDef
prefix := opts[0].Prefix
ownKey, tags := parseKeyForOption(field.Tag.Get(getTagName(opts)))
if ownKey == "" && opts[0].UseFieldNameByDefault {
required := opts.RequiredIfNoDef
ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName))
if ownKey == "" && opts.UseFieldNameByDefault {
ownKey = toEnvName(field.Name)
}
key := prefix + ownKey

for _, tag := range tags {
switch tag {
case "":
Expand All @@ -293,9 +275,12 @@ func get(field reflect.StructField, opts []Options) (val string, err error) {
return "", newNoSupportedTagOptionError(tag)
}
}

prefix := opts.Prefix
key := prefix + ownKey
expand := strings.EqualFold(field.Tag.Get("envExpand"), "true")
defaultValue, defExists := field.Tag.Lookup("envDefault")
val, exists, isDefault = getOr(key, defaultValue, defExists, getEnvironment(opts))
val, exists, isDefault = getOr(key, defaultValue, defExists, opts.Environment)

if expand {
val = os.ExpandEnv(val)
Expand All @@ -321,8 +306,8 @@ func get(field reflect.StructField, opts []Options) (val string, err error) {
}
}

if onSetFn := getOnSetFn(opts); onSetFn != nil {
onSetFn(key, val, isDefault)
if opts.OnSet != nil {
opts.OnSet(key, val, isDefault)
}
return val, err
}
Expand Down Expand Up @@ -529,12 +514,3 @@ func parseTextUnmarshalers(field reflect.Value, data []string, sf reflect.Struct

return nil
}

func optsWithPrefix(field reflect.StructField, opts []Options) []Options {
subOpts := make([]Options, len(opts))
copy(subOpts, opts)
if prefix := field.Tag.Get("envPrefix"); prefix != "" {
subOpts[0].Prefix += prefix
}
return subOpts
}

0 comments on commit 4e0f915

Please sign in to comment.