├── internal ├── file │ ├── tests │ │ ├── broken.env │ │ ├── broken.json │ │ ├── broken.toml │ │ ├── broken.yaml │ │ ├── cfg.env │ │ ├── cfg.yaml │ │ ├── cfg.toml │ │ └── cfg.json │ ├── read.go │ └── read_test.go ├── reflect │ ├── errors.go │ ├── validation.go │ ├── validation_test.go │ ├── parse-tag_test.go │ ├── parse-tag.go │ ├── write-to-struct_test.go │ └── write-to-struct.go ├── env │ ├── read.go │ └── read_test.go ├── dflt │ ├── read.go │ └── read_test.go └── flag │ ├── read.go │ └── read_test.go ├── .gitignore ├── tests ├── stub.env ├── stub.yaml ├── stub.toml ├── stub.json ├── stubs.go └── combination_test.go ├── go.mod ├── .github └── workflows │ └── go.yaml ├── LICENSE.md ├── Makefile ├── doc.go ├── go.sum ├── .golangci.yaml ├── cfg.go └── README.md /internal/file/tests/broken.env: -------------------------------------------------------------------------------- 1 | ! -------------------------------------------------------------------------------- /internal/file/tests/broken.json: -------------------------------------------------------------------------------- 1 | not json -------------------------------------------------------------------------------- /internal/file/tests/broken.toml: -------------------------------------------------------------------------------- 1 | not toml -------------------------------------------------------------------------------- /internal/file/tests/broken.yaml: -------------------------------------------------------------------------------- 1 | not yaml -------------------------------------------------------------------------------- /internal/file/tests/cfg.env: -------------------------------------------------------------------------------- 1 | FIELD="envFieldValue" 2 | NESTED_FIELD="envNestedFieldValue" 3 | -------------------------------------------------------------------------------- /internal/file/tests/cfg.yaml: -------------------------------------------------------------------------------- 1 | field: yamlFieldValue 2 | nested: 3 | field: yamlNestedFieldValue 4 | -------------------------------------------------------------------------------- /internal/file/tests/cfg.toml: -------------------------------------------------------------------------------- 1 | field = "tomlFieldValue" 2 | 3 | [nested] 4 | field = "tomlNestedFieldValue" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Thumbs.db 3 | 4 | .idea 5 | .vscode 6 | 7 | .env 8 | coverage.out 9 | coverage.svg 10 | bin 11 | cmd -------------------------------------------------------------------------------- /internal/file/tests/cfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "field": "jsonFieldValue", 3 | "nested": { 4 | "field": "jsonNestedFieldValue" 5 | } 6 | } -------------------------------------------------------------------------------- /tests/stub.env: -------------------------------------------------------------------------------- 1 | STR="env-file-string" 2 | INT="-642" 3 | INT_8="-68" 4 | INT_16="-616" 5 | INT_32="-632" 6 | INT_64="-664" 7 | UINT="642" 8 | UINT_8="68" 9 | UINT_16="616" 10 | UINT_32="632" 11 | UINT_64="664" 12 | FLOAT_32="632.32" 13 | FLOAT_64="664.64" 14 | BOOL="true" 15 | -------------------------------------------------------------------------------- /tests/stub.yaml: -------------------------------------------------------------------------------- 1 | str: yaml-string 2 | int: -442 3 | int_8: -48 4 | int_16: -416 5 | int_32: -432 6 | int_64: -464 7 | uint: 442 8 | uint_8: 48 9 | uint_16: 416 10 | uint_32: 432 11 | uint_64: 464 12 | float_32: 432.32 13 | float_64: 464.64 14 | parent: 15 | nested: 16 | bool: true 17 | -------------------------------------------------------------------------------- /tests/stub.toml: -------------------------------------------------------------------------------- 1 | str = "toml-string" 2 | int = -542 3 | int_8 = -58 4 | int_16 = -516 5 | int_32 = -532 6 | int_64 = -564 7 | uint = 542 8 | uint_8 = 58 9 | uint_16 = 516 10 | uint_32 = 532 11 | uint_64 = 564 12 | float_32 = 532.32 13 | float_64 = 564.64 14 | 15 | [parent.nested] 16 | bool = true 17 | -------------------------------------------------------------------------------- /tests/stub.json: -------------------------------------------------------------------------------- 1 | { 2 | "str": "json-string", 3 | "int": -342, 4 | "int_8": -38, 5 | "int_16": -316, 6 | "int_32": -332, 7 | "int_64": -364, 8 | "uint": 342, 9 | "uint_8": 38, 10 | "uint_16": 316, 11 | "uint_32": 332, 12 | "uint_64": 364, 13 | "float_32": 332.32, 14 | "float_64": 364.64, 15 | "parent": { 16 | "nested": { 17 | "bool": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dsbasko/go-cfg 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/caarlos0/env/v10 v10.0.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/spf13/pflag v1.0.5 10 | github.com/stretchr/testify v1.8.4 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /internal/reflect/errors.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import "fmt" 4 | 5 | var ( 6 | // ErrNil is returned when the value is nil 7 | ErrNil = fmt.Errorf("should not be nil") 8 | 9 | // ErrNotPointer is returned when the value is not a pointer 10 | ErrNotPointer = fmt.Errorf("must be a pointer to a struct") 11 | 12 | // ErrNotStruct is returned when the value is not a struct 13 | ErrNotStruct = fmt.Errorf("must be a pointer to a struct") 14 | ) 15 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 2 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: '1.18' 22 | 23 | - name: Run tests 24 | run: go test -race -coverprofile=cover.out -covermode=atomic $(go list ./... | grep -v -E 'cmd$$') 25 | 26 | - name: Upload coverage to Codecov 27 | uses: codecov/codecov-action@v3 -------------------------------------------------------------------------------- /internal/reflect/validation.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import "reflect" 4 | 5 | // Validation is a function that checks the validity of the struct pointer passed to it. 6 | // It takes a struct pointer as an argument and returns an error if the struct pointer 7 | // is invalid. 8 | // 9 | // Errors: 10 | // 11 | // ErrNil: Returned when the struct pointer is invalid. 12 | // ErrNotPointer: Returned when the struct pointer is not a pointer. 13 | // ErrNotStruct: Returned when the struct pointer does not point to a struct. 14 | func Validation(structPtr any) error { 15 | valueOf := reflect.ValueOf(structPtr) 16 | 17 | if valueOf.Kind() == reflect.Invalid { 18 | return ErrNil 19 | } 20 | 21 | if valueOf.Kind() != reflect.Ptr { 22 | return ErrNotPointer 23 | } 24 | 25 | valueOf = valueOf.Elem() 26 | if valueOf.Kind() != reflect.Struct { 27 | return ErrNotStruct 28 | } 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/env/read.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/caarlos0/env/v10" 7 | 8 | "github.com/dsbasko/go-cfg/internal/dflt" 9 | "github.com/dsbasko/go-cfg/internal/reflect" 10 | ) 11 | 12 | // Read is a function that parses environment variables into the provided cfg structure. 13 | // The cfg parameter should be a pointer to a struct where each field represents an 14 | // environment variable. The function returns an error if the parsing process fails, 15 | // wrapping the original error with a message. 16 | func Read(structPtr any) error { 17 | if err := reflect.Validation(structPtr); err != nil { 18 | return fmt.Errorf("error validating struct: %w", err) 19 | } 20 | 21 | if err := dflt.Read(structPtr); err != nil { 22 | return fmt.Errorf("error setting default values: %w", err) 23 | } 24 | 25 | if err := env.Parse(structPtr); err != nil { 26 | return fmt.Errorf("failed to parse env: %w", err) 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Basenko Dmitriy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /internal/dflt/read.go: -------------------------------------------------------------------------------- 1 | package dflt 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/dsbasko/go-cfg/internal/reflect" 8 | ) 9 | 10 | var once sync.Once 11 | 12 | // Read is a function that parses default values into the provided cfg structure. 13 | // The cfg parameter should be a pointer to a struct where each field represents a default value. 14 | // The function returns an error if the parsing process fails, wrapping the original error with a message. 15 | func Read(structPtr any) error { 16 | if err := reflect.Validation(structPtr); err != nil { 17 | return fmt.Errorf("error validating struct: %w", err) 18 | } 19 | 20 | var result error 21 | once.Do(func() { 22 | parsedStruct, err := reflect.ParseTag(structPtr, "default") 23 | if err != nil { 24 | result = fmt.Errorf("error parsing struct: %w", err) 25 | } 26 | 27 | if err = reflect.WriteToStruct(structPtr, func(fieldName string) string { 28 | return parsedStruct[fieldName].TagValue 29 | }); err != nil { 30 | result = fmt.Errorf("error writing to struct: %w", err) 31 | } 32 | }) 33 | 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /internal/reflect/validation_test.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_Validation(t *testing.T) { 10 | type InStruct struct{} 11 | InString := "" 12 | 13 | tableTests := []struct { 14 | name string 15 | tagName string 16 | structPtr any 17 | wantErr error 18 | }{ 19 | { 20 | name: "Happy Path", 21 | tagName: "testTag", 22 | structPtr: &InStruct{}, 23 | wantErr: nil, 24 | }, 25 | { 26 | name: "Nil", 27 | tagName: "testTag", 28 | structPtr: nil, 29 | wantErr: ErrNil, 30 | }, 31 | { 32 | name: "Not Pointer", 33 | tagName: "testTag", 34 | structPtr: InStruct{}, 35 | wantErr: ErrNotPointer, 36 | }, 37 | { 38 | name: "Not Struct", 39 | tagName: "testTag", 40 | structPtr: &InString, 41 | wantErr: ErrNotStruct, 42 | }, 43 | } 44 | 45 | for _, tt := range tableTests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | err := Validation(tt.structPtr) 48 | assert.ErrorIs(t, err, tt.wantErr) 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint test-cover test-cover-svg test-cover-html install-deps 2 | .SILENT: 3 | 4 | lint: 5 | @#clear 6 | @$(CURDIR)/bin/golangci-lint run -c .golangci.yaml --path-prefix . --fix 7 | 8 | test: 9 | @#clear 10 | @go test --cover --coverprofile=coverage.out $(TEST_COVER_EXCLUDE_DIR) -count=1 11 | 12 | test-cover: 13 | @#clear 14 | @go test --coverprofile=coverage.out $(TEST_COVER_EXCLUDE_DIR) > /dev/null 15 | @go tool cover -func=coverage.out | grep total | grep -oE '[0-9]+(\.[0-9]+)?%' 16 | 17 | test-cover-svg: 18 | @#clear 19 | @go test --coverprofile=coverage.out $(TEST_COVER_EXCLUDE_DIR) > /dev/null 20 | @$(CURDIR)/bin/go-cover-treemap -coverprofile coverage.out > coverage.svg 21 | @xdg-open ./coverage.svg 22 | 23 | test-cover-html: 24 | @#clear 25 | @go test --coverprofile=coverage.out $(TEST_COVER_EXCLUDE_DIR) > /dev/null 26 | @go tool cover -html="coverage.out" 27 | 28 | install-deps: 29 | @#clear 30 | @GOBIN=$(CURDIR)/bin go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 31 | @GOBIN=$(CURDIR)/bin go install github.com/nikolaydubina/go-cover-treemap@v1.3.0 32 | @GOBIN=$(CURDIR)/bin go install golang.org/x/tools/cmd/godoc@latest 33 | @go mod tidy 34 | 35 | # --------------- 36 | 37 | TEST_COVER_EXCLUDE_DIR := `go list ./... | grep -v -E 'cmd$$'` -------------------------------------------------------------------------------- /internal/reflect/parse-tag_test.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_ParseTag(t *testing.T) { 10 | type InStructFld struct { 11 | FldInt int `testTag:"field-int"` 12 | } 13 | type InStruct struct { 14 | FldString string `testTag:"field-string"` 15 | FldStruct InStructFld 16 | } 17 | 18 | tableTests := []struct { 19 | name string 20 | tagName string 21 | structPtr any 22 | wantStruct map[string]StructFields 23 | wantErr error 24 | }{ 25 | { 26 | name: "Happy Path", 27 | tagName: "testTag", 28 | structPtr: &InStruct{}, 29 | wantStruct: map[string]StructFields{ 30 | "FldString": { 31 | FieldName: "FldString", 32 | TagName: "testTag", 33 | TagValue: "field-string", 34 | }, 35 | "FldStruct.FldInt": { 36 | FieldName: "FldStruct.FldInt", 37 | TagName: "testTag", 38 | TagValue: "field-int", 39 | }, 40 | }, 41 | wantErr: nil, 42 | }, 43 | } 44 | 45 | for _, tt := range tableTests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | parsedData, err := ParseTag(tt.structPtr, tt.tagName) 48 | 49 | assert.ErrorIs(t, err, tt.wantErr) 50 | assert.Equal(t, parsedData, tt.wantStruct) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/dflt/read_test.go: -------------------------------------------------------------------------------- 1 | package dflt 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/dsbasko/go-cfg/internal/reflect" 10 | ) 11 | 12 | func Test_Read(t *testing.T) { 13 | type NestedStruct struct { 14 | Field string `default:"nestedFieldDefValue"` 15 | } 16 | type InStruct struct { 17 | Field string `default:"fieldDefValue"` 18 | Nested NestedStruct 19 | } 20 | mockString := "" 21 | 22 | tableTests := []struct { 23 | name string 24 | structPtr any 25 | wantStruct *InStruct 26 | wantErr error 27 | }{ 28 | { 29 | name: "Happy Path", 30 | structPtr: &InStruct{}, 31 | wantStruct: &InStruct{ 32 | Field: "fieldDefValue", 33 | Nested: NestedStruct{ 34 | Field: "nestedFieldDefValue", 35 | }, 36 | }, 37 | wantErr: nil, 38 | }, 39 | { 40 | name: "Validate error", 41 | structPtr: &mockString, 42 | wantStruct: &InStruct{}, 43 | wantErr: reflect.ErrNotStruct, 44 | }, 45 | { 46 | name: "Parse error", 47 | structPtr: InStruct{}, 48 | wantStruct: &InStruct{}, 49 | wantErr: reflect.ErrNotPointer, 50 | }, 51 | } 52 | 53 | for _, tt := range tableTests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | err := Read(tt.structPtr) 56 | if !errors.Is(err, tt.wantErr) { 57 | t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr) 58 | } 59 | if err == nil { 60 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gocfg is a Go library for reading configuration data from various sources. 2 | // It provides a unified interface for reading configuration data from environment variables, 3 | // command-line flags, and configuration files. 4 | // 5 | // # Installation 6 | // 7 | // To install the library, use the go get command: 8 | // 9 | // go get github.com/dsbasko/go-cfg 10 | // 11 | // # Usage 12 | // 13 | // The library provides several functions for reading configuration data: 14 | // 15 | // ReadEnv(cfg any) error 16 | // Reads environment variables into the provided cfg structure. Each field in the cfg structure represents an environment variable. 17 | // 18 | // MustReadEnv(cfg any) 19 | // Similar to ReadEnv but panics if the reading process fails. 20 | // 21 | // ReadFlag(cfg any) error 22 | // Reads command-line flags into the provided cfg structure. Each field in the cfg structure represents a command-line flag. 23 | // 24 | // MustReadFlag(cfg any) 25 | // Similar to ReadFlag but panics if the reading process fails. 26 | // 27 | // ReadFile(path string, cfg any) error 28 | // Reads configuration from a file into the provided cfg structure. The path parameter is the path to the configuration file. Each field in the cfg structure represents a configuration option. Supported file formats include JSON and YAML. 29 | // 30 | // MustReadFile(path string, cfg any) 31 | // Similar to ReadFile but panics if the reading process fails. 32 | // 33 | // Here is an example of how to use the library: 34 | package gocfg 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA= 4 | github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 8 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 12 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 13 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 14 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /internal/env/read_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/dsbasko/go-cfg/internal/reflect" 10 | ) 11 | 12 | func TestRead(t *testing.T) { 13 | type InStructNested struct { 14 | Field string `env:"NESTED_FIELD" default:"defValueNestedField"` 15 | } 16 | type InStruct struct { 17 | Field string `env:"FIELD" default:"defValueField"` 18 | Nested InStructNested 19 | } 20 | 21 | tests := []struct { 22 | name string 23 | osCfg func() 24 | structPtr any 25 | wantStruct *InStruct 26 | wantErr error 27 | }{ 28 | { 29 | name: "Default", 30 | osCfg: func() {}, 31 | structPtr: &InStruct{}, 32 | wantStruct: &InStruct{ 33 | Field: "defValueField", 34 | Nested: InStructNested{ 35 | Field: "defValueNestedField", 36 | }, 37 | }, 38 | wantErr: nil, 39 | }, 40 | { 41 | name: "Happy Path", 42 | osCfg: func() { 43 | _ = os.Setenv("FIELD", "fieldValue") 44 | _ = os.Setenv("NESTED_FIELD", "nestedFieldValue") 45 | }, 46 | structPtr: &InStruct{}, 47 | wantStruct: &InStruct{ 48 | Field: "fieldValue", 49 | Nested: InStructNested{ 50 | Field: "nestedFieldValue", 51 | }, 52 | }, 53 | wantErr: nil, 54 | }, 55 | { 56 | name: "Validation Fail", 57 | osCfg: func() {}, 58 | structPtr: InStruct{}, 59 | wantStruct: &InStruct{}, 60 | wantErr: reflect.ErrNotPointer, 61 | }, 62 | } 63 | 64 | for _, tt := range tests { 65 | t.Run(tt.name, func(t *testing.T) { 66 | tt.osCfg() 67 | err := Read(tt.structPtr) 68 | 69 | if err != nil || tt.wantErr != nil { 70 | assert.ErrorIs(t, err, tt.wantErr) 71 | } else { 72 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 73 | } 74 | 75 | os.Clearenv() 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /internal/reflect/parse-tag.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // StructFields represents a struct field and its associated tag. 9 | type StructFields struct { 10 | FieldName string // The name of the field. 11 | TagName string // The name of the tag. 12 | TagValue string // The value of the tag. 13 | } 14 | 15 | // ParseTag parses the tags of the struct pointed to by structPtr. 16 | // It returns a map where the keys are the fully qualified names of the fields and the values are the associated tags. 17 | // If a field is a nested struct, its fields are also included in the map, with their names prefixed by the name of the parent struct. 18 | // The function returns an error if structPtr is not a pointer to a struct. 19 | func ParseTag( 20 | structPtr any, 21 | tagNames ...string, 22 | ) (map[string]StructFields, error) { 23 | return parseTagRecursive(structPtr, tagNames, "") 24 | } 25 | 26 | // parseTagRecursive is a helper function for ParseTag. 27 | // It recursively parses the tags of the struct pointed to by structPtr and any nested structs. 28 | // The prefix parameter is used to build the fully qualified names of the fields. 29 | func parseTagRecursive(structPtr any, tagNames []string, prefix string) (map[string]StructFields, error) { 30 | valueOf := reflect.ValueOf(structPtr) 31 | if valueOf.Kind() == reflect.Ptr { 32 | valueOf = valueOf.Elem() 33 | } 34 | 35 | var result = map[string]StructFields{} 36 | for i := 0; i < valueOf.NumField(); i++ { 37 | field := valueOf.Type().Field(i) 38 | 39 | if field.Type.Kind() == reflect.Struct { 40 | ptr := valueOf.Field(i).Addr().Interface() 41 | newPrefix := fmt.Sprintf("%s%s.", prefix, field.Name) 42 | recursionValue, _ := parseTagRecursive(ptr, tagNames, newPrefix) 43 | for k, v := range recursionValue { 44 | result[k] = v 45 | } 46 | continue 47 | } 48 | 49 | for _, tagName := range tagNames { 50 | if tag := field.Tag.Get(tagName); tag != "" { 51 | fieldName := fmt.Sprintf("%s%s", prefix, field.Name) 52 | result[fieldName] = StructFields{ 53 | FieldName: fieldName, 54 | TagName: tagName, 55 | TagValue: tag, 56 | } 57 | } 58 | } 59 | } 60 | 61 | return result, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/reflect/write-to-struct_test.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_WriteToStruct(t *testing.T) { 10 | type InStructFld struct { 11 | FldBool bool `testTag:"field-bool"` 12 | } 13 | type InStruct struct { 14 | FldString string `testTag:"field-string"` 15 | FldInt int `testTag:"field-int"` 16 | FldInt8 int8 `testTag:"field-int8"` 17 | FldInt16 int16 `testTag:"field-int16"` 18 | FldInt32 int32 `testTag:"field-int32"` 19 | FldInt64 int64 `testTag:"field-int64"` 20 | FldUint uint `testTag:"field-uint"` 21 | FldUint8 uint8 `testTag:"field-uint8"` 22 | FldUint16 uint16 `testTag:"field-uint16"` 23 | FldUint32 uint32 `testTag:"field-uint32"` 24 | FldUint64 uint64 `testTag:"field-uint64"` 25 | FldFloat32 float32 `testTag:"field-float32"` 26 | FldFloat64 float64 `testTag:"field-float64"` 27 | FldStruct InStructFld 28 | } 29 | wantStruct := InStruct{ 30 | FldString: "allons-y", 31 | FldInt: -42, 32 | FldInt8: -8, 33 | FldInt16: -16, 34 | FldInt32: -32, 35 | FldInt64: -64, 36 | FldUint: 42, 37 | FldUint8: 8, 38 | FldUint16: 16, 39 | FldUint32: 32, 40 | FldUint64: 64, 41 | FldFloat32: 32.32, 42 | FldFloat64: 64.64, 43 | FldStruct: InStructFld{ 44 | FldBool: true, 45 | }, 46 | } 47 | 48 | tableTests := []struct { 49 | name string 50 | structPtr any 51 | fn func(fieldName string) string 52 | wantErr error 53 | }{ 54 | { 55 | name: "Happy Path", 56 | structPtr: &InStruct{}, 57 | fn: func(fieldName string) string { 58 | mockData := map[string]string{ 59 | "FldString": "allons-y", 60 | "FldInt": "-42", 61 | "FldInt8": "-8", 62 | "FldInt16": "-16", 63 | "FldInt32": "-32", 64 | "FldInt64": "-64", 65 | "FldUint": "42", 66 | "FldUint8": "8", 67 | "FldUint16": "16", 68 | "FldUint32": "32", 69 | "FldUint64": "64", 70 | "FldFloat32": "32.32", 71 | "FldFloat64": "64.64", 72 | "FldStruct.FldBool": "true", 73 | } 74 | return mockData[fieldName] 75 | }, 76 | wantErr: nil, 77 | }, 78 | } 79 | 80 | for _, tt := range tableTests { 81 | t.Run(tt.name, func(t *testing.T) { 82 | err := WriteToStruct(tt.structPtr, tt.fn) 83 | 84 | if err != nil && tt.wantErr != nil { 85 | assert.Equal(t, err, tt.wantErr) 86 | } else { 87 | assert.Equal(t, &wantStruct, tt.structPtr) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/flag/read.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | rf "reflect" 7 | "sync" 8 | 9 | "github.com/spf13/pflag" 10 | 11 | "github.com/dsbasko/go-cfg/internal/dflt" 12 | "github.com/dsbasko/go-cfg/internal/reflect" 13 | ) 14 | 15 | var ( 16 | once sync.Once 17 | flagSet *pflag.FlagSet 18 | dataPtr map[string]*string 19 | ) 20 | 21 | // Read is a function that reads the input structure, validates it, reads the default values, 22 | // parses the flags from the command line arguments and writes the values to the input structure. 23 | // It returns an error if any of these operations fail. 24 | func Read(structPtr any) error { 25 | if err := reflect.Validation(structPtr); err != nil { 26 | return fmt.Errorf("failed to validate in struct: %w", err) 27 | } 28 | 29 | if errDefault := dflt.Read(structPtr); errDefault != nil { 30 | return errDefault 31 | } 32 | 33 | once.Do(func() { 34 | dataPtr = make(map[string]*string) 35 | flagSet = pflag.NewFlagSet("cfg", pflag.ContinueOnError) 36 | }) 37 | 38 | if err := parseFlags(structPtr, flagSet, ""); err != nil { 39 | return fmt.Errorf("failed to parse flags: %w", err) 40 | } 41 | 42 | if err := flagSet.Parse(os.Args[1:]); err != nil { 43 | return fmt.Errorf("failed to parse flags: %w", err) 44 | } 45 | 46 | dataMap := make(map[string]string) 47 | for key, value := range dataPtr { 48 | dataMap[key] = *value 49 | } 50 | 51 | findFn := func(fieldName string) string { return dataMap[fieldName] } 52 | if err := reflect.WriteToStruct(structPtr, findFn); err != nil { 53 | return fmt.Errorf("failed to write to struct: %w", err) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // parseFlags is a recursive function that parses the flags from the input structure and 60 | // the command line arguments. It adds the flags to the flagSet and the dataPtr map. 61 | // It returns an error if the parsing fails. 62 | func parseFlags(structPtr any, flagSet *pflag.FlagSet, prefix string) error { 63 | valueOf := rf.ValueOf(structPtr) 64 | if valueOf.Kind() == rf.Ptr { 65 | valueOf = valueOf.Elem() 66 | } 67 | 68 | for i := 0; i < valueOf.NumField(); i++ { 69 | field := valueOf.Type().Field(i) 70 | flagFullName := field.Tag.Get("flag") 71 | flagShortName := field.Tag.Get("s-flag") 72 | flagUsage := field.Tag.Get("description") 73 | 74 | if field.Type.Kind() == rf.Struct { 75 | if err := parseFlags( 76 | valueOf.Field(i).Addr().Interface(), 77 | flagSet, 78 | fmt.Sprintf("%s%s.", prefix, field.Name), 79 | ); err != nil { 80 | return fmt.Errorf("failed to parse flags: %w", err) 81 | } 82 | } 83 | 84 | fieldName := fmt.Sprintf("%s%s", prefix, field.Name) 85 | foundFlag := flagSet.Lookup(flagFullName) 86 | if _, ok := dataPtr[fieldName]; !ok && foundFlag == nil && (flagFullName != "" || flagShortName != "") { 87 | dataPtr[fieldName] = new(string) 88 | flagSet.StringVarP(dataPtr[fieldName], flagFullName, flagShortName, "", flagUsage) 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/flag/read_test.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/dsbasko/go-cfg/internal/reflect" 10 | ) 11 | 12 | func Test_Read(t *testing.T) { 13 | type InStructNested struct { 14 | Field string `flag:"nested-field" s-flag:"n" default:"defValueNestedField"` 15 | } 16 | type InStruct struct { 17 | Field string `flag:"field" s-flag:"f" default:"defValueField"` 18 | Nested InStructNested 19 | } 20 | osArgs := os.Args 21 | 22 | tableTests := []struct { 23 | name string 24 | osCfg func() 25 | structPtr any 26 | wantStruct InStruct 27 | wantErr error 28 | }{ 29 | { 30 | name: "Default", 31 | osCfg: func() { 32 | os.Args = osArgs 33 | }, 34 | structPtr: &InStruct{}, 35 | wantStruct: InStruct{ 36 | Field: "defValueField", 37 | Nested: InStructNested{ 38 | Field: "defValueNestedField", 39 | }, 40 | }, 41 | wantErr: nil, 42 | }, 43 | { 44 | name: "Happy Path Full", 45 | osCfg: func() { 46 | os.Args = append( //nolint:gocritic 47 | osArgs, 48 | "--field=FULL_FIELD_VALUE", 49 | "--nested-field=FULL_NESTED_VALUE", 50 | ) 51 | }, 52 | structPtr: &InStruct{}, 53 | wantStruct: InStruct{ 54 | Field: "FULL_FIELD_VALUE", 55 | Nested: InStructNested{ 56 | Field: "FULL_NESTED_VALUE", 57 | }, 58 | }, 59 | wantErr: nil, 60 | }, 61 | { 62 | name: "Happy Path Short", 63 | osCfg: func() { 64 | os.Args = append( //nolint:gocritic 65 | osArgs, 66 | "-f=SHORT_FIELD_VALUE", 67 | "-n=SHORT_NESTED_VALUE", 68 | ) 69 | }, 70 | structPtr: &InStruct{}, 71 | wantStruct: InStruct{ 72 | Field: "SHORT_FIELD_VALUE", 73 | Nested: InStructNested{ 74 | Field: "SHORT_NESTED_VALUE", 75 | }, 76 | }, 77 | wantErr: nil, 78 | }, 79 | { 80 | name: "Happy Path All (Short Priority)", 81 | osCfg: func() { 82 | os.Args = append( //nolint:gocritic 83 | osArgs, 84 | "--field=FULL_FIELD_VALUE", 85 | "--nested-field=FULL_NESTED_VALUE", 86 | "-f=SHORT_FIELD_VALUE", 87 | "-n=SHORT_NESTED_VALUE", 88 | ) 89 | }, 90 | structPtr: &InStruct{}, 91 | wantStruct: InStruct{ 92 | Field: "SHORT_FIELD_VALUE", 93 | Nested: InStructNested{ 94 | Field: "SHORT_NESTED_VALUE", 95 | }, 96 | }, 97 | wantErr: nil, 98 | }, 99 | { 100 | name: "Validate Not Pointer", 101 | osCfg: func() { 102 | os.Args = osArgs 103 | }, 104 | structPtr: InStruct{}, 105 | wantStruct: InStruct{}, 106 | wantErr: reflect.ErrNotPointer, 107 | }, 108 | } 109 | 110 | for _, tt := range tableTests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | tt.osCfg() 113 | err := Read(tt.structPtr) 114 | 115 | if err != nil || tt.wantErr != nil { 116 | assert.ErrorIs(t, err, tt.wantErr) 117 | } else { 118 | assert.EqualValues(t, tt.wantStruct.Field, tt.structPtr.(*InStruct).Field) 119 | assert.EqualValues(t, tt.wantStruct.Nested, tt.structPtr.(*InStruct).Nested) 120 | } 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /internal/file/read.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/BurntSushi/toml" 12 | "github.com/joho/godotenv" 13 | "gopkg.in/yaml.v3" 14 | 15 | "github.com/dsbasko/go-cfg/internal/dflt" 16 | "github.com/dsbasko/go-cfg/internal/reflect" 17 | ) 18 | 19 | // Read is a function that parses the content of the file into the provided cfg structure. 20 | // The path parameter should be a string representing the path to the file. The structPtr 21 | // parameter should be a pointer to a struct where each field represents a configuration 22 | // option. The function returns an error if the parsing process fails, wrapping the 23 | // original error with a message. 24 | func Read(path string, structPtr any) error { 25 | if errValidation := reflect.Validation(structPtr); errValidation != nil { 26 | return fmt.Errorf("error validating struct: %w", errValidation) 27 | } 28 | 29 | if errDefault := dflt.Read(structPtr); errDefault != nil { 30 | return fmt.Errorf("error setting default values: %w", errDefault) 31 | } 32 | 33 | file, err := os.OpenFile(path, os.O_RDONLY|os.O_SYNC, 0) 34 | if err != nil { 35 | return fmt.Errorf("failed to open file: %w", err) 36 | } 37 | defer file.Close() 38 | 39 | switch extension := strings.ToLower(filepath.Ext(path)); extension { 40 | case ".json": 41 | if err = parseJSON(file, structPtr); err != nil { 42 | return fmt.Errorf("failed to parse json: %w", err) 43 | } 44 | case ".yaml", ".yml": 45 | if err = parseYAML(file, structPtr); err != nil { 46 | return fmt.Errorf("failed to parse yaml: %w", err) 47 | } 48 | case ".toml": 49 | if err = parseTOML(file, structPtr); err != nil { 50 | return fmt.Errorf("failed to parse toml: %w", err) 51 | } 52 | case ".env": 53 | if err = parseENV(file, structPtr); err != nil { 54 | return fmt.Errorf("failed to parse env: %w", err) 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | // parseJSON is a helper function used by Read to parse the JSON content of the file. 62 | // It takes an io.Reader and a pointer to a struct where each field represents a 63 | // configuration option. The function returns an error if the parsing process fails. 64 | func parseJSON(r io.Reader, structPtr any) error { 65 | return json.NewDecoder(r).Decode(structPtr) 66 | } 67 | 68 | // parseYAML is a helper function used by Read to parse the YAML content of the file. 69 | // It takes an io.Reader and a pointer to a struct where each field represents a 70 | // configuration option. The function returns an error if the parsing process fails. 71 | func parseYAML(r io.Reader, structPtr any) error { 72 | return yaml.NewDecoder(r).Decode(structPtr) 73 | } 74 | 75 | // parseTOML is a helper function used by Read to parse the TOML content of the file. 76 | // It takes an io.Reader and a pointer to a struct where each field represents a 77 | // configuration option. The function returns an error if the parsing process fails. 78 | func parseTOML(r io.Reader, structPtr any) error { 79 | _, err := toml.NewDecoder(r).Decode(structPtr) 80 | return err 81 | } 82 | 83 | // parseENV is a helper function used by Read to parse the ENV content of the file. 84 | // It takes an io.Reader and a pointer to a struct where each field represents a 85 | // configuration option. The function returns an error if the parsing process fails. 86 | func parseENV(r io.Reader, structPtr any) error { 87 | dataEnv, err := godotenv.Parse(r) 88 | if err != nil { 89 | return fmt.Errorf("failed to parse env: %w", err) 90 | } 91 | 92 | parsedStruct, err := reflect.ParseTag(structPtr, "env") 93 | if err != nil { 94 | return fmt.Errorf("failed to parse tag: %w", err) 95 | } 96 | 97 | if errReflection := reflect.WriteToStruct(structPtr, func(fieldName string) string { 98 | if _, ok := parsedStruct[fieldName]; !ok { 99 | return "" 100 | } 101 | return dataEnv[parsedStruct[fieldName].TagValue] 102 | }); errReflection != nil { 103 | return errReflection 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | depguard: 3 | rules: 4 | logger: 5 | deny: 6 | - pkg: "github.com/sirupsen/logrus" 7 | desc: logging is allowed only by logutils.Log 8 | dupl: 9 | threshold: 100 10 | funlen: 11 | lines: -1 12 | statements: 50 13 | goconst: 14 | min-len: 2 15 | min-occurrences: 3 16 | gocritic: 17 | enabled-tags: 18 | - diagnostic 19 | - experimental 20 | - opinionated 21 | - performance 22 | - style 23 | disabled-checks: 24 | - dupImport 25 | - ifElseChain 26 | - octalLiteral 27 | - whyNoLint 28 | - unnamedResult 29 | - rangeValCopy 30 | - hugeParam 31 | gocyclo: 32 | min-complexity: 15 33 | gofmt: 34 | rewrite-rules: 35 | - pattern: 'interface{}' 36 | replacement: 'any' 37 | goimports: 38 | local-prefixes: github.com/dsbasko/go-cfg 39 | gomnd: 40 | checks: 41 | - argument 42 | - case 43 | - condition 44 | - return 45 | ignored-numbers: 46 | - '0' 47 | - '1' 48 | - '2' 49 | - '3' 50 | ignored-functions: 51 | - strings.SplitN 52 | 53 | govet: 54 | check-shadowing: true 55 | settings: 56 | printf: 57 | funcs: 58 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof 59 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf 60 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf 61 | - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf 62 | misspell: 63 | locale: US 64 | nolintlint: 65 | allow-unused: false 66 | require-explanation: false 67 | require-specific: false 68 | revive: 69 | rules: 70 | - name: unexported-return 71 | disabled: true 72 | - name: unused-parameter 73 | 74 | linters: 75 | disable-all: true 76 | enable: 77 | - bodyclose 78 | - depguard 79 | - dogsled 80 | - errcheck 81 | - exportloopref 82 | - funlen 83 | - gocheckcompilerdirectives 84 | - gochecknoinits 85 | - goconst 86 | - gocritic 87 | - gocyclo 88 | - gofmt 89 | - goimports 90 | - gomnd 91 | - goprintffuncname 92 | - gosec 93 | - gosimple 94 | - govet 95 | - ineffassign 96 | - misspell 97 | - nakedret 98 | - noctx 99 | - nolintlint 100 | - revive 101 | - staticcheck 102 | - stylecheck 103 | - typecheck 104 | - unconvert 105 | - unparam 106 | - unused 107 | - whitespace 108 | 109 | issues: 110 | exclude-rules: 111 | - path: _test\.go 112 | linters: 113 | - gomnd 114 | 115 | - path: example_test.go 116 | linters: 117 | - noctx 118 | - nolintlint 119 | - govet 120 | - staticcheck 121 | 122 | - path: pkg/golinters/errcheck.go 123 | text: "SA1019: errCfg.Exclude is deprecated: use ExcludeFunctions instead" 124 | - path: pkg/commands/run.go 125 | text: "SA1019: lsc.Errcheck.Exclude is deprecated: use ExcludeFunctions instead" 126 | - path: pkg/commands/run.go 127 | text: "SA1019: e.cfg.Run.Deadline is deprecated: Deadline exists for historical compatibility and should not be used." 128 | 129 | - path: pkg/golinters/gofumpt.go 130 | text: "SA1019: settings.LangVersion is deprecated: use the global `run.go` instead." 131 | - path: pkg/golinters/staticcheck_common.go 132 | text: "SA1019: settings.GoVersion is deprecated: use the global `run.go` instead." 133 | - path: pkg/lint/lintersdb/manager.go 134 | text: "SA1019: (.+).(GoVersion|LangVersion) is deprecated: use the global `run.go` instead." 135 | - path: pkg/golinters/unused.go 136 | text: "rangeValCopy: each iteration copies 160 bytes \\(consider pointers or indexing\\)" 137 | - path: test/(fix|linters)_test.go 138 | text: "string `gocritic.go` has 3 occurrences, make it a constant" 139 | 140 | run: 141 | timeout: 5m 142 | skip-dirs: 143 | - test/testdata_etc 144 | - internal/cache 145 | - internal/renameio 146 | - internal/robustio -------------------------------------------------------------------------------- /internal/reflect/write-to-struct.go: -------------------------------------------------------------------------------- 1 | package reflect 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | // WriteToStruct is a function that takes a pointer to a struct and a function as 10 | // arguments. The function argument should take a string (field name) and return a string. 11 | // It uses reflection to iterate over the fields of the struct and calls the provided 12 | // function with the field name. The returned value from the function is then used to set 13 | // the value of the field in the struct. If the field is another struct, it recursively 14 | // calls itself to set the values of the nested struct's fields. 15 | func WriteToStruct(structPtr any, fn func(fieldName string) string) error { 16 | return writeToStructRecursive(structPtr, fn, "") 17 | } 18 | 19 | // writeToStructRecursive is a helper function for WriteToStruct. It takes a pointer to a 20 | // struct, a function, and a prefix string as arguments. The function argument should take 21 | // a string (field name) and return a string. The prefix is used to build the field name 22 | // for nested struct fields. It uses reflection to iterate over the fields of the struct 23 | // and calls the provided function with the field name. The returned value from the 24 | // function is then used to set the value of the field in the struct. If the field is 25 | // another struct, it recursively calls itself to set the values of the nested 26 | // struct's fields. 27 | func writeToStructRecursive(structPtr any, fn func(fieldName string) string, prefix string) error { //nolint:funlen,gocyclo 28 | valueOf := reflect.ValueOf(structPtr) 29 | if valueOf.Kind() == reflect.Ptr { 30 | valueOf = valueOf.Elem() 31 | } 32 | 33 | for i := 0; i < valueOf.NumField(); i++ { 34 | field := valueOf.Type().Field(i) 35 | 36 | if field.Type.Kind() == reflect.Struct { 37 | _ = writeToStructRecursive( 38 | valueOf.Field(i).Addr().Interface(), 39 | fn, 40 | fmt.Sprintf("%s%s.", prefix, field.Name), 41 | ) 42 | continue 43 | } 44 | 45 | fieldName := fmt.Sprintf("%s%s", prefix, field.Name) 46 | switch field.Type.Kind() { 47 | case reflect.String: 48 | val := fn(fieldName) 49 | if val != "" { 50 | valueOf.Field(i).SetString(fn(fieldName)) 51 | } 52 | case reflect.Int: 53 | valInt, _ := strconv.ParseInt(fn(fieldName), 10, 0) 54 | if valInt != 0 { 55 | valueOf.Field(i).SetInt(valInt) 56 | } 57 | case reflect.Int8: 58 | valInt, _ := strconv.ParseInt(fn(fieldName), 10, 8) 59 | if valInt != 0 { 60 | valueOf.Field(i).SetInt(valInt) 61 | } 62 | case reflect.Int16: 63 | valInt, _ := strconv.ParseInt(fn(fieldName), 10, 16) 64 | if valInt != 0 { 65 | valueOf.Field(i).SetInt(valInt) 66 | } 67 | case reflect.Int32: 68 | valInt, _ := strconv.ParseInt(fn(fieldName), 10, 32) 69 | if valInt != 0 { 70 | valueOf.Field(i).SetInt(valInt) 71 | } 72 | case reflect.Int64: 73 | valInt, _ := strconv.ParseInt(fn(fieldName), 10, 64) 74 | if valInt != 0 { 75 | valueOf.Field(i).SetInt(valInt) 76 | } 77 | case reflect.Uint: 78 | valUint, _ := strconv.ParseUint(fn(fieldName), 10, 0) 79 | if valUint != 0 { 80 | valueOf.Field(i).SetUint(valUint) 81 | } 82 | case reflect.Uint8: 83 | valUint, _ := strconv.ParseUint(fn(fieldName), 10, 8) 84 | if valUint != 0 { 85 | valueOf.Field(i).SetUint(valUint) 86 | } 87 | case reflect.Uint16: 88 | valUint, _ := strconv.ParseUint(fn(fieldName), 10, 16) 89 | if valUint != 0 { 90 | valueOf.Field(i).SetUint(valUint) 91 | } 92 | case reflect.Uint32: 93 | valUint, _ := strconv.ParseUint(fn(fieldName), 10, 32) 94 | if valUint != 0 { 95 | valueOf.Field(i).SetUint(valUint) 96 | } 97 | case reflect.Uint64: 98 | valUint, _ := strconv.ParseUint(fn(fieldName), 10, 64) 99 | if valUint != 0 { 100 | valueOf.Field(i).SetUint(valUint) 101 | } 102 | case reflect.Float32: 103 | valFloat, _ := strconv.ParseFloat(fn(fieldName), 32) 104 | if valFloat != 0 { 105 | valueOf.Field(i).SetFloat(valFloat) 106 | } 107 | case reflect.Float64: 108 | valFloat, _ := strconv.ParseFloat(fn(fieldName), 64) 109 | if valFloat != 0 { 110 | valueOf.Field(i).SetFloat(valFloat) 111 | } 112 | case reflect.Bool: 113 | valBool, _ := strconv.ParseBool(fn(fieldName)) 114 | valueOf.Field(i).SetBool(valBool) 115 | } 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /tests/stubs.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type InStructParent struct { 8 | FldNested InStructNested `json:"nested" yaml:"nested" toml:"nested"` 9 | } 10 | type InStructNested struct { 11 | FldBool bool `env:"BOOL" flag:"bool" default:"false" json:"bool" yaml:"bool" toml:"bool"` 12 | } 13 | type InStruct struct { 14 | FldString string `env:"STR" flag:"str" default:"default-string" json:"str" yaml:"str" toml:"str"` 15 | FldInt int `env:"INT" flag:"int" default:"-42" json:"int" yaml:"int" toml:"int"` 16 | FldInt8 int8 `env:"INT_8" flag:"int-8" default:"-8" json:"int_8" yaml:"int_8" toml:"int_8"` 17 | FldInt16 int16 `env:"INT_16" flag:"int-16" default:"-16" json:"int_16" yaml:"int_16" toml:"int_16"` 18 | FldInt32 int32 `env:"INT_32" flag:"int-32" default:"-32" json:"int_32" yaml:"int_32" toml:"int_32"` 19 | FldInt64 int64 `env:"INT_64" flag:"int-64" default:"-64" json:"int_64" yaml:"int_64" toml:"int_64"` 20 | FldUint uint `env:"UINT" flag:"uint" default:"42" json:"uint" yaml:"uint" toml:"uint"` 21 | FldUint8 uint8 `env:"UINT_8" flag:"uint-8" default:"8" json:"uint_8" yaml:"uint_8" toml:"uint_8"` 22 | FldUint16 uint16 `env:"UINT_16" flag:"uint-16" default:"32" json:"uint_16" yaml:"uint_16" toml:"uint_16"` 23 | FldUint32 uint16 `env:"UINT_32" flag:"uint-32" default:"32" json:"uint_32" yaml:"uint_32" toml:"uint_32"` 24 | FldUint64 uint64 `env:"UINT_64" flag:"uint-64" default:"64" json:"uint_64" yaml:"uint_64" toml:"uint_64"` 25 | FldFloat32 float32 `env:"FLOAT_32" flag:"float-32" default:"32.32" json:"float_32" yaml:"float_32" toml:"float_32"` 26 | FldFloat64 float64 `env:"FLOAT_64" flag:"float-64" default:"64.64" json:"float_64" yaml:"float_64" toml:"float_64"` 27 | FldParent InStructParent `json:"parent" yaml:"parent" toml:"parent"` 28 | } 29 | 30 | func stubDefault() InStruct { 31 | return InStruct{ 32 | FldString: "default-string", 33 | FldInt: -42, 34 | FldInt8: -8, 35 | FldInt16: -16, 36 | FldInt32: -32, 37 | FldInt64: -64, 38 | FldUint: 42, 39 | FldUint8: 8, 40 | FldUint16: 32, 41 | FldUint32: 32, 42 | FldUint64: 64, 43 | FldFloat32: 32.32, 44 | FldFloat64: 64.64, 45 | FldParent: InStructParent{ 46 | FldNested: InStructNested{ 47 | FldBool: false, 48 | }, 49 | }, 50 | } 51 | } 52 | 53 | func stubEnv() InStruct { 54 | _ = os.Setenv("STR", "env-string") 55 | _ = os.Setenv("INT", "-142") 56 | _ = os.Setenv("INT_8", "-18") 57 | _ = os.Setenv("INT_16", "-116") 58 | _ = os.Setenv("INT_32", "-132") 59 | _ = os.Setenv("INT_64", "-164") 60 | _ = os.Setenv("UINT", "142") 61 | _ = os.Setenv("UINT_8", "18") 62 | _ = os.Setenv("UINT_16", "132") 63 | _ = os.Setenv("UINT_32", "132") 64 | _ = os.Setenv("UINT_64", "164") 65 | _ = os.Setenv("FLOAT_32", "132.32") 66 | _ = os.Setenv("FLOAT_64", "164.64") 67 | _ = os.Setenv("BOOL", "true") 68 | 69 | return InStruct{ 70 | FldString: "env-string", 71 | FldInt: -142, 72 | FldInt8: -18, 73 | FldInt16: -116, 74 | FldInt32: -132, 75 | FldInt64: -164, 76 | FldUint: 142, 77 | FldUint8: 18, 78 | FldUint16: 132, 79 | FldUint32: 132, 80 | FldUint64: 164, 81 | FldFloat32: 132.32, 82 | FldFloat64: 164.64, 83 | FldParent: InStructParent{ 84 | FldNested: InStructNested{ 85 | FldBool: true, 86 | }, 87 | }, 88 | } 89 | } 90 | 91 | func stubFlag() InStruct { 92 | os.Args = append( 93 | os.Args, 94 | "--str", "flag-string", 95 | "--int", "-242", 96 | "--int-8", "-28", 97 | "--int-16", "-216", 98 | "--int-32", "-232", 99 | "--int-64", "-264", 100 | "--uint", "242", 101 | "--uint-8", "28", 102 | "--uint-16", "216", 103 | "--uint-32", "232", 104 | "--uint-64", "264", 105 | "--float-32", "232.32", 106 | "--float-64", "264.64", 107 | "--bool", "true", 108 | ) 109 | 110 | return InStruct{ 111 | FldString: "flag-string", 112 | FldInt: -242, 113 | FldInt8: -28, 114 | FldInt16: -216, 115 | FldInt32: -232, 116 | FldInt64: -264, 117 | FldUint: 242, 118 | FldUint8: 28, 119 | FldUint16: 216, 120 | FldUint32: 232, 121 | FldUint64: 264, 122 | FldFloat32: 232.32, 123 | FldFloat64: 264.64, 124 | FldParent: InStructParent{ 125 | FldNested: InStructNested{ 126 | FldBool: true, 127 | }, 128 | }, 129 | } 130 | } 131 | 132 | func stubJSON() InStruct { 133 | return InStruct{ 134 | FldString: "json-string", 135 | FldInt: -342, 136 | FldInt8: -38, 137 | FldInt16: -316, 138 | FldInt32: -332, 139 | FldInt64: -364, 140 | FldUint: 342, 141 | FldUint8: 38, 142 | FldUint16: 316, 143 | FldUint32: 332, 144 | FldUint64: 364, 145 | FldFloat32: 332.32, 146 | FldFloat64: 364.64, 147 | FldParent: InStructParent{ 148 | FldNested: InStructNested{ 149 | FldBool: true, 150 | }, 151 | }, 152 | } 153 | } 154 | 155 | func stubYAML() InStruct { 156 | return InStruct{ 157 | FldString: "yaml-string", 158 | FldInt: -442, 159 | FldInt8: -48, 160 | FldInt16: -416, 161 | FldInt32: -432, 162 | FldInt64: -464, 163 | FldUint: 442, 164 | FldUint8: 48, 165 | FldUint16: 416, 166 | FldUint32: 432, 167 | FldUint64: 464, 168 | FldFloat32: 432.32, 169 | FldFloat64: 464.64, 170 | FldParent: InStructParent{ 171 | FldNested: InStructNested{ 172 | FldBool: true, 173 | }, 174 | }, 175 | } 176 | } 177 | 178 | func stubTOML() InStruct { 179 | return InStruct{ 180 | FldString: "toml-string", 181 | FldInt: -542, 182 | FldInt8: -58, 183 | FldInt16: -516, 184 | FldInt32: -532, 185 | FldInt64: -564, 186 | FldUint: 542, 187 | FldUint8: 58, 188 | FldUint16: 516, 189 | FldUint32: 532, 190 | FldUint64: 564, 191 | FldFloat32: 532.32, 192 | FldFloat64: 564.64, 193 | FldParent: InStructParent{ 194 | FldNested: InStructNested{ 195 | FldBool: true, 196 | }, 197 | }, 198 | } 199 | } 200 | 201 | func stubEnvFile() InStruct { 202 | return InStruct{ 203 | FldString: "env-file-string", 204 | FldInt: -642, 205 | FldInt8: -68, 206 | FldInt16: -616, 207 | FldInt32: -632, 208 | FldInt64: -664, 209 | FldUint: 642, 210 | FldUint8: 68, 211 | FldUint16: 616, 212 | FldUint32: 632, 213 | FldUint64: 664, 214 | FldFloat32: 632.32, 215 | FldFloat64: 664.64, 216 | FldParent: InStructParent{ 217 | FldNested: InStructNested{ 218 | FldBool: true, 219 | }, 220 | }, 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /cfg.go: -------------------------------------------------------------------------------- 1 | package gocfg 2 | 3 | import ( 4 | "github.com/dsbasko/go-cfg/internal/env" 5 | "github.com/dsbasko/go-cfg/internal/file" 6 | "github.com/dsbasko/go-cfg/internal/flag" 7 | ) 8 | 9 | // ReadEnv is a function that reads environment variables into the provided cfg structure. 10 | // The cfg parameter should be a pointer to a struct where each field represents an environment variable. 11 | // The function returns an error if the reading process fails. 12 | // 13 | // Example of how to use the ReadEnv function: 14 | // 15 | // Define a struct that represents your environment variables. For example: 16 | // 17 | // type Config struct { 18 | // Mode string `env:"MODE"` 19 | // HTTP struct { 20 | // Host string `env:"HTTP_HOST"` 21 | // Port int `env:"HTTP_PORT"` 22 | // } 23 | // } 24 | // 25 | // Then, create an instance of your struct and call the ReadEnv function: 26 | // 27 | // func main() { 28 | // cfg := &Config{} 29 | // if err := gocfg.ReadEnv(cfg); err != nil { 30 | // log.Fatalf("failed to read environment variables: %v", err) 31 | // } 32 | // 33 | // fmt.Printf("Mode: %s\n", cfg.Mode) 34 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 35 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 36 | // } 37 | // 38 | // This will read the MODE, REST_HOST and REST_PORT environment variables into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 39 | func ReadEnv(cfg any) error { 40 | return env.Read(cfg) 41 | } 42 | 43 | // MustReadEnv is similar to ReadEnv but panics if the reading process fails. 44 | // This function is useful when the absence of environment variables should lead to a program termination. 45 | // 46 | // Example: 47 | // 48 | // type Config struct { 49 | // Mode string `env:"MODE"` 50 | // HTTP struct { 51 | // Host string `env:"HTTP_HOST"` 52 | // Port int `env:"HTTP_PORT"` 53 | // } 54 | // } 55 | // 56 | // func main() { 57 | // cfg := &Config{} 58 | // gocfg.MustReadEnv(cfg) 59 | // 60 | // fmt.Printf("Mode: %s\n", cfg.Mode) 61 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 62 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 63 | // } 64 | // 65 | // This will read the MODE, HTTP_HOST and HTTP_PORT environment variables into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 66 | // If any of these environment variables are not set, the program will panic. 67 | func MustReadEnv(cfg any) { 68 | if err := ReadEnv(cfg); err != nil { 69 | panic(err) 70 | } 71 | } 72 | 73 | // ReadFlag reads command-line flags into the provided cfg structure. 74 | // The cfg parameter should be a pointer to a struct where each field represents a command-line flag. 75 | // This function returns an error if the parsing process fails. 76 | // 77 | // Example: 78 | // 79 | // type Config struct { 80 | // Mode string `flag:"mode" s-flag:"m" description:"Application mode"` 81 | // HTTP struct { 82 | // Host string `flag:"http-host" s-flag:"hh" description:"HTTP host"` 83 | // Port int `flag:"http-port" s-flag:"hp" description:"HTTP port"` 84 | // } 85 | // } 86 | // 87 | // func main() { 88 | // cfg := &Config{} 89 | // if err := gocfg.ReadFlag(cfg); err != nil { 90 | // log.Fatalf("failed to read command-line flags: %v", err) 91 | // } 92 | // 93 | // fmt.Printf("Mode: %s\n", cfg.Mode) 94 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 95 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 96 | // } 97 | // 98 | // This will read the command-line flags --mode, --http-host and --http-port (or -m, -hh and -hp respectively) into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 99 | // If any of these flags are not set, the function will return an error. 100 | func ReadFlag(cfg any) error { 101 | return flag.Read(cfg) 102 | } 103 | 104 | // MustReadFlag is similar to ReadFlag but panics if the reading process fails. 105 | // This function is useful when the absence of command-line flags should lead to a program termination. 106 | // 107 | // Example: 108 | // 109 | // type Config struct { 110 | // Mode string `flag:"mode" s-flag:"m" description:"Application mode"` 111 | // HTTP struct { 112 | // Host string `flag:"http-host" s-flag:"hh" description:"HTTP host"` 113 | // Port int `flag:"http-port" s-flag:"hp" description:"HTTP port"` 114 | // } 115 | // } 116 | // 117 | // func main() { 118 | // cfg := &Config{} 119 | // gocfg.MustReadFlag(cfg) 120 | // 121 | // fmt.Printf("Mode: %s\n", cfg.Mode) 122 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 123 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 124 | // } 125 | // 126 | // This will read the command-line flags --mode, --http-host and --http-port (or -m, -hh and -hp respectively) into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 127 | // If any of these flags are not set, the program will panic. 128 | func MustReadFlag(cfg any) { 129 | if err := ReadFlag(cfg); err != nil { 130 | panic(err) 131 | } 132 | } 133 | 134 | // ReadFile reads configuration from a file into the provided cfg structure. 135 | // The path parameter is the path to the configuration file. 136 | // The cfg parameter should be a pointer to a struct where each field represents a configuration option. 137 | // This function returns an error if the reading process fails. 138 | // 139 | // Example: 140 | // 141 | // type Config struct { 142 | // Mode string `yaml:"mode"` 143 | // HTTP struct { 144 | // Host string `yaml:"http_host"` 145 | // Port int `yaml:"http_port"` 146 | // } 147 | // } 148 | // 149 | // func main() { 150 | // cfg := &Config{} 151 | // if err := gocfg.ReadFile("config.yaml", cfg); err != nil { 152 | // log.Fatalf("failed to read configuration file: %v", err) 153 | // } 154 | // 155 | // fmt.Printf("Mode: %s\n", cfg.Mode) 156 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 157 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 158 | // } 159 | // 160 | // This will read the mode, http_host and http_port configuration options from the config.yaml file into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 161 | // If any of these configuration options are not set in the file, the function will return an error. 162 | func ReadFile(path string, cfg any) error { 163 | return file.Read(path, cfg) 164 | } 165 | 166 | // MustReadFile is similar to ReadFile but panics if the reading process fails. 167 | // This function is useful when the absence of a configuration file should lead to a program termination. 168 | // 169 | // Example: 170 | // 171 | // type Config struct { 172 | // Mode string `yaml:"mode"` 173 | // HTTP struct { 174 | // Host string `yaml:"host"` 175 | // Port int `yaml:"port"` 176 | // } `yaml:"http"` 177 | // } 178 | // 179 | // func main() { 180 | // cfg := &Config{} 181 | // gocfg.MustReadFile("config.yaml", cfg) 182 | // 183 | // fmt.Printf("Mode: %s\n", cfg.Mode) 184 | // fmt.Printf("HTTP Host: %s\n", cfg.HTTP.Host) 185 | // fmt.Printf("HTTP Port: %d\n", cfg.HTTP.Port) 186 | // } 187 | // 188 | // This will read the mode, http_host and http_port configuration options from the config.yaml file into the Mode, HTTP.Host and HTTP.Port fields of the cfg variable. 189 | // If any of these configuration options are not set in the file, the program will panic. 190 | func MustReadFile(path string, cfg any) { 191 | if err := ReadFile(path, cfg); err != nil { 192 | panic(err) 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Configuration Library 2 | 3 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 4 | [![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/dsbasko/go-cfg) 5 | 6 | [![GitHub Workflow](https://github.com/dsbasko/go-cfg/actions/workflows/go.yaml/badge.svg?branch=main)](https://github.com/dsbasko/go-cfg/actions/workflows/go.yaml) 7 | [![Test Coverage](https://codecov.io/gh/dsbasko/go-cfg/graph/badge.svg?token=142JTUL2X5)](https://codecov.io/gh/dsbasko/go-cfg) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/dsbasko/go-cfg)](https://goreportcard.com/report/github.com/dsbasko/go-cfg) 9 | ![GitHub Version](https://img.shields.io/github/go-mod/go-version/dsbasko/go-cfg.svg) 10 | ![GitHub Tag](https://img.shields.io/github/tag/dsbasko/go-cfg.svg) 11 | 12 | This project is a Go library for reading configuration data from various sources such as environment variables, command-line flags, and configuration files. The library provides a unified interface for reading configuration data, making it easier to manage and maintain your application's configuration. 13 | 14 | ## Attention 15 | The library uses the [env](https://github.com/caarlos0/env), [pflag](https://github.com/spf13/pflag), [yaml](https://github.com/go-yaml/yaml), [toml](https://github.com/BurntSushi/toml) and [godotenv](https://github.com/joho/godotenv) codebase to work with environment variables and flags. This is a temporary solution, maybe I’ll write my own implementation later. Thanks to the authors of these libraries for the work done! 16 | 17 | ### Installation 18 | To install the library, use the go get command: 19 | ```bash 20 | go get github.com/dsbasko/go-cfg 21 | ``` 22 | 23 | ## Usage 24 | The library provides several functions for reading configuration data: 25 | 26 | - `ReadEnv(cfg any) error`: Reads environment variables into the provided `cfg` structure. Each field in the `cfg` structure represents an environment variable. 27 | - `MustReadEnv(cfg any)`: Similar to `ReadEnv` but panics if the reading process fails. 28 | - `ReadFlag(cfg any) error`: Reads command-line flags into the provided `cfg` structure. Each field in the `cfg` structure represents a command-line flag. 29 | - `MustReadFlag(cfg any)`: Similar to `ReadFlag` but panics if the reading process fails. 30 | - `ReadFile(path string, cfg any) error`: Reads configuration from a file into the provided `cfg` structure. The path parameter is the path to the configuration file. Each field in the `cfg` structure represents a configuration option. Supported file formats include JSON, YAML, TOML and .env. 31 | - `MustReadFile(path string, cfg any)`: Similar to `ReadFile` but panics if the reading process fails. 32 | 33 | Here is an example of how to use the library: 34 | 35 | ```go 36 | package main 37 | 38 | import ( 39 | "github.com/dsbasko/go-cfg" 40 | ) 41 | 42 | type Config struct { 43 | Mode string `default:"prod" json:"mode" yaml:"mode" s-flag:"m" flag:"mode" env:"MODE" description:"mode of the application (dev|prod)"` 44 | HTTP struct { 45 | Host string `default:"localhost" json:"host" yaml:"host" s-flag:"h" flag:"http-host" env:"HTTP_HOST"` 46 | Port int `default:"3000" json:"port" yaml:"port" s-flag:"p" flag:"http-port" env:"HTTP_PORT"` 47 | ReadTimeout int `json:"read_timeout" yaml:"read-timeout" flag:"http-read-timeout" env:"HTTP_READ_TIMEOUT"` 48 | WriteTimeout int `json:"write_timeout" yaml:"write-timeout" flag:"http-write-timeout" env:"HTTP_WRITE_TIMEOUT"` 49 | } `json:"http" yaml:"http"` 50 | } 51 | 52 | func main() { 53 | cfg := Config{} 54 | 55 | gocfg.MustReadFile("configs/config.yaml", &cfg) 56 | gocfg.MustReadEnv(&cfg) 57 | gocfg.MustReadFlag(&cfg) 58 | } 59 | ``` 60 | 61 | Note that you can configure the priority of the configuration. For example, you can first read YAML configs, then environment variables, and finally flags, or vice versa. 62 | 63 | ## Flags 64 | 65 | Run a project with flags: `go run ./cmd/main.go -s="some short flag" --flat=f1 --nested n1` 66 | 67 | ```go 68 | type config struct { 69 | WithShort string `s-flag:"s" flag:"with-short" description:"With short flag"` 70 | Flat string `flag:"flat" description:"Flat flag"` 71 | Parent struct { 72 | Nested string `flag:"nested" description:"Nested flag"` 73 | } 74 | } 75 | 76 | func main() { 77 | var cfg config 78 | if err := gocfg.ReadFlag(&cfg); err != nil { 79 | log.Panicf("failed to read flag: %v", err) 80 | } 81 | // or: gocfg.MustReadFlag(&cfg) 82 | 83 | log.Printf("WithShort: %v\n", cfg.WithShort) 84 | log.Printf("Flat: %v\n", cfg.Flat) 85 | log.Printf("Nested: %v\n", cfg.Parent.Nested) 86 | } 87 | 88 | // WithShort: some short flag 89 | // Flat: f1 90 | // Nested: n1 91 | ``` 92 | 93 | Struct tags are available for working with flags: 94 | - `default` default value; 95 | - `flag` the name of the flag; 96 | - `s-flag` short name of the flask (1 symbol); 97 | - `description` description of the flag that is displayed when running the `--help` command. 98 | 99 | ## Environment variables 100 | 101 | The `env` structure tag is used for environment variables. 102 | Run a project with environment variables: `FLAT=f1 NESTED=n1 go run ./cmd/main.go` 103 | 104 | ```go 105 | type config struct { 106 | Flat string `env:"FLAT"` 107 | Parent struct { 108 | Nested string `env:"NESTED"` 109 | } 110 | } 111 | 112 | func main() { 113 | var cfg config 114 | if err := gocfg.ReadEnv(&cfg); err != nil { 115 | log.Panicf("failed to read env: %v", err) 116 | } 117 | // or: gocfg.MustReadEnv(&cfg) 118 | 119 | log.Printf("Flat: %v\n", cfg.Flat) 120 | log.Printf("Nested: %v\n", cfg.Parent.Nested) 121 | } 122 | 123 | // Flat: f1 124 | // Nested: n1 125 | ``` 126 | 127 | Struct tags are available for working with environment variables: 128 | - `default` default value; 129 | - `env` the name of the environment variable. 130 | 131 | ## Files 132 | 133 | Run a project with environment variables: `go run ./cmd/main.go` 134 | 135 | ```go 136 | type config struct { 137 | Flat string `yaml:"flat"` 138 | Parent struct { 139 | Nested string `yaml:"nested"` 140 | } 141 | } 142 | 143 | func main() { 144 | var cfg config 145 | if err := gocfg.ReadFile(path.Join("cmd", "config.yaml"), &cfg); err != nil { 146 | log.Panicf("failed to read config.yaml file: %v", err) 147 | } 148 | // or: gocfg.MustReadFile(path.Join("cmd", "config.yaml"), &cfg) 149 | 150 | log.Printf("Flat: %v\n", cfg.Flat) 151 | log.Printf("Nested: %v\n", cfg.Parent.Nested) 152 | } 153 | 154 | // Flat: f1 155 | // Nested: n1 156 | ``` 157 | 158 | Structure of the `yaml` file: 159 | ```yaml 160 | flat: f1 161 | foo: 162 | nested: n1 163 | ``` 164 | 165 | Struct tags are available for working with environment variables: 166 | - `default` default value; 167 | - `env` for files of the format `.env` 168 | - `yaml` for files of the format `.yaml` or `.yml` 169 | - `toml` for files of the format `.toml` 170 | - `json` for files of the format `.json` 171 | 172 |
173 | 174 | --- 175 | 176 | If you enjoyed this project, I would appreciate it if you could give it a star! If you notice any problems or have any suggestions for improvement, please feel free to create a new issue. Your feedback means a lot to me! 177 | 178 | ❤️ [Dmitriy Basenko](https://github.com/dsbasko) -------------------------------------------------------------------------------- /internal/file/read_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_Read(t *testing.T) { 14 | type InStructNested struct { 15 | Field string `json:"field" yaml:"field" toml:"field" env:"NESTED_FIELD"` 16 | } 17 | type InStruct struct { 18 | Field string `json:"field" yaml:"field" toml:"field" env:"FIELD"` 19 | Nested InStructNested `json:"nested" yaml:"nested" toml:"nested"` 20 | } 21 | 22 | tests := []struct { 23 | name string 24 | path string 25 | structPtr any 26 | wantStruct *InStruct 27 | wantError bool 28 | }{ 29 | { 30 | name: "Happy Path JSON", 31 | path: path.Join("tests", "cfg.json"), 32 | structPtr: &InStruct{}, 33 | wantStruct: &InStruct{ 34 | Field: "jsonFieldValue", 35 | Nested: InStructNested{ 36 | Field: "jsonNestedFieldValue", 37 | }, 38 | }, 39 | wantError: false, 40 | }, 41 | { 42 | name: "Broken JSON", 43 | path: path.Join("tests", "broken.json"), 44 | structPtr: &InStruct{}, 45 | wantStruct: &InStruct{}, 46 | wantError: true, 47 | }, 48 | { 49 | name: "Happy Path YAML", 50 | path: path.Join("tests", "cfg.yaml"), 51 | structPtr: &InStruct{}, 52 | wantStruct: &InStruct{ 53 | Field: "yamlFieldValue", 54 | Nested: InStructNested{ 55 | Field: "yamlNestedFieldValue", 56 | }, 57 | }, 58 | wantError: false, 59 | }, 60 | { 61 | name: "Broken YAML", 62 | path: path.Join("tests", "broken.yaml"), 63 | structPtr: &InStruct{}, 64 | wantStruct: &InStruct{}, 65 | wantError: true, 66 | }, 67 | { 68 | name: "Happy Path TOML", 69 | path: path.Join("tests", "cfg.toml"), 70 | structPtr: &InStruct{}, 71 | wantStruct: &InStruct{ 72 | Field: "tomlFieldValue", 73 | Nested: InStructNested{ 74 | Field: "tomlNestedFieldValue", 75 | }, 76 | }, 77 | wantError: false, 78 | }, 79 | { 80 | name: "Broken TOML", 81 | path: path.Join("tests", "broken.toml"), 82 | structPtr: &InStruct{}, 83 | wantStruct: &InStruct{}, 84 | wantError: true, 85 | }, 86 | { 87 | name: "Happy Path ENV", 88 | path: path.Join("tests", "cfg.env"), 89 | structPtr: &InStruct{}, 90 | wantStruct: &InStruct{ 91 | Field: "envFieldValue", 92 | Nested: InStructNested{ 93 | Field: "envNestedFieldValue", 94 | }, 95 | }, 96 | wantError: false, 97 | }, 98 | { 99 | name: "Broken ENV", 100 | path: path.Join("tests", "broken.env"), 101 | structPtr: &InStruct{}, 102 | wantStruct: &InStruct{}, 103 | wantError: true, 104 | }, 105 | { 106 | name: "Fail Path", 107 | path: path.Join("not", "found"), 108 | structPtr: &InStruct{}, 109 | wantStruct: &InStruct{}, 110 | wantError: true, 111 | }, 112 | { 113 | name: "Validation Not Pointer", 114 | path: path.Join("tests"), 115 | structPtr: InStruct{}, 116 | wantStruct: &InStruct{}, 117 | wantError: true, 118 | }, 119 | } 120 | 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | err := Read(tt.path, tt.structPtr) 124 | if err != nil { 125 | if (err != nil) != tt.wantError { 126 | t.Errorf("Read() error = %v, wantErr %v", err, tt.wantError) 127 | } 128 | return 129 | } 130 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 131 | }) 132 | } 133 | } 134 | 135 | func Test_parseJSON(t *testing.T) { 136 | type InStructNested struct { 137 | Field string `json:"field"` 138 | } 139 | type InStruct struct { 140 | Field string `json:"field"` 141 | Nested InStructNested `json:"nested"` 142 | } 143 | 144 | tests := []struct { 145 | name string 146 | reader func() io.Reader 147 | structPtr any 148 | wantStruct *InStruct 149 | wantErr bool 150 | }{ 151 | { 152 | name: "Happy Path", 153 | reader: func() io.Reader { 154 | file, err := os.Open(path.Join("tests", "cfg.json")) 155 | assert.NoError(t, err) 156 | return file 157 | }, 158 | structPtr: &InStruct{}, 159 | wantStruct: &InStruct{ 160 | Field: "jsonFieldValue", 161 | Nested: InStructNested{ 162 | Field: "jsonNestedFieldValue", 163 | }, 164 | }, 165 | wantErr: false, 166 | }, 167 | { 168 | name: "Parse Error", 169 | reader: func() io.Reader { 170 | return strings.NewReader(`not json`) 171 | }, 172 | structPtr: &InStruct{}, 173 | wantStruct: &InStruct{}, 174 | wantErr: true, 175 | }, 176 | } 177 | 178 | for _, tt := range tests { 179 | t.Run(tt.name, func(t *testing.T) { 180 | if err := parseJSON(tt.reader(), tt.structPtr); (err != nil) != tt.wantErr { 181 | t.Errorf("parseJSON() error = %v, wantErr %v", err, tt.wantErr) 182 | } else { 183 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 184 | } 185 | }) 186 | } 187 | } 188 | 189 | func Test_parseYAML(t *testing.T) { 190 | type InStructNested struct { 191 | Field string `yaml:"field"` 192 | } 193 | type InStruct struct { 194 | Field string `yaml:"field"` 195 | Nested InStructNested `yaml:"nested"` 196 | } 197 | 198 | tests := []struct { 199 | name string 200 | reader func() io.Reader 201 | structPtr any 202 | wantStruct *InStruct 203 | wantErr bool 204 | }{ 205 | { 206 | name: "Happy Path", 207 | reader: func() io.Reader { 208 | file, err := os.Open(path.Join("tests", "cfg.yaml")) 209 | assert.NoError(t, err) 210 | return file 211 | }, 212 | structPtr: &InStruct{}, 213 | wantStruct: &InStruct{ 214 | Field: "yamlFieldValue", 215 | Nested: InStructNested{ 216 | Field: "yamlNestedFieldValue", 217 | }, 218 | }, 219 | wantErr: false, 220 | }, 221 | { 222 | name: "Parse Error", 223 | reader: func() io.Reader { 224 | return strings.NewReader(`not yaml`) 225 | }, 226 | structPtr: &InStruct{}, 227 | wantStruct: &InStruct{}, 228 | wantErr: true, 229 | }, 230 | } 231 | for _, tt := range tests { 232 | t.Run(tt.name, func(t *testing.T) { 233 | if err := parseYAML(tt.reader(), tt.structPtr); (err != nil) != tt.wantErr { 234 | t.Errorf("parseYAML() error = %v, wantErr %v", err, tt.wantErr) 235 | } else { 236 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | func Test_parseTOML(t *testing.T) { 243 | type InStructNested struct { 244 | Field string `toml:"field"` 245 | } 246 | type InStruct struct { 247 | Field string `toml:"field"` 248 | Nested InStructNested `toml:"nested"` 249 | } 250 | 251 | tests := []struct { 252 | name string 253 | reader func() io.Reader 254 | structPtr any 255 | wantStruct *InStruct 256 | wantErr bool 257 | }{ 258 | { 259 | name: "Happy Path", 260 | reader: func() io.Reader { 261 | file, err := os.Open(path.Join("tests", "cfg.toml")) 262 | assert.NoError(t, err) 263 | return file 264 | }, 265 | structPtr: &InStruct{}, 266 | wantStruct: &InStruct{ 267 | Field: "tomlFieldValue", 268 | Nested: InStructNested{ 269 | Field: "tomlNestedFieldValue", 270 | }, 271 | }, 272 | wantErr: false, 273 | }, 274 | { 275 | name: "Parse Error", 276 | reader: func() io.Reader { 277 | return strings.NewReader(`not toml`) 278 | }, 279 | structPtr: &InStruct{}, 280 | wantStruct: &InStruct{}, 281 | wantErr: true, 282 | }, 283 | } 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | if err := parseTOML(tt.reader(), tt.structPtr); (err != nil) != tt.wantErr { 287 | t.Errorf("parseTOML() error = %v, wantErr %v", err, tt.wantErr) 288 | } else { 289 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 290 | } 291 | }) 292 | } 293 | } 294 | 295 | func Test_parseENV(t *testing.T) { 296 | type InStructNested struct { 297 | Field string `env:"NESTED_FIELD"` 298 | } 299 | type InStruct struct { 300 | Field string `env:"FIELD"` 301 | Nested InStructNested 302 | } 303 | 304 | tests := []struct { 305 | name string 306 | reader func() io.Reader 307 | structPtr any 308 | wantStruct *InStruct 309 | wantErr bool 310 | }{ 311 | { 312 | name: "Happy Path", 313 | reader: func() io.Reader { 314 | file, err := os.Open(path.Join("tests", "cfg.env")) 315 | assert.NoError(t, err) 316 | return file 317 | }, 318 | structPtr: &InStruct{}, 319 | wantStruct: &InStruct{ 320 | Field: "envFieldValue", 321 | Nested: InStructNested{ 322 | Field: "envNestedFieldValue", 323 | }, 324 | }, 325 | wantErr: false, 326 | }, 327 | { 328 | name: "Parse Error", 329 | reader: func() io.Reader { 330 | return strings.NewReader(`{}`) 331 | }, 332 | structPtr: &InStruct{}, 333 | wantStruct: &InStruct{}, 334 | wantErr: true, 335 | }, 336 | } 337 | for _, tt := range tests { 338 | t.Run(tt.name, func(t *testing.T) { 339 | if err := parseENV(tt.reader(), tt.structPtr); (err != nil) != tt.wantErr { 340 | t.Errorf("parseTOML() error = %v, wantErr %v", err, tt.wantErr) 341 | } else { 342 | assert.EqualValues(t, tt.wantStruct, tt.structPtr) 343 | } 344 | }) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /tests/combination_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | gocfg "github.com/dsbasko/go-cfg" 11 | ) 12 | 13 | const ( 14 | ENV = "env" 15 | Flag = "flag" 16 | YAML = "yaml" 17 | TOML = "toml" 18 | JSON = "json" 19 | ENVFile = "env-file" 20 | ) 21 | 22 | func Test_Default(t *testing.T) { 23 | var structPtr InStruct 24 | gocfg.MustReadEnv(&structPtr) 25 | wantStruct := stubDefault() 26 | assert.EqualValues(t, wantStruct, structPtr) 27 | } 28 | 29 | func Test_Once_Env(t *testing.T) { 30 | var structPtr InStruct 31 | wantStruct := stubEnv() 32 | gocfg.MustReadEnv(&structPtr) 33 | assert.Equal(t, wantStruct, structPtr) 34 | } 35 | 36 | func Test_Once_Env_Panic(t *testing.T) { 37 | assert.Panics(t, func() { 38 | gocfg.MustReadEnv("not-a-pointer") 39 | }) 40 | } 41 | 42 | func Test_Once_Flag(t *testing.T) { 43 | var structPtr InStruct 44 | wantStruct := stubFlag() 45 | gocfg.MustReadFlag(&structPtr) 46 | assert.Equal(t, wantStruct, structPtr) 47 | } 48 | 49 | func Test_Once_Flag_Panic(t *testing.T) { 50 | assert.Panics(t, func() { 51 | gocfg.MustReadFlag("not-a-pointer") 52 | }) 53 | } 54 | 55 | func Test_Once_JSON(t *testing.T) { 56 | var structPtr InStruct 57 | wantStruct := stubJSON() 58 | gocfg.MustReadFile("stub.json", &structPtr) 59 | assert.Equal(t, wantStruct, structPtr) 60 | } 61 | 62 | func Test_Once_File_Panic(t *testing.T) { 63 | assert.Panics(t, func() { 64 | gocfg.MustReadFile("", "not-a-pointer") 65 | }) 66 | } 67 | 68 | func Test_Once_YAML(t *testing.T) { 69 | var structPtr InStruct 70 | wantStruct := stubYAML() 71 | gocfg.MustReadFile("stub.yaml", &structPtr) 72 | assert.Equal(t, wantStruct, structPtr) 73 | } 74 | 75 | func Test_Once_TOML(t *testing.T) { 76 | var structPtr InStruct 77 | wantStruct := stubTOML() 78 | gocfg.MustReadFile("stub.toml", &structPtr) 79 | assert.Equal(t, wantStruct, structPtr) 80 | } 81 | 82 | func Test_Once_ENV(t *testing.T) { 83 | var structPtr InStruct 84 | wantStruct := stubEnvFile() 85 | gocfg.MustReadFile("stub.env", &structPtr) 86 | assert.Equal(t, wantStruct, structPtr) 87 | } 88 | 89 | func Test_Permute_All(t *testing.T) { 90 | test := []string{ENV, Flag, JSON, YAML, TOML, ENVFile} 91 | permute := NewPermute(test) 92 | 93 | for _, perm := range permute.items { 94 | name := strings.Join(perm, ",") 95 | t.Run(name, func(t *testing.T) { 96 | var structPtr, wantStruct InStruct 97 | 98 | for _, p := range perm { 99 | switch p { 100 | case ENV: 101 | gocfg.MustReadEnv(&structPtr) 102 | case Flag: 103 | gocfg.MustReadFlag(&structPtr) 104 | case JSON: 105 | gocfg.MustReadFile("stub.json", &structPtr) 106 | case YAML: 107 | gocfg.MustReadFile("stub.yaml", &structPtr) 108 | case TOML: 109 | gocfg.MustReadFile("stub.toml", &structPtr) 110 | case ENVFile: 111 | gocfg.MustReadFile("stub.env", &structPtr) 112 | default: 113 | t.Fatalf("Unknown permutation: %s", p) 114 | } 115 | } 116 | 117 | switch perm[len(perm)-1] { 118 | case ENV: 119 | wantStruct = stubEnv() 120 | case Flag: 121 | wantStruct = stubFlag() 122 | case JSON: 123 | wantStruct = stubJSON() 124 | case YAML: 125 | wantStruct = stubYAML() 126 | case TOML: 127 | wantStruct = stubTOML() 128 | case ENVFile: 129 | wantStruct = stubEnvFile() 130 | default: 131 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 132 | } 133 | 134 | assert.EqualValues(t, wantStruct, structPtr) 135 | }) 136 | } 137 | } 138 | 139 | func Test_Permute_Overlay_Env_flag(t *testing.T) { 140 | test := []string{ENV, ENV, Flag, Flag} 141 | permute := NewPermute(test) 142 | 143 | for _, perm := range permute.items { 144 | name := strings.Join(perm, ",") 145 | t.Run(name, func(t *testing.T) { 146 | var structPtr, wantStruct InStruct 147 | 148 | for _, p := range perm { 149 | switch p { 150 | case ENV: 151 | gocfg.MustReadEnv(&structPtr) 152 | case Flag: 153 | gocfg.MustReadFlag(&structPtr) 154 | default: 155 | t.Fatalf("Unknown permutation: %s", p) 156 | } 157 | } 158 | 159 | switch perm[len(perm)-1] { 160 | case ENV: 161 | wantStruct = stubEnv() 162 | case Flag: 163 | wantStruct = stubFlag() 164 | default: 165 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 166 | } 167 | 168 | assert.EqualValues(t, wantStruct, structPtr) 169 | }) 170 | } 171 | } 172 | 173 | func Test_Permute_Overlay_JSON(t *testing.T) { 174 | test := []string{ENV, Flag, JSON, JSON} 175 | permute := NewPermute(test) 176 | 177 | for _, perm := range permute.items { 178 | name := strings.Join(perm, ",") 179 | t.Run(name, func(t *testing.T) { 180 | var structPtr, wantStruct InStruct 181 | 182 | for _, p := range perm { 183 | switch p { 184 | case ENV: 185 | gocfg.MustReadEnv(&structPtr) 186 | case Flag: 187 | gocfg.MustReadFlag(&structPtr) 188 | case JSON: 189 | gocfg.MustReadFile("stub.json", &structPtr) 190 | } 191 | } 192 | 193 | switch perm[len(perm)-1] { 194 | case ENV: 195 | wantStruct = stubEnv() 196 | case Flag: 197 | wantStruct = stubFlag() 198 | case JSON: 199 | wantStruct = stubJSON() 200 | case YAML: 201 | wantStruct = stubYAML() 202 | case TOML: 203 | wantStruct = stubTOML() 204 | case ENVFile: 205 | wantStruct = stubEnvFile() 206 | default: 207 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 208 | } 209 | 210 | assert.EqualValues(t, wantStruct, structPtr) 211 | }) 212 | } 213 | } 214 | 215 | func Test_Permute_Overlay_YAML(t *testing.T) { 216 | test := []string{ENV, ENV, Flag, Flag} 217 | permute := NewPermute(test) 218 | 219 | for _, perm := range permute.items { 220 | name := strings.Join(perm, ",") 221 | t.Run(name, func(t *testing.T) { 222 | var structPtr, wantStruct InStruct 223 | 224 | for _, p := range perm { 225 | switch p { 226 | case ENV: 227 | gocfg.MustReadEnv(&structPtr) 228 | case Flag: 229 | gocfg.MustReadFlag(&structPtr) 230 | case YAML: 231 | gocfg.MustReadFile("stub.yaml", &structPtr) 232 | default: 233 | t.Fatalf("Unknown permutation: %s", p) 234 | } 235 | } 236 | 237 | switch perm[len(perm)-1] { 238 | case ENV: 239 | wantStruct = stubEnv() 240 | case Flag: 241 | wantStruct = stubFlag() 242 | case YAML: 243 | wantStruct = stubYAML() 244 | default: 245 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 246 | } 247 | 248 | assert.EqualValues(t, wantStruct, structPtr) 249 | }) 250 | } 251 | } 252 | 253 | func Test_Permute_Overlay_TOML(t *testing.T) { 254 | test := []string{ENV, Flag, TOML, TOML} 255 | permute := NewPermute(test) 256 | 257 | for _, perm := range permute.items { 258 | name := strings.Join(perm, ",") 259 | t.Run(name, func(t *testing.T) { 260 | var structPtr, wantStruct InStruct 261 | 262 | for _, p := range perm { 263 | switch p { 264 | case ENV: 265 | gocfg.MustReadEnv(&structPtr) 266 | case Flag: 267 | gocfg.MustReadFlag(&structPtr) 268 | case TOML: 269 | gocfg.MustReadFile("stub.toml", &structPtr) 270 | default: 271 | t.Fatalf("Unknown permutation: %s", p) 272 | } 273 | } 274 | 275 | switch perm[len(perm)-1] { 276 | case ENV: 277 | wantStruct = stubEnv() 278 | case Flag: 279 | wantStruct = stubFlag() 280 | case TOML: 281 | wantStruct = stubTOML() 282 | default: 283 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 284 | } 285 | 286 | assert.EqualValues(t, wantStruct, structPtr) 287 | }) 288 | } 289 | } 290 | 291 | func Test_Permute_Overlay_Env(t *testing.T) { 292 | test := []string{ENV, Flag, ENV, ENV} 293 | permute := NewPermute(test) 294 | 295 | for _, perm := range permute.items { 296 | name := strings.Join(perm, ",") 297 | t.Run(name, func(t *testing.T) { 298 | var structPtr, wantStruct InStruct 299 | 300 | for _, p := range perm { 301 | switch p { 302 | case ENV: 303 | gocfg.MustReadEnv(&structPtr) 304 | case Flag: 305 | gocfg.MustReadFlag(&structPtr) 306 | case ENVFile: 307 | gocfg.MustReadFile("stub.env", &structPtr) 308 | default: 309 | t.Fatalf("Unknown permutation: %s", p) 310 | } 311 | } 312 | 313 | switch perm[len(perm)-1] { 314 | case ENV: 315 | wantStruct = stubEnv() 316 | case Flag: 317 | wantStruct = stubFlag() 318 | case ENVFile: 319 | wantStruct = stubEnvFile() 320 | default: 321 | t.Fatalf("Unknown permutation: %s", perm[len(perm)-1]) 322 | } 323 | 324 | assert.EqualValues(t, wantStruct, structPtr) 325 | }) 326 | } 327 | } 328 | 329 | type Permute struct { 330 | items [][]string 331 | wantStruct []InStruct 332 | } 333 | 334 | func NewPermute(data []string) *Permute { 335 | permuteItems := permuteFn(data) 336 | permute := Permute{items: permuteItems} 337 | 338 | permute.wantStruct = make([]InStruct, len(permuteItems)) 339 | for i, perm := range permuteItems { 340 | lastItem := perm[len(perm)-1] 341 | switch lastItem { 342 | case ENV: 343 | permute.wantStruct[i] = stubEnv() 344 | case Flag: 345 | permute.wantStruct[i] = stubFlag() 346 | case JSON: 347 | permute.wantStruct[i] = stubJSON() 348 | case YAML: 349 | permute.wantStruct[i] = stubYAML() 350 | case TOML: 351 | permute.wantStruct[i] = stubTOML() 352 | case ENVFile: 353 | permute.wantStruct[i] = stubEnvFile() 354 | 355 | default: 356 | log.Fatalf("Unknown permutation: %s", lastItem) 357 | } 358 | } 359 | 360 | return &permute 361 | } 362 | 363 | func permuteFn(data []string) [][]string { 364 | if len(data) == 1 { 365 | return [][]string{data} 366 | } 367 | 368 | var permutations [][]string 369 | for i, v := range data { 370 | temp := make([]string, len(data)) 371 | copy(temp, data) 372 | temp = append(temp[:i], temp[i+1:]...) 373 | for _, perm := range permuteFn(temp) { 374 | permutations = append(permutations, append([]string{v}, perm...)) 375 | } 376 | } 377 | return permutations 378 | } 379 | --------------------------------------------------------------------------------