Skip to content

Commit

Permalink
feat: add options (#131)
Browse files Browse the repository at this point in the history
* Add support for decrypting vars.

* Changed from WithEncryption to WithDecrypt.

* Moved common functions.

* Added tests.

* added support for decrypting content of file.

* updated tests to support decrypt of file content.

* updated readme with info about decrypt.

* Fixed typos in readme.

* check if decryptor is nil.

* increased coverage.

* updated example to use real life example.

* increased code coverage.

* added test for failing on inner struct.

* removed commented copy/paste test.

* added support for options.
added decryptor support.
added set env support.

* updated name of test.
removed commented test no longer needed.

* changed to not actually setting env vars when supplying env map.

* added support for changing tag name.

* made options not mandatory.

* removed required from JSON tag.

* added required and envDefault.

* fix: remove decryptor

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* refactor: no need to be a pointer

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* test: added back missing test

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* refactor: os.Environ

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

* docs: readme

Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>

Co-authored-by: Anders Wallin <touch.dev.null@gmail.com>
  • Loading branch information
caarlos0 and nuttmeister committed Jul 12, 2020
1 parent a1ac5e4 commit 52290f9
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 22 deletions.
75 changes: 75 additions & 0 deletions README.md
Expand Up @@ -175,6 +175,81 @@ $ SECRET=/tmp/secret \
{Secret:qwerty Password:dvorak Certificate:coleman}
```


## Options

### Environment

By setting the `Options.Environment` map you can tell `Parse` to add those `keys` and `values`
as env vars before parsing is done. These envs are stored in the map and never actually set by `os.Setenv`.
This option effectively makes `env` ignore the OS environment variables: only the ones provided in the option are used.

This can make your testing scenarios a bit more clean and easy to handle.

```go
package main

import (
"fmt"
"log"

"github.com/caarlos0/env"
)

type Config struct {
Password string `env:"PASSWORD"`
}

func main() {
cfg := &Config{}
opts := &env.Options{Environment: map[string]string{
"PASSWORD": "MY_PASSWORD",
}}

// Load env vars.
if err := env.Parse(cfg, opts); err != nil {
log.Fatal(err)
}

// Print the loaded data.
fmt.Printf("%+v\n", cfg.envData)
}
```

### Changing default tag name

You can change what tag name to use for setting the env vars by setting the `Options.TagName`
variable.

For example
```go
package main

import (
"fmt"
"log"

"github.com/caarlos0/env"
)

type Config struct {
Password string `json:"PASSWORD"`
}

func main() {
cfg := &Config{}
opts := &env.Options{TagName: "json"}

// Load env vars.
if err := env.Parse(cfg, opts); err != nil {
log.Fatal(err)
}

// Print the loaded data.
fmt.Printf("%+v\n", cfg.envData)
}
```

## Stargazers over time

[![Stargazers over time](https://starchart.cc/caarlos0/env.svg)](https://starchart.cc/caarlos0/env)
Expand Down
108 changes: 87 additions & 21 deletions env.go
Expand Up @@ -95,15 +95,76 @@ var (
// ParserFunc defines the signature of a function that can be used within `CustomParsers`
type ParserFunc func(v string) (interface{}, error)

// Options for the parser.
type Options struct {
// Environment keys and values that will be accessible for the service.
Environment map[string]string
// TagName specifies another tagname to use rather than the default env.
TagName string

// Sets to true if we have already configured once.
configured bool
}

// 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{
TagName: "env",
Environment: toMap(os.Environ()),
configured: true,
}

// 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
}
}

return []Options{opt}
}

func toMap(env []string) map[string]string {
r := map[string]string{}
for _, e := range env {
p := strings.SplitN(e, "=", 2)
r[p[0]] = p[1]
}
return r
}

// getTagName returns the tag name.
func getTagName(opts []Options) string {
return opts[0].TagName
}

// 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 ParseWithFuncs(v, map[reflect.Type]ParserFunc{})
func Parse(v interface{}, opts ...Options) error {
return ParseWithFuncs(v, map[reflect.Type]ParserFunc{}, 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) error {
func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc, opts ...Options) error {
opts = configure(opts)

ptrRef := reflect.ValueOf(v)
if ptrRef.Kind() != reflect.Ptr {
return ErrNotAStructPtr
Expand All @@ -116,10 +177,11 @@ func ParseWithFuncs(v interface{}, funcMap map[reflect.Type]ParserFunc) error {
for k, v := range funcMap {
parsers[k] = v
}
return doParse(ref, parsers)

return doParse(ref, parsers, opts)
}

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

for i := 0; i < refType.NumField(); i++ {
Expand All @@ -128,27 +190,27 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc) error {
continue
}
if reflect.Ptr == refField.Kind() && !refField.IsNil() {
err := ParseWithFuncs(refField.Interface(), funcMap)
err := ParseWithFuncs(refField.Interface(), funcMap, opts...)
if err != nil {
return err
}
continue
}
if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" {
err := Parse(refField.Addr().Interface())
err := Parse(refField.Addr().Interface(), opts...)
if err != nil {
return err
}
continue
}
refTypeField := refType.Field(i)
value, err := get(refTypeField)
value, err := get(refTypeField, opts)
if err != nil {
return err
}
if value == "" {
if reflect.Struct == refField.Kind() {
if err := doParse(refField, funcMap); err != nil {
if err := doParse(refField, funcMap, opts); err != nil {
return err
}
}
Expand All @@ -161,29 +223,29 @@ func doParse(ref reflect.Value, funcMap map[reflect.Type]ParserFunc) error {
return nil
}

func get(field reflect.StructField) (val string, err error) {
func get(field reflect.StructField, opts []Options) (val string, err error) {
var required bool
var exists bool
var loadFile bool
var expand = strings.EqualFold(field.Tag.Get("envExpand"), "true")

key, opts := parseKeyForOption(field.Tag.Get("env"))
key, tags := parseKeyForOption(field.Tag.Get(getTagName(opts)))

for _, opt := range opts {
switch opt {
for _, tag := range tags {
switch tag {
case "":
break
case "file":
loadFile = true
case "required":
required = true
default:
return "", fmt.Errorf("env: tag option %q not supported", opt)
return "", fmt.Errorf("env: tag option %q not supported", tag)
}
}

defaultValue := field.Tag.Get("envDefault")
val, exists = getOr(key, defaultValue)
defaultValue, defExists := field.Tag.Lookup("envDefault")
val, exists = getOr(key, defaultValue, defExists, getEnvironment(opts))

if expand {
val = os.ExpandEnv(val)
Expand Down Expand Up @@ -215,12 +277,16 @@ func getFromFile(filename string) (value string, err error) {
return string(b), err
}

func getOr(key, defaultValue string) (value string, exists bool) {
value, exists = os.LookupEnv(key)
if !exists {
value = defaultValue
func getOr(key, defaultValue string, defExists bool, envs map[string]string) (value string, exists bool) {
value, exists = envs[key]
switch {
case !exists && defExists:
return defaultValue, true
case !exists:
return "", false
}
return value, exists

return value, true
}

func set(field reflect.Value, sf reflect.StructField, value string, funcMap map[reflect.Type]ParserFunc) error {
Expand Down
37 changes: 36 additions & 1 deletion env_test.go
Expand Up @@ -386,6 +386,39 @@ func TestParsesEnv(t *testing.T) {
assert.Empty(t, cfg.unexported)
}

func TestSetEnvAndTagOptsChain(t *testing.T) {
defer os.Clearenv()
type config struct {
Key1 string `mytag:"KEY1,required"`
Key2 int `mytag:"KEY2,required"`
}
envs := map[string]string{
"KEY1": "VALUE1",
"KEY2": "3",
}

cfg := config{}
require.NoError(t, Parse(&cfg, Options{TagName: "mytag"}, Options{Environment: envs}))
assert.Equal(t, "VALUE1", cfg.Key1)
assert.Equal(t, 3, cfg.Key2)
}

func TestJSONTag(t *testing.T) {
defer os.Clearenv()
type config struct {
Key1 string `json:"KEY1"`
Key2 int `json:"KEY2"`
}

os.Setenv("KEY1", "VALUE7")
os.Setenv("KEY2", "5")

cfg := config{}
require.NoError(t, Parse(&cfg, Options{TagName: "json"}))
assert.Equal(t, "VALUE7", cfg.Key1)
assert.Equal(t, 5, cfg.Key2)
}

func TestParsesEnvInner(t *testing.T) {
os.Setenv("innervar", "someinnervalue")
os.Setenv("innernum", "8")
Expand Down Expand Up @@ -663,7 +696,9 @@ func TestErrorRequiredNotSetWithDefault(t *testing.T) {
}

cfg := &config{}
assert.EqualError(t, Parse(cfg), "env: required environment variable \"IS_REQUIRED\" is not set")

assert.NoError(t, Parse(cfg))
assert.Equal(t, "important", cfg.IsRequired)
}

func TestParseExpandOption(t *testing.T) {
Expand Down

0 comments on commit 52290f9

Please sign in to comment.