├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── configuro.go ├── configuro_test.go ├── error.go ├── example ├── config.yml ├── main.go └── other_formats │ ├── config.json │ └── config.toml ├── go.mod ├── go.sum ├── load.go └── validate.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | 14 | [{Makefile, *.mk}] 15 | indent_style = tab 16 | 17 | [*.proto] 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sherifabdlnaby@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to make participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, sex characteristics, gender identity and expression, 10 | level of experience, education, socio-economic status, nationality, personal 11 | appearance, race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all project spaces, and it also applies when 50 | an individual is representing the project or its community in public spaces. 51 | Examples of representing a project or community include using an official 52 | project e-mail address, posting via an official social media account, or acting 53 | as an appointed representative at an online or offline event. Representation of 54 | a project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a Question 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Ask a question... 11 | 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | (Thanks for sending a pull request! Please make sure you click the link above to view the contribution guidelines, then fill out the blanks below.) 2 | 3 | What does this implement/fix? Explain your changes. 4 | --------------------------------------------------- 5 | … 6 | 7 | Does this close any currently open issues? 8 | ------------------------------------------ 9 | … 10 | 11 | 12 | Any relevant logs, error output, etc? 13 | ------------------------------------- 14 | (If it’s long, please paste to https://ghostbin.com/ and insert the link here.) 15 | 16 | Any other comments? 17 | ------------------- 18 | … 19 | 20 | Where has this been tested? 21 | --------------------------- 22 | … 23 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Build 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.14.x, 1.13.x, 1.12.x, 1.11.x] 8 | platform: [ubuntu-latest, macos-latest, windows-latest] 9 | runs-on: ${{ matrix.platform }} 10 | steps: 11 | - name: Install Go 12 | if: success() 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | - name: Checkout code 17 | uses: actions/checkout@v1 18 | - name: Run tests 19 | run: go test -v -race -covermode=atomic 20 | 21 | 22 | coverage: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Install Go 26 | if: success() 27 | uses: actions/setup-go@v1 28 | with: 29 | go-version: 1.13.x 30 | - name: Checkout code 31 | uses: actions/checkout@v1 32 | - name: Calc coverage 33 | run: | 34 | export PATH=$PATH:$(go env GOPATH)/bin 35 | go test -v -covermode=count -coverprofile=coverage.out 36 | - name: Convert coverage to lcov 37 | uses: jandelgado/gcov2lcov-action@v1.0.0 38 | with: 39 | infile: coverage.out 40 | outfile: coverage.lcov 41 | - name: Coveralls 42 | uses: coverallsapp/github-action@v1.0.1 43 | with: 44 | github-token: ${{ secrets.github_token }} 45 | path-to-lcov: coverage.lcov 46 | 47 | # build: 48 | # runs-on: ubuntu-latest 49 | # needs: [lint, test] 50 | # steps: 51 | # - name: Install Go 52 | # uses: actions/setup-go@v1 53 | # with: 54 | # go-version: 1.13.x 55 | # - name: Checkout code 56 | # uses: actions/checkout@v1 57 | # - name: build 58 | # run: | 59 | # export GO111MODULE=on 60 | # GOOS=windows GOARCH=amd64 go build -o bin/ci-test-windows-amd64.exe 61 | # GOOS=linux GOARCH=amd64 go build -o bin/ci-test-linux-amd64 62 | # GOOS=darwin GOARCH=amd64 go build -o bin/ci-test-darwin-amd64 63 | # - name: upload artifacts 64 | # uses: actions/upload-artifact@master 65 | # with: 66 | # name: binaries 67 | # path: bin/ 68 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Linter 3 | jobs: 4 | lint: 5 | name: Lint project using GolangCI Lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out code into the Go module directory 9 | uses: actions/checkout@v1 10 | 11 | - name: GolangCI-Lint Action 12 | uses: matoous/golangci-lint-action@v1.23.3 13 | with: 14 | config: .golangci.yml 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### General 2 | bin 3 | vendor 4 | .env 5 | 6 | ### JetBrains template 7 | # User-specific stuff 8 | .idea 9 | 10 | 11 | ### Windows template 12 | # Windows thumbnail cache files 13 | Thumbs.db 14 | 15 | ### macOS template 16 | # General 17 | .DS_Store 18 | 19 | # Thumbnails 20 | ._* 21 | 22 | ### Go template 23 | # Binaries for programs and plugins 24 | *.exe 25 | *.exe~ 26 | *.dll 27 | *.so 28 | *.dylib 29 | 30 | # Test binary, built with `go test -c` 31 | *.test 32 | 33 | # Output of the go coverage tool, specifically when used with LiteIDE 34 | *.out 35 | 36 | ### VisualStudioCode template 37 | .vscode/* 38 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - .gen 4 | 5 | skip-files: 6 | - ".*_gen\\.go$" 7 | 8 | linters-settings: 9 | gocyclo: 10 | min-complexity: 30 11 | goconst: 12 | min-len: 2 13 | min-occurrences: 2 14 | misspell: 15 | locale: US 16 | golint: 17 | min-confidence: 0.8 18 | 19 | linters: 20 | disable-all: true 21 | enable: 22 | - goconst 23 | - gocyclo 24 | - golint 25 | - govet 26 | - gofmt 27 | - errcheck 28 | - staticcheck 29 | - unused 30 | - gosimple 31 | - structcheck 32 | - varcheck 33 | - ineffassign 34 | - deadcode 35 | - unconvert 36 | - gosec 37 | - golint 38 | - bodyclose 39 | - misspell 40 | - unconvert 41 | - unparam 42 | - dogsled 43 | - goimports 44 | 45 | service: 46 | golangci-lint-version: 1.21.x 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Sherif Abdel-Naby 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 | 5 |

6 |

Opinionated configuration loading framework for Containerized and 12-Factor compliant applications.

7 |
Read configurations from Environment Variables, and/or Configuration Files. With support to Environment Variables Expanding and Validation Methods.
8 |

9 | 10 | Mentioned in Awesome Go 11 | 12 | 13 | Go Doc 14 | 15 | 16 | 17 | 18 | 19 | Go Version 20 | 21 | 22 | 23 | 24 | Coverage Status 25 | 26 | Go Report 27 | 28 | 29 | GitHub license 30 | 31 |

32 | 33 | # Introduction 34 | 35 | Configuro is an opinionated configuration loading and validation framework with not very much to configure. It *defines* a method for loading configurations without so many options for a straight-forward and simple code. 36 | 37 | The method defined by Configuro allow you to implement [12-Factor's Config](https://12factor.net/config) and it mimics how many mature applications do configurations (e.g Elastic, Neo4J, etc); and is also fit for containerized applications. 38 | 39 | ------------ 40 | **With Only with two lines of code, and zero setting up** you get loading configuration from Config File, Overwrite values /or rely exclusively on Environment Variables, Expanding values using ${ENV} expressions, and validation tags. 41 | 42 | # Loading Configuration 43 |

44 | 45 |

46 | 47 | ### 1. Define Application **configurations** in a struct 48 | - Which *Configuro* will `Load()` the read configuration into. 49 | 50 | ### 2. Setting Configuration by **Environment Variables**. 51 | - Value for key `database.password` can be set by setting `CONFIG_DATABASE_PASSWORD`. (`CONFIG_` default prefix can be changed) 52 | - If the key itself contains `_` then replace with `__` in the Environment Variable. 53 | - You can express **Maps** and **Lists** in Environment Variables by JSON encoding them. (e.g `CONFIG: {"a":123, "b": "abc"}`) 54 | - You can provide a `.env` file to load environment variables that are not set by the OS. 55 | 56 | ### 3. Setting Configuration by Configuration File. 57 | - Defaults to `config.yml`; name and extension can be configured. 58 | - Supported extensions are `.yml`, `.yaml`, `.json`, and `.toml`. 59 | 60 | ### 4. Support Environment Variables Expanding. 61 | - Configuration Values can have ${ENV|default} expression that will be expanded at loading time. 62 | - Example `host: www.example.com:%{PORT|3306}` with `3306` being the default value if env ${PORT} is not set). 63 | 64 | ### 5. Validate Loaded Values 65 | - Configuro can validate structs recursively using [Validation Tags](https://godoc.org/github.com/go-playground/validator). 66 | - By Implementing `Validatable` Interface `Validate() error`. 67 | 68 | #### Notes 69 | - 📣 Values' precedence is `OS EnvVar` > `.env EnvVars` > `Config File` > `Value set in Struct before loading.` 70 | 71 | 72 | ------------------------------------------------------------------------- 73 | 74 | # Install 75 | ``` bash 76 | go get github.com/sherifabdlnaby/configuro 77 | ``` 78 | ``` go 79 | import "github.com/sherifabdlnaby/configuro" 80 | ``` 81 | 82 | # Usage 83 | 84 | ### 1. Define You Config Struct. 85 | This is the struct you're going to use to retrieve your config values in-code. 86 | ```go 87 | type Config struct { 88 | Database struct { 89 | Host string 90 | Port int 91 | } 92 | Logging struct { 93 | Level string 94 | LineFormat string `config:"line_format"` 95 | } 96 | } 97 | ``` 98 | 99 | - Nested fields accessed with `.` (e.g `Database.Host` ) 100 | - Use `config` tag to change field name if you want it to be different from the Struct field name. 101 | - All [mapstructure](https://github.com/mitchellh/mapstructure) tags apply to `config` tag for unmarshalling. 102 | - Fields must be public to be accessible by Configuro. 103 | 104 | ### 2. Create and Configure the `Configuro.Config` object. 105 | 106 | ```go 107 | // Create Configuro Object with supplied options (explained below) 108 | config, err := configuro.NewConfig( opts ...configuro.ConfigOption ) 109 | 110 | // Create our Config Struct 111 | configStruct := &Config{ /*put defaults config here*/ } 112 | 113 | // Load values in our struct 114 | err = config.Load(configStruct) 115 | ``` 116 | 117 | - Create Configuro Object Passing to the constructor `opts ...configuro.ConfigOption` which is explained in the below sections. 118 | - This should happen as early as possible in the application. 119 | 120 | ### 3. Loading from Environment Variables 121 | 122 | - Values found in Environment Variables take precedence over values found in config file. 123 | - The key `database.host` can be expressed in environment variables as `CONFIG_DATABASE_HOST`. 124 | - If the key itself contains `_` then replace them with `__` in the Environment Variable. 125 | - `CONFIG_` prefix can be configured. 126 | - You can express **Maps** and **Lists** in Environment Variables by JSON encoding them. (e.g `CONFIG: {"a":123, "b": "abc"}`) 127 | - You can provide a `.env` file to load environment variables that are not set by the OS. (notice that .env is loaded globally in the application scope) 128 | 129 | The above settings can be changed upon constructing the configuro object via passing these options. 130 | ```go 131 | configuro.WithLoadFromEnvVars(EnvPrefix string) // Enable Env loading and set Prefix. 132 | configuro.WithoutLoadFromEnvVars() // Disable Env Loading Entirely 133 | configuro.WithLoadDotEnv(envDotFilePath string) // Enable loading .env into Environment Variables 134 | configuro.WithoutLoadDotEnv() // Disable loading .env 135 | ``` 136 | 137 | ### 4. Loading from Configuration Files 138 | - Upon setting up you will declare the config `filepath`. 139 | - Default `filename` => "config.yml" 140 | - Supported formats are `Yaml`, `Json`, and `Toml`. 141 | - Config file directory can be overloaded with a defined Environment Variable. 142 | - Default: `CONFIG_DIR`. 143 | - If file **was not found** Configuro won't raise an error unless configured too. This is you can rely 100% on Environment Variables. 144 | 145 | The above settings can be changed upon constructing the configuro object via passing these options. 146 | ```go 147 | configuro.WithLoadFromConfigFile(Filepath string, ErrIfFileNotFound bool) // Enable Config File Load 148 | configuro.WithoutLoadFromConfigFile() // Disable Config File Load 149 | configuro.WithEnvConfigPathOverload(configFilepathENV string) // Enable Overloading Path with ENV var. 150 | configuro.WithoutEnvConfigPathOverload() // Disable Overloading Path with ENV var. 151 | ``` 152 | 153 | ### 5. Expanding Environment Variables in Config 154 | 155 | - `${ENV}` and `${ENV|default}` expressions are evaluated and expanded if the Environment Variable is set or with the default value if defined, otherwise it leaves it as it is. 156 | ``` 157 | config: 158 | database: 159 | host: xyz:${PORT:3306} 160 | username: admin 161 | password: ${PASSWORD} 162 | ``` 163 | 164 | The above settings can be changed upon constructing the configuro object via passing these options. 165 | ```go 166 | configuro.WithExpandEnvVars() // Enable Expanding 167 | configuro.WithoutExpandEnvVars() // Disable Expanding 168 | ``` 169 | 170 | ### 6. Validate Struct 171 | 172 | ```go 173 | err := config.Validate(configStruct) 174 | if err != nil { 175 | return err 176 | } 177 | ```` 178 | 179 | - Configuro Validate Config Structs using two methods. 180 | 1. Using [Validation Tags](https://godoc.org/github.com/go-playground/validator) for quick validations. 181 | 2. Using `Validatable` Interface that will be called on any type that implements it recursively, also on each element of a Map or a Slice. 182 | - Validation returns an error of type configuro.ErrValidationErrors if more than error occurred. 183 | - It can be configured to not recursively validate types with `Validatable` Interface. (default: recursively) 184 | - It can be configured to stop at the first error. (default: false) 185 | - It can be configured to not use Validation Tags. (default: false) 186 | The above settings can be changed upon constructing the configuro object via passing these options. 187 | ```go 188 | configuro.WithValidateByTags() 189 | configuro.WithoutValidateByTags() 190 | configuro.WithValidateByFunc(stopOnFirstErr bool, recursive bool) 191 | configuro.WithoutValidateByFunc() 192 | ``` 193 | 194 | ### 7. Miscellaneous 195 | 196 | - `config` and `validate` tag can be renamed using `configuro.Tag(structTag, validateTag)` construction option. 197 | 198 | # Built on top of 199 | - [spf13/viper](https://github.com/spf13/viper) 200 | - [mitchellh/mapstructure](github.com/mitchellh/mapstructure) 201 | - [go-playground/validator](https://github.com/go-playground/validator) 202 | - [joho/godotenv](https://github.com/joho/godotenv) 203 | 204 | # License 205 | [MIT License](https://raw.githubusercontent.com/sherifabdlnaby/configuro/master/LICENSE) 206 | Copyright (c) 2020 Sherif Abdel-Naby 207 | 208 | # Contribution 209 | 210 | PR(s) are Open and Welcomed. 211 | -------------------------------------------------------------------------------- /configuro.go: -------------------------------------------------------------------------------- 1 | package configuro 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/go-playground/locales/en" 10 | ut "github.com/go-playground/universal-translator" 11 | ens "github.com/go-playground/validator/translations/en" 12 | "github.com/joho/godotenv" 13 | "github.com/mitchellh/mapstructure" 14 | "github.com/spf13/viper" 15 | "gopkg.in/go-playground/validator.v9" 16 | ) 17 | 18 | //Config Loads and WithValidateByTags Arbitrary structs based on options (set at constructing) 19 | type Config struct { 20 | envLoad bool 21 | envPrefix string 22 | envDotFileLoad bool 23 | envDotFilePath string 24 | configFileLoad bool 25 | configFileErrIfNotFound bool 26 | configFilepath string 27 | configFilepathEnv bool 28 | configFilepathEnvName string 29 | configEnvExpand bool 30 | validateFuncStopOnFirstErr bool 31 | validateRecursive bool 32 | validateUsingTags bool 33 | validateUsingFunc bool 34 | validateTag string 35 | tag string 36 | keyDelimiter string 37 | viper *viper.Viper 38 | validator *validator.Validate 39 | validatorTrans ut.Translator 40 | decodeHook viper.DecoderConfigOption 41 | } 42 | 43 | //NewConfig Create config Loader/Validator according to options. 44 | func NewConfig(opts ...ConfigOptions) (*Config, error) { 45 | var err error 46 | 47 | config := &Config{} 48 | 49 | options := DefaultOptions() 50 | 51 | options = append(options, opts...) 52 | 53 | // Loop through each option 54 | for _, opt := range options { 55 | // Call the option giving the instantiated 56 | // *House as the argument 57 | err = opt(config) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | } 63 | 64 | err = config.initialize() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return config, nil 70 | } 71 | 72 | //DefaultOptions Returns The Default Configuro Options 73 | func DefaultOptions() []ConfigOptions { 74 | return []ConfigOptions{ 75 | WithLoadFromEnvVars("CONFIG"), 76 | WithLoadFromConfigFile("./config.yml", false), 77 | WithEnvConfigPathOverload("CONFIG_DIR"), 78 | WithLoadDotEnv("./env"), 79 | WithExpandEnvVars(), 80 | WithValidateByTags(), 81 | WithValidateByFunc(false, true), 82 | Tag("config", "validate"), 83 | KeyDelimiter("."), 84 | } 85 | } 86 | 87 | func (c *Config) initialize() error { 88 | 89 | // Init Viper 90 | c.viper = viper.NewWithOptions(viper.KeyDelimiter(c.keyDelimiter)) 91 | 92 | if c.envDotFileLoad { 93 | // load .env vars 94 | if _, err := os.Stat(c.envDotFilePath); err == nil || !os.IsNotExist(err) { 95 | err := godotenv.Load(c.envDotFilePath) 96 | if err != nil { 97 | return fmt.Errorf("error loading .env envvars from \"%s\": %s", c.envDotFilePath, err.Error()) 98 | } 99 | } 100 | } 101 | 102 | if c.envLoad { 103 | c.enableEnvLoad() 104 | } 105 | 106 | if c.configFileLoad { 107 | err := c.enableConfigFileLoad() 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | 113 | // decoder config 114 | c.addDecoderConfig() 115 | 116 | // Tag validator 117 | if c.validateUsingTags { 118 | c.enableValidateUsingTag() 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // --------------------------------------------------------------------------------------------------------------------- 125 | 126 | //ConfigOptions Modify Config Options Accordingly 127 | type ConfigOptions func(*Config) error 128 | 129 | //WithLoadFromEnvVars Load Configuration from Environment Variables if they're set. 130 | // - Require Environment Variables to prefixed with the set prefix (All CAPS) 131 | // - For Nested fields replace `.` with `_` and if key itself has any `_` replace it with `__` (e.g `config.host` to be `CONFIG_HOST`) 132 | // - Arrays can be declared in environment variables using 133 | // 1. comma separated list. 134 | // 2. json encoded array in a string. 135 | // - Maps and objects can be declared in environment using a json encoded object in a string. 136 | func WithLoadFromEnvVars(EnvPrefix string) ConfigOptions { 137 | return func(h *Config) error { 138 | if EnvPrefix == "" { 139 | return fmt.Errorf("env prefix must be declared") 140 | } 141 | 142 | h.envLoad = true 143 | h.envPrefix = strings.ToUpper(EnvPrefix) 144 | 145 | return nil 146 | } 147 | } 148 | 149 | //WithoutLoadFromEnvVars will not load configuration from Environment Variables. 150 | func WithoutLoadFromEnvVars() ConfigOptions { 151 | return func(h *Config) error { 152 | h.envLoad = false 153 | h.envPrefix = "" 154 | return nil 155 | } 156 | } 157 | 158 | //WithLoadDotEnv Allow loading .env file (notice that this is application global not to this config instance only) 159 | func WithLoadDotEnv(envDotFilePath string) ConfigOptions { 160 | return func(h *Config) error { 161 | h.envDotFileLoad = true 162 | h.envDotFilePath = envDotFilePath 163 | return nil 164 | } 165 | } 166 | 167 | //WithoutLoadDotEnv disable loading .env file into Environment Variables 168 | func WithoutLoadDotEnv() ConfigOptions { 169 | return func(h *Config) error { 170 | h.envDotFileLoad = false 171 | h.envDotFilePath = "" 172 | return nil 173 | } 174 | } 175 | 176 | //WithLoadFromConfigFile Load Config from file provided by filepath. 177 | // - Supported Formats/Extensions (.yml, .yaml, .toml, .json) 178 | // - ErrIfFileNotFound let you determine behavior when files is not found. 179 | // Typically if you rely on Environment Variables you may not need to Error if file is not found. 180 | func WithLoadFromConfigFile(Filepath string, ErrIfFileNotFound bool) ConfigOptions { 181 | return func(h *Config) error { 182 | h.configFileLoad = true 183 | h.configFileErrIfNotFound = ErrIfFileNotFound 184 | return h.setConfigFilepath(Filepath) 185 | } 186 | } 187 | 188 | //WithoutLoadFromConfigFile Disable loading configuration from a file. 189 | func WithoutLoadFromConfigFile() ConfigOptions { 190 | return func(h *Config) error { 191 | h.configFileLoad = false 192 | h.configFileErrIfNotFound = false 193 | h.configFilepath = "" 194 | return nil 195 | } 196 | } 197 | 198 | //WithEnvConfigPathOverload Allow to override Config file Path with an Env Variable 199 | func WithEnvConfigPathOverload(configFilepathENV string) ConfigOptions { 200 | return func(h *Config) error { 201 | h.configFilepathEnv = true 202 | h.configFilepathEnvName = strings.ToUpper(configFilepathENV) 203 | return nil 204 | } 205 | } 206 | 207 | //WithoutEnvConfigPathOverload Disallow overriding Config file Path with an Env Variable 208 | func WithoutEnvConfigPathOverload() ConfigOptions { 209 | return func(h *Config) error { 210 | h.configFilepathEnv = false 211 | h.configFilepathEnvName = "" 212 | return nil 213 | } 214 | } 215 | 216 | //WithExpandEnvVars Expand config values with ${ENVVAR} with the value of ENVVAR in environment variables. 217 | // Example: ${DB_URI}:3201 ==> localhost:3201 (Where $DB_URI was equal "localhost" ) 218 | // You can set default if ENVVAR is not set using the following format ${ENVVAR|defaultValue} 219 | func WithExpandEnvVars() ConfigOptions { 220 | return func(h *Config) error { 221 | h.configEnvExpand = true 222 | return nil 223 | } 224 | } 225 | 226 | //WithoutExpandEnvVars Disable Expanding Environment Variables in Config. 227 | func WithoutExpandEnvVars() ConfigOptions { 228 | return func(h *Config) error { 229 | h.configEnvExpand = false 230 | return nil 231 | } 232 | } 233 | 234 | //WithValidateByTags Validate using struct tags. 235 | func WithValidateByTags() ConfigOptions { 236 | return func(h *Config) error { 237 | h.validateUsingTags = true 238 | return nil 239 | } 240 | } 241 | 242 | //WithoutValidateByTags Disable validate using struct tags. 243 | func WithoutValidateByTags() ConfigOptions { 244 | return func(h *Config) error { 245 | h.validateUsingTags = false 246 | return nil 247 | } 248 | } 249 | 250 | //WithValidateByFunc Validate struct by calling the Validate() function on every type that implement Validatable interface. 251 | // - stopOnFirstErr will abort validation after the first error. 252 | // - recursive will call Validate() on nested interfaces where the parent itself is also Validatable. 253 | func WithValidateByFunc(stopOnFirstErr bool, recursive bool) ConfigOptions { 254 | return func(h *Config) error { 255 | h.validateFuncStopOnFirstErr = stopOnFirstErr 256 | h.validateRecursive = recursive 257 | h.validateUsingFunc = true 258 | return nil 259 | } 260 | } 261 | 262 | //WithoutValidateByFunc Disable validating using Validatable interface. 263 | func WithoutValidateByFunc() ConfigOptions { 264 | return func(h *Config) error { 265 | h.validateFuncStopOnFirstErr = false 266 | h.validateUsingFunc = false 267 | h.validateRecursive = false 268 | return nil 269 | } 270 | } 271 | 272 | //Tag Change default tag. 273 | func Tag(structTag, validateTag string) ConfigOptions { 274 | return func(h *Config) error { 275 | h.tag = structTag 276 | h.validateTag = validateTag 277 | return nil 278 | } 279 | } 280 | 281 | //KeyDelimiter Сhange default key delimiter. 282 | func KeyDelimiter(keyDelimiter string) ConfigOptions { 283 | return func(h *Config) error { 284 | h.keyDelimiter = keyDelimiter 285 | return nil 286 | } 287 | } 288 | 289 | var supportedExt = []string{".json", ".toml", ".yaml", ".yml"} 290 | 291 | func isSupportedExtension(ext string) bool { 292 | found := false 293 | for _, supportedExt := range supportedExt { 294 | if ext == supportedExt { 295 | found = true 296 | } 297 | } 298 | return found 299 | } 300 | 301 | func (c *Config) setConfigFilepath(path string) error { 302 | 303 | // Turn into ABS filepath 304 | path, err := filepath.Abs(path) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | // Check extension 310 | ext := filepath.Ext(path) 311 | if ext == "" { 312 | return fmt.Errorf("config file has no extension") 313 | } 314 | 315 | isSupported := isSupportedExtension(ext) 316 | 317 | if !isSupported { 318 | return fmt.Errorf("file with extension %s is not supported", ext) 319 | } 320 | 321 | c.configFilepath = path 322 | return nil 323 | } 324 | 325 | func (c *Config) enableValidateUsingTag() { 326 | c.validator = validator.New() 327 | c.validator.SetTagName(c.validateTag) 328 | // Get English Errors 329 | uni := ut.New(en.New(), en.New()) 330 | c.validatorTrans, _ = uni.GetTranslator("en") 331 | _ = ens.RegisterDefaultTranslations(c.validator, c.validatorTrans) 332 | } 333 | 334 | func (c *Config) addDecoderConfig() { 335 | DefaultDecodeHookFuncs := []mapstructure.DecodeHookFunc{ 336 | stringJSONArrayToSlice(), 337 | stringJSONObjToMap(), 338 | stringJSONObjToStruct(), 339 | mapstructure.StringToTimeDurationHookFunc(), 340 | mapstructure.StringToIPHookFunc(), 341 | } 342 | if c.configEnvExpand { 343 | DefaultDecodeHookFuncs = append([]mapstructure.DecodeHookFunc{expandEnvVariablesWithDefaults()}, DefaultDecodeHookFuncs...) 344 | } 345 | c.decodeHook = viper.DecodeHook(mapstructure.ComposeDecodeHookFunc( 346 | DefaultDecodeHookFuncs..., 347 | )) 348 | } 349 | 350 | func (c *Config) enableConfigFileLoad() error { 351 | 352 | if c.configFilepathEnv { 353 | configDirEnvValue, isSet := os.LookupEnv(c.configFilepathEnvName) 354 | if isSet { 355 | err := c.setConfigFilepath(configDirEnvValue) 356 | if err != nil { 357 | return err 358 | } 359 | } 360 | } 361 | 362 | c.viper.SetConfigFile(c.configFilepath) 363 | return nil 364 | } 365 | 366 | func (c *Config) enableEnvLoad() { 367 | c.viper.SetEnvPrefix(c.envPrefix) 368 | // Viper add the `prefix` + '_' to the Key *before* passing it to Key Replacer,causing the replacer to replace the '_' with '__' when it shouldn't. 369 | // by adding the Prefix to the replacer twice, this will let the replacer escapes the prefix as it scans through the string. 370 | c.viper.SetEnvKeyReplacer(strings.NewReplacer(c.envPrefix+"_", c.envPrefix+"_", "_", "__", ".", "_")) 371 | c.viper.AutomaticEnv() 372 | } 373 | -------------------------------------------------------------------------------- /configuro_test.go: -------------------------------------------------------------------------------- 1 | //nolint 2 | package configuro_test 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/sherifabdlnaby/configuro" 14 | "go.uber.org/multierr" 15 | ) 16 | 17 | type Example struct { 18 | Nested Nested 19 | } 20 | 21 | type Nested struct { 22 | Key Key 23 | Key_A *Key 24 | Key_X *Key `config:"key-b"` 25 | Number int 26 | NumberList1 []int 27 | NumberList2 []int 28 | KeyList []Key 29 | KeyMap map[string]Key 30 | KeySlice []Key 31 | IntMap map[string]int 32 | private string 33 | } 34 | 35 | type Key struct { 36 | A string 37 | B string 38 | C string `validate:"required"` 39 | D string `validate:"required"` 40 | E string 41 | EMPTY string 42 | } 43 | 44 | type test struct { 45 | name string 46 | config *configuro.Config 47 | expected Example 48 | } 49 | 50 | type testkey struct { 51 | name string 52 | config *configuro.Config 53 | key string 54 | expected Key 55 | } 56 | 57 | func (k Key) Validate() error { 58 | if k.A != k.B { 59 | return fmt.Errorf("failed to validate key because A(%s) != B(%s)", k.A, k.B) 60 | } 61 | return nil 62 | } 63 | 64 | func TestEnvVarsEscaping(t *testing.T) { 65 | 66 | envOnlyWithoutPrefix, err := configuro.NewConfig( 67 | configuro.WithLoadFromEnvVars("CONFIG"), 68 | configuro.WithoutLoadDotEnv(), 69 | configuro.WithoutLoadFromConfigFile(), 70 | ) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | // Set Values 76 | _ = os.Setenv("CONFIG_NESTED_KEY_A", "A") 77 | _ = os.Setenv("CONFIG_NESTED_KEY_B", "B") 78 | _ = os.Setenv("CONFIG_NESTED_KEY__A_A", "AA") 79 | _ = os.Setenv("CONFIG_NESTED_KEY__A_B", "AB") 80 | _ = os.Setenv("CONFIG_NESTED_KEY-B_A", "BA") 81 | _ = os.Setenv("CONFIG_NESTED_KEY-B_B", "BB") 82 | _ = os.Setenv("CONFIG_NESTED_KEY-B_B", "BB") 83 | _ = os.Setenv("CONFIG_NESTED_KEY-B_B", "BB") 84 | 85 | tests := []test{ 86 | {name: "Renaming", config: envOnlyWithoutPrefix, expected: Example{ 87 | Nested: Nested{ 88 | Key: Key{ 89 | A: "A", 90 | B: "B", 91 | }, 92 | Key_A: &Key{ 93 | A: "AA", 94 | B: "AB", 95 | }, 96 | Key_X: &Key{ 97 | A: "BA", 98 | B: "BB", 99 | }, 100 | }, 101 | }}, 102 | } 103 | for _, test := range tests { 104 | t.Run(test.name, func(t *testing.T) { 105 | example := &Example{} 106 | err := test.config.Load(example) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | if example.Nested.Key.A != test.expected.Nested.Key.A || 111 | example.Nested.Key.B != test.expected.Nested.Key.B || 112 | example.Nested.Key_A.A != test.expected.Nested.Key_A.A || 113 | example.Nested.Key_A.B != test.expected.Nested.Key_A.B || 114 | example.Nested.Key_X.A != test.expected.Nested.Key_X.A || 115 | example.Nested.Key_X.B != test.expected.Nested.Key_X.B { 116 | t.Errorf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 117 | } 118 | }) 119 | } 120 | } 121 | 122 | func TestLoadFromEnvVarsOnly(t *testing.T) { 123 | 124 | envOnlyWithDefaultPrefix, err := configuro.NewConfig( 125 | configuro.WithoutLoadDotEnv(), 126 | configuro.WithoutLoadFromConfigFile(), 127 | configuro.WithoutEnvConfigPathOverload(), 128 | ) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | envOnlyWithPrefix, err := configuro.NewConfig( 134 | configuro.WithLoadFromEnvVars("PREFIX"), 135 | configuro.WithoutLoadDotEnv(), 136 | configuro.WithoutLoadFromConfigFile(), 137 | configuro.WithoutEnvConfigPathOverload(), 138 | configuro.WithoutExpandEnvVars(), 139 | ) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | // Set osEnvVars 145 | _ = os.Setenv("PREFIX_NESTED_KEY_A", "X") 146 | _ = os.Setenv("PREFIX_NESTED_KEY_B", "Y") 147 | _ = os.Setenv("CONFIG_NESTED_KEY_A", "A") 148 | _ = os.Setenv("CONFIG_NESTED_KEY_B", "B") 149 | _ = os.Setenv("CONFIG_NESTED_KEY_EMPTY", "") 150 | 151 | tests := []test{ 152 | {name: "withoutPrefix", config: envOnlyWithDefaultPrefix, expected: Example{Nested{Key: Key{ 153 | A: "A", 154 | B: "B", 155 | EMPTY: "", 156 | }}}}, 157 | {name: "withPrefix", config: envOnlyWithPrefix, expected: Example{Nested{Key: Key{ 158 | A: "X", 159 | B: "Y", 160 | EMPTY: "", 161 | }}}}, 162 | } 163 | for _, test := range tests { 164 | t.Run(test.name, func(t *testing.T) { 165 | example := &Example{} 166 | err := test.config.Load(example) 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | if example.Nested.Key.A != test.expected.Nested.Key.A || 171 | example.Nested.Key.B != test.expected.Nested.Key.B || 172 | example.Nested.Key.EMPTY != test.expected.Nested.Key.EMPTY { 173 | t.Fatal("Loaded Values doesn't equal expected values.") 174 | } 175 | }) 176 | } 177 | } 178 | 179 | func TestLoadDotEnv(t *testing.T) { 180 | 181 | // Clear Env that may be set up by previous tests. So that .env values are not overridden 182 | os.Clearenv() 183 | 184 | dotEnvFile, err := ioutil.TempFile("", "*.env") 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | defer func() { 189 | dotEnvFile.Close() 190 | os.RemoveAll(dotEnvFile.Name()) 191 | }() 192 | 193 | // Write Config to File 194 | dotEnvFile.WriteString(` 195 | PREFIX_NESTED_KEY_A: XXX 196 | PREFIX_NESTED_KEY_B: YYY 197 | #PREFIX_NESTED_KEY_B: XYZ 198 | PREFIX_NESTED_EMPTY: 199 | #PREFIX_NESTED_EMPTY: FD 200 | CONFIG_NESTED_KEY_A: A 201 | CONFIG_NESTED_KEY_B: B 202 | CONFIG_NESTED_KEY_EMPTY: 203 | `) 204 | 205 | envOnlyWithPrefix, err := configuro.NewConfig( 206 | configuro.WithLoadFromEnvVars("PREFIX"), 207 | configuro.WithLoadDotEnv(dotEnvFile.Name()), 208 | configuro.WithoutLoadFromConfigFile(), 209 | configuro.WithoutEnvConfigPathOverload(), 210 | ) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | 215 | tests := []test{ 216 | {name: "withPrefix", config: envOnlyWithPrefix, expected: Example{Nested{Key: Key{ 217 | A: "XXX", 218 | B: "YYY", 219 | EMPTY: "", 220 | }}}}, 221 | } 222 | for _, test := range tests { 223 | t.Run(test.name, func(t *testing.T) { 224 | example := &Example{} 225 | err := test.config.Load(example) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | 230 | if example.Nested.Key.A != test.expected.Nested.Key.A || 231 | example.Nested.Key.B != test.expected.Nested.Key.B || 232 | example.Nested.Key.EMPTY != test.expected.Nested.Key.EMPTY { 233 | t.Fatal("Loaded Values doesn't equal expected values.") 234 | } 235 | }) 236 | } 237 | } 238 | 239 | func TestLoadFromFileThatDoesntExist(t *testing.T) { 240 | configLoader, err := configuro.NewConfig( 241 | configuro.WithLoadFromEnvVars("XXX"), 242 | configuro.WithLoadDotEnv(""), 243 | configuro.WithLoadFromConfigFile("filethatdoesntexist.yml", false), 244 | configuro.WithEnvConfigPathOverload(""), 245 | ) 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | 250 | tests := []test{ 251 | {name: "LoadFromFileThatDoesntExist", config: configLoader, expected: Example{}}, 252 | } 253 | for _, test := range tests { 254 | t.Run(test.name, func(t *testing.T) { 255 | example := &Example{} 256 | err := test.config.Load(example) 257 | if err != nil { 258 | t.Fatal(err) 259 | } 260 | 261 | if example.Nested.Key.A != test.expected.Nested.Key.A || 262 | example.Nested.Key.B != test.expected.Nested.Key.B || 263 | example.Nested.Key_A != test.expected.Nested.Key_A || 264 | example.Nested.Key_X != test.expected.Nested.Key_X { 265 | t.Fatalf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | func TestLoadFromFileThatDoesntExistError(t *testing.T) { 272 | configLoader, err := configuro.NewConfig( 273 | configuro.WithLoadFromConfigFile("filethatdoesntexist.yml", true), 274 | ) 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | 279 | err = configLoader.Load(&Example{}) 280 | 281 | if err == nil { 282 | t.Fatal("Load should raise error if ErrIfFileNotFound was set to true") 283 | } 284 | if !strings.Contains(err.Error(), "error config file not found") { 285 | t.Fatal("Load should raise not found error if ErrIfFileNotFound was set to true") 286 | } 287 | } 288 | 289 | func TestLoadFromFileOnly(t *testing.T) { 290 | configFileYaml, err := ioutil.TempFile("", "TestLoadFromFileOnly*.yml") 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | defer func() { 296 | configFileYaml.Close() 297 | os.RemoveAll(configFileYaml.Name()) 298 | }() 299 | 300 | // Write Config to File 301 | configFileYaml.WriteString(` 302 | nested: 303 | key: 304 | a: A 305 | b: B 306 | key_a: 307 | a: AA 308 | b: AB 309 | key-b: 310 | a: BA 311 | b: BB 312 | `) 313 | 314 | configLoader, err := configuro.NewConfig( 315 | configuro.WithLoadFromEnvVars("X"), 316 | configuro.WithLoadDotEnv(""), 317 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 318 | configuro.WithEnvConfigPathOverload(""), 319 | ) 320 | if err != nil { 321 | t.Fatal(err) 322 | } 323 | 324 | tests := []test{ 325 | {name: "LoadFromFile", config: configLoader, expected: Example{ 326 | Nested: Nested{ 327 | Key: Key{ 328 | A: "A", 329 | B: "B", 330 | }, 331 | Key_A: &Key{ 332 | A: "AA", 333 | B: "AB", 334 | }, 335 | Key_X: &Key{ 336 | A: "BA", 337 | B: "BB", 338 | }, 339 | }, 340 | }}, 341 | } 342 | for _, test := range tests { 343 | t.Run(test.name, func(t *testing.T) { 344 | example := &Example{} 345 | err := test.config.Load(example) 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | 350 | if example.Nested.Key.A != test.expected.Nested.Key.A || 351 | example.Nested.Key.B != test.expected.Nested.Key.B || 352 | example.Nested.Key_A.A != test.expected.Nested.Key_A.A || 353 | example.Nested.Key_A.B != test.expected.Nested.Key_A.B || 354 | example.Nested.Key_X.A != test.expected.Nested.Key_X.A || 355 | example.Nested.Key_X.B != test.expected.Nested.Key_X.B { 356 | t.Fatalf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 357 | } 358 | }) 359 | } 360 | } 361 | 362 | func TestLoadKey(t *testing.T) { 363 | configFileYaml, err := ioutil.TempFile("", "TestLoadFromFileOnly*.yml") 364 | if err != nil { 365 | t.Fatal(err) 366 | } 367 | 368 | defer func() { 369 | configFileYaml.Close() 370 | os.RemoveAll(configFileYaml.Name()) 371 | }() 372 | 373 | // Write Config to File 374 | configFileYaml.WriteString(` 375 | nested: 376 | key: 377 | a: A 378 | b: B 379 | key_a: 380 | a: AA 381 | b: AB 382 | key-b: 383 | a: BA 384 | b: BB 385 | `) 386 | 387 | configLoader, err := configuro.NewConfig( 388 | configuro.WithLoadFromEnvVars("X"), 389 | configuro.WithLoadDotEnv(""), 390 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 391 | configuro.WithEnvConfigPathOverload(""), 392 | ) 393 | if err != nil { 394 | t.Fatal(err) 395 | } 396 | 397 | tests := []testkey{ 398 | {name: "LoadKey", config: configLoader, key: "nested.key", expected: Key{A: "A", B: "B"}}, 399 | {name: "LoadKey", config: configLoader, key: "nested.key_a", expected: Key{A: "AA", B: "AB"}}, 400 | {name: "LoadKey", config: configLoader, key: "nested.key-b", expected: Key{A: "BA", B: "BB"}}, 401 | } 402 | for _, test := range tests { 403 | t.Run(test.name, func(t *testing.T) { 404 | k := &Key{} 405 | err := test.config.LoadKey(test.key, k) 406 | if err != nil { 407 | t.Fatal(err) 408 | } 409 | 410 | if k.A != test.expected.A || k.B != test.expected.B { 411 | t.Fatalf("LoadKey Loaded Values doesn't equal expected values. loaded: %v, expected: %v", k, test.expected) 412 | } 413 | }) 414 | } 415 | } 416 | 417 | //TODO Too long, try make it better. 418 | func TestOverloadConfigDirWithEnv(t *testing.T) { 419 | 420 | os.Clearenv() 421 | 422 | err := os.MkdirAll(os.TempDir()+"/conf/", 0777) 423 | if err != nil { 424 | t.Fatal(err) 425 | } 426 | err = os.MkdirAll(os.TempDir()+"/confOverloaded1/", 0777) 427 | if err != nil { 428 | t.Fatal(err) 429 | } 430 | 431 | err = os.MkdirAll(os.TempDir()+"/confOverloaded2/", 0777) 432 | if err != nil { 433 | t.Fatal(err) 434 | } 435 | 436 | configFileYaml, err := ioutil.TempFile(os.TempDir()+"/conf/", "TestOverloadConfigDirWithEnv*.yml") 437 | if err != nil { 438 | t.Fatal(err) 439 | } 440 | 441 | configFileOverloaded1, err := os.OpenFile(os.TempDir()+"/confOverloaded1/"+filepath.Base(configFileYaml.Name()), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 442 | if err != nil { 443 | t.Fatal(err) 444 | } 445 | 446 | configFileOverloaded2, err := os.OpenFile(os.TempDir()+"/confOverloaded2/"+filepath.Base(configFileYaml.Name()), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) 447 | if err != nil { 448 | t.Fatal(err) 449 | } 450 | 451 | defer func() { 452 | configFileYaml.Close() 453 | configFileOverloaded1.Close() 454 | configFileOverloaded2.Close() 455 | os.RemoveAll(configFileYaml.Name()) 456 | os.RemoveAll(configFileOverloaded1.Name()) 457 | os.RemoveAll(configFileOverloaded2.Name()) 458 | }() 459 | 460 | // Write Config to File 461 | configFileYaml.WriteString(` 462 | nested: 463 | key: 464 | a: AA 465 | b: BB 466 | `) 467 | 468 | configFileOverloaded1.WriteString(` 469 | nested: 470 | key: 471 | a: XX 472 | b: YY 473 | `) 474 | 475 | configFileOverloaded2.WriteString(` 476 | nested: 477 | key: 478 | a: MM 479 | b: NN 480 | `) 481 | 482 | _ = os.Setenv("CONFIG_DIR", configFileOverloaded1.Name()) 483 | _ = os.Setenv("APPNAME_CONFIG_DIR", configFileOverloaded2.Name()) 484 | 485 | configLoaderEnvDefault, err := configuro.NewConfig( 486 | configuro.WithoutLoadFromEnvVars(), 487 | configuro.WithoutLoadDotEnv(), 488 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 489 | ) 490 | if err != nil { 491 | t.Fatal(err) 492 | } 493 | 494 | configLoaderWithEnvDir, err := configuro.NewConfig( 495 | configuro.WithoutLoadFromEnvVars(), 496 | configuro.WithoutLoadDotEnv(), 497 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 498 | configuro.WithEnvConfigPathOverload("APPNAME_CONFIG_DIR"), 499 | ) 500 | if err != nil { 501 | t.Fatal(err) 502 | } 503 | 504 | tests := []test{ 505 | {name: "configLoaderEnvDirDefault", config: configLoaderEnvDefault, expected: Example{ 506 | Nested: Nested{ 507 | Key: Key{ 508 | A: "XX", 509 | B: "YY", 510 | }, 511 | }, 512 | }}, 513 | {name: "configLoaderWithEnvDir", config: configLoaderWithEnvDir, expected: Example{ 514 | Nested: Nested{ 515 | Key: Key{ 516 | A: "MM", 517 | B: "NN", 518 | }, 519 | }, 520 | }}, 521 | } 522 | for _, test := range tests { 523 | t.Run(test.name, func(t *testing.T) { 524 | example := &Example{} 525 | err := test.config.Load(example) 526 | if err != nil { 527 | t.Fatal(err) 528 | } 529 | 530 | if example.Nested.Key.A != test.expected.Nested.Key.A || 531 | example.Nested.Key.B != test.expected.Nested.Key.B { 532 | t.Fatalf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 533 | } 534 | }) 535 | } 536 | } 537 | 538 | func TestExpandEnvVar(t *testing.T) { 539 | configFileYaml, err := ioutil.TempFile("", "TestExpandEnvVar*.yml") 540 | if err != nil { 541 | t.Fatal(err) 542 | } 543 | 544 | defer func() { 545 | configFileYaml.Close() 546 | os.RemoveAll(configFileYaml.Name()) 547 | }() 548 | 549 | _ = os.Setenv("KEY_A", "AA") 550 | _ = os.Setenv("KEY_B", "B") 551 | _ = os.Setenv("KEY_D", "") 552 | _ = os.Setenv("OBJECT", `{"a":123, "b": "abc"}`) 553 | _ = os.Setenv("NUMBER", "123456") 554 | _ = os.Setenv("NUMBERLIST1", "1,2,3") 555 | _ = os.Setenv("NUMBERLIST2", "[\"4\",5,6]") 556 | _ = os.Setenv("INTMAP", `{"a":123, "b": "456"}`) 557 | 558 | // Write Config to File 559 | configFileYaml.WriteString(` 560 | nested: 561 | key: 562 | a: ${KEY_A} 563 | b: A${KEY_B}C 564 | c: ${KEY_C|defaultC} 565 | d: ${KEY_D|defaultD} 566 | e: ${KEY_E|} 567 | key_a: ${OBJECT} 568 | number: ${NUMBER} 569 | numberList1: ${NUMBERLIST1} 570 | numberList2: ${NUMBERLIST2} 571 | IntMap: ${INTMAP} 572 | `) 573 | 574 | envOnlyWithoutPrefix, err := configuro.NewConfig( 575 | configuro.WithExpandEnvVars(), 576 | configuro.WithLoadFromEnvVars("X"), 577 | configuro.WithLoadDotEnv(""), 578 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 579 | configuro.WithEnvConfigPathOverload(""), 580 | ) 581 | if err != nil { 582 | t.Fatal(err) 583 | } 584 | 585 | tests := []test{ 586 | {name: "withoutPrefix", config: envOnlyWithoutPrefix, expected: Example{ 587 | Nested: Nested{ 588 | Key: Key{ 589 | A: "AA", 590 | B: "ABC", 591 | C: "defaultC", 592 | D: "", 593 | E: "", 594 | EMPTY: "", 595 | }, 596 | Key_A: &Key{ 597 | A: "123", 598 | B: "abc", 599 | }, 600 | Number: 123456, 601 | NumberList1: []int{1, 2, 3}, 602 | NumberList2: []int{4, 5, 6}, 603 | IntMap: map[string]int{"a": 123, "b": 456}, 604 | }, 605 | }}, 606 | } 607 | for _, test := range tests { 608 | t.Run(test.name, func(t *testing.T) { 609 | example := &Example{} 610 | err := test.config.Load(example) 611 | if err != nil { 612 | t.Fatal(err) 613 | } 614 | 615 | if example.Nested.Key.A != test.expected.Nested.Key.A || 616 | example.Nested.Key.B != test.expected.Nested.Key.B || 617 | example.Nested.Key.C != test.expected.Nested.Key.C || 618 | example.Nested.Key.D != test.expected.Nested.Key.D || 619 | example.Nested.Key.E != test.expected.Nested.Key.E || 620 | example.Nested.Key.EMPTY != test.expected.Nested.Key.EMPTY || 621 | example.Nested.Key_A.A != test.expected.Nested.Key_A.A || 622 | example.Nested.Key_A.B != test.expected.Nested.Key_A.B || 623 | example.Nested.Number != test.expected.Nested.Number || 624 | example.Nested.IntMap["a"] != test.expected.Nested.IntMap["a"] || 625 | example.Nested.IntMap["b"] != test.expected.Nested.IntMap["b"] || 626 | !equalSlice(example.Nested.NumberList1, test.expected.Nested.NumberList1) || 627 | !equalSlice(example.Nested.NumberList2, test.expected.Nested.NumberList2) { 628 | t.Fatalf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 629 | } 630 | }) 631 | } 632 | } 633 | 634 | func TestChangeTagName(t *testing.T) { 635 | configFileYaml, err := ioutil.TempFile("", "TestChangeTagName*.yml") 636 | if err != nil { 637 | t.Fatal(err) 638 | } 639 | defer func() { 640 | configFileYaml.Close() 641 | os.RemoveAll(configFileYaml.Name()) 642 | }() 643 | 644 | // Write Config to File 645 | configFileYaml.WriteString(` 646 | Object: 647 | keyA: aa 648 | keyB: bb 649 | key_b: xx 650 | `) 651 | 652 | type Object struct { 653 | KeyA string `config:"key_b"` 654 | KeyB string `newtag:"key_b"` 655 | } 656 | 657 | type Obj struct { 658 | Object Object 659 | } 660 | 661 | configLoaderDefaultTag, err := configuro.NewConfig( 662 | configuro.WithLoadFromEnvVars("X"), 663 | configuro.WithLoadDotEnv(""), 664 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 665 | configuro.WithEnvConfigPathOverload(""), 666 | ) 667 | if err != nil { 668 | t.Fatal(err) 669 | } 670 | 671 | configLoaderNewTag, err := configuro.NewConfig( 672 | configuro.Tag("newtag", "validate"), 673 | configuro.WithLoadFromEnvVars("X"), 674 | configuro.WithLoadDotEnv(""), 675 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 676 | configuro.WithEnvConfigPathOverload(""), 677 | ) 678 | if err != nil { 679 | t.Fatal(err) 680 | } 681 | 682 | tests := []struct { 683 | name string 684 | config *configuro.Config 685 | expected Obj 686 | }{ 687 | {name: "defaultTag", config: configLoaderDefaultTag, expected: Obj{ 688 | Object: Object{ 689 | KeyA: "xx", 690 | KeyB: "bb", 691 | }, 692 | }}, 693 | {name: "newTag", config: configLoaderNewTag, expected: Obj{ 694 | Object: Object{ 695 | KeyA: "aa", 696 | KeyB: "xx", 697 | }, 698 | }}, 699 | } 700 | for _, test := range tests { 701 | t.Run(test.name, func(t *testing.T) { 702 | example := &Obj{} 703 | err := test.config.Load(example) 704 | if err != nil { 705 | t.Fatal(err) 706 | } 707 | 708 | if example.Object.KeyA != test.expected.Object.KeyA || 709 | example.Object.KeyB != test.expected.Object.KeyB { 710 | t.Fatalf("Loaded Values doesn't equal expected values. loaded: %v, expected: %v", example, test.expected) 711 | } 712 | }) 713 | } 714 | } 715 | 716 | func TestChangeKeyDelimiter(t *testing.T) { 717 | configFileYaml, err := ioutil.TempFile("", "TestChangeKeyDelimiter*.yml") 718 | if err != nil { 719 | t.Fatal(err) 720 | } 721 | defer func() { 722 | configFileYaml.Close() 723 | os.RemoveAll(configFileYaml.Name()) 724 | }() 725 | 726 | // Write Config to File 727 | configFileYaml.WriteString(` 728 | Object: 729 | dot.key: "aa" 730 | key: "bb" 731 | `) 732 | 733 | type Obj struct { 734 | Object map[string]string 735 | } 736 | 737 | configLoaderDefaultDelimiter, err := configuro.NewConfig( 738 | configuro.WithoutEnvConfigPathOverload(), 739 | configuro.WithoutExpandEnvVars(), 740 | configuro.WithoutLoadDotEnv(), 741 | configuro.WithoutLoadFromEnvVars(), 742 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), true), 743 | ) 744 | if err != nil { 745 | t.Fatal(err) 746 | } 747 | 748 | configLoaderNewDelimiter, err := configuro.NewConfig( 749 | configuro.WithoutEnvConfigPathOverload(), 750 | configuro.WithoutExpandEnvVars(), 751 | configuro.WithoutLoadDotEnv(), 752 | configuro.WithoutLoadFromEnvVars(), 753 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), true), 754 | configuro.KeyDelimiter("::"), 755 | ) 756 | if err != nil { 757 | t.Fatal(err) 758 | } 759 | 760 | tests := []struct { 761 | name string 762 | config *configuro.Config 763 | want Obj 764 | wantErr bool 765 | }{ 766 | { 767 | name: "default key delimiter", 768 | config: configLoaderDefaultDelimiter, 769 | want: Obj{ 770 | Object: map[string]string{ 771 | "key": "bb", 772 | }}, 773 | wantErr: true, 774 | }, 775 | { 776 | name: "new key delimiter", 777 | config: configLoaderNewDelimiter, 778 | want: Obj{ 779 | Object: map[string]string{ 780 | "dot.key": "aa", 781 | "key": "bb", 782 | }}, 783 | wantErr: false, 784 | }, 785 | } 786 | 787 | for _, tt := range tests { 788 | t.Run(tt.name, func(t *testing.T) { 789 | obj := &Obj{} 790 | err := tt.config.Load(obj) 791 | if (err != nil) != tt.wantErr { 792 | t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr) 793 | return 794 | } 795 | if !reflect.DeepEqual(*obj, tt.want) { 796 | t.Errorf("Load() obj = %v, want %v", obj, tt.want) 797 | } 798 | }) 799 | } 800 | } 801 | 802 | func TestValidateByTag(t *testing.T) { 803 | configFileYaml, err := ioutil.TempFile("", "TestValidateByTag*.yml") 804 | if err != nil { 805 | t.Fatal(err) 806 | } 807 | defer func() { 808 | configFileYaml.Close() 809 | os.RemoveAll(configFileYaml.Name()) 810 | }() 811 | 812 | // Write Config to File 813 | configFileYaml.WriteString(` 814 | nested: 815 | key: 816 | a: A 817 | b: A 818 | c: X 819 | `) 820 | 821 | configLoader, err := configuro.NewConfig( 822 | configuro.WithValidateByTags(), 823 | configuro.WithLoadFromEnvVars("X"), 824 | configuro.WithLoadDotEnv(""), 825 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 826 | configuro.WithEnvConfigPathOverload(""), 827 | ) 828 | if err != nil { 829 | t.Fatal(err) 830 | } 831 | 832 | example := &Example{} 833 | err = configLoader.Load(example) 834 | if err != nil { 835 | t.Fatal(err) 836 | } 837 | 838 | err = configLoader.Validate(example) 839 | if err == nil { 840 | t.Fatal("Validation with Tags was bypassed.") 841 | } else { 842 | _ = err.Error() 843 | errx, ok := err.(*configuro.ErrValidationTag) 844 | if !ok { 845 | t.Fatal("Validation with Tags Returned Wrong Error Type.") 846 | } 847 | _ = errx.Unwrap() 848 | } 849 | } 850 | 851 | func TestValidateByTagMultiErr(t *testing.T) { 852 | configFileYaml, err := ioutil.TempFile("", "TestValidateByTag*.yml") 853 | if err != nil { 854 | t.Fatal(err) 855 | } 856 | defer func() { 857 | configFileYaml.Close() 858 | os.RemoveAll(configFileYaml.Name()) 859 | }() 860 | 861 | // Write Config to File 862 | configFileYaml.WriteString(` 863 | nested: 864 | key: 865 | a: A 866 | b: A 867 | `) 868 | 869 | configLoader, err := configuro.NewConfig( 870 | configuro.WithValidateByTags(), 871 | configuro.WithoutValidateByFunc(), 872 | configuro.WithLoadFromEnvVars("X"), 873 | configuro.WithLoadDotEnv(""), 874 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 875 | configuro.WithEnvConfigPathOverload(""), 876 | ) 877 | if err != nil { 878 | t.Fatal(err) 879 | } 880 | 881 | example := &Example{} 882 | err = configLoader.Load(example) 883 | if err != nil { 884 | t.Fatal(err) 885 | } 886 | 887 | err = configLoader.Validate(example) 888 | if err == nil { 889 | t.Fatal("Validation with Tags was bypassed.") 890 | } 891 | errsType, ok := err.(configuro.ErrValidationErrors) 892 | if !ok { 893 | t.Fatal("Validation with Tags didn't return ErrValidationErrors when multi error happen.") 894 | 895 | } 896 | 897 | Errs := errsType.Errors() 898 | 899 | for _, err := range Errs { 900 | _, ok := err.(*configuro.ErrValidationTag) 901 | if !ok { 902 | t.Fatal("Validation with Tags Returned Wrong Error Type.") 903 | } 904 | } 905 | _ = errsType.Unwrap() 906 | _ = err.Error() 907 | } 908 | 909 | func TestValidateByInterface(t *testing.T) { 910 | configFileYaml, err := ioutil.TempFile("", "TestValidateByInterface*.yml") 911 | if err != nil { 912 | t.Fatal(err) 913 | } 914 | 915 | defer func() { 916 | configFileYaml.Close() 917 | os.RemoveAll(configFileYaml.Name()) 918 | }() 919 | 920 | // Write Config to File 921 | configFileYaml.WriteString(` 922 | nested: 923 | key: 924 | a: A 925 | b: B 926 | `) 927 | 928 | configLoader, err := configuro.NewConfig( 929 | configuro.WithoutValidateByTags(), 930 | configuro.WithValidateByFunc(true, true), 931 | configuro.WithLoadFromEnvVars("X"), 932 | configuro.WithLoadDotEnv(""), 933 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 934 | configuro.WithEnvConfigPathOverload(""), 935 | ) 936 | if err != nil { 937 | t.Fatal(err) 938 | } 939 | 940 | example := &Example{} 941 | err = configLoader.Load(example) 942 | if err != nil { 943 | t.Fatal(err) 944 | } 945 | 946 | err = configLoader.Validate(example) 947 | Errs := multierr.Errors(err) 948 | if Errs == nil { 949 | t.Fatal("Validation using validator interface was bypassed.") 950 | } else { 951 | for _, err := range Errs { 952 | errx, ok := err.(*configuro.ErrValidationFunc) 953 | if !ok { 954 | t.Fatal("Validation using validator interface Returned Wrong Error Type.") 955 | } 956 | _ = errx.Unwrap() 957 | } 958 | } 959 | 960 | _ = err.Error() 961 | } 962 | 963 | func TestValidateMaps(t *testing.T) { 964 | configFileYaml, err := ioutil.TempFile("", "TestValidateMaps*.yml") 965 | if err != nil { 966 | t.Fatal(err) 967 | } 968 | defer func() { 969 | configFileYaml.Close() 970 | os.RemoveAll(configFileYaml.Name()) 971 | }() 972 | 973 | // Write Config to File 974 | configFileYaml.WriteString(` 975 | nested: 976 | KeyMap: 977 | One: 978 | a: ONE 979 | b: ONE 980 | Two: 981 | a: ONE 982 | b: TWO 983 | `) 984 | 985 | configLoader, err := configuro.NewConfig( 986 | configuro.WithoutValidateByTags(), 987 | configuro.WithLoadFromEnvVars("X"), 988 | configuro.WithLoadDotEnv(""), 989 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 990 | configuro.WithEnvConfigPathOverload(""), 991 | ) 992 | if err != nil { 993 | t.Fatal(err) 994 | } 995 | 996 | example := &Example{} 997 | err = configLoader.Load(example) 998 | if err != nil { 999 | t.Fatal(err) 1000 | } 1001 | 1002 | err = configLoader.Validate(example) 1003 | Errs := multierr.Errors(err) 1004 | if Errs == nil { 1005 | t.Fatal("Validation using validator interface was bypassed.") 1006 | } else { 1007 | for _, err := range Errs { 1008 | _, ok := err.(*configuro.ErrValidationFunc) 1009 | if !ok { 1010 | t.Fatal("Validation using validator interface Returned Wrong Error Type.") 1011 | } 1012 | } 1013 | } 1014 | } 1015 | 1016 | func TestValidateSlices(t *testing.T) { 1017 | configFileYaml, err := ioutil.TempFile("", "TestValidateSlices*.yml") 1018 | if err != nil { 1019 | t.Fatal(err) 1020 | } 1021 | 1022 | defer func() { 1023 | configFileYaml.Close() 1024 | os.RemoveAll(configFileYaml.Name()) 1025 | }() 1026 | 1027 | // Write Config to File 1028 | configFileYaml.WriteString(` 1029 | nested: 1030 | KeySlice: 1031 | - a: ONE 1032 | b: ONE 1033 | - a: ONE 1034 | b: TWO 1035 | `) 1036 | 1037 | configLoader, err := configuro.NewConfig( 1038 | configuro.WithoutValidateByTags(), 1039 | configuro.WithLoadFromEnvVars("X"), 1040 | configuro.WithLoadDotEnv(""), 1041 | configuro.WithLoadFromConfigFile(configFileYaml.Name(), false), 1042 | configuro.WithEnvConfigPathOverload(""), 1043 | ) 1044 | if err != nil { 1045 | t.Fatal(err) 1046 | } 1047 | 1048 | example := &Example{} 1049 | err = configLoader.Load(example) 1050 | if err != nil { 1051 | t.Fatal(err) 1052 | } 1053 | 1054 | err = configLoader.Validate(example) 1055 | Errs := multierr.Errors(err) 1056 | if Errs == nil { 1057 | t.Fatal("Validation using validator interface was bypassed.") 1058 | } else { 1059 | for _, err := range Errs { 1060 | _, ok := err.(*configuro.ErrValidationFunc) 1061 | if !ok { 1062 | t.Fatal("Validation using validator interface Returned Wrong Error Type.") 1063 | } 1064 | } 1065 | } 1066 | } 1067 | 1068 | func equalSlice(a, b []int) bool { 1069 | if len(a) != len(b) { 1070 | return false 1071 | } 1072 | for i, v := range a { 1073 | if v != b[i] { 1074 | return false 1075 | } 1076 | } 1077 | return true 1078 | } 1079 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package configuro 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.uber.org/multierr" 7 | "gopkg.in/go-playground/validator.v9" 8 | ) 9 | 10 | //ErrValidationErrors Error that hold multiple errors. 11 | type ErrValidationErrors struct { 12 | error 13 | } 14 | 15 | //ErrValidationTag Error if validation failed by a tag 16 | type ErrValidationTag struct { 17 | field string 18 | message string 19 | err error 20 | tag string 21 | } 22 | 23 | //ErrValidationFunc Error if validation failed by Validatable Interface. 24 | type ErrValidationFunc struct { 25 | field string 26 | err error 27 | } 28 | 29 | func newErrFieldTagValidation(field validator.FieldError, message string) *ErrValidationTag { 30 | return &ErrValidationTag{ 31 | field: field.Namespace(), 32 | message: message, 33 | err: field.(error), 34 | tag: field.Tag(), 35 | } 36 | } 37 | 38 | func newErrValidate(field string, err error) *ErrValidationFunc { 39 | //TODO Get field name for extra context (need to update the recursive validate function to supply the path of the err) 40 | return &ErrValidationFunc{ 41 | field: field, 42 | err: err, 43 | } 44 | } 45 | 46 | func (e *ErrValidationTag) Error() string { 47 | return fmt.Sprintf(`%s: %s`, e.field, e.message) 48 | } 49 | 50 | func (e *ErrValidationFunc) Error() string { 51 | return fmt.Sprintf(`%s`, e.err) 52 | } 53 | 54 | //Errors Return a list of Errors held inside ErrValidationErrors. 55 | func (e *ErrValidationErrors) Errors() []error { 56 | return multierr.Errors(e.error) 57 | } 58 | 59 | //Unwrap to support errors IS|AS 60 | func (e *ErrValidationTag) Unwrap() error { 61 | return e.err 62 | } 63 | 64 | //Unwrap to support errors IS|AS 65 | func (e *ErrValidationFunc) Unwrap() error { 66 | return e.err 67 | } 68 | 69 | //Unwrap to support errors IS|AS 70 | func (e *ErrValidationErrors) Unwrap() error { 71 | return e.error 72 | } 73 | -------------------------------------------------------------------------------- /example/config.yml: -------------------------------------------------------------------------------- 1 | number: 6800 2 | number_list: 1,2,3,4 # List can be declared using comma separated lists too. 3 | word: String set in YAML file 4 | another_word: String set in YAML file (Should be Overriden by ENV) # Replaced by CONFIG_ANOTHER__WORD in OS Env or .env. *(notice the double __ ) 5 | word_map: 6 | key1: value1 7 | key2: value2 8 | database: 9 | hosts: 10 | - localhost1:2022 11 | - localhost2:3200 12 | - ${DB_HOST_3}:4200 # DB_HOST_3 declared in OS Environment or .env will be substituted here. 13 | username: ${DB_USERNAME|John} # DB_USERNAME too, if not found, "john" is the default value. 14 | password: 123456 # Real value will be substituted by CONFIG_DATABASE_PASSWORD value declared at OS or .env 15 | logger: 16 | level: INFO 17 | debug: true 18 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sherifabdlnaby/configuro" 7 | ) 8 | 9 | //Config Is our main Application Config Struct. 10 | type Config struct { 11 | // Primitive Types 12 | Number int 13 | NumberList []int `config:"number_list"` 14 | Word string 15 | AnotherWord string `config:"another_word"` 16 | WordMap map[string]string `config:"word_map"` 17 | // Nested Objects (Ptr and None) 18 | Database *Database 19 | Logger Logger 20 | } 21 | 22 | //Database A sub-config struct 23 | type Database struct { 24 | Hosts []string 25 | Username string 26 | Password string 27 | } 28 | 29 | //Logger Another sub-config struct 30 | type Logger struct { 31 | Level string 32 | Debug bool 33 | } 34 | 35 | func main() { 36 | 37 | // Create Configuro Object 38 | Loader, err := configuro.NewConfig( 39 | configuro.WithLoadFromConfigFile("./config.yml", false)) 40 | if err != nil { 41 | panic(err) 42 | } 43 | 44 | // Create our Config holding Struct 45 | config := &Config{Word: "default value in struct."} 46 | 47 | // Load Our Config. 48 | err = Loader.Load(config) 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | // Print Result. 54 | fmt.Printf(` 55 | Config Struct: 56 | Number: %d 57 | NumberList: %v 58 | -------------- 59 | Word: %s 60 | AnotherWord: %s 61 | WordMap: %v 62 | -------------- 63 | Database: 64 | Hosts: %v 65 | Username: %s 66 | Password: %s 67 | -------------- 68 | Logger: 69 | Level: %s 70 | Debug: %t 71 | `, config.Number, config.NumberList, config.Word, config.AnotherWord, config.WordMap, config.Database.Hosts, 72 | config.Database.Username, config.Database.Password, config.Logger.Level, config.Logger.Debug) 73 | } 74 | -------------------------------------------------------------------------------- /example/other_formats/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "number": 6800, 3 | "number_list": "1,2,3,4", 4 | "word": "String set in YAML file", 5 | "another_word": "String set in YAML file (Should be Overriden by ENV)", 6 | "word_map": { 7 | "key1": "value1", 8 | "key2": "value2" 9 | }, 10 | "database": { 11 | "hosts": [ 12 | "localhost1:2022", 13 | "localhost2:3200", 14 | "${DB_HOST_3}:4200" 15 | ], 16 | "username": "${DB_USERNAME|John}", 17 | "password": 123456 18 | }, 19 | "logger": { 20 | "level": "INFO", 21 | "debug": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/other_formats/config.toml: -------------------------------------------------------------------------------- 1 | number = 6800.0 2 | number_list = "1,2,3,4" 3 | word = "String set in YAML file" 4 | another_word = "String set in YAML file (Should be Overriden by ENV)" 5 | 6 | [word_map] 7 | key1 = "value1" 8 | key2 = "value2" 9 | 10 | [database] 11 | hosts = [ 12 | "localhost1:2022", 13 | "localhost2:3200", 14 | "${DB_HOST_3}:4200" 15 | ] 16 | username = "${DB_USERNAME|John}" 17 | password = 123456.0 18 | 19 | [logger] 20 | level = "INFO" 21 | debug = true 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sherifabdlnaby/configuro 2 | 3 | require ( 4 | github.com/go-playground/locales v0.13.0 5 | github.com/go-playground/universal-translator v0.17.0 6 | github.com/go-playground/validator v9.31.0+incompatible 7 | github.com/joho/godotenv v1.3.0 8 | github.com/kr/text v0.2.0 // indirect 9 | github.com/leodido/go-urn v1.2.0 // indirect 10 | github.com/mitchellh/mapstructure v1.2.2 11 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 12 | github.com/spf13/viper v1.6.2 13 | github.com/stretchr/testify v1.6.1 // indirect 14 | go.uber.org/multierr v1.5.0 15 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect 16 | golang.org/x/mod v0.3.0 // indirect 17 | golang.org/x/sys v0.0.0-20200610111108-226ff32320da // indirect 18 | golang.org/x/tools v0.0.0-20200612220849-54c614fe050c // indirect 19 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 20 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 21 | gopkg.in/go-playground/validator.v9 v9.31.0 22 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect 23 | honnef.co/go/tools v0.0.1-2020.1.4 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 17 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 22 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 23 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 28 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 29 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 30 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 31 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 32 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 33 | github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= 34 | github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 35 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 36 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 37 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 38 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 39 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 40 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 41 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 42 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 44 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 45 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 46 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 47 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 48 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 49 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 50 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 51 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 52 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 53 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 54 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 55 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 56 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 57 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 58 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 59 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 60 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 61 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 62 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 63 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 64 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 65 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 66 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 67 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 68 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 69 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 70 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 71 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 72 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 73 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 74 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 75 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 76 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 77 | github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= 78 | github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 79 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 80 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 81 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 82 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 83 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 84 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 85 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 86 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 87 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 88 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 89 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 90 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 91 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 92 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 93 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 94 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 95 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 96 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 97 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 98 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 99 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 100 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 101 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 102 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 103 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 104 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 105 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 106 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 107 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 108 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 109 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 110 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 111 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 112 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 113 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 114 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 115 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 116 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 117 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 118 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 119 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 120 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 121 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 122 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 123 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 124 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 125 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 126 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 127 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 128 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 129 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 130 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 131 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 132 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 133 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 134 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 135 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 136 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 137 | go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= 138 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 139 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 140 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 141 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 142 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 143 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 144 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 145 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 146 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 147 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 148 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 149 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 150 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 151 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 152 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 153 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 154 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 155 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 156 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 157 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 158 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 159 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 160 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 161 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 162 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 163 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 165 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 166 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 167 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 171 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 172 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 175 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 176 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= 178 | golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 181 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 182 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 183 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 184 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 185 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 186 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 187 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 188 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 189 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 190 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 191 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 192 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 193 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 194 | golang.org/x/tools v0.0.0-20200612220849-54c614fe050c h1:g6oFfz6Cmw68izP3xsdud3Oxu145IPkeFzyRg58AKHM= 195 | golang.org/x/tools v0.0.0-20200612220849-54c614fe050c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 196 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 198 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 199 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 200 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 201 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 202 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 203 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 204 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 205 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 206 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 207 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 208 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 209 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 211 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 212 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 213 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 214 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 215 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 216 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 217 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 218 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 219 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 220 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 221 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 222 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 223 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 224 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo= 225 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 226 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 227 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 228 | honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= 229 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 230 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package configuro 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/mitchellh/mapstructure" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // Load load config into supported struct. 16 | func (c *Config) Load(configStruct interface{}) error { 17 | return c.loadInternal("", configStruct) 18 | } 19 | 20 | // Load load config with key into supported struct. 21 | func (c *Config) LoadKey(key string, configStruct interface{}) error { 22 | return c.loadInternal(key, configStruct) 23 | } 24 | 25 | func (c *Config) loadInternal(key string, configStruct interface{}) error { 26 | var err error 27 | 28 | // Bind Env Vars 29 | if c.envLoad { 30 | c.bindAllEnvsWithPrefix() 31 | } 32 | 33 | if c.configFileLoad { 34 | err := c.viper.ReadInConfig() 35 | if err != nil { 36 | pathErr, ok := err.(*os.PathError) 37 | if !ok { 38 | return fmt.Errorf("error reading config data: %v", err) 39 | } 40 | 41 | if c.configFileErrIfNotFound && pathErr.Op == "open" { 42 | return fmt.Errorf("error config file not found. err: %v", err) 43 | } 44 | } 45 | } 46 | 47 | // Unmarshalling 48 | if key == "" { 49 | err = c.viper.Unmarshal(configStruct, c.decodeHook, setTagName(c.tag)) 50 | } else { 51 | err = c.viper.UnmarshalKey(key, configStruct, c.decodeHook, setTagName(c.tag)) 52 | } 53 | 54 | if err != nil { 55 | return fmt.Errorf("error unmarshalling config: %v", err) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (c *Config) bindAllEnvsWithPrefix() { 62 | envKVRegex := regexp.MustCompile("^" + c.envPrefix + "_" + "(.*)=.*$") 63 | Envvars := os.Environ() 64 | for _, env := range Envvars { 65 | match := envKVRegex.FindSubmatch([]byte(env)) 66 | if match != nil { 67 | matchUnescaper := strings.NewReplacer("__", "_", "_", ".") 68 | matchUnescaped := matchUnescaper.Replace(string(match[1])) 69 | err := c.viper.BindEnv(matchUnescaped) 70 | 71 | if err != nil { 72 | //Should never happen tho. 73 | panic(err) 74 | } 75 | } 76 | } 77 | } 78 | 79 | func setTagName(hook string) viper.DecoderConfigOption { 80 | return func(c *mapstructure.DecoderConfig) { 81 | c.TagName = hook 82 | } 83 | } 84 | 85 | // StringJSONArrayOrSlicesToConfig will convert Json Encoded Strings to Maps or Slices, Used Primarily to support Slices and Maps in Environment variables 86 | func stringJSONArrayToSlice() func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) { 87 | return func( 88 | f reflect.Kind, 89 | t reflect.Kind, 90 | data interface{}) (interface{}, error) { 91 | if f != reflect.String || t != reflect.Slice { 92 | return data, nil 93 | } 94 | 95 | raw := data.(string) 96 | if raw == "" { 97 | return []string{}, nil 98 | } 99 | 100 | var ret interface{} 101 | if t == reflect.Slice { 102 | jsonArray := make([]interface{}, 0) 103 | err := json.Unmarshal([]byte(raw), &jsonArray) 104 | if err != nil { 105 | // Try comma separated format too 106 | val, err := mapstructure.StringToSliceHookFunc(",").(func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error))(f, t, data) 107 | if err != nil { 108 | return val, err 109 | } 110 | ret = val 111 | } else { 112 | ret = jsonArray 113 | } 114 | } 115 | 116 | return ret, nil 117 | } 118 | } 119 | 120 | func stringJSONObjToMap() func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) { 121 | return func( 122 | f reflect.Kind, 123 | t reflect.Kind, 124 | data interface{}) (interface{}, error) { 125 | if f != reflect.String || t != reflect.Map { 126 | return data, nil 127 | } 128 | 129 | raw := data.(string) 130 | if raw == "" { 131 | return []string{}, nil 132 | } 133 | 134 | var ret interface{} 135 | jsonMap := make(map[string]interface{}) 136 | err := json.Unmarshal([]byte(raw), &jsonMap) 137 | if err != nil { 138 | return raw, fmt.Errorf("couldn't map string-ifed Json to Map: %s", err.Error()) 139 | } 140 | ret = jsonMap 141 | 142 | return ret, nil 143 | } 144 | } 145 | 146 | func stringJSONObjToStruct() func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) { 147 | return func( 148 | f reflect.Kind, 149 | t reflect.Kind, 150 | data interface{}) (interface{}, error) { 151 | if f != reflect.String || t != reflect.Struct { 152 | return data, nil 153 | } 154 | 155 | raw := data.(string) 156 | if raw == "" { 157 | return []string{}, nil 158 | } 159 | 160 | var ret interface{} 161 | err := json.Unmarshal([]byte(raw), &data) 162 | if err != nil { 163 | return raw, fmt.Errorf("couldn't map string-ifed Json to Object: %s", err.Error()) 164 | } 165 | ret = data 166 | return ret, nil 167 | } 168 | } 169 | 170 | func expandEnvVariablesWithDefaults() func(f reflect.Kind, t reflect.Kind, data interface{}) (interface{}, error) { 171 | var configWithEnvExpand = regexp.MustCompile(`(\${([\w@.]+)(\|([\w@.:,]+)?)?})`) 172 | var exactMatchEnvExpand = regexp.MustCompile(`^` + configWithEnvExpand.String() + `$`) 173 | return func( 174 | f reflect.Kind, 175 | t reflect.Kind, 176 | data interface{}) (interface{}, error) { 177 | if f != reflect.String { 178 | return data, nil 179 | } 180 | 181 | raw := data.(string) 182 | ret := configWithEnvExpand.ReplaceAllStringFunc(raw, func(s string) string { 183 | matches := exactMatchEnvExpand.FindAllStringSubmatch(s, -1) 184 | if matches == nil { 185 | return s 186 | } 187 | envKey := matches[0][2] 188 | isEnvDefaultSet := matches[0][3] != "" 189 | envDefault := matches[0][4] 190 | envValue, found := os.LookupEnv(envKey) 191 | if !found { 192 | if isEnvDefaultSet { 193 | return envDefault 194 | } 195 | return s 196 | } 197 | return envValue 198 | }) 199 | return ret, nil 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package configuro 2 | 3 | import ( 4 | "reflect" 5 | 6 | "go.uber.org/multierr" 7 | "gopkg.in/go-playground/validator.v9" 8 | ) 9 | 10 | //Validatable Any Type that Implements this interface will have its WithValidateByTags() function called when validating config. 11 | type Validatable interface { 12 | Validate() error 13 | } 14 | 15 | //Validate Validates Struct using Tags and Any Fields that Implements the Validatable interface. 16 | func (c *Config) Validate(configStruct interface{}) error { 17 | 18 | var errs error 19 | 20 | if c.validateUsingTags { 21 | errs = multierr.Append(c.validateTags(configStruct), errs) 22 | } 23 | 24 | if c.validateUsingFunc { 25 | err := recursiveValidate(configStruct, c.validateRecursive, c.validateFuncStopOnFirstErr) 26 | if err != nil { 27 | errs = multierr.Append(errs, err) 28 | } 29 | } 30 | 31 | // cast errs to ErrValidationErrs if it is multierr (So we use the package error instead of 3rd party error type) 32 | if len(multierr.Errors(errs)) > 1 { 33 | return ErrValidationErrors{error: errs} 34 | } 35 | return errs 36 | } 37 | 38 | func (c *Config) validateTags(configStruct interface{}) error { 39 | var errs error 40 | // validate tags 41 | err := c.validator.Struct(configStruct) 42 | if err != nil { 43 | errorLists, ok := err.(validator.ValidationErrors) 44 | if ok { 45 | for _, tagErr := range errorLists { 46 | // Add translated Tag error. 47 | errs = multierr.Append(errs, newErrFieldTagValidation(tagErr, tagErr.Translate(c.validatorTrans))) 48 | } 49 | } else { 50 | errs = multierr.Append(errs, err) 51 | } 52 | } 53 | return errs 54 | } 55 | 56 | func recursiveValidate(obj interface{}, recursive bool, returnOnFirstErr bool) error { 57 | 58 | var errs error 59 | 60 | if reflect.ValueOf(obj).Kind() == reflect.Ptr && reflect.ValueOf(obj).IsNil() { 61 | return nil 62 | } 63 | 64 | val := reflect.ValueOf(obj) 65 | if val.Kind() == reflect.Ptr { 66 | val = val.Elem() 67 | } 68 | 69 | // Recursively WithValidateByTags Obj Fields / Elements 70 | switch val.Kind() { 71 | case reflect.Struct: 72 | if recursive { 73 | for i := 0; i < val.NumField(); i++ { 74 | field := val.Field(i) 75 | if field.CanInterface() { 76 | err := recursiveValidate(field.Interface(), recursive, returnOnFirstErr) 77 | if err != nil { 78 | errs = multierr.Append(errs, err) 79 | if returnOnFirstErr { 80 | return errs 81 | } 82 | } 83 | } 84 | } 85 | } 86 | case reflect.Map: 87 | for _, e := range val.MapKeys() { 88 | v := val.MapIndex(e) 89 | if v.CanInterface() { 90 | err := recursiveValidate(v.Interface(), recursive, returnOnFirstErr) 91 | if err != nil { 92 | errs = multierr.Append(errs, err) 93 | if returnOnFirstErr { 94 | return err 95 | } 96 | } 97 | } 98 | } 99 | case reflect.Array, reflect.Slice: 100 | for i := 0; i < val.Len(); i++ { 101 | v := val.Index(i) 102 | if v.CanInterface() { 103 | err := recursiveValidate(v.Interface(), recursive, returnOnFirstErr) 104 | if err != nil { 105 | errs = multierr.Append(errs, err) 106 | if returnOnFirstErr { 107 | return err 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // Call WithValidateByTags on the value it self 115 | if val.CanInterface() { 116 | validatable, ok := val.Interface().(Validatable) 117 | if ok { 118 | err := validatable.Validate() 119 | if err != nil { 120 | errs = multierr.Append(errs, newErrValidate("", err)) 121 | } 122 | } 123 | } 124 | 125 | return errs 126 | } 127 | --------------------------------------------------------------------------------