├── testdata └── configmap │ ├── kinesis_role │ ├── ..data │ ├── ..2022_07_21_18_38_28.530076303 │ ├── kinesis_role │ ├── kinesis_stream_name │ └── collector_kinesis_endpoint │ ├── kinesis_stream_name │ └── collector_kinesis_endpoint ├── example ├── config.yml └── main.go ├── Makefile ├── terminal_plan9.go ├── go.mod ├── .gitignore ├── terminal_bsd.go ├── terminal_linux.go ├── .github └── workflows │ └── ci.yml ├── flag.go ├── terminal.go ├── LICENSE ├── snakecase_test.go ├── terminal_windows.go ├── source_test.go ├── pprof.go ├── doc.go ├── go.sum ├── snakecase.go ├── print_test.go ├── configmap_source_test.go ├── configmap_source.go ├── source.go ├── print.go ├── load.go ├── README.md ├── node_test.go ├── load_test.go └── node.go /testdata/configmap/kinesis_role: -------------------------------------------------------------------------------- 1 | ./..data/kinesis_role -------------------------------------------------------------------------------- /testdata/configmap/..data: -------------------------------------------------------------------------------- 1 | ./..2022_07_21_18_38_28.530076303 -------------------------------------------------------------------------------- /testdata/configmap/..2022_07_21_18_38_28.530076303/kinesis_role: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/configmap/kinesis_stream_name: -------------------------------------------------------------------------------- 1 | ./..data/kinesis_stream_name -------------------------------------------------------------------------------- /example/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | msg: 'hello {{ .USER }}! (from the config file)' 3 | -------------------------------------------------------------------------------- /testdata/configmap/collector_kinesis_endpoint: -------------------------------------------------------------------------------- 1 | ./..data/collector_kinesis_endpoint -------------------------------------------------------------------------------- /testdata/configmap/..2022_07_21_18_38_28.530076303/kinesis_stream_name: -------------------------------------------------------------------------------- 1 | segment-logs 2 | -------------------------------------------------------------------------------- /testdata/configmap/..2022_07_21_18_38_28.530076303/collector_kinesis_endpoint: -------------------------------------------------------------------------------- 1 | https://example.com/blah 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export GO111MODULE=on 2 | 3 | lint: 4 | go install honnef.co/go/tools/cmd/staticcheck@latest 5 | staticcheck ./... 6 | go vet ./... 7 | 8 | test: 9 | go test -race ./... 10 | 11 | ci: lint test 12 | -------------------------------------------------------------------------------- /terminal_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | // isTerminal returns true if the given file descriptor is a terminal. 8 | func isTerminal(fd int) bool { 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segmentio/conf 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/segmentio/objconv v1.0.1 7 | gopkg.in/go-playground/mold.v2 v2.2.0 8 | gopkg.in/validator.v2 v2.0.1 9 | ) 10 | 11 | require ( 12 | github.com/segmentio/go-snakecase v1.2.0 // indirect 13 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 14 | gopkg.in/yaml.v2 v2.4.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Emacs 27 | *~ 28 | -------------------------------------------------------------------------------- /terminal_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || netbsd || openbsd 6 | // +build darwin dragonfly freebsd netbsd openbsd 7 | 8 | package conf 9 | 10 | import "syscall" 11 | 12 | const ioctlReadTermios = syscall.TIOCGETA 13 | -------------------------------------------------------------------------------- /terminal_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package conf 6 | 7 | // These constants are declared here, rather than importing 8 | // them from the syscall package as some syscall packages, even 9 | // on linux, for example gccgo, do not declare them. 10 | const ioctlReadTermios = 0x5401 // syscall.TCGETS 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v5 9 | with: 10 | go-version: '1.23.x' 11 | - uses: actions/checkout@v4 12 | with: 13 | path: './src/github.com/segmentio/conf' 14 | - run: echo "PATH=$GITHUB_WORKSPACE/bin:$PATH" >> $GITHUB_ENV 15 | - name: Run tests 16 | run: make ci 17 | working-directory: './src/github.com/segmentio/conf' 18 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | func newFlagSet(cfg Map, name string, sources ...Source) *flag.FlagSet { 10 | set := flag.NewFlagSet(name, flag.ContinueOnError) 11 | set.SetOutput(io.Discard) 12 | 13 | cfg.Scan(func(path []string, item MapItem) { 14 | set.Var(item.Value, strings.Join(append(path, item.Name), "."), item.Help) 15 | }) 16 | 17 | for _, source := range sources { 18 | if f, ok := source.(FlagSource); ok { 19 | set.Var(f, f.Flag(), f.Help()) 20 | } 21 | } 22 | 23 | return set 24 | } 25 | -------------------------------------------------------------------------------- /terminal.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd 6 | // +build darwin dragonfly freebsd linux,!appengine netbsd openbsd 7 | 8 | package conf 9 | 10 | import ( 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | // isTerminal returns true if the given file descriptor is a terminal. 16 | func isTerminal(fd int) bool { 17 | var termios syscall.Termios 18 | _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) 19 | return err == 0 20 | } 21 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | // This example program can be used to test the features provided by the 2 | // github.com/segmentio/conf package. 3 | // 4 | // Passing configuration via the program arguments: 5 | // 6 | // $ go run ./example/main.go -msg 'hello! (from the arguments)' 7 | // [main] hello! (from the arguments) 8 | // 9 | // Passing configuration via the environment variables: 10 | // 11 | // $ MAIN_MSG='hello! (from the environment)' go run ./example/main.go 12 | // [main] hello! (from the environment) 13 | // 14 | // Passing configuration via a configuration file: 15 | // 16 | // $ go run ./example/main.go -config-file ./example/config.yml 17 | // [main] hello ${USER}! (from the config file) 18 | // 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "os" 24 | "path/filepath" 25 | 26 | "github.com/segmentio/conf" 27 | ) 28 | 29 | func main() { 30 | config := struct { 31 | Message string `conf:"msg" help:"The message to print out."` 32 | }{ 33 | Message: "default", 34 | } 35 | conf.Load(&config) 36 | fmt.Printf("[%s] %s\n", filepath.Base(os.Args[0]), config.Message) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Segment 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 | -------------------------------------------------------------------------------- /snakecase_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "testing" 4 | 5 | var ( 6 | snakecaseTests = []struct { 7 | in string 8 | out string 9 | }{ 10 | {"", ""}, 11 | {"A", "a"}, 12 | {"HelloWorld", "hello_world"}, 13 | {"HELLOWorld", "hello_world"}, 14 | {"Hello1World2", "hello1_world2"}, 15 | {"123_", "123_"}, 16 | {"_", "_"}, 17 | {"___", "___"}, 18 | {"HELLO_WORLD", "hello_world"}, 19 | {"HelloWORLD", "hello_world"}, 20 | {"test_P_x", "test_p_x"}, 21 | {"__hello_world__", "__hello_world__"}, 22 | {"__Hello_World__", "__hello_world__"}, 23 | {"__Hello__World__", "__hello__world__"}, 24 | {"hello-world", "hello_world"}, 25 | } 26 | ) 27 | 28 | func TestSnakecaseLower(t *testing.T) { 29 | for _, test := range snakecaseTests { 30 | t.Run(test.in, func(t *testing.T) { 31 | if s := snakecaseLower(test.in); s != test.out { 32 | t.Error(s) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | func BenchmarkSnakecase(b *testing.B) { 39 | for _, test := range snakecaseTests { 40 | b.Run(test.in, func(b *testing.B) { 41 | for i := 0; i != b.N; i++ { 42 | snakecase(test.in) 43 | } 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /terminal_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | //go:build windows 6 | // +build windows 7 | 8 | package conf 9 | 10 | import ( 11 | "syscall" 12 | "unsafe" 13 | ) 14 | 15 | var kernel32 = syscall.NewLazyDLL("kernel32.dll") 16 | 17 | var ( 18 | procGetConsoleMode = kernel32.NewProc("GetConsoleMode") 19 | ) 20 | 21 | type ( 22 | short int16 23 | word uint16 24 | 25 | coord struct { 26 | x short 27 | y short 28 | } 29 | smallRect struct { 30 | left short 31 | top short 32 | right short 33 | bottom short 34 | } 35 | consoleScreenBufferInfo struct { 36 | size coord 37 | cursorPosition coord 38 | attributes word 39 | window smallRect 40 | maximumWindowSize coord 41 | } 42 | ) 43 | 44 | // isTerminal returns true if the given file descriptor is a terminal. 45 | func isTerminal(fd int) bool { 46 | var st uint32 47 | r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0) 48 | return r != 0 && e == 0 49 | } 50 | -------------------------------------------------------------------------------- /source_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type kinesisConfig struct { 8 | StreamName string 9 | } 10 | type testConfig struct { 11 | Kinesis kinesisConfig `conf:"kinesis"` 12 | } 13 | 14 | func TestEnvSource(t *testing.T) { 15 | t.Run("Struct", func(t *testing.T) { 16 | src := NewEnvSource("collector", "COLLECTOR_KINESIS_STREAM_NAME=blah") 17 | a := testConfig{} 18 | loader := Loader{ 19 | Name: "collector", 20 | Args: []string{}, 21 | Sources: []Source{src}, 22 | } 23 | if _, _, err := loader.Load(&a); err != nil { 24 | t.Fatal(err) 25 | } 26 | if a.Kinesis.StreamName != "blah" { 27 | t.Errorf("expected StreamName to get populated, got %q", a.Kinesis.StreamName) 28 | } 29 | }) 30 | 31 | t.Run("Map", func(t *testing.T) { 32 | src := NewEnvSource("", "STREAM_NAME=blah") 33 | cfg := struct { 34 | StreamName string 35 | }{} 36 | loader := Loader{ 37 | Name: "collector", 38 | Args: []string{}, 39 | Sources: []Source{src}, 40 | } 41 | if _, _, err := loader.Load(&cfg); err != nil { 42 | t.Fatal(err) 43 | } 44 | if cfg.StreamName != "blah" { 45 | t.Errorf("expected 'blah' stream name, got %q", cfg.StreamName) 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /pprof.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "runtime" 4 | 5 | // PPROF is a configuration struct which can be used to configure the runtime 6 | // profilers of programs. 7 | // 8 | // config := struct{ 9 | // PPROF `conf:"pprof"` 10 | // }{ 11 | // PPROF: conf.DefaultPPROF(), 12 | // } 13 | // conf.Load(&config) 14 | // conf.SetPPROF(config.PPROF) 15 | // 16 | type PPROF struct { 17 | BlockProfileRate int `conf:"block-profile-rate" help:"Sets the block profile rate to enable runtime profiling of blocking operations, zero disables block profiling." validate:"min=0"` 18 | MutexProfileFraction int `conf:"mutex-profile-fraction" help:"Sets the mutex profile fraction to enable runtime profiling of lock contention, zero disables mutex profiling." validate:"min=0"` 19 | } 20 | 21 | // DefaultPPROF returns the default value of a PPROF struct. Note that the 22 | // zero-value is valid, DefaultPPROF differs because it captures the current 23 | // configuration of the program's runtime. 24 | func DefaultPPROF() PPROF { 25 | return PPROF{ 26 | MutexProfileFraction: runtime.SetMutexProfileFraction(-1), 27 | } 28 | } 29 | 30 | // SetPPROF configures the runtime profilers based on the given PPROF config. 31 | func SetPPROF(config PPROF) { 32 | runtime.SetBlockProfileRate(config.BlockProfileRate) 33 | runtime.SetMutexProfileFraction(config.MutexProfileFraction) 34 | } 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package conf package provides tools for easily loading program configurations 2 | // from multiple sources such as the command line arguments, environment, or a 3 | // configuration file. 4 | // 5 | // Most applications only need to use the Load function to get their settings 6 | // loaded into an object. By default, Load will read from a configurable file 7 | // defined by the -config-file command line argument, load values present in the 8 | // environment, and finally load the program arguments. 9 | // 10 | // The object in which the configuration is loaded must be a struct, the names 11 | // and types of its fields are introspected by the Load function to understand 12 | // how to load the configuration. 13 | // 14 | // The name deduction from the struct field obeys the same rules than those 15 | // implemented by the standard encoding/json package, which means the program 16 | // can set the "conf" tag to override the default field names in the command 17 | // line arguments and configuration file. 18 | // 19 | // A "help" tag may also be set on the fields of the configuration object to 20 | // add documentation to the setting, which will be shown when the program is 21 | // asked to print its help. 22 | // 23 | // When values are loaded from the environment the Load function looks for 24 | // variables matching the struct fields names in snake-upper-case form. 25 | package conf 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 2 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 3 | github.com/segmentio/go-snakecase v1.2.0 h1:4cTmEjPGi03WmyAHWBjX53viTpBkn/z+4DO++fqYvpw= 4 | github.com/segmentio/go-snakecase v1.2.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= 5 | github.com/segmentio/objconv v1.0.1 h1:QjfLzwriJj40JibCV3MGSEiAoXixbp4ybhwfTB8RXOM= 6 | github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 9 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 10 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 11 | gopkg.in/go-playground/mold.v2 v2.2.0 h1:Y4IYB4/HYQfuq43zaKh6vs9cVelLE9qbqe2fkyfCTWQ= 12 | gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= 13 | gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= 14 | gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= 15 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 16 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 17 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 18 | -------------------------------------------------------------------------------- /snakecase.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "strings" 4 | 5 | func snakecaseLower(s string) string { 6 | return strings.ToLower(snakecase(s)) 7 | } 8 | 9 | func snakecaseUpper(s string) string { 10 | return strings.ToUpper(snakecase(s)) 11 | } 12 | 13 | func snakecase(s string) string { 14 | b := make([]byte, 0, 64) 15 | i := len(s) - 1 16 | 17 | // search sequences, starting from the end of the string 18 | for i >= 0 { 19 | switch { 20 | case isLower(s[i]): // sequence of lowercase, maybe starting with an uppercase 21 | for i >= 0 && !isSeparator(s[i]) && !isUpper(s[i]) { 22 | b = append(b, s[i]) 23 | i-- 24 | } 25 | 26 | if i >= 0 { 27 | b = append(b, snakebyte(s[i])) 28 | i-- 29 | if isSeparator(s[i+1]) { // avoid double underscore if we have "_word" 30 | continue 31 | } 32 | } 33 | 34 | if i >= 0 && !isSeparator(s[i]) { // avoid double underscores if we have "_Word" 35 | b = append(b, '_') 36 | } 37 | 38 | case isUpper(s[i]): // sequence of uppercase 39 | for i >= 0 && !isSeparator(s[i]) && !isLower(s[i]) { 40 | b = append(b, s[i]) 41 | i-- 42 | } 43 | 44 | if i >= 0 { 45 | if isSeparator(s[i]) { 46 | i-- 47 | } 48 | b = append(b, '_') 49 | } 50 | 51 | default: // not a letter, it'll be part of the next sequence 52 | b = append(b, snakebyte(s[i])) 53 | i-- 54 | } 55 | } 56 | 57 | // reverse 58 | for i, j := 0, len(b)-1; i < j; { 59 | b[i], b[j] = b[j], b[i] 60 | i++ 61 | j-- 62 | } 63 | 64 | return string(b) 65 | } 66 | 67 | func snakebyte(b byte) byte { 68 | if isSeparator(b) { 69 | return '_' 70 | } 71 | return b 72 | } 73 | 74 | func isSeparator(c byte) bool { 75 | return c == '_' || c == '-' 76 | } 77 | 78 | func isUpper(c byte) bool { 79 | return c >= 'A' && c <= 'Z' 80 | } 81 | 82 | func isLower(c byte) bool { 83 | return c >= 'a' && c <= 'z' 84 | } 85 | -------------------------------------------------------------------------------- /print_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type Bytes uint64 12 | 13 | func TestPrettyType(t *testing.T) { 14 | tests := []struct { 15 | v interface{} 16 | s string 17 | }{ 18 | {nil, "unknown"}, 19 | {false, "bool"}, 20 | 21 | {int(0), "int"}, 22 | {int8(0), "int8"}, 23 | {int16(0), "int16"}, 24 | {int32(0), "int32"}, 25 | {int64(0), "int64"}, 26 | 27 | {uint(0), "uint"}, 28 | {uint8(0), "uint8"}, 29 | {uint16(0), "uint16"}, 30 | {uint32(0), "uint32"}, 31 | {uint64(0), "uint64"}, 32 | 33 | {float32(0), "float32"}, 34 | {float64(0), "float64"}, 35 | 36 | {time.Duration(0), "duration"}, 37 | {time.Time{}, "time"}, 38 | 39 | {"", "string"}, 40 | {[]byte{}, "base64"}, 41 | 42 | {[]int{}, "list"}, 43 | {[1]int{}, "list"}, 44 | 45 | {map[int]int{}, "object"}, 46 | {struct{}{}, "object"}, 47 | {&struct{}{}, "object"}, 48 | 49 | {Bytes(0), "bytes"}, 50 | } 51 | 52 | for _, test := range tests { 53 | t.Run(test.s, func(t *testing.T) { 54 | if s := prettyType(reflect.TypeOf(test.v)); s != test.s { 55 | t.Error(s) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestPrintError(t *testing.T) { 62 | ld := Loader{} 63 | b := &bytes.Buffer{} 64 | 65 | ld.FprintError(b, errors.New("A: missing value")) 66 | 67 | const txt = "Error:\n A: missing value\n\n" 68 | 69 | if s := b.String(); s != txt { 70 | t.Error(s) 71 | } 72 | } 73 | 74 | func TestPrintHelp(t *testing.T) { 75 | ld := Loader{ 76 | Name: "test", 77 | Args: []string{"-A=1", "-B=2", "-C=3"}, 78 | Commands: []Command{{"run", "Run something"}, {"version", "Print the version"}}, 79 | } 80 | b := &bytes.Buffer{} 81 | 82 | ld.FprintHelp(b, struct { 83 | A int 84 | B int 85 | C int 86 | D bool `help:"Set D"` 87 | E bool `conf:"enable" help:"Enable E"` 88 | T time.Duration 89 | }{A: 1, T: time.Second}) 90 | 91 | const txt = "Usage:\n" + 92 | " test [command] [options...]\n" + 93 | "\n" + 94 | "Commands:\n" + 95 | " run Run something\n" + 96 | " version Print the version\n" + 97 | "\n" + 98 | "Options:\n" + 99 | " -A int\n" + 100 | " \t(default 1)\n" + 101 | "\n" + 102 | " -B int\n" + 103 | "\n" + 104 | " -C int\n" + 105 | "\n" + 106 | " -D\tSet D\n" + 107 | "\n" + 108 | " -T duration\n" + 109 | " \t(default 1s)\n" + 110 | "\n" + 111 | " -enable\n" + 112 | " \tEnable E\n" + 113 | "\n" 114 | 115 | if s := b.String(); s != txt { 116 | t.Error(s) 117 | t.Error(txt) 118 | t.Error(len(s), len(txt)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /configmap_source_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestConfigMap(t *testing.T) { 13 | t.Run("Source", func(t *testing.T) { 14 | src := NewKubernetesConfigMapSource("", "./testdata/configmap") 15 | cfg := struct { 16 | CollectorKinesisEndpoint string 17 | }{} 18 | loader := Loader{ 19 | Name: "collector", 20 | Args: []string{}, 21 | Sources: []Source{src}, 22 | } 23 | if _, _, err := loader.Load(&cfg); err != nil { 24 | t.Fatal(err) 25 | } 26 | if cfg.CollectorKinesisEndpoint != "https://example.com/blah" { 27 | t.Fatalf("bad value: want example.com/blah got %q", cfg.CollectorKinesisEndpoint) 28 | } 29 | }) 30 | 31 | t.Run("NestedConfig", func(t *testing.T) { 32 | a := testConfig{} 33 | loader := Loader{ 34 | Name: "collector", 35 | Args: []string{}, 36 | Sources: []Source{ 37 | NewKubernetesConfigMapSource("", "./testdata/configmap"), 38 | }, 39 | } 40 | loader.Load(&a) 41 | if a.Kinesis.StreamName != "segment-logs" { 42 | t.Errorf("loading nested config did not work correctly") 43 | } 44 | }) 45 | 46 | t.Run("Prefix", func(t *testing.T) { 47 | a := struct { 48 | Kinesis struct { 49 | Endpoint string 50 | } 51 | }{} 52 | loader := Loader{ 53 | Name: "name", 54 | Args: []string{}, 55 | Sources: []Source{ 56 | NewKubernetesConfigMapSource("collector", "./testdata/configmap"), 57 | }, 58 | } 59 | loader.Load(&a) 60 | if a.Kinesis.Endpoint != "https://example.com/blah" { 61 | t.Errorf("loading config with prefix did not work correctly") 62 | } 63 | }) 64 | } 65 | 66 | func TestSubscriber(t *testing.T) { 67 | tmp, _ := os.MkdirTemp("", "conf-configmap-") 68 | defer os.RemoveAll(tmp) 69 | oldInterval := kubernetesSleepInterval 70 | defer func() { 71 | kubernetesSleepInterval = oldInterval 72 | }() 73 | kubernetesSleepInterval = 3 * time.Millisecond 74 | t.Run("ValueExists", func(t *testing.T) { 75 | path := filepath.Join(tmp, "test1") 76 | if err := os.WriteFile(path, []byte("5\n"), 0640); err != nil { 77 | t.Fatal(err) 78 | } 79 | sc := NewKubernetesSubscriber("", tmp) 80 | ctx, cancel := context.WithCancel(context.Background()) 81 | count := 0 82 | sc.Subscribe(ctx, func(key, newValue string) { 83 | count++ 84 | }) 85 | time.Sleep(10 * time.Millisecond) 86 | cancel() 87 | if count != 0 { 88 | t.Fatalf("expected f to get called zero times, got called %d times", count) 89 | } 90 | }) 91 | 92 | t.Run("ValueChanges", func(t *testing.T) { 93 | path := filepath.Join(tmp, "test2") 94 | if err := os.WriteFile(path, []byte("7\n"), 0640); err != nil { 95 | t.Fatal(err) 96 | } 97 | sc := NewKubernetesSubscriber("", tmp) 98 | ctx, cancel := context.WithCancel(context.Background()) 99 | count := 0 100 | value := "" 101 | var mu sync.Mutex 102 | sc.Subscribe(ctx, func(key, newValue string) { 103 | mu.Lock() 104 | defer mu.Unlock() 105 | count++ 106 | value = newValue 107 | }) 108 | go func() { 109 | time.Sleep(2 * time.Millisecond) 110 | if err := os.WriteFile(path, []byte("11\n"), 0640); err != nil { 111 | panic(err) 112 | } 113 | }() 114 | time.Sleep(10 * time.Millisecond) 115 | cancel() 116 | mu.Lock() 117 | defer mu.Unlock() 118 | if count == 0 { 119 | t.Fatalf("expected f to get called at least once, got called %d times", count) 120 | } 121 | if value != "11" { 122 | t.Fatalf("bad value: want 11 got %q", value) 123 | } 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /configmap_source.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // NewKubernetesConfigMapSource loads configuration from a Kubernetes ConfigMap 13 | // that has been mounted as a volume. 14 | // 15 | // A prefix may be set to namespace the environment variables that the source 16 | // will be looking at. 17 | func NewKubernetesConfigMapSource(prefix string, dir string) Source { 18 | base := make([]string, 0, 10) 19 | if prefix != "" { 20 | base = append(base, prefix) 21 | } 22 | return SourceFunc(func(dst Map) error { 23 | f, err := os.Open(dir) 24 | if err != nil { 25 | return err 26 | } 27 | defer f.Close() 28 | entries, err := f.Readdirnames(0) 29 | if err != nil { 30 | return err 31 | } 32 | vars := make(map[string]string, 0) 33 | for _, entry := range entries { 34 | if len(entry) > 0 && entry[0] == '.' { 35 | continue 36 | } 37 | path := filepath.Join(f.Name(), entry) 38 | data, err := os.ReadFile(path) 39 | if err != nil { 40 | return err 41 | } 42 | vars[snakecaseUpper(entry)] = string(bytes.TrimSuffix(data, []byte{'\n'})) 43 | } 44 | dst.Scan(func(path []string, item MapItem) { 45 | path = append(base, path...) 46 | path = append(path, item.Name) 47 | 48 | k := snakecaseUpper(strings.Join(path, "_")) 49 | if v, ok := vars[k]; ok { 50 | // this only matches at the very end 51 | if e := item.Value.Set(v); e != nil { 52 | err = e 53 | } 54 | } 55 | }) 56 | return nil 57 | }) 58 | } 59 | 60 | type Subscriber interface { 61 | // Subscribe listens for new configuration, invoking the callback when 62 | // values change. f should be invoked any time Subscribe detects a new key, 63 | // or an existing key with a new value. If a key is deleted f will be 64 | // invoked with the value set to the empty string. There is no way to 65 | // distinguish between a deleted key and an empty key. 66 | // 67 | // If the value is retrieved and is empty (file not found), f is invoked 68 | // with the empty string. At most one instance of f will be invoked at any 69 | // time per Subscriber instance. If the value cannot be retrieved (read 70 | // error), f will not be invoked. 71 | Subscribe(ctx context.Context, f func(key, newValue string)) 72 | 73 | // Snapshot returns a copy of the current configuration. 74 | Snapshot(ctx context.Context) (map[string]string, error) 75 | } 76 | 77 | type kubernetesSubscriber struct { 78 | prefix string 79 | dir string 80 | } 81 | 82 | func NewKubernetesSubscriber(prefix string, dir string) Subscriber { 83 | return kubernetesSubscriber{prefix: prefix, dir: dir} 84 | } 85 | 86 | // can be overridden in tests 87 | var kubernetesSleepInterval = 30 * time.Second 88 | 89 | func (k kubernetesSubscriber) Subscribe(ctx context.Context, f func(key, newValue string)) { 90 | ticker := time.NewTicker(kubernetesSleepInterval) 91 | state, initialErr := k.Snapshot(ctx) 92 | go func() { 93 | for { 94 | select { 95 | case <-ctx.Done(): 96 | return 97 | case <-ticker.C: 98 | newState, err := k.Snapshot(ctx) 99 | if err != nil { 100 | continue 101 | } 102 | if initialErr != nil { 103 | initialErr = nil 104 | // We shouldn't hit any callbacks if we don't have any 105 | // values to diff 106 | continue 107 | } 108 | newset := make(map[string]bool, len(newState)) 109 | for key, value := range newState { 110 | newset[key] = true 111 | oldVal, found := state[key] 112 | if !found { 113 | // key has been added 114 | f(key, value) 115 | continue 116 | } 117 | if oldVal != value { 118 | // key has been changed. 119 | f(key, value) 120 | continue 121 | } 122 | } 123 | for key := range state { 124 | if !newset[key] { 125 | // key has been deleted 126 | f(key, "") 127 | continue 128 | } 129 | } 130 | state = newState 131 | } 132 | } 133 | }() 134 | } 135 | 136 | func (k kubernetesSubscriber) Snapshot(ctx context.Context) (map[string]string, error) { 137 | f, err := os.Open(k.dir) 138 | if err != nil { 139 | return nil, err 140 | } 141 | defer f.Close() 142 | names, err := f.Readdirnames(10000) 143 | if err != nil { 144 | return nil, err 145 | } 146 | mp := make(map[string]string, len(names)) 147 | for i := range names { 148 | data, err := os.ReadFile(filepath.Join(k.dir, names[i])) 149 | if err != nil && !os.IsNotExist(err) { 150 | return nil, err 151 | } 152 | mp[names[i]] = strings.TrimSuffix(string(data), "\n") 153 | } 154 | return mp, nil 155 | } 156 | -------------------------------------------------------------------------------- /source.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "strings" 7 | "text/template" 8 | 9 | "github.com/segmentio/objconv/json" 10 | ) 11 | 12 | // Source is the interface that allow new types to be plugged into a loader to 13 | // make it possible to load configuration from new places. 14 | // 15 | // When the configuration is loaded the Load method of each source that was set 16 | // on a loader is called with a Node representing the configuration struct. The 17 | // typical implementation of a source is to load the serialized version of the 18 | // configuration and use an objconv decoder to build the node. 19 | type Source interface { 20 | Load(dst Map) error 21 | } 22 | 23 | // FlagSource is a special case of a source that receives a configuration value 24 | // from the arguments of a loader. It makes it possible to provide runtime 25 | // configuration to the source from the command line arguments of a program. 26 | type FlagSource interface { 27 | Source 28 | 29 | // Flag is the name of the flag that sets the source's configuration value. 30 | Flag() string 31 | 32 | // Help is called to get the help message to display for the source's flag. 33 | Help() string 34 | 35 | // flag.Value must be implemented by a FlagSource to receive their value 36 | // when the loader's arguments are parsed. 37 | flag.Value 38 | } 39 | 40 | // SourceFunc makes it possible to use basic function types as configuration 41 | // sources. 42 | type SourceFunc func(dst Map) error 43 | 44 | // Load calls f. 45 | func (f SourceFunc) Load(dst Map) error { 46 | return f(dst) 47 | } 48 | 49 | // NewEnvSource creates a new source which loads values from the environment 50 | // variables given in env. 51 | // 52 | // A prefix may be set to namespace the environment variables that the source 53 | // will be looking at. 54 | func NewEnvSource(prefix string, env ...string) Source { 55 | vars := makeEnvVars(env) 56 | base := make([]string, 0, 10) 57 | 58 | if prefix != "" { 59 | base = append(base, prefix) 60 | } 61 | 62 | return SourceFunc(func(dst Map) (err error) { 63 | dst.Scan(func(path []string, item MapItem) { 64 | path = append(base, path...) 65 | path = append(path, item.Name) 66 | 67 | k := snakecaseUpper(strings.Join(path, "_")) 68 | 69 | if v, ok := vars[k]; ok { 70 | // this only matches at the very end 71 | if e := item.Value.Set(v); e != nil { 72 | err = e 73 | } 74 | } 75 | }) 76 | return 77 | }) 78 | } 79 | 80 | // NewFileSource creates a new source which loads a configuration from a file 81 | // identified by a path (or URL). 82 | // 83 | // The returned source satisfies the FlagSource interface because it loads the 84 | // file location from the given flag. 85 | // 86 | // The vars argument may be set to render the configuration file if it's a 87 | // template. 88 | // 89 | // The readFile function loads the file content in-memory from a file location 90 | // given as argument, usually this is ioutil.ReadFile. 91 | // 92 | // The unmarshal function decodes the content of the configuration file into a 93 | // configuration object. 94 | func NewFileSource(flag string, vars interface{}, readFile func(string) ([]byte, error), unmarshal func([]byte, interface{}) error) FlagSource { 95 | return &fileSource{ 96 | flag: flag, 97 | vars: vars, 98 | readFile: readFile, 99 | unmarshal: unmarshal, 100 | } 101 | } 102 | 103 | type fileSource struct { 104 | flag string 105 | path string 106 | vars interface{} 107 | readFile func(string) ([]byte, error) 108 | unmarshal func([]byte, interface{}) error 109 | } 110 | 111 | func (f *fileSource) Load(dst Map) (err error) { 112 | var b []byte 113 | 114 | if len(f.path) == 0 { 115 | return 116 | } 117 | 118 | if b, err = f.readFile(f.path); err != nil { 119 | return 120 | } 121 | 122 | tpl := template.New(f.flag) 123 | buf := &bytes.Buffer{} 124 | buf.Grow(len(b)) 125 | 126 | tpl = tpl.Funcs(template.FuncMap{ 127 | "json": func(v interface{}) (string, error) { 128 | b, err := json.Marshal(v) 129 | return string(b), err 130 | }, 131 | }) 132 | 133 | if _, err = tpl.Parse(string(b)); err != nil { 134 | return 135 | } 136 | 137 | if err = tpl.Execute(buf, f.vars); err != nil { 138 | return 139 | } 140 | 141 | err = f.unmarshal(buf.Bytes(), dst) 142 | return 143 | } 144 | 145 | func (f *fileSource) Flag() string { 146 | return f.flag 147 | } 148 | 149 | func (f *fileSource) Help() string { 150 | return "Location to load the configuration file from." 151 | } 152 | 153 | func (f *fileSource) Set(s string) error { 154 | f.path = s 155 | return nil 156 | } 157 | 158 | func (f *fileSource) String() string { 159 | return f.path 160 | } 161 | -------------------------------------------------------------------------------- /print.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/segmentio/objconv" 13 | ) 14 | 15 | // PrintError outputs the error message for err to stderr. 16 | func (ld Loader) PrintError(err error) { 17 | w := bufio.NewWriter(os.Stderr) 18 | ld.fprintError(w, err, stderr()) 19 | w.Flush() 20 | } 21 | 22 | // FprintError outputs the error message for err to w. 23 | func (ld Loader) FprintError(w io.Writer, err error) { 24 | ld.fprintError(w, err, monochrome()) 25 | } 26 | 27 | // PrintHelp outputs the help message for cfg to stderr. 28 | func (ld Loader) PrintHelp(cfg interface{}) { 29 | w := bufio.NewWriter(os.Stderr) 30 | ld.fprintHelp(w, cfg, stderr()) 31 | w.Flush() 32 | } 33 | 34 | // FprintHelp outputs the help message for cfg to w. 35 | func (ld Loader) FprintHelp(w io.Writer, cfg interface{}) { 36 | ld.fprintHelp(w, cfg, monochrome()) 37 | } 38 | 39 | func (ld Loader) fprintError(w io.Writer, err error, col colors) { 40 | var errors errorList 41 | 42 | if e, ok := err.(errorList); ok { 43 | errors = e 44 | } else { 45 | errors = errorList{err} 46 | } 47 | 48 | fmt.Fprintf(w, "%s\n", col.titles("Error:")) 49 | 50 | for _, e := range errors { 51 | fmt.Fprintf(w, " %s\n", col.errors(e.Error())) 52 | } 53 | 54 | fmt.Fprintln(w) 55 | } 56 | 57 | func (ld Loader) fprintHelp(w io.Writer, cfg interface{}, col colors) { 58 | var m Map 59 | 60 | if cfg != nil { 61 | v := reflect.ValueOf(cfg) 62 | if v.Kind() == reflect.Ptr { 63 | v = v.Elem() 64 | } 65 | m = makeNodeStruct(v, v.Type()) 66 | } 67 | 68 | fmt.Fprintf(w, "%s\n", col.titles("Usage:")) 69 | switch { 70 | case len(ld.Usage) != 0: 71 | fmt.Fprintf(w, " %s %s\n\n", ld.Name, ld.Usage) 72 | case len(ld.Commands) != 0: 73 | fmt.Fprintf(w, " %s [command] [options...]\n\n", ld.Name) 74 | default: 75 | fmt.Fprintf(w, " %s [-h] [-help] [options...]\n\n", ld.Name) 76 | } 77 | 78 | if len(ld.Commands) != 0 { 79 | fmt.Fprintf(w, "%s\n", col.titles("Commands:")) 80 | width := 0 81 | 82 | for _, c := range ld.Commands { 83 | if n := len(col.cmds(c.Name)); n > width { 84 | width = n 85 | } 86 | } 87 | 88 | cmdfmt := fmt.Sprintf(" %%-%ds %%s\n", width) 89 | 90 | for _, c := range ld.Commands { 91 | fmt.Fprintf(w, cmdfmt, col.cmds(c.Name), c.Help) 92 | } 93 | 94 | fmt.Fprintln(w) 95 | } 96 | 97 | set := newFlagSet(m, ld.Name, ld.Sources...) 98 | if m.Len() != 0 { 99 | fmt.Fprintf(w, "%s\n", col.titles("Options:")) 100 | } 101 | 102 | // Outputs the flags following the same format than the standard flag 103 | // package. The main difference is in the type names which are set to 104 | // values returned by prettyType. 105 | set.VisitAll(func(f *flag.Flag) { 106 | var t string 107 | var h []string 108 | var empty bool 109 | var boolean bool 110 | var object bool 111 | var list bool 112 | 113 | switch v := f.Value.(type) { 114 | case Node: 115 | x := reflect.ValueOf(v.Value()) 116 | t = prettyType(x.Type()) 117 | empty = isEmptyValue(x) 118 | 119 | switch v.(type) { 120 | case Map: 121 | object = true 122 | case Array: 123 | list = true 124 | default: 125 | boolean = isBoolFlag(x) 126 | } 127 | 128 | case FlagSource: 129 | t = "source" 130 | default: 131 | t = "value" 132 | } 133 | 134 | fmt.Fprintf(w, " %s", col.keys("-"+f.Name)) 135 | 136 | switch { 137 | case !boolean: 138 | fmt.Fprintf(w, " %s\n", col.types(t)) 139 | case len(f.Name) >= 4: // put help message inline for boolean flags 140 | fmt.Fprint(w, "\n") 141 | } 142 | 143 | if s := f.Usage; len(s) != 0 { 144 | h = append(h, s) 145 | } 146 | 147 | if s := f.DefValue; len(s) != 0 && !empty && !(boolean || object || list) { 148 | h = append(h, col.defvals("(default "+s+")")) 149 | } 150 | 151 | if len(h) != 0 { 152 | if !boolean || len(f.Name) >= 4 { 153 | fmt.Fprint(w, " ") 154 | } 155 | fmt.Fprintf(w, "\t%s\n", strings.Join(h, " ")) 156 | } 157 | 158 | fmt.Fprint(w, "\n") 159 | }) 160 | } 161 | 162 | func prettyType(t reflect.Type) string { 163 | if t == nil { 164 | return "unknown" 165 | } 166 | 167 | if _, ok := objconv.AdapterOf(t); ok { 168 | return "value" 169 | } 170 | 171 | switch { 172 | case t.Implements(objconvValueDecoderInterface): 173 | return "value" 174 | case t.Implements(textUnmarshalerInterface): 175 | return "string" 176 | } 177 | 178 | switch t { 179 | case timeDurationType: 180 | return "duration" 181 | case timeTimeType: 182 | return "time" 183 | } 184 | 185 | switch t.Kind() { 186 | case reflect.Struct, reflect.Map: 187 | return "object" 188 | case reflect.Slice, reflect.Array: 189 | if t.Elem().Kind() == reflect.Uint8 { 190 | return "base64" 191 | } 192 | return "list" 193 | case reflect.Ptr: 194 | return prettyType(t.Elem()) 195 | default: 196 | s := strings.ToLower(t.String()) 197 | if i := strings.LastIndexByte(s, '.'); i >= 0 { 198 | s = s[i+1:] 199 | } 200 | return s 201 | } 202 | } 203 | 204 | type colors struct { 205 | titles func(string) string 206 | cmds func(string) string 207 | keys func(string) string 208 | types func(string) string 209 | defvals func(string) string 210 | errors func(string) string 211 | } 212 | 213 | func stderr() colors { 214 | if isTerminal(2) { 215 | return colorized() 216 | } 217 | return monochrome() 218 | } 219 | 220 | func colorized() colors { 221 | return colors{ 222 | titles: bold, 223 | cmds: magenta, 224 | keys: blue, 225 | types: green, 226 | defvals: grey, 227 | errors: red, 228 | } 229 | } 230 | 231 | func monochrome() colors { 232 | return colors{ 233 | titles: normal, 234 | cmds: normal, 235 | keys: normal, 236 | types: normal, 237 | defvals: normal, 238 | errors: normal, 239 | } 240 | } 241 | 242 | func bold(s string) string { 243 | return "\033[1m" + s + "\033[0m" 244 | } 245 | 246 | func blue(s string) string { 247 | return "\033[1;34m" + s + "\033[0m" 248 | } 249 | 250 | func green(s string) string { 251 | return "\033[1;32m" + s + "\033[0m" 252 | } 253 | 254 | func red(s string) string { 255 | return "\033[1;31m" + s + "\033[0m" 256 | } 257 | 258 | func magenta(s string) string { 259 | return "\033[1;35m" + s + "\033[0m" 260 | } 261 | 262 | func grey(s string) string { 263 | return "\033[1;30m" + s + "\033[0m" 264 | } 265 | 266 | func normal(s string) string { 267 | return s 268 | } 269 | 270 | func isEmptyValue(v reflect.Value) bool { 271 | if !v.IsValid() { 272 | return true 273 | } 274 | 275 | switch v.Kind() { 276 | case reflect.Slice, reflect.Map: 277 | return v.Len() == 0 278 | 279 | case reflect.Struct: 280 | return v.NumField() == 0 281 | } 282 | 283 | return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) 284 | } 285 | 286 | func isBoolFlag(v reflect.Value) bool { 287 | type iface interface { 288 | IsBoolFlag() bool 289 | } 290 | 291 | if !v.IsValid() { 292 | return false 293 | } 294 | 295 | if x, ok := v.Interface().(iface); ok { 296 | return x.IsBoolFlag() 297 | } 298 | 299 | return v.Kind() == reflect.Bool 300 | } 301 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "sort" 13 | "strings" 14 | 15 | "gopkg.in/go-playground/mold.v2/modifiers" 16 | 17 | validator "gopkg.in/validator.v2" 18 | 19 | // Load all default adapters of the objconv package. 20 | _ "github.com/segmentio/objconv/adapters" 21 | "github.com/segmentio/objconv/yaml" 22 | ) 23 | 24 | var ( 25 | // Modifier is the default modification lib using the "mod" tag; it is 26 | // exposed to allow registering of custom modifiers and aliases or to 27 | // be set to a more central instance located in another repo. 28 | Modifier = modifiers.New() 29 | ) 30 | 31 | // Load the program's configuration into cfg, and returns the list of leftover 32 | // arguments. 33 | // 34 | // The cfg argument is expected to be a pointer to a struct type where exported 35 | // fields or fields with a "conf" tag will be used to load the program 36 | // configuration. 37 | // The function panics if cfg is not a pointer to struct, or if it's a nil 38 | // pointer. 39 | // 40 | // The configuration is loaded from the command line, environment and optional 41 | // configuration file if the -config-file option is present in the program 42 | // arguments. 43 | // 44 | // Values found in the program arguments take precedence over those found in 45 | // the environment, which takes precedence over the configuration file. 46 | // 47 | // If an error is detected with the configurable the function print the usage 48 | // message to stderr and exit with status code 1. 49 | func Load(cfg interface{}) (args []string) { 50 | _, args = LoadWith(cfg, DefaultLoader) 51 | return 52 | } 53 | 54 | // LoadWith behaves like Load but uses ld as a loader to parse the program 55 | // configuration. 56 | // 57 | // The function panics if cfg is not a pointer to struct, or if it's a nil 58 | // pointer and no commands were set. 59 | func LoadWith(cfg interface{}, ld Loader) (cmd string, args []string) { 60 | var err error 61 | switch cmd, args, err = ld.Load(cfg); err { 62 | case nil: 63 | case flag.ErrHelp: 64 | ld.PrintHelp(cfg) 65 | os.Exit(0) 66 | default: 67 | ld.PrintHelp(cfg) 68 | ld.PrintError(err) 69 | os.Exit(1) 70 | } 71 | return 72 | } 73 | 74 | // A Command represents a command supported by a configuration loader. 75 | type Command struct { 76 | Name string // name of the command 77 | Help string // help message describing what the command does 78 | } 79 | 80 | // A Loader exposes an API for customizing how a configuration is loaded and 81 | // where it's loaded from. 82 | type Loader struct { 83 | Name string // program name 84 | Usage string // program usage 85 | Args []string // list of arguments 86 | Commands []Command // list of commands 87 | Sources []Source // list of sources to load configuration from. 88 | } 89 | 90 | // Load uses the loader ld to load the program configuration into cfg, and 91 | // returns the list of program arguments that were not used. 92 | // 93 | // The function returns flag.ErrHelp when the list of arguments contained -h, 94 | // -help, or --help. 95 | // 96 | // The cfg argument is expected to be a pointer to a struct type where exported 97 | // fields or fields with a "conf" tag will be used to load the program 98 | // configuration. 99 | // The function panics if cfg is not a pointer to struct, or if it's a nil 100 | // pointer and no commands were set. 101 | func (ld Loader) Load(cfg interface{}) (cmd string, args []string, err error) { 102 | var v reflect.Value 103 | 104 | if cfg == nil { 105 | v = reflect.ValueOf(&struct{}{}) 106 | } else { 107 | v = reflect.ValueOf(cfg) 108 | } 109 | 110 | if v.Kind() != reflect.Ptr { 111 | panic(fmt.Sprintf("cannot load configuration into non-pointer type: %T", cfg)) 112 | } 113 | 114 | if v.IsNil() { 115 | panic(fmt.Sprintf("cannot load configuration into nil pointer of type: %T", cfg)) 116 | } 117 | 118 | if v = v.Elem(); v.Kind() != reflect.Struct { 119 | panic(fmt.Sprintf("cannot load configuration into non-struct pointer: %T", cfg)) 120 | } 121 | 122 | if len(ld.Commands) != 0 { 123 | if len(ld.Args) == 0 { 124 | err = errors.New("missing command") 125 | return 126 | } 127 | 128 | found := false 129 | for _, c := range ld.Commands { 130 | if c.Name == ld.Args[0] { 131 | found, cmd, ld.Args = true, ld.Args[0], ld.Args[1:] 132 | break 133 | } 134 | } 135 | 136 | if !found { 137 | err = errors.New("unknown command: " + ld.Args[0]) 138 | return 139 | } 140 | 141 | if cfg == nil { 142 | args = ld.Args 143 | return 144 | } 145 | } 146 | 147 | if args, err = ld.load(v); err != nil { 148 | return 149 | } 150 | 151 | if err = Modifier.Struct(context.Background(), cfg); err != nil { 152 | return 153 | } 154 | 155 | if err = validator.Validate(v.Interface()); err != nil { 156 | err = makeValidationError(err, v.Type()) 157 | } 158 | 159 | return 160 | } 161 | 162 | func (ld Loader) load(cfg reflect.Value) (args []string, err error) { 163 | node := makeNodeStruct(cfg, cfg.Type()) 164 | set := newFlagSet(node, ld.Name, ld.Sources...) 165 | 166 | // Parse the arguments a first time so the sources that implement the 167 | // FlagSource interface get their values loaded. 168 | if err = set.Parse(ld.Args); err != nil { 169 | return 170 | } 171 | 172 | // Load the configuration from the sources that have been configured on the 173 | // loader. 174 | // Order is important here because the values will get overwritten by each 175 | // source that loads the configuration. 176 | for _, source := range ld.Sources { 177 | if err = source.Load(node); err != nil { 178 | return 179 | } 180 | } 181 | 182 | // Parse the arguments a second time to overwrite values loaded by sources 183 | // which were also passed to the program arguments. 184 | if err = set.Parse(ld.Args); err != nil { 185 | return 186 | } 187 | 188 | args = set.Args() 189 | return 190 | } 191 | 192 | var DefaultLoader Loader 193 | 194 | func init() { 195 | env := os.Environ() 196 | args := os.Args 197 | DefaultLoader = defaultLoader(args, env) 198 | } 199 | 200 | func defaultLoader(args []string, env []string) Loader { 201 | var name = filepath.Base(args[0]) 202 | return Loader{ 203 | Name: name, 204 | Args: args[1:], 205 | Sources: []Source{ 206 | NewFileSource("config-file", makeEnvVars(env), os.ReadFile, yaml.Unmarshal), 207 | NewEnvSource(name, env...), 208 | }, 209 | } 210 | } 211 | 212 | func makeEnvVars(env []string) (vars map[string]string) { 213 | vars = make(map[string]string) 214 | 215 | for _, e := range env { 216 | var k string 217 | var v string 218 | 219 | if off := strings.IndexByte(e, '='); off >= 0 { 220 | k, v = e[:off], e[off+1:] 221 | } else { 222 | k = e 223 | } 224 | 225 | vars[k] = v 226 | } 227 | 228 | return vars 229 | } 230 | 231 | func makeValidationError(err error, typ reflect.Type) error { 232 | if errmap, ok := err.(validator.ErrorMap); ok { 233 | errkeys := make([]string, 0, len(errmap)) 234 | errlist := make(errorList, 0, len(errmap)) 235 | 236 | for errkey := range errmap { 237 | errkeys = append(errkeys, errkey) 238 | } 239 | 240 | sort.Strings(errkeys) 241 | 242 | for _, errkey := range errkeys { 243 | path := fieldPath(typ, errkey) 244 | 245 | if len(errmap[errkey]) == 1 { 246 | errlist = append(errlist, fmt.Errorf("invalid value passed to %s: %s", path, errmap[errkey][0])) 247 | } else { 248 | buf := &bytes.Buffer{} 249 | fmt.Fprintf(buf, "invalid value passed to %s: ", path) 250 | 251 | for i, errval := range errmap[errkey] { 252 | if i != 0 { 253 | buf.WriteString("; ") 254 | } 255 | buf.WriteString(errval.Error()) 256 | } 257 | 258 | errlist = append(errlist, errors.New(buf.String())) 259 | } 260 | } 261 | 262 | err = errlist 263 | } 264 | return err 265 | } 266 | 267 | type errorList []error 268 | 269 | func (err errorList) Error() string { 270 | if len(err) > 0 { 271 | return err[0].Error() 272 | } 273 | return "" 274 | } 275 | 276 | func fieldPath(typ reflect.Type, path string) string { 277 | var name string 278 | 279 | if sep := strings.IndexByte(path, '.'); sep >= 0 { 280 | name, path = path[:sep], path[sep+1:] 281 | } else { 282 | name, path = path, "" 283 | } 284 | 285 | if field, ok := typ.FieldByName(name); ok { 286 | name = field.Tag.Get("conf") 287 | if len(name) == 0 { 288 | name = field.Name 289 | } else if name == "_" { 290 | name = "" 291 | } 292 | 293 | if len(path) != 0 { 294 | path = fieldPath(field.Type, path) 295 | } 296 | } 297 | 298 | if len(path) != 0 { 299 | if len(name) == 0 { 300 | name = path 301 | } else { 302 | name += "." + path 303 | } 304 | } 305 | 306 | return name 307 | } 308 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conf [![CircleCI](https://circleci.com/gh/segmentio/conf.svg?style=shield)](https://circleci.com/gh/segmentio/conf) [![Go Report Card](https://goreportcard.com/badge/github.com/segmentio/conf)](https://goreportcard.com/report/github.com/segmentio/conf) [![GoDoc](https://godoc.org/github.com/segmentio/conf?status.svg)](https://godoc.org/github.com/segmentio/conf) 2 | Go package for loading program configuration from multiple sources. 3 | 4 | Motivations 5 | ----------- 6 | 7 | Loading program configurations is usually done by parsing the arguments passed 8 | to the command line, and in this case the standard library offers a good support 9 | with the `flag` package. 10 | However, there are times where the standard is just too limiting, for example 11 | when the program needs to load configuration from other sources (like a file, or 12 | the environment variables). 13 | The `conf` package was built to address these issues, here were the goals: 14 | 15 | - **Loading the configuration has to be type-safe**, there were other packages 16 | available that were covering the same use-cases but they often required doing 17 | type assertions on the configuration values which is always an opportunity to 18 | get the program to panic. 19 | 20 | - **Keeping the API minimal**, while the `flag` package offered the type safety 21 | we needed it is also very verbose to setup. With `conf`, only a single function 22 | call is needed to setup and load the entire program configuration. 23 | 24 | - **Supporting richer syntaxes**, because program configurations are often 25 | generated dynamically, the `conf` package accepts YAML values as input to all 26 | configuration values. It also has support for sub-commands on the command line, 27 | which is a common approach used by CLI tools. 28 | 29 | - **Supporting multiple sources**, because passing values through the command 30 | line is not always the best approach, programs may need to receive their 31 | configuration from files, environment variables, secret stores, or other network 32 | locations. 33 | 34 | Basic Usage 35 | ----------- 36 | 37 | A program using the `conf` package needs to declare a struct which is passed to 38 | `conf.Load` to populate the fields with the configuration that was made 39 | available at runtime through a configuration file, environment variables or the 40 | program arguments. 41 | 42 | Each field of the structure may declare a `conf` tag which sets the name of the 43 | property, and a `help` tag to provide a help message for the configuration. 44 | 45 | The `conf` package will automatically understand the structure of the program 46 | configuration based on the struct it receives, as well as generating the program 47 | usage and help messages if the `-h` or `-help` options are passed (or an error 48 | is detected). 49 | 50 | The `conf.Load` function adds support for a `-config-file` option on the program 51 | arguments which accepts the path to a file that the configuration may be loaded 52 | from as well. 53 | 54 | Here's an example of how a program would typically use the package: 55 | ```go 56 | package main 57 | 58 | import ( 59 | "fmt" 60 | 61 | "github.com/segmentio/conf" 62 | ) 63 | 64 | func main() { 65 | var config struct { 66 | Message string `conf:"m" help:"A message to print."` 67 | } 68 | 69 | // Load the configuration, either from a config file, the environment or the program arguments. 70 | conf.Load(&config) 71 | 72 | fmt.Println(config.Message) 73 | } 74 | ``` 75 | ``` 76 | $ go run ./example.go -m 'Hello World!' 77 | Hello World! 78 | ``` 79 | 80 | Environment Variables 81 | --------------------- 82 | 83 | By default, `conf` will look for environment variables before loading command-line configuration flags with one important caveat: environment variables are prefixed with the program name. For example, given a program named "foobar": 84 | 85 | ``` 86 | func main() { 87 | config := struct { 88 | Name string `conf:"name"` 89 | }{ 90 | Name: "default", 91 | } 92 | conf.Load(&config) 93 | fmt.Println("Hello", config.Name) 94 | } 95 | ``` 96 | 97 | The following will be output: 98 | 99 | ``` 100 | $ ./foobar // "Hello default" 101 | $ FOOBAR_NAME=world ./foobar // "Hello world" 102 | $ FOOBAR_NAME=world ./foobar --name neighbor // "Hello neighbor" 103 | $ MAIN_NAME=world go run main.go // "Hello world" 104 | ``` 105 | 106 | If you want to hard-code the prefix to guarantee immutability or just to customize it, you can supply a custom loader config: 107 | 108 | ``` 109 | loader := conf.Loader{ 110 | Name: "my-service", 111 | Args: os.Args[1:], 112 | Sources: []conf.Source{ 113 | conf.NewEnvSource("MY_SVC", os.Environ()...), 114 | }, 115 | } 116 | conf.LoadWith(&config, loader) 117 | ``` 118 | 119 | Advanced Usage 120 | -------------- 121 | 122 | While the `conf.Load` function is good enough for common use cases, programs 123 | sometimes need to customize the default behavior. 124 | A program may then use the `conf.LoadWith` function, which accepts a 125 | `conf.Loader` as second argument to gain more control over how the configuration 126 | is loaded. 127 | 128 | Here's the `conf.Loader` definition: 129 | ```go 130 | package conf 131 | 132 | type Loader struct { 133 | Name string // program name 134 | Usage string // program usage 135 | Args []string // list of arguments 136 | Commands []Command // list of commands 137 | Sources []Source // list of sources to load configuration from. 138 | } 139 | ``` 140 | 141 | The `conf.Load` function is actually just a wrapper around `conf.LoadWith` that 142 | passes a default loader. The default loader gets the program name from the first 143 | program argument, supports no sub-commands, and has two custom sources setup to 144 | potentially load its configuration from a configuration file or the environment 145 | variables. 146 | 147 | Here's an example showing how to configure a CLI tool that supports a couple of 148 | sub-commands: 149 | ```go 150 | package main 151 | 152 | import ( 153 | "fmt" 154 | 155 | "github.com/segmentio/conf" 156 | ) 157 | 158 | func main() { 159 | // If nil is passed instead of a configuration struct no arguments are 160 | // parsed, only the command is extracted. 161 | cmd, args := conf.LoadWith(nil, conf.Loader{ 162 | Name: "example", 163 | Args: os.Args[1:], 164 | Commands: []conf.Command{ 165 | {"print", "Print the message passed to -m"}, 166 | {"version", "Show the program version"}, 167 | }, 168 | }) 169 | 170 | switch cmd { 171 | case "print": 172 | var config struct{ 173 | Message string `conf:"m" help:"A message to print."` 174 | } 175 | 176 | conf.LoadWith(&config, conf.Loader{ 177 | Name: "example print", 178 | Args: args, 179 | }) 180 | 181 | fmt.Println(config.Message) 182 | 183 | case "version": 184 | fmt.Println("1.2.3") 185 | } 186 | } 187 | ``` 188 | ``` 189 | $ go run ./example.go version 190 | 1.2.3 191 | $ go run ./example.go print -m 'Hello World!' 192 | Hello World! 193 | ``` 194 | 195 | Custom Sources 196 | -------------- 197 | 198 | We mentioned the `conf.Loader` type supported setting custom sources that the 199 | program configuration can be loaded from. Here's the the `conf.Source` interface 200 | definition: 201 | ```go 202 | package conf 203 | 204 | type Source interface { 205 | Load(dst Map) 206 | } 207 | ``` 208 | 209 | The source has a single method which receives a `conf.Map` value which is an 210 | intermediate representation of the configuration struct that was received by the 211 | loader. 212 | The package uses this type internally as well for loading configuration values 213 | from the program arguments, it can be seen as a reflective representation of the 214 | original value which exposes an API that is more convenient to use that having 215 | a raw `reflect.Value`. 216 | 217 | One of the advantages of the `conf.Map` type is that it implements the 218 | [objconv.ValueDecoder](https://godoc.org/github.com/segmentio/objconv#ValueDecoder) 219 | interface and therefore can be used directly to load configurations from a 220 | serialized format (like JSON for example). 221 | 222 | Validation 223 | ---------- 224 | 225 | Last but not least, the `conf` package also supports automatic validation of the 226 | fields in the configuration struct. This happens after the values were loaded 227 | and is based on [gopkg.in/validator.v2](https://godoc.org/gopkg.in/validator.v2). 228 | 229 | This step could have been done outside the package however it is both convenient 230 | and useful to have all configuration errors treated the same way (getting the 231 | usage and help message shown when something is wrong). 232 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/segmentio/objconv/json" 11 | ) 12 | 13 | func TestEqualNode(t *testing.T) { 14 | now := time.Now() 15 | 16 | tests := []struct { 17 | name string 18 | node1 Node 19 | node2 Node 20 | equal bool 21 | }{ 22 | { 23 | name: "nil nodes", 24 | node1: nil, 25 | node2: nil, 26 | equal: true, 27 | }, 28 | 29 | { 30 | name: "scalar and nil", 31 | node1: Scalar{}, 32 | node2: nil, 33 | equal: false, 34 | }, 35 | 36 | { 37 | name: "two empty scalars", 38 | node1: Scalar{}, 39 | node2: Scalar{}, 40 | equal: true, 41 | }, 42 | 43 | { 44 | name: "42 and empty scalar", 45 | node1: Scalar{reflect.ValueOf(42)}, 46 | node2: Scalar{}, 47 | equal: false, 48 | }, 49 | 50 | { 51 | name: "empty scalar and 42", 52 | node1: Scalar{}, 53 | node2: Scalar{reflect.ValueOf(42)}, 54 | equal: false, 55 | }, 56 | 57 | { 58 | name: "42 and 42", 59 | node1: Scalar{reflect.ValueOf(42)}, 60 | node2: Scalar{reflect.ValueOf(42)}, 61 | equal: true, 62 | }, 63 | 64 | { 65 | name: "42 and empty array", 66 | node1: Scalar{reflect.ValueOf(42)}, 67 | node2: Array{}, 68 | equal: false, 69 | }, 70 | 71 | { 72 | name: "non-equal scalars (type mismatch)", 73 | node1: Scalar{reflect.ValueOf(42)}, 74 | node2: Scalar{reflect.ValueOf("Hello World!")}, 75 | equal: false, 76 | }, 77 | 78 | { 79 | name: "equal scalars (time values)", 80 | node1: Scalar{reflect.ValueOf(now)}, 81 | node2: Scalar{reflect.ValueOf(now.In(time.UTC))}, 82 | equal: true, 83 | }, 84 | 85 | { 86 | name: "two empty arrays", 87 | node1: Array{}, 88 | node2: Array{}, 89 | equal: true, 90 | }, 91 | 92 | { 93 | name: "equal non-empty arrays", 94 | node1: Array{items: newArrayItems( 95 | Scalar{reflect.ValueOf(1)}, 96 | Scalar{reflect.ValueOf(2)}, 97 | Scalar{reflect.ValueOf(3)}, 98 | )}, 99 | node2: Array{items: newArrayItems( 100 | Scalar{reflect.ValueOf(1)}, 101 | Scalar{reflect.ValueOf(2)}, 102 | Scalar{reflect.ValueOf(3)}, 103 | )}, 104 | equal: true, 105 | }, 106 | 107 | { 108 | name: "non-equal arrays (value mismatch)", 109 | node1: Array{items: newArrayItems( 110 | Scalar{reflect.ValueOf(1)}, 111 | Scalar{reflect.ValueOf(2)}, 112 | Scalar{reflect.ValueOf(3)}, 113 | )}, 114 | node2: Array{items: newArrayItems( 115 | Scalar{reflect.ValueOf(1)}, 116 | Scalar{reflect.ValueOf(1)}, 117 | Scalar{reflect.ValueOf(1)}, 118 | )}, 119 | equal: false, 120 | }, 121 | 122 | { 123 | name: "non-equal arrays (length mismatch)", 124 | node1: Array{items: newArrayItems( 125 | Scalar{reflect.ValueOf(1)}, 126 | Scalar{reflect.ValueOf(2)}, 127 | Scalar{reflect.ValueOf(3)}, 128 | )}, 129 | node2: Array{items: newArrayItems( 130 | Scalar{reflect.ValueOf(1)}, 131 | Scalar{reflect.ValueOf(2)}, 132 | )}, 133 | equal: false, 134 | }, 135 | 136 | { 137 | name: "two empty maps", 138 | node1: Map{}, 139 | node2: Map{}, 140 | equal: true, 141 | }, 142 | 143 | { 144 | name: "equal non-empty maps", 145 | node1: Map{items: newMapItems( 146 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 147 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 148 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 149 | )}, 150 | node2: Map{items: newMapItems( 151 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 152 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 153 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 154 | )}, 155 | equal: true, 156 | }, 157 | 158 | { 159 | name: "non-equal maps (value mismatch)", 160 | node1: Map{items: newMapItems( 161 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 162 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 163 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 164 | )}, 165 | node2: Map{items: newMapItems( 166 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 167 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(1)}}, 168 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(1)}}, 169 | )}, 170 | equal: false, 171 | }, 172 | 173 | { 174 | name: "non-equal maps (value not found)", 175 | node1: Map{items: newMapItems( 176 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 177 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 178 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 179 | )}, 180 | node2: Map{items: newMapItems( 181 | MapItem{Name: "D", Value: Scalar{reflect.ValueOf(1)}}, 182 | MapItem{Name: "E", Value: Scalar{reflect.ValueOf(2)}}, 183 | MapItem{Name: "F", Value: Scalar{reflect.ValueOf(3)}}, 184 | )}, 185 | equal: false, 186 | }, 187 | 188 | { 189 | name: "non-equal maps (length mismatch)", 190 | node1: Map{items: newMapItems( 191 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 192 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 193 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 194 | )}, 195 | node2: Map{items: newMapItems( 196 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 197 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 198 | )}, 199 | equal: false, 200 | }, 201 | } 202 | 203 | for _, test := range tests { 204 | t.Run(test.name, func(t *testing.T) { 205 | if equal := EqualNode(test.node1, test.node2); equal != test.equal { 206 | t.Errorf("EqualNode: expected %t but found %t", test.equal, equal) 207 | } 208 | }) 209 | } 210 | } 211 | 212 | func TestMakeNode(t *testing.T) { 213 | now := time.Now() 214 | 215 | tests := []struct { 216 | name string 217 | value interface{} 218 | node Node 219 | }{ 220 | { 221 | name: "nil", 222 | value: nil, 223 | node: Scalar{reflect.ValueOf(nil)}, 224 | }, 225 | 226 | { 227 | name: "scalar (integer)", 228 | value: 42, 229 | node: Scalar{reflect.ValueOf(42)}, 230 | }, 231 | 232 | { 233 | name: "scalar (time)", 234 | value: now, 235 | node: Scalar{reflect.ValueOf(now)}, 236 | }, 237 | 238 | { 239 | name: "slice", 240 | value: []int{1, 2, 3}, 241 | node: Array{items: newArrayItems( 242 | Scalar{reflect.ValueOf(1)}, 243 | Scalar{reflect.ValueOf(2)}, 244 | Scalar{reflect.ValueOf(3)}, 245 | )}, 246 | }, 247 | 248 | { 249 | name: "map", 250 | value: map[string]int{"A": 1, "B": 2, "C": 3}, 251 | node: Map{items: newMapItems( 252 | MapItem{Name: "A", Value: Scalar{reflect.ValueOf(1)}}, 253 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 254 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 255 | )}, 256 | }, 257 | 258 | { 259 | name: "struct", 260 | value: struct { 261 | A int `conf:"a" help:"value of A"` // override name 262 | B int `conf:"-" help:"value of B"` // skip 263 | C int // default name 264 | D *int // allocate the pointer 265 | e int // unexported 266 | }{1, 2, 3, nil, 42}, 267 | node: Map{items: newMapItems( 268 | MapItem{Name: "a", Help: "value of A", Value: Scalar{reflect.ValueOf(1)}}, 269 | MapItem{Name: "C", Value: Scalar{reflect.ValueOf(3)}}, 270 | MapItem{Name: "D", Value: Scalar{reflect.ValueOf(0)}}, 271 | )}, 272 | }, 273 | } 274 | 275 | for _, test := range tests { 276 | t.Run(test.name, func(t *testing.T) { 277 | if node := MakeNode(test.value); !EqualNode(node, test.node) { 278 | t.Errorf("\n<<< %s\n>>> %s", test.node, node) 279 | } 280 | }) 281 | } 282 | } 283 | 284 | func TestNodeValue(t *testing.T) { 285 | tests := []struct { 286 | node Node 287 | value interface{} 288 | }{ 289 | { 290 | node: Scalar{}, 291 | value: nil, 292 | }, 293 | { 294 | node: Scalar{reflect.ValueOf(42)}, 295 | value: 42, 296 | }, 297 | { 298 | node: Array{}, 299 | value: nil, 300 | }, 301 | { 302 | node: Array{value: reflect.ValueOf([]int{1, 2, 3})}, 303 | value: []int{1, 2, 3}, 304 | }, 305 | { 306 | node: Map{}, 307 | value: nil, 308 | }, 309 | { 310 | node: Map{value: reflect.ValueOf(map[string]int{"A": 1, "B": 2, "C": 3})}, 311 | value: map[string]int{"A": 1, "B": 2, "C": 3}, 312 | }, 313 | } 314 | 315 | for _, test := range tests { 316 | t.Run(fmt.Sprint(test.value), func(t *testing.T) { 317 | if value := test.node.Value(); !reflect.DeepEqual(value, test.value) { 318 | t.Error(value) 319 | } 320 | }) 321 | } 322 | } 323 | 324 | func TestNodeString(t *testing.T) { 325 | date := time.Date(2016, 12, 31, 23, 42, 59, 0, time.UTC) 326 | 327 | tests := []struct { 328 | repr string 329 | node Node 330 | }{ 331 | { 332 | repr: `null`, 333 | node: Scalar{}, 334 | }, 335 | { 336 | repr: `42`, 337 | node: Scalar{reflect.ValueOf(42)}, 338 | }, 339 | { 340 | repr: `Hello World!`, 341 | node: Scalar{reflect.ValueOf("Hello World!")}, 342 | }, 343 | { 344 | repr: `"2016-12-31T23:42:59Z"`, 345 | node: Scalar{reflect.ValueOf(date)}, 346 | }, 347 | { 348 | repr: `[ ]`, 349 | node: Array{}, 350 | }, 351 | { 352 | repr: `[1, 2, 3]`, 353 | node: Array{items: newArrayItems( 354 | Scalar{reflect.ValueOf(1)}, 355 | Scalar{reflect.ValueOf(2)}, 356 | Scalar{reflect.ValueOf(3)}, 357 | )}, 358 | }, 359 | { 360 | repr: `{ }`, 361 | node: Map{}, 362 | }, 363 | { 364 | repr: `{ A: 1 (first), B: 2, C: 3 (last) }`, 365 | node: Map{items: newMapItems( 366 | MapItem{Name: "A", Help: "first", Value: Scalar{reflect.ValueOf(1)}}, 367 | MapItem{Name: "B", Value: Scalar{reflect.ValueOf(2)}}, 368 | MapItem{Name: "C", Help: "last", Value: Scalar{reflect.ValueOf(3)}}, 369 | )}, 370 | }, 371 | } 372 | 373 | for _, test := range tests { 374 | t.Run(test.repr, func(t *testing.T) { 375 | if repr := test.node.String(); repr != test.repr { 376 | t.Error("representation mismatch") 377 | t.Logf("want: %v, %#v", reflect.TypeOf(test.repr), test.repr) 378 | t.Logf("got: %v, %#v", reflect.TypeOf(repr), repr) 379 | } 380 | }) 381 | } 382 | } 383 | 384 | func TestNodeJSON(t *testing.T) { 385 | tests := []struct { 386 | node Node 387 | json string 388 | }{ 389 | { 390 | node: MakeNode(nil), 391 | json: `null`, 392 | }, 393 | { 394 | node: MakeNode(42), 395 | json: `42`, 396 | }, 397 | { 398 | node: MakeNode("Hello World!"), 399 | json: `"Hello World!"`, 400 | }, 401 | { 402 | node: MakeNode([]int{}), 403 | json: `[]`, 404 | }, 405 | { 406 | node: MakeNode([]int{1, 2, 3}), 407 | json: `[1,2,3]`, 408 | }, 409 | { 410 | node: MakeNode(map[string]int{}), 411 | json: `{}`, 412 | }, 413 | { 414 | node: MakeNode(map[string]int{"A": 1, "B": 2, "C": 3}), 415 | json: `{"A":1,"B":2,"C":3}`, 416 | }, 417 | { 418 | node: MakeNode(struct{ A, B, C int }{1, 2, 3}), 419 | json: `{"A":1,"B":2,"C":3}`, 420 | }, 421 | { 422 | node: MakeNode(struct{ A []int }{[]int{1, 2, 3}}), 423 | json: `{"A":[1,2,3]}`, 424 | }, 425 | } 426 | 427 | t.Run("Encode", func(t *testing.T) { 428 | for _, test := range tests { 429 | t.Run(test.json, func(t *testing.T) { 430 | b, err := json.Marshal(test.node) 431 | 432 | if err != nil { 433 | t.Error(err) 434 | } 435 | 436 | if s := string(b); s != test.json { 437 | t.Error(s) 438 | } 439 | }) 440 | } 441 | }) 442 | 443 | t.Run("Decode", func(t *testing.T) { 444 | for _, test := range tests { 445 | t.Run(test.json, func(t *testing.T) { 446 | if test.node.Value() == nil { 447 | return // skip 448 | } 449 | 450 | value := reflect.New(reflect.TypeOf(test.node.Value())) 451 | node := MakeNode(value.Interface()) 452 | 453 | if err := json.Unmarshal([]byte(test.json), &node); err != nil { 454 | t.Error(err) 455 | } 456 | if !EqualNode(node, test.node) { 457 | t.Errorf("%+v", node) 458 | } 459 | }) 460 | } 461 | }) 462 | } 463 | 464 | func Test_FlattenedEmbeddedStructs(t *testing.T) { 465 | 466 | type Smallest struct { 467 | SmallestOne string 468 | } 469 | 470 | type Small struct { 471 | Smallest `conf:"_"` 472 | SmallOne string 473 | } 474 | 475 | type Medium struct { 476 | Small `conf:"_"` 477 | MediumOne string 478 | } 479 | 480 | type Matroska struct { 481 | Medium `conf:"_"` 482 | LargeOne string 483 | } 484 | 485 | m := Matroska{} 486 | node := makeNodeStruct(reflect.ValueOf(m), reflect.TypeOf(m)) 487 | if len(node.Items()) != 4 { 488 | t.Errorf("expected to find four flattened fields...got %d", len(node.Items())) 489 | } 490 | 491 | for _, name := range []string{"SmallestOne", "SmallOne", "MediumOne", "LargeOne"} { 492 | f := node.Item(name) 493 | if f == nil { 494 | t.Errorf("flattened field %s is missing", name) 495 | } 496 | if f.Kind() != ScalarNode { 497 | t.Errorf("flattened field %s should have been scalar but was %d", name, f.Kind()) 498 | } 499 | } 500 | } 501 | 502 | func Test_InvalidFlattenedEmbeddedStructs(t *testing.T) { 503 | 504 | type Thing1 struct { 505 | Stuff string 506 | } 507 | 508 | type Thing2 struct { 509 | Stuff string 510 | } 511 | 512 | type ConflictingName struct { 513 | Thing1 `conf:"_"` 514 | Thing2 `conf:"_"` 515 | } 516 | 517 | type EmbedPrimitive struct { 518 | Str string `conf:"_"` 519 | } 520 | 521 | type EmbedNamedStruct struct { 522 | Thing Thing1 `conf:"_"` 523 | } 524 | 525 | tests := []struct { 526 | val interface{} 527 | errFragments []string 528 | }{ 529 | { 530 | val: ConflictingName{}, 531 | errFragments: []string{"'Stuff'", "duplicate"}, 532 | }, 533 | { 534 | val: EmbedPrimitive{}, 535 | errFragments: []string{"\"_\"", "at path EmbedPrimitive.Str"}, 536 | }, 537 | { 538 | val: EmbedNamedStruct{}, 539 | errFragments: []string{"\"_\"", "at path EmbedNamedStruct.Thing"}, 540 | }, 541 | } 542 | 543 | for _, tt := range tests { 544 | t.Run(reflect.TypeOf(tt.val).Name(), func(t *testing.T) { 545 | defer func() { 546 | recovered := recover() 547 | msg, ok := recovered.(string) 548 | if !ok { 549 | t.Errorf("expected a string to be recovered...got %v", recovered) 550 | } 551 | 552 | // NOTE : ensure that the type name is included in the message! 553 | for _, frag := range append(tt.errFragments, reflect.TypeOf(tt.val).Name()) { 554 | if !strings.Contains(msg, frag) { 555 | t.Errorf("message should have contained fragment \"%s\": %s", frag, msg) 556 | } 557 | } 558 | }() 559 | 560 | makeNodeStruct(reflect.ValueOf(tt.val), reflect.TypeOf(tt.val)) 561 | t.Error("test should have panicked") 562 | }) 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/mail" 7 | "net/url" 8 | "os" 9 | "reflect" 10 | "testing" 11 | "time" 12 | 13 | "github.com/segmentio/objconv/yaml" 14 | ) 15 | 16 | func TestFieldPath(t *testing.T) { 17 | 18 | type Embedded struct { 19 | Str string `conf:"str"` 20 | } 21 | 22 | type Container struct { 23 | Embedded `conf:"_"` 24 | } 25 | 26 | tests := []struct { 27 | value interface{} 28 | input string 29 | output string 30 | }{ 31 | { 32 | value: struct{}{}, 33 | input: "", 34 | output: "", 35 | }, 36 | { 37 | value: struct{ A int }{}, 38 | input: "A", 39 | output: "A", 40 | }, 41 | { 42 | value: struct{ A int }{}, 43 | input: "1.2.3", 44 | output: "1.2.3", 45 | }, 46 | { 47 | value: struct { 48 | A int `conf:"a"` 49 | }{}, 50 | input: "A", 51 | output: "a", 52 | }, 53 | { 54 | value: struct { 55 | A int `conf:"a"` 56 | }{}, 57 | input: "a", 58 | output: "a", 59 | }, 60 | { 61 | value: struct { 62 | A struct { 63 | B struct { 64 | C int `conf:"c"` 65 | } `conf:"b"` 66 | } `conf:"a"` 67 | }{}, 68 | input: "A.B.C", 69 | output: "a.b.c", 70 | }, 71 | { 72 | value: struct { 73 | A struct { 74 | B struct { 75 | C int `conf:"c"` 76 | } `conf:"b"` 77 | } `conf:"a"` 78 | }{}, 79 | input: "A.B", 80 | output: "a.b", 81 | }, 82 | { 83 | value: Container{}, 84 | input: "Str", 85 | output: "str", 86 | }, 87 | { 88 | value: struct { 89 | A struct { 90 | Container `conf:"_"` 91 | } `conf:"a"` 92 | }{}, 93 | input: "a.Str", 94 | output: "a.Str", 95 | }, 96 | } 97 | 98 | for _, test := range tests { 99 | t.Run(test.input, func(t *testing.T) { 100 | if output := fieldPath(reflect.TypeOf(test.value), test.input); output != test.output { 101 | t.Error(output) 102 | } 103 | }) 104 | } 105 | } 106 | 107 | type point struct { 108 | X int `conf:"x"` 109 | Y int `conf:"y"` 110 | } 111 | 112 | var ( 113 | testTime = time.Date(2016, 12, 6, 1, 1, 42, 123456789, time.UTC) 114 | 115 | loadTests = []struct { 116 | val interface{} 117 | file string 118 | args []string 119 | env []string 120 | }{ 121 | { 122 | val: struct{ A bool }{true}, 123 | file: `A: true`, 124 | args: []string{"-A"}, 125 | env: []string{"TEST_A=true"}, 126 | }, 127 | 128 | { 129 | val: struct{ A bool }{false}, 130 | file: `A: false`, 131 | args: []string{}, 132 | env: []string{"TEST_A=false"}, 133 | }, 134 | 135 | { 136 | val: struct{ A int }{42}, 137 | file: `A: 42`, 138 | args: []string{"-A", "42"}, 139 | env: []string{"TEST_A=42"}, 140 | }, 141 | 142 | { 143 | val: struct{ A int }{0}, // missing => zero value 144 | file: ``, 145 | args: []string{}, 146 | env: []string{}, 147 | }, 148 | 149 | { 150 | val: struct{ A string }{"42"}, // convert digit sequence to string 151 | file: `A: '42'`, 152 | args: []string{"-A", "42"}, 153 | env: []string{"TEST_A=42"}, 154 | }, 155 | { 156 | val: struct{ A string }{"0123456789"}, // convert a longer digit sequence to string 157 | file: `A: '0123456789'`, 158 | args: []string{"-A", "0123456789"}, 159 | env: []string{"TEST_A=0123456789"}, 160 | }, 161 | 162 | { 163 | val: struct{ S string }{"Hello World!"}, 164 | file: `S: Hello World!`, 165 | args: []string{"-S", "Hello World!"}, 166 | env: []string{"TEST_S=Hello World!"}, 167 | }, 168 | 169 | { 170 | val: struct{ L []int }{[]int{1, 2, 3}}, 171 | file: `L: [1, 2, 3]`, 172 | args: []string{"-L", "[1,2,3]"}, 173 | env: []string{"TEST_L=[1, 2, 3]"}, 174 | }, 175 | 176 | { 177 | val: struct{ L []string }{[]string{"A", "42"}}, 178 | file: `L: [A, 42]`, 179 | args: []string{"-L", "[A, 42]"}, 180 | env: []string{"TEST_L=[A, 42]"}, 181 | }, 182 | 183 | { 184 | val: struct{ L []string }{[]string{"A", "B", "C"}}, 185 | file: `L: [A,B,C]`, 186 | args: []string{"-L", "[A,B,C]"}, 187 | env: []string{"TEST_L=[A,B,C]"}, 188 | }, 189 | 190 | { 191 | val: struct{ L []string }{[]string{"A", "B", "C"}}, 192 | file: `L: [A,B,C]`, 193 | args: []string{"-L", `["A","B","C"]`}, 194 | env: []string{`TEST_L=["A","B","C"]`}, 195 | }, 196 | 197 | { 198 | val: struct{ P *point }{&point{1, 2}}, 199 | file: `P: { 'x': 1, 'y': 2 }`, 200 | args: []string{"-P.x", "1", "-P.y", "2"}, 201 | env: []string{"TEST_P_X=1", "TEST_P_Y=2"}, 202 | }, 203 | 204 | { 205 | val: struct{ P *point }{&point{1, 2}}, 206 | file: `P: { 'x': 1, 'y': 2 }`, 207 | args: []string{"-P", "{ 'x': 1, 'y': 2 }"}, 208 | env: []string{"TEST_P={ 'x': 1, 'y': 2 }"}, 209 | }, 210 | 211 | { 212 | val: struct{ D time.Duration }{10 * time.Second}, 213 | file: `D: 10s`, 214 | args: []string{"-D=10s"}, 215 | env: []string{"TEST_D=10s"}, 216 | }, 217 | 218 | { 219 | val: struct{ T time.Time }{testTime}, 220 | file: `T: 2016-12-06T01:01:42.123456789Z`, 221 | args: []string{"-T=2016-12-06T01:01:42.123456789Z"}, 222 | env: []string{"TEST_T=2016-12-06T01:01:42.123456789Z"}, 223 | }, 224 | 225 | { 226 | val: struct{ T *time.Time }{&testTime}, 227 | file: `T: 2016-12-06T01:01:42.123456789Z`, 228 | args: []string{"-T=2016-12-06T01:01:42.123456789Z"}, 229 | env: []string{"TEST_T=2016-12-06T01:01:42.123456789Z"}, 230 | }, 231 | 232 | { 233 | val: struct{ M map[string]int }{map[string]int{"answer": 42}}, 234 | file: `M: { answer: 42 }`, 235 | args: []string{"-M={ answer: 42 }"}, 236 | env: []string{"TEST_M={ answer: 42 }"}, 237 | }, 238 | 239 | { 240 | val: struct{ A net.TCPAddr }{net.TCPAddr{IP: net.ParseIP("::1"), Port: 80, Zone: "11"}}, 241 | file: `A: '[::1%11]:80'`, 242 | args: []string{"-A", "[::1%11]:80"}, 243 | env: []string{"TEST_A=[::1%11]:80"}, 244 | }, 245 | 246 | { 247 | val: struct{ A net.UDPAddr }{net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 53, Zone: ""}}, 248 | file: `A: 127.0.0.1:53`, 249 | args: []string{"-A", "127.0.0.1:53"}, 250 | env: []string{"TEST_A=127.0.0.1:53"}, 251 | }, 252 | 253 | { 254 | val: struct{ U url.URL }{parseURL("http://localhost:8080/hello/world?answer=42#OK")}, 255 | file: `U: http://localhost:8080/hello/world?answer=42#OK`, 256 | args: []string{"-U", "http://localhost:8080/hello/world?answer=42#OK"}, 257 | env: []string{"TEST_U=http://localhost:8080/hello/world?answer=42#OK"}, 258 | }, 259 | 260 | { 261 | val: struct{ E mail.Address }{parseEmail("Bob ")}, 262 | file: `E: Bob `, 263 | args: []string{"-E", "Bob "}, 264 | env: []string{"TEST_E=Bob "}, 265 | }, 266 | } 267 | ) 268 | 269 | func TestLoad(t *testing.T) { 270 | for _, test := range loadTests { 271 | t.Run(fmt.Sprint(test.val), func(t *testing.T) { 272 | ld := Loader{ 273 | Name: "test", 274 | Args: test.args, 275 | Sources: []Source{ 276 | SourceFunc(func(dst Map) (err error) { return yaml.Unmarshal([]byte(test.file), dst) }), 277 | NewEnvSource("test", test.env...), 278 | }, 279 | } 280 | 281 | val := reflect.New(reflect.TypeOf(test.val)) 282 | 283 | if _, _, err := ld.Load(val.Interface()); err != nil { 284 | t.Error(err) 285 | t.Log("<<<", test.val) 286 | t.Log(">>>", val.Elem().Interface()) 287 | return 288 | } 289 | 290 | if v := val.Elem().Interface(); !reflect.DeepEqual(test.val, v) { 291 | t.Errorf("bad value:\n<<< %#v\n>>> %#v", test.val, v) 292 | } 293 | }) 294 | } 295 | } 296 | 297 | func TestDefaultLoader(t *testing.T) { 298 | const configFile = "/tmp/conf-test.yml" 299 | os.WriteFile(configFile, []byte(`--- 300 | points: 301 | - { 'x': 0, 'y': 0 } 302 | - { 'x': 1, 'y': 2 } 303 | - { 'x': {{ .X }}, 'y': {{ .Y }} } 304 | `), 0644) 305 | defer os.Remove(configFile) 306 | 307 | tests := []struct { 308 | args []string 309 | env []string 310 | }{ 311 | { 312 | args: []string{"test", "-points", `[{'x':0,'y':0},{'x':1,'y':2},{'x':21,'y':42}]`, "A", "B", "C"}, 313 | env: []string{}, 314 | }, 315 | { 316 | args: []string{"test", "A", "B", "C"}, 317 | env: []string{"TEST_POINTS=[{'x':0,'y':0},{'x':1,'y':2},{'x':21,'y':42}]"}, 318 | }, 319 | { 320 | args: []string{"test", "-config-file", configFile, "A", "B", "C"}, 321 | env: []string{"X=21", "Y=42"}, 322 | }, 323 | { 324 | args: []string{"test", "-config-file", configFile, "-points", `[{'x':0,'y':0},{'x':1,'y':2},{'x':21,'y':42}]`, "A", "B", "C"}, 325 | env: []string{"TEST_POINTS=[{'x':0,'y':0},{'x':1,'y':2},{'x':21,'y':42}]", "X=3", "Y=4"}, 326 | }, 327 | } 328 | 329 | type point struct { 330 | X int `conf:"x"` 331 | Y int `conf:"y"` 332 | } 333 | 334 | type extra struct { 335 | Dummy []map[string]string 336 | } 337 | 338 | type config struct { 339 | // should not impact loading configuration 340 | //lint:ignore U1000 part of the test 341 | unexported bool 342 | Ignored string `conf:"-"` 343 | 344 | // these fields only are getting configured 345 | Points []point `conf:"points"` 346 | Extra *extra 347 | Time time.Time 348 | } 349 | 350 | for _, test := range tests { 351 | t.Run("", func(t *testing.T) { 352 | var cfg config 353 | _, args, err := defaultLoader(test.args, test.env).Load(&cfg) 354 | 355 | if err != nil { 356 | t.Error(err) 357 | } 358 | 359 | if !reflect.DeepEqual(args, []string{"A", "B", "C"}) { 360 | t.Error("bad args:", args) 361 | } 362 | 363 | if !reflect.DeepEqual(cfg, config{Points: []point{{0, 0}, {1, 2}, {21, 42}}, Extra: &extra{}}) { 364 | t.Errorf("bad config: %#v", cfg) 365 | } 366 | }) 367 | } 368 | } 369 | 370 | func TestTemplateFunc(t *testing.T) { 371 | const configFile = "/tmp/conf-json-test.yml" 372 | os.WriteFile(configFile, []byte(`--- 373 | hello: {{ .NAME | json }} 374 | `), 0644) 375 | defer os.Remove(configFile) 376 | 377 | var cfg struct { 378 | Hello string `conf:"hello"` 379 | } 380 | 381 | _, _, err := defaultLoader([]string{"test", "-config-file", configFile}, []string{ 382 | "NAME=first: Luke, second: Leia", 383 | }).Load(&cfg) 384 | 385 | if err != nil { 386 | t.Error(err) 387 | } 388 | 389 | if cfg.Hello != "first: Luke, second: Leia" { 390 | t.Error("bad value:", cfg.Hello) 391 | } 392 | } 393 | 394 | func TestCommand(t *testing.T) { 395 | t.Run("Success", func(t *testing.T) { 396 | ld := Loader{ 397 | Name: "test", 398 | Args: []string{"run", "A", "B", "C"}, 399 | Commands: []Command{{"run", ""}, {"version", ""}}, 400 | } 401 | 402 | config := struct{}{} 403 | cmd, args, err := ld.Load(&config) 404 | 405 | if err != nil { 406 | t.Error(err) 407 | } 408 | if cmd != "run" { 409 | t.Error("bad command:", cmd) 410 | } 411 | if !reflect.DeepEqual(args, []string{"A", "B", "C"}) { 412 | t.Error("bad arguments:", args) 413 | } 414 | }) 415 | 416 | t.Run("Missing Command", func(t *testing.T) { 417 | ld := Loader{ 418 | Name: "test", 419 | Args: []string{}, 420 | Commands: []Command{{"run", ""}, {"version", ""}}, 421 | } 422 | 423 | config := struct{}{} 424 | _, _, err := ld.Load(&config) 425 | 426 | if err == nil || err.Error() != "missing command" { 427 | t.Error("bad error:", err) 428 | } 429 | }) 430 | 431 | t.Run("Unknown Command", func(t *testing.T) { 432 | ld := Loader{ 433 | Name: "test", 434 | Args: []string{"test"}, 435 | Commands: []Command{{"run", ""}, {"version", ""}}, 436 | } 437 | 438 | config := struct{}{} 439 | _, _, err := ld.Load(&config) 440 | 441 | if err == nil || err.Error() != "unknown command: test" { 442 | t.Error("bad error:", err) 443 | } 444 | }) 445 | } 446 | 447 | func TestValidator(t *testing.T) { 448 | config := struct { 449 | A struct { 450 | Bind string `conf:"bind" validate:"nonzero"` 451 | } 452 | }{} 453 | 454 | _, _, err := (Loader{}).Load(&config) 455 | 456 | if err == nil { 457 | t.Error("bad error:", err) 458 | } else { 459 | t.Log(err) 460 | } 461 | } 462 | 463 | func TestModifiers(t *testing.T) { 464 | config := struct { 465 | Email string `conf:"email" validate:"nonzero" mod:"trim,lcase"` 466 | }{ 467 | Email: " Test.Email@email.com", 468 | } 469 | 470 | _, _, err := (Loader{}).Load(&config) 471 | if err != nil { 472 | t.Error("bad error:", err) 473 | } 474 | if config.Email != "test.email@email.com" { 475 | t.Error("bad mod value:", config.Email) 476 | } 477 | } 478 | 479 | func parseURL(s string) url.URL { 480 | u, _ := url.Parse(s) 481 | return *u 482 | } 483 | 484 | func parseEmail(s string) mail.Address { 485 | a, _ := mail.ParseAddress(s) 486 | return *a 487 | } 488 | 489 | func TestMakeEnvVars(t *testing.T) { 490 | envList := []string{ 491 | "A=123", 492 | "B=456", 493 | "C=789", 494 | "Hello=World", 495 | "Answer=42", 496 | "Key=", 497 | "Key=Value", 498 | "Other", 499 | } 500 | 501 | envVars := makeEnvVars(envList) 502 | 503 | if !reflect.DeepEqual(envVars, map[string]string{ 504 | "A": "123", 505 | "B": "456", 506 | "C": "789", 507 | "Hello": "World", 508 | "Answer": "42", 509 | "Key": "Value", 510 | "Other": "", 511 | }) { 512 | t.Error(envVars) 513 | } 514 | } 515 | 516 | func TestEmbeddedStruct(t *testing.T) { 517 | 518 | type Child struct { 519 | ChildField1 string 520 | ChildField2 string 521 | } 522 | 523 | type Branch struct { 524 | Child `conf:"_"` 525 | BranchField string 526 | } 527 | 528 | type Container struct { 529 | Branch `conf:"_"` 530 | OtherBranch Branch 531 | } 532 | 533 | testVal := Container{ 534 | Branch: Branch{ 535 | Child: Child{ 536 | ChildField1: "embedded-child-1", 537 | ChildField2: "embedded-child-2", 538 | }, 539 | BranchField: "embedded-branch", 540 | }, 541 | OtherBranch: Branch{ 542 | Child: Child{ 543 | ChildField1: "no-embedded-child-1", 544 | ChildField2: "no-embedded-child-2", 545 | }, 546 | BranchField: "no-embedded-branch", 547 | }, 548 | } 549 | 550 | ld := Loader{ 551 | Name: "test", 552 | Args: []string{ 553 | "-ChildField1", "embedded-child-1", 554 | "-ChildField2", "embedded-child-2", 555 | "-BranchField", "embedded-branch", 556 | "-OtherBranch.ChildField1", "no-embedded-child-1", 557 | "-OtherBranch.ChildField2", "no-embedded-child-2", 558 | "-OtherBranch.BranchField", "no-embedded-branch", 559 | }, 560 | } 561 | 562 | val := reflect.New(reflect.TypeOf(testVal)) 563 | 564 | if _, _, err := ld.Load(val.Interface()); err != nil { 565 | t.Error(err) 566 | t.Log("<<<", testVal) 567 | t.Log(">>>", val.Elem().Interface()) 568 | return 569 | } 570 | 571 | if v := val.Elem().Interface(); !reflect.DeepEqual(testVal, v) { 572 | t.Errorf("bad value:\n<<< %#v\n>>> %#v", testVal, v) 573 | } 574 | } 575 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "flag" 7 | "fmt" 8 | "reflect" 9 | "sort" 10 | "time" 11 | 12 | "github.com/segmentio/objconv" 13 | "github.com/segmentio/objconv/json" 14 | "github.com/segmentio/objconv/yaml" 15 | ) 16 | 17 | // NodeKind is an enumeration which describes the different types of nodes that 18 | // are supported in a configuration. 19 | type NodeKind int 20 | 21 | const ( 22 | // ScalarNode represents configuration nodes of type Scalar. 23 | ScalarNode NodeKind = iota 24 | 25 | // ArrayNode represents configuration nodes of type Array. 26 | ArrayNode 27 | 28 | // MapNode represents configuration nodes of type Map. 29 | MapNode 30 | ) 31 | 32 | // The Node interface defines the common interface supported by the different 33 | // types of configuration nodes supported by the conf package. 34 | type Node interface { 35 | flag.Value 36 | objconv.ValueEncoder 37 | objconv.ValueDecoder 38 | 39 | // Kind returns the NodeKind of the configuration node. 40 | Kind() NodeKind 41 | 42 | // Value returns the underlying value wrapped by the configuration node. 43 | Value() interface{} 44 | } 45 | 46 | // EqualNode compares n1 and n2, returning true if they are deeply equal. 47 | func EqualNode(n1 Node, n2 Node) bool { 48 | if n1 == nil || n2 == nil { 49 | return n1 == n2 50 | } 51 | 52 | k1 := n1.Kind() 53 | k2 := n2.Kind() 54 | 55 | if k1 != k2 { 56 | return false 57 | } 58 | 59 | switch k1 { 60 | case ArrayNode: 61 | return equalNodeArray(n1.(Array), n2.(Array)) 62 | case MapNode: 63 | return equalNodeMap(n1.(Map), n2.(Map)) 64 | default: 65 | return equalNodeScalar(n1.(Scalar), n2.(Scalar)) 66 | } 67 | } 68 | 69 | func equalNodeArray(a1 Array, a2 Array) bool { 70 | n1 := a1.Len() 71 | n2 := a2.Len() 72 | 73 | if n1 != n2 { 74 | return false 75 | } 76 | 77 | for i := 0; i != n1; i++ { 78 | if !EqualNode(a1.Item(i), a2.Item(i)) { 79 | return false 80 | } 81 | } 82 | 83 | return true 84 | } 85 | 86 | func equalNodeMap(m1 Map, m2 Map) bool { 87 | n1 := m1.Len() 88 | n2 := m2.Len() 89 | 90 | if n1 != n2 { 91 | return false 92 | } 93 | 94 | for _, item := range m1.Items() { 95 | if !EqualNode(item.Value, m2.Item(item.Name)) { 96 | return false 97 | } 98 | } 99 | 100 | return true 101 | } 102 | 103 | func equalNodeScalar(s1 Scalar, s2 Scalar) bool { 104 | v1 := s1.value.IsValid() 105 | v2 := s2.value.IsValid() 106 | 107 | if !v1 || !v2 { 108 | return v1 == v2 109 | } 110 | 111 | t1 := s1.value.Type() 112 | t2 := s2.value.Type() 113 | 114 | if t1 != t2 { 115 | return false 116 | } 117 | 118 | switch t1 { 119 | case timeTimeType: 120 | return s1.Value().(time.Time).Equal(s2.Value().(time.Time)) 121 | } 122 | 123 | return reflect.DeepEqual(s1.Value(), s2.Value()) 124 | } 125 | 126 | // MakeNode builds a Node from the value v. 127 | // 128 | // The function panics if v contains unrepresentable values. 129 | func MakeNode(v interface{}) Node { 130 | return makeNode(reflect.ValueOf(v)) 131 | } 132 | 133 | func makeNode(v reflect.Value) Node { 134 | if !v.IsValid() { 135 | return makeNodeScalar(v) 136 | } 137 | 138 | t := v.Type() 139 | 140 | switch t { 141 | case timeTimeType, timeDurationType: 142 | return makeNodeScalar(v) 143 | } 144 | 145 | if _, ok := objconv.AdapterOf(t); ok { 146 | return makeNodeScalar(v) 147 | } 148 | 149 | switch { 150 | case 151 | t.Implements(objconvValueDecoderInterface), 152 | t.Implements(textUnmarshalerInterface): 153 | return makeNodeScalar(v) 154 | } 155 | 156 | switch t.Kind() { 157 | case reflect.Array, reflect.Chan, reflect.Func, reflect.UnsafePointer, reflect.Interface: 158 | panic("unsupported type found in configuration: " + t.String()) 159 | 160 | case reflect.Struct: 161 | return makeNodeStruct(v, t) 162 | 163 | case reflect.Map: 164 | return makeNodeMap(v, t) 165 | 166 | case reflect.Slice: 167 | return makeNodeSlice(v, t) 168 | 169 | case reflect.Ptr: 170 | return makeNodePtr(v, t) 171 | 172 | default: 173 | return makeNodeScalar(v) 174 | } 175 | } 176 | 177 | func makeNodeStruct(v reflect.Value, t reflect.Type) (m Map) { 178 | m.value = v 179 | m.items = newMapItems() 180 | 181 | populateNodeStruct(t, t.Name(), v, t, m) 182 | 183 | // if using the "_" notation to embed structs, it's possible that names are no longer unique. 184 | props := make(map[string]struct{}) 185 | for _, item := range m.Items() { 186 | if _, ok := props[item.Name]; ok { 187 | panic("duplicate name '" + item.Name + "' found after collapsing embedded structs in configuration: " + t.String()) 188 | } 189 | props[item.Name] = struct{}{} 190 | } 191 | 192 | return 193 | } 194 | 195 | // populateNodeStruct is the mutually recursive helper of makeNodeStruct to create the node struct with potentially 196 | // embedded types. It will populate m with the struct fields from v. The original type and path of the current field 197 | // are passed in order to create decent panic strings if an invalid configuration is detected. 198 | func populateNodeStruct(originalT reflect.Type, path string, v reflect.Value, t reflect.Type, m Map) { 199 | 200 | for i, n := 0, v.NumField(); i != n; i++ { 201 | fv := v.Field(i) 202 | ft := t.Field(i) 203 | 204 | if !isExported(ft) { 205 | continue 206 | } 207 | 208 | name, help := ft.Tag.Get("conf"), ft.Tag.Get("help") 209 | switch name { 210 | case "-": 211 | continue 212 | case "_": 213 | path = path + "." + ft.Name 214 | if ft.Type.Kind() != reflect.Struct || !ft.Anonymous { 215 | panic("found \"_\" on invalid type at path " + path + " in configuration: " + originalT.Name()) 216 | } 217 | populateNodeStruct(originalT, path, fv, ft.Type, m) 218 | continue 219 | case "": 220 | name = ft.Name 221 | } 222 | 223 | m.items.push(MapItem{ 224 | Name: name, 225 | Help: help, 226 | Value: makeNode(fv), 227 | }) 228 | } 229 | } 230 | 231 | func makeNodeMap(v reflect.Value, t reflect.Type) (m Map) { 232 | if v.IsNil() && v.CanSet() { 233 | v.Set(reflect.MakeMap(v.Type())) 234 | } 235 | 236 | m.value = v 237 | m.items = newMapItems() 238 | 239 | for _, key := range v.MapKeys() { 240 | m.items.push(MapItem{ 241 | Name: key.String(), // only string keys are supported for now 242 | Value: makeNode(v.MapIndex(key)), 243 | }) 244 | } 245 | 246 | sort.Sort(m.items) 247 | return 248 | } 249 | 250 | func makeNodeSlice(v reflect.Value, t reflect.Type) (a Array) { 251 | n := v.Len() 252 | a.value = v 253 | a.items = newArrayItems() 254 | 255 | for i := 0; i != n; i++ { 256 | a.items.push(makeNode(v.Index(i))) 257 | } 258 | 259 | return 260 | } 261 | 262 | func makeNodePtr(v reflect.Value, t reflect.Type) Node { 263 | if v.IsNil() { 264 | p := reflect.New(t.Elem()) 265 | 266 | if v.CanSet() { 267 | v.Set(p) 268 | } 269 | 270 | v = p 271 | } 272 | return makeNode(v.Elem()) 273 | } 274 | 275 | func makeNodeScalar(value reflect.Value) (s Scalar) { 276 | s.value = value 277 | return 278 | } 279 | 280 | func isExported(f reflect.StructField) bool { 281 | return len(f.PkgPath) == 0 282 | } 283 | 284 | // A Scalar is a node type that wraps a basic value. 285 | type Scalar struct { 286 | value reflect.Value 287 | } 288 | 289 | func (s Scalar) Kind() NodeKind { 290 | return ScalarNode 291 | } 292 | 293 | func (s Scalar) Value() interface{} { 294 | if !s.value.IsValid() { 295 | return nil 296 | } 297 | return s.value.Interface() 298 | } 299 | 300 | func (s Scalar) String() string { 301 | b, _ := yaml.Marshal(s) 302 | return string(bytes.TrimSpace(b)) 303 | } 304 | 305 | func (s Scalar) Set(str string) (err error) { 306 | defer func() { 307 | if x := recover(); x != nil { 308 | err = fmt.Errorf("%s", x) 309 | } 310 | }() 311 | 312 | if s.value.Kind() == reflect.String { 313 | s.value.SetString(str) 314 | return 315 | } 316 | 317 | ptr := s.value.Addr().Interface() 318 | if err = yaml.Unmarshal([]byte(str), ptr); err != nil { 319 | if b, _ := json.Marshal(str); b != nil { 320 | if json.Unmarshal(b, ptr) == nil { 321 | err = nil 322 | } 323 | } 324 | } 325 | 326 | return 327 | } 328 | 329 | func (s Scalar) EncodeValue(e objconv.Encoder) error { 330 | return e.Encode(s.Value()) 331 | } 332 | 333 | func (s Scalar) DecodeValue(d objconv.Decoder) error { 334 | return d.Decode(s.value.Addr().Interface()) 335 | } 336 | 337 | func (s Scalar) IsBoolFlag() bool { 338 | return s.value.IsValid() && s.value.Kind() == reflect.Bool 339 | } 340 | 341 | // Array is a node type that wraps a slice value. 342 | type Array struct { 343 | value reflect.Value 344 | items *arrayItems 345 | } 346 | 347 | func (a Array) Kind() NodeKind { 348 | return ArrayNode 349 | } 350 | 351 | func (a Array) Value() interface{} { 352 | if !a.value.IsValid() { 353 | return nil 354 | } 355 | return a.value.Interface() 356 | } 357 | 358 | func (a Array) Items() []Node { 359 | if a.items == nil { 360 | return nil 361 | } 362 | return a.items.items() 363 | } 364 | 365 | func (a Array) Item(i int) Node { 366 | return a.items.index(i) 367 | } 368 | 369 | func (a Array) Len() int { 370 | if a.items == nil { 371 | return 0 372 | } 373 | return a.items.len() 374 | } 375 | 376 | func (a Array) String() string { 377 | if a.Len() == 0 { 378 | return "[ ]" 379 | } 380 | b := &bytes.Buffer{} 381 | b.WriteByte('[') 382 | 383 | for i, item := range a.Items() { 384 | if i != 0 { 385 | b.WriteString(", ") 386 | } 387 | b.WriteString(item.String()) 388 | } 389 | 390 | b.WriteByte(']') 391 | return b.String() 392 | } 393 | 394 | func (a Array) Set(s string) error { 395 | return yaml.Unmarshal([]byte(s), a) 396 | } 397 | 398 | func (a Array) EncodeValue(e objconv.Encoder) (err error) { 399 | i := 0 400 | return e.EncodeArray(a.Len(), func(e objconv.Encoder) (err error) { 401 | if err = a.Item(i).EncodeValue(e); err != nil { 402 | return 403 | } 404 | i++ 405 | return 406 | }) 407 | } 408 | 409 | func (a Array) DecodeValue(d objconv.Decoder) (err error) { 410 | a.pop(a.Len()) 411 | return d.DecodeArray(func(d objconv.Decoder) (err error) { 412 | if err = a.push().DecodeValue(d); err != nil { 413 | a.pop(1) 414 | } 415 | return 416 | }) 417 | } 418 | 419 | func (a Array) push() Node { 420 | i := a.Len() 421 | a.value.Set(reflect.Append(a.value, reflect.Zero(a.value.Type().Elem()))) 422 | a.items.push(makeNode(a.value.Index(i))) 423 | return a.items.index(i) 424 | } 425 | 426 | func (a Array) pop(n int) { 427 | if n != 0 { 428 | a.value.Set(a.value.Slice(0, a.Len()-n)) 429 | a.items.pop(n) 430 | } 431 | } 432 | 433 | // Map is a map type that wraps a map or struct value. 434 | type Map struct { 435 | value reflect.Value 436 | items *mapItems 437 | } 438 | 439 | // MapItem is the type of elements stored in a Map. 440 | type MapItem struct { 441 | Name string 442 | Help string 443 | Value Node 444 | } 445 | 446 | func (m Map) Kind() NodeKind { 447 | return MapNode 448 | } 449 | 450 | func (m Map) Value() interface{} { 451 | if !m.value.IsValid() { 452 | return nil 453 | } 454 | return m.value.Interface() 455 | } 456 | 457 | func (m Map) Items() []MapItem { 458 | if m.items == nil { 459 | return nil 460 | } 461 | return m.items.items() 462 | } 463 | 464 | func (m Map) Item(name string) Node { 465 | if m.items == nil { 466 | return nil 467 | } 468 | return m.items.get(name) 469 | } 470 | 471 | func (m Map) Len() int { 472 | if m.items == nil { 473 | return 0 474 | } 475 | return m.items.len() 476 | } 477 | 478 | func (m Map) String() string { 479 | if m.Len() == 0 { 480 | return "{ }" 481 | } 482 | 483 | b := &bytes.Buffer{} 484 | b.WriteString("{ ") 485 | 486 | for i, item := range m.Items() { 487 | if i != 0 { 488 | b.WriteString(", ") 489 | } 490 | fmt.Fprintf(b, "%s: %s", item.Name, item.Value) 491 | 492 | if len(item.Help) != 0 { 493 | fmt.Fprintf(b, " (%s)", item.Help) 494 | } 495 | } 496 | 497 | b.WriteString(" }") 498 | return b.String() 499 | } 500 | 501 | func (m Map) Set(s string) error { 502 | return yaml.Unmarshal([]byte(s), m) 503 | } 504 | 505 | func (m Map) EncodeValue(e objconv.Encoder) error { 506 | i := 0 507 | return e.EncodeMap(m.Len(), func(ke objconv.Encoder, ve objconv.Encoder) (err error) { 508 | item := &m.items.nodes[i] 509 | if err = ke.Encode(item.Name); err != nil { 510 | return 511 | } 512 | if err = item.Value.EncodeValue(ve); err != nil { 513 | return 514 | } 515 | i++ 516 | return 517 | }) 518 | } 519 | 520 | func (m Map) DecodeValue(d objconv.Decoder) error { 521 | return d.DecodeMap(func(kd objconv.Decoder, vd objconv.Decoder) (err error) { 522 | var key string 523 | 524 | if err = kd.Decode(&key); err != nil { 525 | return 526 | } 527 | 528 | if m.value.Kind() == reflect.Struct { 529 | if item := m.Item(key); item != nil { 530 | return item.DecodeValue(vd) 531 | } 532 | return vd.Decode(nil) // discard 533 | } 534 | 535 | name := reflect.ValueOf(key) 536 | node := makeNode(reflect.New(m.value.Type().Elem())) 537 | 538 | if err = node.DecodeValue(vd); err != nil { 539 | return 540 | } 541 | 542 | m.value.SetMapIndex(name, reflect.ValueOf(node.Value())) 543 | m.items.put(MapItem{ 544 | Name: key, 545 | Value: makeNode(m.value.MapIndex(name)), 546 | }) 547 | return 548 | }) 549 | } 550 | 551 | func (m Map) Scan(do func([]string, MapItem)) { 552 | m.scan(make([]string, 0, 10), do) 553 | } 554 | 555 | func (m Map) scan(path []string, do func([]string, MapItem)) { 556 | for _, item := range m.Items() { 557 | do(path, item) 558 | 559 | switch v := item.Value.(type) { 560 | case Map: 561 | v.scan(append(path, item.Name), do) 562 | } 563 | } 564 | } 565 | 566 | type arrayItems struct { 567 | nodes []Node 568 | } 569 | 570 | func newArrayItems(nodes ...Node) *arrayItems { 571 | return &arrayItems{nodes} 572 | } 573 | 574 | func (a *arrayItems) push(n Node) { 575 | a.nodes = append(a.nodes, n) 576 | } 577 | 578 | func (a *arrayItems) pop(n int) { 579 | a.nodes = a.nodes[:len(a.nodes)-n] 580 | } 581 | 582 | func (a *arrayItems) len() int { 583 | return len(a.nodes) 584 | } 585 | 586 | func (a *arrayItems) index(i int) Node { 587 | return a.nodes[i] 588 | } 589 | 590 | func (a *arrayItems) items() []Node { 591 | return a.nodes 592 | } 593 | 594 | type mapItems struct { 595 | nodes []MapItem 596 | } 597 | 598 | func newMapItems(nodes ...MapItem) *mapItems { 599 | return &mapItems{nodes} 600 | } 601 | 602 | func (m *mapItems) get(name string) Node { 603 | if i := m.index(name); i >= 0 { 604 | return m.nodes[i].Value 605 | } 606 | return nil 607 | } 608 | 609 | func (m *mapItems) index(name string) int { 610 | for i, node := range m.nodes { 611 | if node.Name == name { 612 | return i 613 | } 614 | } 615 | return -1 616 | } 617 | 618 | func (m *mapItems) len() int { 619 | return len(m.nodes) 620 | } 621 | 622 | func (m *mapItems) items() []MapItem { 623 | return m.nodes 624 | } 625 | 626 | func (m *mapItems) push(item MapItem) { 627 | m.nodes = append(m.nodes, item) 628 | } 629 | 630 | func (m *mapItems) put(item MapItem) { 631 | if i := m.index(item.Name); i >= 0 { 632 | m.nodes[i] = item 633 | } else { 634 | m.push(item) 635 | } 636 | } 637 | 638 | func (m *mapItems) Less(i int, j int) bool { 639 | return m.nodes[i].Name < m.nodes[j].Name 640 | } 641 | 642 | func (m *mapItems) Swap(i int, j int) { 643 | m.nodes[i], m.nodes[j] = m.nodes[j], m.nodes[i] 644 | } 645 | 646 | func (m *mapItems) Len() int { 647 | return len(m.nodes) 648 | } 649 | 650 | var ( 651 | timeTimeType = reflect.TypeOf(time.Time{}) 652 | timeDurationType = reflect.TypeOf(time.Duration(0)) 653 | 654 | objconvValueDecoderInterface = reflect.TypeOf((*objconv.ValueDecoder)(nil)).Elem() 655 | textUnmarshalerInterface = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 656 | ) 657 | --------------------------------------------------------------------------------