├── .github ├── dependabot.yml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd └── merge-env-config │ ├── .gitignore │ └── main.go ├── config.go ├── config_test.go ├── data_test.go ├── delims_test.go ├── funcs_test.go ├── go.mod ├── go.sum ├── json_test.go ├── read_test.go └── tests ├── empty.yaml └── foo.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go: 14 | - "1.17" 15 | - "1.19" 16 | - "1.20" 17 | name: Build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go }} 24 | id: go 25 | 26 | - name: Check out code into the Go module directory 27 | uses: actions/checkout@v3 28 | 29 | - name: Build & Test 30 | run: | 31 | make test 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - "!**/*" 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Set up Go 15 | uses: actions/setup-go@v3 16 | with: 17 | go-version: "1.20" 18 | 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v3 21 | 22 | - name: setup tools 23 | run: | 24 | go install github.com/mitchellh/gox@v1.0.1 25 | curl -sL https://github.com/tcnksm/ghr/releases/download/v0.13.0/ghr_v0.13.0_linux_amd64.tar.gz | tar zxvf - && install ghr_v0.13.0_linux_amd64/ghr ~/go/bin/ 26 | 27 | - name: dist 28 | run: PATH=~/go/bin:$PATH make dist 29 | env: 30 | CGO_ENABLED: 0 31 | 32 | - name: release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: PATH=~/go/bin:$PATH make release 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | *~ 17 | dist/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 KAYAC Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_VER := $(shell git describe --tags) 2 | .PHONY: dist install clean test release 3 | 4 | cmd/merge-env-config/merge-env-config: *.go cmd/merge-env-config/*.go 5 | cd cmd/merge-env-config && go build -ldflags "-s -w -X main.Version=${GIT_VER}" -gcflags="-trimpath=${PWD}" 6 | 7 | install: cmd/merge-env-config/merge-env-config 8 | install cmd/merge-env-config/merge-env-config ${GOPATH}/bin 9 | 10 | dist: 11 | mkdir -p dist 12 | cd cmd/merge-env-config && gox -os="linux darwin" -arch="amd64" -output "../../dist/{{.Dir}}-${GIT_VER}-{{.OS}}-{{.Arch}}" -ldflags "-w -s -X main.Version=${GIT_VER}" 13 | cd dist && find . -name "*${GIT_VER}*" -type f -exec zip {}.zip {} \; 14 | 15 | clean: 16 | rm -f cmd/merge-env-config/merge-env-config 17 | rm -f dist/* 18 | 19 | test: 20 | go test -v -race ./... 21 | 22 | release: 23 | ghr -u kayac -r go-config -n "$(GIT_VER)" $(GIT_VER) dist/ 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-config 2 | 3 | go-config is a configuration loader package. 4 | 5 | See [godoc.org/github.com/kayac/go-config](https://godoc.org/github.com/kayac/go-config). 6 | 7 | ## merge-env-config 8 | 9 | merge-env-config is the cli tool to deal with template files. 10 | 11 | for example: 12 | ``` 13 | { 14 | "name": "some function name", 15 | "description": "some description", 16 | "environment": { 17 | "account_id": "{{ must_env `SOME_MUST_ACCOUNT_ID` }}", 18 | "secret_key": "{{ env `SOME_SECRET_KEY` }}" 19 | } 20 | } 21 | ``` 22 | 23 | ``` 24 | $ SOME_MUST_ACCOUNT_ID=must_account_id SOME_SECRET_KEY=some_secret_key merge-env-config -json function.prod.json.tmpl 25 | { 26 | "name": "some function name", 27 | "description": "some description", 28 | "environment": { 29 | "account_id": "must_account_id", 30 | "secret_key": "some_secret_key" 31 | } 32 | } 33 | ``` 34 | 35 | ## Author 36 | 37 | Copyright (c) 2017 KAYAC Inc. 38 | 39 | ## LICENSE 40 | 41 | MIT 42 | -------------------------------------------------------------------------------- /cmd/merge-env-config/.gitignore: -------------------------------------------------------------------------------- 1 | merge-env-config 2 | -------------------------------------------------------------------------------- /cmd/merge-env-config/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | config "github.com/kayac/go-config" 9 | ) 10 | 11 | var Version = "current" 12 | 13 | type Loader func(interface{}, ...string) error 14 | 15 | type Marshaler func(interface{}) ([]byte, error) 16 | 17 | func main() { 18 | os.Exit(_main()) 19 | } 20 | 21 | func _main() int { 22 | var isJSON, showVersion bool 23 | 24 | flag.BoolVar(&isJSON, "json", false, "file(s) is JSON") 25 | flag.BoolVar(&showVersion, "v", false, "show version number") 26 | flag.BoolVar(&showVersion, "version", false, "show version number") 27 | flag.Parse() 28 | 29 | if showVersion { 30 | fmt.Println("merge-env-config ", Version) 31 | return 0 32 | } 33 | 34 | args := flag.Args() 35 | 36 | if len(args) == 0 { 37 | printUsage() 38 | return 1 39 | } 40 | 41 | var ( 42 | load Loader 43 | marshal Marshaler 44 | conf map[string]interface{} 45 | ) 46 | 47 | if isJSON { 48 | load = config.LoadWithEnvJSON 49 | marshal = config.MarshalJSON 50 | } else { 51 | load = config.LoadWithEnv 52 | marshal = config.Marshal 53 | } 54 | 55 | err := load(&conf, args...) 56 | if err != nil { 57 | fmt.Fprintln(os.Stderr, err) 58 | return 1 59 | } 60 | b, err := marshal(&conf) 61 | if err != nil { 62 | fmt.Fprintln(os.Stderr, err) 63 | } 64 | os.Stdout.Write(b) 65 | return 0 66 | } 67 | 68 | func printUsage() { 69 | fmt.Fprintln(os.Stderr, `Usage of merge-env-config: 70 | 71 | merge-env-config [-json] config1.yaml [config2.yaml ...]`) 72 | flag.PrintDefaults() 73 | } 74 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | // Yaml Config Loader 2 | package config 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "strings" 11 | "sync" 12 | "text/template" 13 | 14 | "github.com/BurntSushi/toml" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | func init() { 19 | defaultLoader = New() 20 | } 21 | 22 | type customFunc func(data []byte) ([]byte, error) 23 | 24 | type unmarshaler func([]byte, interface{}) error 25 | 26 | func ReadWithEnv(configPath string) ([]byte, error) { 27 | return defaultLoader.ReadWithEnv(configPath) 28 | } 29 | 30 | func ReadWithEnvBytes(b []byte) ([]byte, error) { 31 | return defaultLoader.ReadWithEnvBytes(b) 32 | } 33 | 34 | // Load loads YAML files from `configPaths`. 35 | // and assigns decoded values into the `conf` value. 36 | func Load(conf interface{}, configPaths ...string) error { 37 | return defaultLoader.Load(conf, configPaths...) 38 | } 39 | 40 | // Load loads JSON files from `configPaths`. 41 | // and assigns decoded values into the `conf` value. 42 | func LoadJSON(conf interface{}, configPaths ...string) error { 43 | return defaultLoader.LoadJSON(conf, configPaths...) 44 | } 45 | 46 | // Load loads TOML files from `configPaths`. 47 | // and assigns decoded values into the `conf` value. 48 | func LoadTOML(conf interface{}, configPaths ...string) error { 49 | return defaultLoader.LoadTOML(conf, configPaths...) 50 | } 51 | 52 | // LoadBytes loads YAML bytes 53 | func LoadBytes(conf interface{}, src []byte) error { 54 | return defaultLoader.LoadBytes(conf, src) 55 | } 56 | 57 | // LoadJSONBytes loads JSON bytes 58 | func LoadJSONBytes(conf interface{}, src []byte) error { 59 | return defaultLoader.LoadJSONBytes(conf, src) 60 | } 61 | 62 | // LoadTOMLBytes loads TOML bytes 63 | func LoadTOMLBytes(conf interface{}, src []byte) error { 64 | return defaultLoader.LoadTOMLBytes(conf, src) 65 | } 66 | 67 | // LoadWithEnv loads YAML files with Env 68 | // replace {{ env "ENV" }} to os.Getenv("ENV") 69 | // if you set default value then {{ env "ENV" "default" }} 70 | func LoadWithEnv(conf interface{}, configPaths ...string) error { 71 | return defaultLoader.LoadWithEnv(conf, configPaths...) 72 | } 73 | 74 | // LoadWithEnvJSON loads JSON files with Env 75 | func LoadWithEnvJSON(conf interface{}, configPaths ...string) error { 76 | return defaultLoader.LoadWithEnvJSON(conf, configPaths...) 77 | } 78 | 79 | // LoadWithEnvTOML loads TOML files with Env 80 | func LoadWithEnvTOML(conf interface{}, configPaths ...string) error { 81 | return defaultLoader.LoadWithEnvTOML(conf, configPaths...) 82 | } 83 | 84 | // LoadWithEnvBytes loads YAML bytes with Env 85 | func LoadWithEnvBytes(conf interface{}, src []byte) error { 86 | return defaultLoader.LoadWithEnvBytes(conf, src) 87 | } 88 | 89 | // LoadWithEnvJSONBytes loads JSON bytes with Env 90 | func LoadWithEnvJSONBytes(conf interface{}, src []byte) error { 91 | return defaultLoader.LoadWithEnvJSONBytes(conf, src) 92 | } 93 | 94 | // LoadWithEnvTOMLBytes loads TOML bytes with Env 95 | func LoadWithEnvTOMLBytes(conf interface{}, src []byte) error { 96 | return defaultLoader.LoadWithEnvTOMLBytes(conf, src) 97 | } 98 | 99 | // Marshal serializes the value provided into a YAML document. 100 | var Marshal = yaml.Marshal 101 | 102 | // MarshalJSON returns the JSON encoding of v with indent by 2 white spaces. 103 | func MarshalJSON(v interface{}) ([]byte, error) { 104 | return json.MarshalIndent(v, "", " ") 105 | } 106 | 107 | func loadWithFunc(conf interface{}, configPaths []string, custom customFunc, unmarshal unmarshaler) error { 108 | for _, configPath := range configPaths { 109 | err := loadConfig(conf, configPath, custom, unmarshal) 110 | if err != nil { 111 | return err 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func loadConfig(conf interface{}, configPath string, custom customFunc, unmarshal unmarshaler) error { 118 | data, err := ioutil.ReadFile(configPath) 119 | if err != nil { 120 | return fmt.Errorf("%s read failed: %w", configPath, err) 121 | } 122 | if err := loadConfigBytes(conf, data, custom, unmarshal); err != nil { 123 | return fmt.Errorf("%s load failed: %w", configPath, err) 124 | } 125 | return nil 126 | } 127 | 128 | func loadConfigBytes(conf interface{}, data []byte, custom customFunc, unmarshal unmarshaler) error { 129 | data, err := readConfigBytes(data, custom) 130 | if err != nil { 131 | return err 132 | } 133 | if err := unmarshal(data, conf); err != nil { 134 | return fmt.Errorf("parse failed: %w", err) 135 | } 136 | return nil 137 | } 138 | 139 | func readConfigBytes(data []byte, custom customFunc) ([]byte, error) { 140 | if custom == nil { 141 | return data, nil 142 | } 143 | data, err := custom(data) 144 | if err != nil { 145 | // Go 1.12 text/template catches a panic raised in user-defined function. 146 | // https://golang.org/doc/go1.12#text/template 147 | if strings.Contains(err.Error(), "must_env: environment variable") { 148 | panic(err) 149 | } 150 | return nil, err 151 | } 152 | return data, nil 153 | } 154 | 155 | // Delims sets the action delimiters to the specified strings. 156 | func Delims(left, right string) { 157 | defaultLoader.Delims(left, right) 158 | } 159 | 160 | // Funcs adds the elements of the argument map. 161 | // Caution: global settings are overwritten. can't go back. 162 | func Funcs(funcMap template.FuncMap) { 163 | defaultLoader.Funcs(funcMap) 164 | } 165 | 166 | var defaultLoader *Loader 167 | 168 | // Loader represents config loader. 169 | type Loader struct { 170 | Data interface{} 171 | 172 | mu sync.Mutex 173 | leftDelim string 174 | rightDelim string 175 | funcMap template.FuncMap 176 | } 177 | 178 | // DefaultFuncMap defines built-in template functions. 179 | var DefaultFuncMap = template.FuncMap{ 180 | "env": func(keys ...string) string { 181 | v := "" 182 | for _, k := range keys { 183 | v = os.Getenv(k) 184 | if v != "" { 185 | return v 186 | } 187 | v = k 188 | } 189 | return v 190 | }, 191 | "must_env": func(key string) string { 192 | if v, ok := os.LookupEnv(key); ok { 193 | return v 194 | } 195 | panic(fmt.Sprintf("environment variable %s is not defined", key)) 196 | }, 197 | "json_escape": func(s string) string { 198 | b, _ := json.Marshal(s) // marshal as JSON string 199 | return string(b[1 : len(b)-1]) // remove " on head and tail 200 | }, 201 | } 202 | 203 | // New creates a Loader instance. 204 | func New() *Loader { 205 | l := &Loader{ 206 | funcMap: make(template.FuncMap, len(DefaultFuncMap)), 207 | } 208 | l.Funcs(DefaultFuncMap) 209 | return l 210 | } 211 | 212 | func (l *Loader) newTemplate() *template.Template { 213 | l.mu.Lock() 214 | defer l.mu.Unlock() 215 | tmpl := template.New("conf").Funcs(l.funcMap) 216 | if l.leftDelim != "" && l.rightDelim != "" { 217 | tmpl.Delims(l.leftDelim, l.rightDelim) 218 | } 219 | return tmpl 220 | } 221 | 222 | func (l *Loader) replacer(data []byte) ([]byte, error) { 223 | t, err := l.newTemplate().Parse(string(data)) 224 | if err != nil { 225 | return nil, fmt.Errorf("config parse by template failed: %w", err) 226 | } 227 | buf := &bytes.Buffer{} 228 | if err = t.Execute(buf, l.Data); err != nil { 229 | return nil, fmt.Errorf("template attach failed: %w", err) 230 | } 231 | return buf.Bytes(), nil 232 | } 233 | 234 | // Load loads YAML files from `configPaths`. 235 | // and assigns decoded values into the `conf` value. 236 | func (l *Loader) Load(conf interface{}, configPaths ...string) error { 237 | return loadWithFunc(conf, configPaths, nil, yaml.Unmarshal) 238 | } 239 | 240 | // LoadJSON loads JSON files from `configPaths`. 241 | // and assigns decoded values into the `conf` value. 242 | func (l *Loader) LoadJSON(conf interface{}, configPaths ...string) error { 243 | return loadWithFunc(conf, configPaths, nil, json.Unmarshal) 244 | } 245 | 246 | // LoadTOML loads TOML files from `configPaths`. 247 | // and assigns decoded values into the `conf` value. 248 | func (l *Loader) LoadTOML(conf interface{}, configPaths ...string) error { 249 | return loadWithFunc(conf, configPaths, nil, toml.Unmarshal) 250 | } 251 | 252 | // LoadBytes loads YAML bytes 253 | func (l *Loader) LoadBytes(conf interface{}, src []byte) error { 254 | return loadConfigBytes(conf, src, nil, yaml.Unmarshal) 255 | } 256 | 257 | // LoadJSONBytes loads JSON bytes 258 | func (l *Loader) LoadJSONBytes(conf interface{}, src []byte) error { 259 | return loadConfigBytes(conf, src, nil, json.Unmarshal) 260 | } 261 | 262 | // LoadTOMLBytes loads TOML bytes 263 | func (l *Loader) LoadTOMLBytes(conf interface{}, src []byte) error { 264 | return loadConfigBytes(conf, src, nil, toml.Unmarshal) 265 | } 266 | 267 | // LoadWithEnv loads YAML files with Env 268 | // replace {{ env "ENV" }} to os.Getenv("ENV") 269 | // if you set default value then {{ env "ENV" "default" }} 270 | func (l *Loader) LoadWithEnv(conf interface{}, configPaths ...string) error { 271 | return loadWithFunc(conf, configPaths, l.replacer, yaml.Unmarshal) 272 | } 273 | 274 | // LoadWithEnvJSON loads JSON files with Env 275 | func (l *Loader) LoadWithEnvJSON(conf interface{}, configPaths ...string) error { 276 | return loadWithFunc(conf, configPaths, l.replacer, json.Unmarshal) 277 | } 278 | 279 | // LoadWithEnvTOML loads TOML files with Env 280 | func (l *Loader) LoadWithEnvTOML(conf interface{}, configPaths ...string) error { 281 | return loadWithFunc(conf, configPaths, l.replacer, toml.Unmarshal) 282 | } 283 | 284 | // LoadWithEnvBytes loads YAML bytes with Env 285 | func (l *Loader) LoadWithEnvBytes(conf interface{}, src []byte) error { 286 | return loadConfigBytes(conf, src, l.replacer, yaml.Unmarshal) 287 | } 288 | 289 | // LoadWithEnvJSONBytes loads JSON bytes with Env 290 | func (l *Loader) LoadWithEnvJSONBytes(conf interface{}, src []byte) error { 291 | return loadConfigBytes(conf, src, l.replacer, json.Unmarshal) 292 | } 293 | 294 | // LoadWithEnvTOMLBytes loads TOML bytes with Env 295 | func (l *Loader) LoadWithEnvTOMLBytes(conf interface{}, src []byte) error { 296 | return loadConfigBytes(conf, src, l.replacer, toml.Unmarshal) 297 | } 298 | 299 | // Delims sets the action delimiters to the specified strings. 300 | func (l *Loader) Delims(left, right string) { 301 | l.mu.Lock() 302 | defer l.mu.Unlock() 303 | l.leftDelim = left 304 | l.rightDelim = right 305 | } 306 | 307 | // Funcs adds the elements of the argument map. 308 | func (l *Loader) Funcs(funcMap template.FuncMap) { 309 | l.mu.Lock() 310 | defer l.mu.Unlock() 311 | for name, fn := range funcMap { 312 | l.funcMap[name] = fn 313 | } 314 | } 315 | 316 | func (l *Loader) ReadWithEnv(configPath string) ([]byte, error) { 317 | b, err := ioutil.ReadFile(configPath) 318 | if err != nil { 319 | return nil, err 320 | } 321 | return readConfigBytes(b, l.replacer) 322 | } 323 | 324 | func (l *Loader) ReadWithEnvBytes(b []byte) ([]byte, error) { 325 | return readConfigBytes(b, l.replacer) 326 | } 327 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | "testing" 10 | "time" 11 | 12 | "github.com/kayac/go-config" 13 | ) 14 | 15 | var dir string 16 | 17 | type DBConfig struct { 18 | Master string `yaml:"master"` 19 | Slave string `yaml:"slave"` 20 | Timeout time.Duration `yml:"timeout"` 21 | } 22 | type Conf struct { 23 | Domain string `yaml:"domain"` 24 | IsDev bool `yaml:"is_dev"` 25 | Timeout time.Duration `yml:"timeout"` 26 | DB DBConfig `yaml:"db"` 27 | } 28 | 29 | func TestMain(m *testing.M) { 30 | runner := func() int { 31 | var err error 32 | dir, err = ioutil.TempDir("", "go-config") 33 | if err != nil { 34 | panic(err) 35 | } 36 | defer os.RemoveAll(dir) 37 | return m.Run() 38 | } 39 | os.Exit(runner()) 40 | } 41 | 42 | func ExampleLoad() { 43 | type DBConfig struct { 44 | Master string `yaml:"master"` 45 | Slave string `yaml:"slave"` 46 | Timeout time.Duration `yml:"timeout"` 47 | } 48 | type Conf struct { 49 | Domain string `yaml:"domain"` 50 | IsDev bool `yaml:"is_dev"` 51 | Timeout time.Duration `yml:"timeout"` 52 | DB DBConfig `yaml:"db"` 53 | } 54 | 55 | baseConfig := ` 56 | # config.yml 57 | domain: example.com 58 | db: 59 | master: rw@/example 60 | slave: ro@/example 61 | timeout: 0.5s 62 | ` 63 | localConfig := ` 64 | # config_local.yml 65 | domain: dev.example.com 66 | is_dev: true 67 | ` 68 | conf := &Conf{} 69 | baseConfigYaml, _ := genConfigFile("config.yml", baseConfig) // /path/to/config.yml 70 | localConfigYaml, _ := genConfigFile("config_local.yml", localConfig) // /path/to/config_local.yml 71 | 72 | err := config.Load(conf, baseConfigYaml, localConfigYaml) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | fmt.Printf("%+v", conf) 78 | // Output: 79 | // &{Domain:dev.example.com IsDev:true Timeout:0s DB:{Master:rw@/example Slave:ro@/example Timeout:500ms}} 80 | } 81 | 82 | func ExampleLoadWithEnv() { 83 | type DBConfig struct { 84 | Master string `yaml:"master"` 85 | Slave string `yaml:"slave"` 86 | Timeout time.Duration `yml:"timeout"` 87 | } 88 | type Conf struct { 89 | Domain string `yaml:"domain"` 90 | IsDev bool `yaml:"is_dev"` 91 | Timeout time.Duration `yml:"timeout"` 92 | DB DBConfig `yaml:"db"` 93 | } 94 | 95 | baseConfig := ` 96 | # config.yml 97 | domain: {{ env "DOMAIN"}} 98 | db: 99 | master: rw@/example 100 | slave: ro@/example 101 | timeout: 0.5s 102 | ` 103 | localConfig := ` 104 | # config_local.yml 105 | is_dev: true 106 | db: 107 | master: {{ env "RW_USER" "rw" }}@/{{ env "DB_NAME" "example_dev" }} 108 | slave: {{ env "RO_USER" "ro" }}@/{{ env "DB_NAME" "example_dev" }} 109 | ` 110 | os.Setenv("DOMAIN", "dev.example.com") 111 | os.Setenv("DB_NAME", "example_local") 112 | 113 | conf := &Conf{} 114 | baseConfigYaml, _ := genConfigFile("config.yml", baseConfig) // /path/to/config.yml 115 | localConfigYaml, _ := genConfigFile("config_local.yml", localConfig) // /path/to/config_local.yml 116 | 117 | err := config.LoadWithEnv(conf, baseConfigYaml, localConfigYaml) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | fmt.Printf("%+v", conf) 123 | // Output: 124 | // &{Domain:dev.example.com IsDev:true Timeout:0s DB:{Master:rw@/example_local Slave:ro@/example_local Timeout:500ms}} 125 | } 126 | 127 | func TestLoad(t *testing.T) { 128 | a, err := genConfigFile("a.yml", `## a.yml 129 | domain: example.com 130 | db: 131 | master: rw@/example 132 | slave: ro@/example 133 | timeout: 0.5s 134 | `) 135 | if err != nil { 136 | t.Error(err) 137 | return 138 | } 139 | b, err := genConfigFile("b.yml", `## b.yml 140 | is_dev: true 141 | `) 142 | if err != nil { 143 | t.Error(err) 144 | return 145 | } 146 | c, err := genConfigFile("c.yml", `## c.yml 147 | db: 148 | master: rw@/example2 149 | slave: ro@/example2 150 | timeout: 200ms 151 | `) 152 | if err != nil { 153 | t.Error(err) 154 | return 155 | } 156 | er, err := genConfigFile("err.yml", `## err.yml 157 | db: 158 | master rw@/example2 159 | `) 160 | if err != nil { 161 | t.Error(err) 162 | return 163 | } 164 | 165 | e := &Conf{ 166 | Domain: "example.com", 167 | IsDev: false, 168 | Timeout: time.Duration(0), 169 | DB: DBConfig{ 170 | Master: "rw@/example", 171 | Slave: "ro@/example", 172 | Timeout: time.Duration(500) * time.Millisecond, 173 | }, 174 | } 175 | t1 := &Conf{} 176 | err = config.Load(t1, a) 177 | if err != nil { 178 | t.Errorf("a.yml load error %s", err) 179 | } 180 | if !reflect.DeepEqual(t1, e) { 181 | t.Errorf("a.yml not match. got: %#v, expect: %#v", t1, e) 182 | } 183 | 184 | t2 := &Conf{} 185 | e.IsDev = true 186 | err = config.Load(t2, a, b) 187 | if err != nil { 188 | t.Errorf("a.yml, b.yml load error %s", err) 189 | } 190 | if !reflect.DeepEqual(t2, e) { 191 | t.Errorf("a.yml, b.yml not match. got: %#v, expect: %#v", t2, e) 192 | } 193 | 194 | t3 := &Conf{} 195 | e.DB.Master = "rw@/example2" 196 | e.DB.Slave = "ro@/example2" 197 | e.DB.Timeout = time.Duration(200) * time.Millisecond 198 | err = config.Load(t3, a, b, c) 199 | if err != nil { 200 | t.Errorf("a.yml, b.yml, c.yml load error %s", err) 201 | } 202 | if !reflect.DeepEqual(t3, e) { 203 | t.Errorf("a.yml, b.yml, c.yml not match. got: %#v, expect: %#v", t3, e) 204 | } 205 | 206 | t4 := &Conf{} 207 | err = config.Load(t4, filepath.Join(dir, "nothing.yml")) 208 | if err == nil { 209 | t.Errorf("nothing.yml is not found.") 210 | } 211 | t.Log(err) 212 | 213 | t5 := &Conf{} 214 | err = config.Load(t5, er) 215 | if err == nil { 216 | t.Errorf("err.yml is format err.") 217 | } 218 | t.Log(err) 219 | } 220 | 221 | func genConfigFile(name string, config string) (string, error) { 222 | path := filepath.Join(dir, name) 223 | io, err := os.Create(path) 224 | if err != nil { 225 | return "", err 226 | } 227 | if _, err := io.WriteString(config); err != nil { 228 | return "", err 229 | } 230 | return path, nil 231 | } 232 | 233 | func TestLoadMustEnvPanic(t *testing.T) { 234 | f, err := genConfigFile("must-panic.yml", `## must.yml 235 | domain: '{{ must_env "MUST_DOMAIN_PANIC" }}' 236 | `) 237 | if err != nil { 238 | t.Error(err) 239 | return 240 | } 241 | defer func() { 242 | if r := recover(); r == nil { 243 | t.Error("must_env must raise panic") 244 | } else { 245 | t.Logf("must_env raise panic:%s", r) 246 | } 247 | }() 248 | 249 | c := &Conf{} 250 | config.LoadWithEnv(c, f) 251 | } 252 | 253 | func TestLoadMustEnvPanicBytes(t *testing.T) { 254 | src := []byte(`## must.yml 255 | domain: '{{ must_env "MUST_DOMAIN_PANIC" }}' 256 | `) 257 | defer func() { 258 | if r := recover(); r == nil { 259 | t.Error("must_env must raise panic") 260 | } else { 261 | t.Logf("must_env raise panic:%s", r) 262 | } 263 | }() 264 | 265 | c := &Conf{} 266 | err := config.LoadWithEnvBytes(c, src) 267 | t.Log(err) 268 | } 269 | 270 | func TestLoadMustEnv(t *testing.T) { 271 | f, err := genConfigFile("must.yml", `## must.yml 272 | domain: '{{ must_env "MUST_DOMAIN" }}' 273 | `) 274 | if err != nil { 275 | t.Error(err) 276 | return 277 | } 278 | mustDomain := "must.example.com" 279 | t.Setenv("MUST_DOMAIN", mustDomain) 280 | c := &Conf{} 281 | if err := config.LoadWithEnv(c, f); err != nil { 282 | t.Error(err) 283 | } 284 | if c.Domain != mustDomain { 285 | t.Errorf("domain expected %s got %s", mustDomain, c.Domain) 286 | } 287 | 288 | t.Setenv("MUST_DOMAIN", "") 289 | c2 := &Conf{} 290 | if err := config.LoadWithEnv(c2, f); err != nil { 291 | t.Error(err) 292 | } 293 | if c2.Domain != "" { 294 | t.Errorf("domain expected \"\" got %s", c2.Domain) 295 | } 296 | } 297 | 298 | func TestLoadMustEnvBytes(t *testing.T) { 299 | src := []byte(`## must.yml 300 | domain: '{{ must_env "MUST_DOMAIN" }}' 301 | `) 302 | mustDomain := "must.example.com" 303 | t.Setenv("MUST_DOMAIN", mustDomain) 304 | c := &Conf{} 305 | if err := config.LoadWithEnvBytes(c, src); err != nil { 306 | t.Error(err) 307 | } 308 | if c.Domain != mustDomain { 309 | t.Errorf("domain expected %s got %s", mustDomain, c.Domain) 310 | } 311 | 312 | t.Setenv("MUST_DOMAIN", "") 313 | c2 := &Conf{} 314 | if err := config.LoadWithEnvBytes(c2, src); err != nil { 315 | t.Error(err) 316 | } 317 | if c2.Domain != "" { 318 | t.Errorf("domain expected \"\" got %s", c2.Domain) 319 | } 320 | } 321 | 322 | func TestLoadJSON(t *testing.T) { 323 | a, err := genConfigFile("a.json", `{ 324 | "foo": "bar", 325 | "env_foo": "{{ env "FOO" }}" 326 | }`) 327 | if err != nil { 328 | t.Error(err) 329 | } 330 | b, err := genConfigFile("b.json", `{ 331 | "bar": "baz" 332 | }`) 333 | if err != nil { 334 | t.Error(err) 335 | } 336 | t.Setenv("FOO", "BOO") 337 | c := make(map[string]string) 338 | err = config.LoadWithEnvJSON(&c, a, b) 339 | if err != nil { 340 | t.Error(err) 341 | } 342 | if c["foo"] != "bar" { 343 | t.Errorf("foo expected bar got %s", c["foo"]) 344 | } 345 | if c["env_foo"] != "BOO" { 346 | t.Errorf("env_foo expected BOO got %s", c["env_foo"]) 347 | } 348 | if c["bar"] != "baz" { 349 | t.Errorf("bar expected baz got %s", c["bar"]) 350 | } 351 | } 352 | 353 | func TestLoadTOML(t *testing.T) { 354 | a, err := genConfigFile("a.toml", ` 355 | foo = "bar" 356 | env_foo = '{{ env "FOO" }}' 357 | `) 358 | if err != nil { 359 | t.Error(err) 360 | } 361 | b, err := genConfigFile("b.toml", ` 362 | bar = "baz" 363 | `) 364 | if err != nil { 365 | t.Error(err) 366 | } 367 | t.Setenv("FOO", "BOO") 368 | c := make(map[string]string) 369 | err = config.LoadWithEnvTOML(&c, a, b) 370 | if err != nil { 371 | t.Error(err) 372 | } 373 | if c["foo"] != "bar" { 374 | t.Errorf("foo expected bar got %s", c["foo"]) 375 | } 376 | if c["env_foo"] != "BOO" { 377 | t.Errorf("env_foo expected BOO got %s", c["env_foo"]) 378 | } 379 | if c["bar"] != "baz" { 380 | t.Errorf("bar expected baz got %s", c["bar"]) 381 | } 382 | } 383 | 384 | func TestLoadEmpty(t *testing.T) { 385 | conf := make(map[string]string) 386 | if err := config.LoadWithEnv(&conf, "tests/foo.yaml"); err != nil { 387 | t.Error(err) 388 | } 389 | if conf["foo"] != "bar" { 390 | t.Errorf("unexpected foo: %s", conf["foo"]) 391 | } 392 | 393 | newConf := make(map[string]string) 394 | if err := config.LoadWithEnv(&newConf, "tests/empty.yaml"); err != nil { 395 | t.Error(err) 396 | } 397 | if v, exists := newConf["foo"]; exists { 398 | t.Errorf("unexpected foo must be not exist but got %s", v) 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /data_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kayac/go-config" 7 | ) 8 | 9 | func TestData(t *testing.T) { 10 | loader := config.New() 11 | data := make(map[string]string) 12 | data["foo"] = "DataFoo" 13 | loader.Data = data 14 | 15 | src := []byte(`foo: '{{ .foo }}'`) 16 | c := make(map[string]string) 17 | if err := loader.LoadWithEnvBytes(&c, src); err != nil { 18 | t.Error(err) 19 | } 20 | if c["foo"] != "DataFoo" { 21 | t.Errorf("failed to inject foo: %#v", c) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /delims_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/kayac/go-config" 7 | ) 8 | 9 | func TestDelims(t *testing.T) { 10 | t.Setenv("FOO", "test_foo") 11 | config.Delims("<%", "%>") 12 | defer config.Delims("{{", "}}") 13 | 14 | src := []byte(`foo: '<% env "FOO" %>'`) 15 | c := make(map[string]string) 16 | if err := config.LoadWithEnvBytes(&c, src); err != nil { 17 | t.Error(err) 18 | } 19 | if c["foo"] != "test_foo" { 20 | t.Errorf("failed to inject FOO: %#v", c) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /funcs_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "text/template" 7 | 8 | "github.com/kayac/go-config" 9 | ) 10 | 11 | func TestFuncs(t *testing.T) { 12 | t.Setenv("PREFIX", "test_") 13 | loader := config.New() 14 | loader.Funcs(template.FuncMap{ 15 | "word": func(keys ...string) string { 16 | return strings.Join(keys, "_") 17 | }, 18 | }) 19 | 20 | src := []byte(`foo: '{{ env "PREFIX" }}{{ word "foo" "bar" }}'`) 21 | c := make(map[string]string) 22 | if err := loader.LoadWithEnvBytes(&c, src); err != nil { 23 | t.Error(err) 24 | } 25 | if c["foo"] != "test_foo_bar" { 26 | t.Errorf("failed to inject FOO: %#v", c) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kayac/go-config 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.0 7 | github.com/google/go-cmp v0.5.9 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.0 h1:Ws8e5YmnrGEHzZEzg0YvK/7COGYtTC5PbaH9oSSbgfA= 2 | github.com/BurntSushi/toml v1.3.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 4 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 6 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 7 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 8 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 9 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/kayac/go-config" 10 | ) 11 | 12 | var testsJSON = []string{ 13 | `{"foo":"bar"}`, 14 | `{"foo":"b\nar"}`, 15 | `{"foo":"b\"ar"}`, 16 | `{"foo":"b\u1234ar\t"}`, 17 | `{"foo":"\u2029"}`, 18 | `["A", "B", "C"]`, 19 | `"string"`, 20 | } 21 | 22 | var templateTestJSON = []byte(`{ 23 | "json": "{{ env "JSON" | json_escape }}" 24 | }`) 25 | 26 | func TestJSONEncode(t *testing.T) { 27 | defer os.Unsetenv("JSON") 28 | for _, s := range testsJSON { 29 | t.Setenv("JSON", s) 30 | 31 | var before interface{} 32 | if err := json.Unmarshal([]byte(s), &before); err != nil { 33 | t.Error("failed to unmarshal before", err) 34 | } 35 | conf := make(map[string]string, 0) 36 | if err := config.LoadWithEnvJSONBytes(&conf, templateTestJSON); err != nil { 37 | t.Error("failed to LoadWithEnvJSONBytes", err) 38 | } 39 | t.Logf("%#v", conf) 40 | var after interface{} 41 | if err := json.Unmarshal([]byte(conf["json"]), &after); err != nil { 42 | t.Error("failed to unmarshal after", err) 43 | } 44 | if cmp.Diff(before, after) != "" { 45 | t.Errorf("%v != %v", before, after) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/kayac/go-config" 8 | ) 9 | 10 | var templateTestRead = []byte(`xxx{{ env "FOO" }}xxx`) 11 | 12 | func TestRead(t *testing.T) { 13 | t.Setenv("FOO", "foobar") 14 | 15 | loader := config.New() 16 | b, err := loader.ReadWithEnvBytes(templateTestRead) 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | if !bytes.Equal(b, []byte("xxxfoobarxxx")) { 21 | t.Error("unexpected read result", string(b)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/empty.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kayac/go-config/b80ea0e9d5f20c9a80b5bfc1e90ecc5173e1574a/tests/empty.yaml -------------------------------------------------------------------------------- /tests/foo.yaml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | --------------------------------------------------------------------------------