Skip to content

Commit

Permalink
Merge pull request #47 from antonmashko/feat/custom-external-injection
Browse files Browse the repository at this point in the history
Feat/custom external injection
  • Loading branch information
antonmashko authored Oct 8, 2023
2 parents 4f738f4 + b01a783 commit e041edf
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 58 deletions.
47 changes: 14 additions & 33 deletions config_field.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,6 @@ func newEnvSource(f *configField, tag reflect.StructField) *envSource {
}
}

func newEnvInjection(value string) *envSource {
var ok bool
const prefix = "${"
value, ok = strings.CutPrefix(value, prefix)
if !ok {
return nil
}

const suffix = "}"
value, ok = strings.CutSuffix(value, suffix)
if !ok {
return nil
}

value = strings.TrimSpace(value)
const envInjectionPrefix = ".env."
value, ok = strings.CutPrefix(value, envInjectionPrefix)
if !ok {
return nil
}
return &envSource{name: value}
}

func (s *envSource) Name() string {
return s.name
}
Expand All @@ -129,14 +106,14 @@ func (s *envSource) Value() (interface{}, option.ConfigSource) {
}

type externalSource struct {
f field
allowEnvInjection bool
f field
opts *option.Options
}

func newExternalSource(f field, allowEnvInjection bool) *externalSource {
func newExternalSource(f field, opts *option.Options) *externalSource {
return &externalSource{
f: f,
allowEnvInjection: allowEnvInjection,
f: f,
opts: opts,
}
}

Expand All @@ -149,18 +126,22 @@ func (s *externalSource) Value() (interface{}, option.ConfigSource) {
if !ok {
return nil, option.NoConfigValue
}
if !s.allowEnvInjection {
envInjF := s.opts.ExternalInjection()
if envInjF == nil {
return v, option.ExternalSource
}
str, ok := v.(string)
if !ok {
return v, option.ExternalSource
}
envInj := newEnvInjection(str)
if envInj == nil {
var cs option.ConfigSource
str, cs = envInjF(str)
switch cs {
case option.EnvVariable:
return (&envSource{name: str}).Value()
default:
return v, option.ExternalSource
}
return envInj.Value()
}

type defaultValueSource struct {
Expand Down Expand Up @@ -226,7 +207,7 @@ func (f *configField) init(fl field) error {
f.property.description = f.Tag.Get(tagDescription)
f.configuration.flag = newFlagSource(f, f.StructField, f.property.description)
f.configuration.env = newEnvSource(f, f.StructField)
f.configuration.external = newExternalSource(fl, f.parser.opts.AllowExternalEnvInjection)
f.configuration.external = newExternalSource(fl, f.parser.opts)
f.configuration.defaultValue = newDefaultValueSource(f.StructField)
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions external/external_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ func TestJsonConfig_EnvVarInjection_Ok(t *testing.T) {
os.Setenv("JSON_TEST_ENVVAR_INJECTION", expectedValue)
err := envconf.Parse(&tc,
option.WithExternal(jsonconf.Json([]byte(json))),
option.WithExternalEnvInjection(true),
option.WithExternalInjection(),
)
if err != nil {
t.Fatalf("failed to external parse. err=%s", err)
Expand All @@ -359,7 +359,7 @@ func TestJsonConfig_EnvVarInjectionFromCollection_Ok(t *testing.T) {
os.Setenv("JSON_TEST_ENVVAR_INJECTION", expectedValue)
err := envconf.Parse(&tc,
option.WithExternal(jsonconf.Json([]byte(json))),
option.WithExternalEnvInjection(true),
option.WithExternalInjection(),
)
if err != nil {
t.Fatalf("failed to external parse. err=%s", err)
Expand Down
19 changes: 19 additions & 0 deletions external/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# External
This package provides implementations of External option for EnvConf parsing process.

Implement your own external or use any of already implemented

## Usage
```golang
json := `{"foo":"bar"}`
tc := struct {
Foo string `env:"ENV_FOO"`
}{}
err := envconf.Parse(&tc,
option.WithExternal(jsonconf.Json([]byte(json))))
```
If env variable is it not specified for Foo variable, EnvConf will define it from json input.

## Wrapped external
- json (encoding/json)
- yaml (gopkg.in/yaml.v3)
13 changes: 3 additions & 10 deletions option/client_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ type Options struct {
onFieldInitialized func(FieldInitializedArg)
onFieldDefined func(FieldDefinedArg)
onFieldDefineErr func(FieldDefineErrorArg)

AllowExternalEnvInjection bool
externalInjection func(string) (string, ConfigSource)
}

func (o *Options) External() external.External {
Expand Down Expand Up @@ -98,12 +97,6 @@ func (o *Options) OnFieldDefineErr(arg FieldDefineErrorArg) {
}
}

type withAllowExternalEnvInjection bool

func (o withAllowExternalEnvInjection) Apply(opts *Options) {
opts.AllowExternalEnvInjection = bool(o)
}

func WithExternalEnvInjection(allow bool) ClientOption {
return withAllowExternalEnvInjection(allow)
func (o *Options) ExternalInjection() func(string) (string, ConfigSource) {
return o.externalInjection
}
12 changes: 0 additions & 12 deletions option/client_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,3 @@ func TestOptions_DefaultPriorityOrder_Ok(t *testing.T) {
t.Fatal("unexpected result:", opts.PriorityOrder())
}
}

func TestWithExternalEnvInjection_Ok(t *testing.T) {
opts := &Options{}
WithExternalEnvInjection(true).Apply(opts)
if !opts.AllowExternalEnvInjection {
t.Fatal("got false")
}
WithExternalEnvInjection(false).Apply(opts)
if opts.AllowExternalEnvInjection {
t.Fatal("got true")
}
}
43 changes: 43 additions & 0 deletions option/external_injection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package option

import "strings"

type withExternalEnvInjection func(string) (string, ConfigSource)

func (f withExternalEnvInjection) Apply(opts *Options) {
opts.externalInjection = f
}

// WithExternalInjection allows to inject variables from an external source
// use format ${ .env.<ENV_VAR_NAME> } to make an env variable lookup while getting a field from the external source
func WithExternalInjection() ClientOption {
return WithCustomExternalInjection(nil)
}

func WithCustomExternalInjection(f func(string) (string, ConfigSource)) ClientOption {
if f == nil {
f = func(value string) (string, ConfigSource) {
var ok bool
const prefix = "${"
value, ok = strings.CutPrefix(value, prefix)
if !ok {
return "", NoConfigValue
}

const suffix = "}"
value, ok = strings.CutSuffix(value, suffix)
if !ok {
return "", NoConfigValue
}

value = strings.TrimSpace(value)
const envInjectionPrefix = ".env."
value, ok = strings.CutPrefix(value, envInjectionPrefix)
if !ok {
return "", NoConfigValue
}
return value, EnvVariable
}
}
return withExternalEnvInjection(f)
}
49 changes: 49 additions & 0 deletions option/external_injection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package option

import "testing"

func TestWithCustomExternalInjection(t *testing.T) {
tcs := []struct {
name string
value string
resultVal string
resultCS ConfigSource
}{
{name: "ValidEnvVar_Ok", value: "${ .env.ENV_VAR }",
resultVal: "ENV_VAR", resultCS: EnvVariable},
{name: "InvalidEnvVar_WithoutPrefix_Err", value: ".env.ENV_VAR }",
resultVal: "", resultCS: NoConfigValue},
{name: "InvalidEnvVar_WithoutBracketSuffix_Err", value: "${ .env.ENV_VAR",
resultVal: "", resultCS: NoConfigValue},
{name: "InvalidEnvVar_WithoutInjectionName_Err", value: "${ .ENV_VAR }",
resultVal: "", resultCS: NoConfigValue},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
f := WithExternalInjection()
opts := &Options{}
f.Apply(opts)
res, cs := opts.ExternalInjection()(tc.value)
if res != tc.resultVal || cs != tc.resultCS {
t.Fatalf("unexpected result: expected={'%s','%s'} actual={'%s','%s'}",
tc.resultVal, tc.resultCS, res, cs)
}
})
}
}

func TestWithCustomExternalInjection_CustomFunc_Ok(t *testing.T) {
const suffix = "_test"
const expectedCS = ExternalSource
f := WithCustomExternalInjection(func(s string) (string, ConfigSource) {
return s + suffix, expectedCS
})
opts := &Options{}
f.Apply(opts)
const value = "abc"
res, cs := opts.ExternalInjection()(value)
if res != value+suffix || cs != expectedCS {
t.Fatalf("unexpected result: expected={'%s','%s'} actual={'%s','%s'}",
value+suffix, expectedCS, res, cs)
}
}
4 changes: 3 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ Options allow intercept into `EnvConf.Parse` process

Name|Option|Description
---|---|---
External source|`option.WithExternal`|Add external configuration source to the parsing process. This option allows you to define field from configuration files, remote servers, etc. Some of Externals already predefined in `external` folder, e.g. external/json or external/yaml. see: [External Doc](external/readme.md)
Read configuration priority|`option.WithPriorityOrder`|Change default parsing priority. Default: *Flag*, *Environment variable*, *External source*, *Default Value*
Log|`option.WithLog`|Enable logging over parsing process. Prints defined and not defined configuration fields
Custom Usage|`option.WithCustomUsage`|Generate usage for `-help` flag from input structure. By default this option is enabled, use `option.WithoutCustomUsage` option
Flag Parsed Callback|`option.WithFlagParsed`|This callback allow to use flags after flag.Parse() and before EnvConf.Define process
Read config file|`external.WithFlagConfigFile`|Read config file from the path specified in the flag
Read config file|`option.WithFlagConfigFile`|Read config file from the path specified in the flag. This option working with `External` option.
External Injection|`option.WithExternalInjection`|Inject environment variables into external source. Override default injection with `option.WithCustomExternalInjection`

0 comments on commit e041edf

Please sign in to comment.