diff --git a/core/conf/config.go b/core/conf/config.go index a98e5fd8..29f180bf 100644 --- a/core/conf/config.go +++ b/core/conf/config.go @@ -16,13 +16,26 @@ var loaders = map[string]func([]byte, interface{}) error{ ".yml": LoadConfigFromYamlBytes, } -func LoadConfig(file string, v interface{}) error { - if content, err := ioutil.ReadFile(file); err != nil { +func LoadConfig(file string, v interface{}, opts ...Option) error { + content, err := ioutil.ReadFile(file) + if err != nil { return err - } else if loader, ok := loaders[path.Ext(file)]; ok { + } + + loader, ok := loaders[path.Ext(file)] + if !ok { + return fmt.Errorf("unrecoginized file type: %s", file) + } + + var opt options + for _, o := range opts { + o(&opt) + } + + if opt.env { return loader([]byte(os.ExpandEnv(string(content))), v) } else { - return fmt.Errorf("unrecoginized file type: %s", file) + return loader(content, v) } } @@ -34,8 +47,8 @@ func LoadConfigFromYamlBytes(content []byte, v interface{}) error { return mapping.UnmarshalYamlBytes(content, v) } -func MustLoad(path string, v interface{}) { - if err := LoadConfig(path, v); err != nil { +func MustLoad(path string, v interface{}, opts ...Option) { + if err := LoadConfig(path, v, opts...); err != nil { log.Fatalf("error: config file %s, %s", path, err.Error()) } } diff --git a/core/conf/config_test.go b/core/conf/config_test.go index 8f114e4a..f0767e9f 100644 --- a/core/conf/config_test.go +++ b/core/conf/config_test.go @@ -30,7 +30,8 @@ func TestConfigJson(t *testing.T) { text := `{ "a": "foo", "b": 1, - "c": "${FOO}" + "c": "${FOO}", + "d": "abcd!@#$112" }` for _, test := range tests { test := test @@ -45,11 +46,49 @@ func TestConfigJson(t *testing.T) { A string `json:"a"` B int `json:"b"` C string `json:"c"` + D string `json:"d"` } MustLoad(tmpfile, &val) assert.Equal(t, "foo", val.A) assert.Equal(t, 1, val.B) + assert.Equal(t, "${FOO}", val.C) + assert.Equal(t, "abcd!@#$112", val.D) + }) + } +} + +func TestConfigJsonEnv(t *testing.T) { + tests := []string{ + ".json", + ".yaml", + ".yml", + } + text := `{ + "a": "foo", + "b": 1, + "c": "${FOO}", + "d": "abcd!@#$a12 3" +}` + for _, test := range tests { + test := test + t.Run(test, func(t *testing.T) { + os.Setenv("FOO", "2") + defer os.Unsetenv("FOO") + tmpfile, err := createTempFile(test, text) + assert.Nil(t, err) + defer os.Remove(tmpfile) + + var val struct { + A string `json:"a"` + B int `json:"b"` + C string `json:"c"` + D string `json:"d"` + } + MustLoad(tmpfile, &val, UseEnv()) + assert.Equal(t, "foo", val.A) + assert.Equal(t, 1, val.B) assert.Equal(t, "2", val.C) + assert.Equal(t, "abcd!@# 3", val.D) }) } } diff --git a/core/conf/options.go b/core/conf/options.go new file mode 100644 index 00000000..d7d5ad72 --- /dev/null +++ b/core/conf/options.go @@ -0,0 +1,15 @@ +package conf + +type ( + Option func(opt *options) + + options struct { + env bool + } +) + +func UseEnv() Option { + return func(opt *options) { + opt.env = true + } +} diff --git a/core/conf/properties.go b/core/conf/properties.go index 79fa1ebf..268961f6 100644 --- a/core/conf/properties.go +++ b/core/conf/properties.go @@ -2,6 +2,7 @@ package conf import ( "fmt" + "os" "strconv" "strings" "sync" @@ -32,12 +33,17 @@ type mapBasedProperties struct { // Loads the properties into a properties configuration instance. // Returns an error that indicates if there was a problem loading the configuration. -func LoadProperties(filename string) (Properties, error) { +func LoadProperties(filename string, opts ...Option) (Properties, error) { lines, err := iox.ReadTextLines(filename, iox.WithoutBlank(), iox.OmitWithPrefix("#")) if err != nil { return nil, err } + var opt options + for _, o := range opts { + o(&opt) + } + raw := make(map[string]string) for i := range lines { pair := strings.Split(lines[i], "=") @@ -50,7 +56,11 @@ func LoadProperties(filename string) (Properties, error) { key := strings.TrimSpace(pair[0]) value := strings.TrimSpace(pair[1]) - raw[key] = value + if opt.env { + raw[key] = os.ExpandEnv(value) + } else { + raw[key] = value + } } return &mapBasedProperties{ diff --git a/core/conf/properties_test.go b/core/conf/properties_test.go index a83a8447..2c8fd100 100644 --- a/core/conf/properties_test.go +++ b/core/conf/properties_test.go @@ -31,6 +31,39 @@ func TestProperties(t *testing.T) { assert.Contains(t, val, "app.threads") } +func TestPropertiesEnv(t *testing.T) { + text := `app.name = test + + app.program=app + + app.env1 = ${FOO} + app.env2 = $none + + # this is comment + app.threads = 5` + tmpfile, err := fs.TempFilenameWithText(text) + assert.Nil(t, err) + defer os.Remove(tmpfile) + + os.Setenv("FOO", "2") + defer os.Unsetenv("FOO") + + props, err := LoadProperties(tmpfile, UseEnv()) + assert.Nil(t, err) + assert.Equal(t, "test", props.GetString("app.name")) + assert.Equal(t, "app", props.GetString("app.program")) + assert.Equal(t, 5, props.GetInt("app.threads")) + assert.Equal(t, "2", props.GetString("app.env1")) + assert.Equal(t, "", props.GetString("app.env2")) + + val := props.ToString() + assert.Contains(t, val, "app.name") + assert.Contains(t, val, "app.program") + assert.Contains(t, val, "app.threads") + assert.Contains(t, val, "app.env1") + assert.Contains(t, val, "app.env2") +} + func TestLoadProperties_badContent(t *testing.T) { filename, err := fs.TempFilenameWithText("hello") assert.Nil(t, err) diff --git a/tools/goctl/goctl.go b/tools/goctl/goctl.go index 2406b3ad..997a40f7 100644 --- a/tools/goctl/goctl.go +++ b/tools/goctl/goctl.go @@ -28,7 +28,7 @@ import ( ) var ( - BuildVersion = "1.1.3" + BuildVersion = "1.1.5" commands = []cli.Command{ { Name: "upgrade",