├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── go.yml │ ├── lint.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── TODO.md ├── _examples ├── ini.go ├── json.go ├── toml.go ├── watch_file.go ├── yaml.go └── yamlv2.go ├── config.go ├── config_test.go ├── driver.go ├── export.go ├── export_test.go ├── go.mod ├── go.sum ├── hcl ├── hcl.go └── hcl_test.go ├── hclv2 ├── hcl_v2.go └── hcl_v2_test.go ├── ini ├── ini.go └── ini_test.go ├── issues_test.go ├── json ├── json.go └── json_test.go ├── json5 ├── json5.go └── json5_test.go ├── load.go ├── load_test.go ├── options.go ├── other ├── other.go └── other_test.go ├── properties ├── properties.go └── properties_test.go ├── read.go ├── read_test.go ├── testdata ├── config.bak.json ├── emptydir │ └── .keep ├── hcl2_base.hcl ├── hcl2_example.hcl ├── hcl_base.hcl ├── hcl_example.conf ├── ini_base.ini ├── ini_base.other ├── ini_other.ini ├── issues59.ini ├── issues_139.conf ├── json-decode-example.txt ├── json_base.json ├── json_base.json5 ├── json_error.json ├── json_other.json ├── subdir │ ├── subdata.json │ └── task.json ├── test_dump_file.yaml ├── toml_base.toml ├── toml_other.toml ├── yaml-decode-example.txt ├── yaml-v3-decode-example.txt ├── yml_base.yml └── yml_other.yml ├── toml ├── toml.go └── toml_test.go ├── util.go ├── write.go ├── write_test.go ├── yaml ├── yaml.go └── yaml_test.go └── yamlv3 ├── yamlv3.go └── yamlv3_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/goutil 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: [feature, 'feat:'] 35 | - name: Update 36 | start_withs: [up] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: gomod 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | # Check for updates to GitHub Actions every weekday 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '40 14 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Unit-Tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Test on go ${{ matrix.go_version }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | go_version: [1.24, 1.23, 1.22, 1.21, 1.19, '1.20'] 22 | 23 | steps: 24 | - name: Check out code 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Go Faster 28 | uses: WillAbides/setup-go-faster@v1.14.0 29 | timeout-minutes: 3 30 | with: 31 | go-version: ${{ matrix.go_version }} 32 | 33 | - name: Run unit tests 34 | # run: go test -v -cover ./... 35 | # must add " for profile.cov on windows OS 36 | run: go test -coverprofile="profile.cov" ./... 37 | 38 | - name: Send coverage 39 | uses: shogo82148/actions-goveralls@v1 40 | with: 41 | path-to-profile: profile.cov 42 | flag-name: Go-${{ matrix.go_version }} 43 | parallel: true 44 | 45 | # notifies that all test jobs are finished. 46 | # https://github.com/shogo82148/actions-goveralls 47 | finish: 48 | needs: test 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: shogo82148/actions-goveralls@v1 52 | with: 53 | parallel-finished: true -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: CodeLinter 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - '**.go' 11 | - 'go.mod' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Code linter 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Go Faster 25 | uses: WillAbides/setup-go-faster@v1.14.0 26 | timeout-minutes: 3 27 | with: 28 | go-version: "*" 29 | 30 | - name: Revive lint check 31 | uses: docker://morphy/revive-action:v2 32 | with: 33 | # Exclude patterns, separated by semicolons (optional) 34 | exclude: "./_examples/...;./testdata/..." 35 | 36 | - name: Run static check 37 | uses: reviewdog/action-staticcheck@v1 38 | if: ${{ github.event_name == 'pull_request'}} 39 | with: 40 | github_token: ${{ secrets.github_token }} 41 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 42 | reporter: github-pr-check 43 | # Report all results. [added,diff_context,file,nofilter]. 44 | filter_mode: added 45 | # Exit with 1 when it find at least one finding. 46 | fail_on_error: true 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup ENV 21 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 22 | run: | 23 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 24 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 25 | 26 | - name: Generate changelog 27 | run: | 28 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 29 | chmod a+x /usr/local/bin/chlog 30 | chlog -c .github/changelog.yml -o changelog.md prev last 31 | 32 | # https://github.com/softprops/action-gh-release 33 | - name: Create release and upload assets 34 | uses: softprops/action-gh-release@v2 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | name: ${{ env.RELEASE_TAG }} 39 | tag_name: ${{ env.RELEASE_TAG }} 40 | body_path: changelog.md 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | # files: macos-chlog.exe 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | .idea 4 | *.patch 5 | ### Go template 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | .DS_Store 19 | vendor 20 | #go.sum -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Config 2 | 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/gookit/config?style=flat-square) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/1e0f0ca096d94ffdab375234ec4167ee)](https://app.codacy.com/gh/gookit/config?utm_source=github.com&utm_medium=referral&utm_content=gookit/config&utm_campaign=Badge_Grade_Settings) 5 | [![Build Status](https://travis-ci.org/gookit/config.svg?branch=master)](https://travis-ci.org/gookit/config) 6 | [![Actions Status](https://github.com/gookit/config/workflows/Unit-Tests/badge.svg)](https://github.com/gookit/config/actions) 7 | [![Coverage Status](https://coveralls.io/repos/github/gookit/config/badge.svg?branch=master)](https://coveralls.io/github/gookit/config?branch=master) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/gookit/config)](https://goreportcard.com/report/github.com/gookit/config) 9 | [![Go Reference](https://pkg.go.dev/badge/github.com/gookit/config/v2.svg)](https://pkg.go.dev/github.com/gookit/config/v2) 10 | 11 | `config` - 简洁、功能完善的 Go 应用程序配置管理工具库 12 | 13 | > **[EN README](README.md)** 14 | 15 | ## 功能简介 16 | 17 | - 支持多种格式: `JSON`(默认), `JSON5`, `INI`, `Properties`, `YAML`, `TOML`, `HCL`, `ENV`, `Flags` 18 | - `JSON` 内容支持注释,可以设置解析时清除注释 19 | - 其他驱动都是按需使用,不使用的不会加载编译到应用中 20 | - 支持多个文件、多数据加载 21 | - 支持从 OS ENV 变量数据加载配置 22 | - 支持从远程 URL 加载配置数据 23 | - 支持从命令行参数(`flags`)设置配置数据 24 | - 数据自动覆盖合并,加载多份数据时将按`key`自动合并 25 | - 支持丰富的自定义选项设置 26 | - `Readonly` 支持设置配置数据只读 27 | - `EnableCache` 支持设置配置数据缓存 28 | - `ParseEnv` 支持获取时自动解析string值里的ENV变量(`shell: ${SHELL}` -> `shell: /bin/zsh`) 29 | - `ParseDefault` 支持在绑定数据到结构体时解析默认值 (tag: `default:"def_value"`, 配合`ParseEnv`也支持ENV变量) 30 | - `ParseTime` 支持绑定数据到struct时自动转换 `10s`,`2m` 为 `time.Duration` 31 | - 完整选项设置请查看 `config.Options` 32 | - 支持将全部或部分配置数据绑定到结构体 `config.BindStruct("key", &s)` 33 | - 支持通过结构体标签 `default` 解析并设置默认值. eg: `default:"def_value"` 34 | - 支持从 ENV 初始化设置字段值 `default:"${APP_ENV | dev}"` 35 | - 支持通过 `.` 分隔符来按路径获取子级值,也支持自定义分隔符。 e.g `map.key` `arr.2` 36 | - 支持在配置数据更改时触发事件 37 | - 可用事件: `set.value`, `set.data`, `load.data`, `clean.data`, `reload.data` 38 | - 简洁的使用API `Get` `Int` `Uint` `Int64` `String` `Bool` `Ints` `IntMap` `Strings` `StringMap` ... 39 | - 完善的单元测试(code coverage > 95%) 40 | 41 | ## 只使用INI 42 | 43 | 如果你仅仅想用INI来做简单配置管理,推荐使用 [gookit/ini](https://github.com/gookit/ini) 44 | 45 | ### 加载 .env 文件 46 | 47 | `gookit/ini`: 提供一个子包 `dotenv`,支持从文件(eg `.env`)中导入数据到ENV 48 | 49 | ```shell 50 | go get github.com/gookit/ini/v2/dotenv 51 | ``` 52 | 53 | ## GoDoc 54 | 55 | - [godoc for github](https://godoc.org/github.com/gookit/config) 56 | 57 | ## 安装包 58 | 59 | ```bash 60 | go get github.com/gookit/config/v2 61 | ``` 62 | 63 | ## 快速使用 64 | 65 | 这里使用yaml格式内容作为示例(`testdata/yml_other.yml`): 66 | 67 | ```yaml 68 | name: app2 69 | debug: false 70 | baseKey: value2 71 | shell: ${SHELL} 72 | envKey1: ${NotExist|defValue} 73 | 74 | map1: 75 | key: val2 76 | key2: val20 77 | 78 | arr1: 79 | - val1 80 | - val21 81 | ``` 82 | 83 | ### 载入数据 84 | 85 | > 示例代码请看 [_examples/yaml.go](_examples/yaml.go): 86 | 87 | ```go 88 | package main 89 | 90 | import ( 91 | "github.com/gookit/config/v2" 92 | "github.com/gookit/config/v2/yaml" 93 | ) 94 | 95 | // go run ./examples/yaml.go 96 | func main() { 97 | // 设置选项支持ENV变量解析:当获取的值为string类型时,会尝试解析其中的ENV变量 98 | config.WithOptions(config.ParseEnv) 99 | 100 | // 添加驱动程序以支持yaml内容解析(除了JSON是默认支持,其他的则是按需使用) 101 | config.AddDriver(yaml.Driver) 102 | 103 | // 加载配置,可以同时传入多个文件 104 | err := config.LoadFiles("testdata/yml_base.yml") 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | // fmt.Printf("config data: \n %#v\n", config.Data()) 110 | 111 | // 加载更多文件 112 | err = config.LoadFiles("testdata/yml_other.yml") 113 | // 也可以一次性加载多个文件 114 | // err := config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 115 | if err != nil { 116 | panic(err) 117 | } 118 | } 119 | ``` 120 | 121 | **使用提示**: 122 | 123 | - 可以使用 `WithOptions()` 添加更多额外选项功能. 例如: `ParseEnv`, `ParseDefault` 124 | - 可以使用 `AddDriver()` 添加需要使用的格式驱动器(`json` 是默认加载的的,无需添加) 125 | - 然后就可以使用 `LoadFiles()` `LoadStrings()` 等方法加载配置数据 126 | - 可以传入多个文件,也可以调用多次 127 | - 多次加载的数据会自动按key进行合并处理 128 | 129 | ### 绑定数据到结构体 130 | 131 | > 注意:结构体默认的绑定映射tag是 `mapstructure`,可以通过设置 `Options.TagName` 来更改它 132 | 133 | ```go 134 | type User struct { 135 | Age int `mapstructure:"age"` 136 | Key string `mapstructure:"key"` 137 | UserName string `mapstructure:"user_name"` 138 | Tags []int `mapstructure:"tags"` 139 | } 140 | 141 | user := User{} 142 | err = config.BindStruct("user", &user) 143 | 144 | fmt.Println(user.UserName) // inhere 145 | ``` 146 | 147 | **更改结构标签名称** 148 | 149 | ```go 150 | config.WithOptions(func(opt *Options) { 151 | options.DecoderConfig.TagName = "config" 152 | }) 153 | 154 | // use custom tag name. 155 | type User struct { 156 | Age int `config:"age"` 157 | Key string `config:"key"` 158 | UserName string `config:"user_name"` 159 | Tags []int `config:"tags"` 160 | } 161 | 162 | user := User{} 163 | err = config.Decode(&user) 164 | ``` 165 | 166 | 将所有配置数据绑定到结构: 167 | 168 | ```go 169 | config.Decode(&myConf) 170 | // 也可以 171 | config.BindStruct("", &myConf) 172 | ``` 173 | 174 | > `config.MapOnExists` 与 `BindStruct` 一样,但仅当 key 存在时才进行映射绑定 175 | 176 | ### 快速获取数据 177 | 178 | ```go 179 | // 获取整型 180 | age := config.Int("age") 181 | fmt.Print(age) // 100 182 | 183 | // 获取布尔值 184 | val := config.Bool("debug") 185 | fmt.Print(val) // true 186 | 187 | // 获取字符串 188 | name := config.String("name") 189 | fmt.Print(name) // inhere 190 | 191 | // 获取字符串数组 192 | arr1 := config.Strings("arr1") 193 | fmt.Printf("%#v", arr1) // []string{"val1", "val21"} 194 | 195 | // 获取字符串KV映射 196 | val := config.StringMap("map1") 197 | fmt.Printf("%#v",val) // map[string]string{"key":"val2", "key2":"val20"} 198 | 199 | // 值包含ENV变量 200 | value := config.String("shell") 201 | fmt.Print(value) // /bin/zsh 202 | 203 | // 通过key路径获取值 204 | // from array 205 | value := config.String("arr1.0") 206 | fmt.Print(value) // "val1" 207 | 208 | // from map 209 | value := config.String("map1.key") 210 | fmt.Print(value) // "val2" 211 | ``` 212 | 213 | ### 设置新的值 214 | 215 | ```go 216 | // set value 217 | config.Set("name", "new name") 218 | // get 219 | name = config.String("name") 220 | fmt.Print(name) // new name 221 | ``` 222 | 223 | ## 加载配置文件 224 | 225 | - `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件 226 | - `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic 227 | 228 | > **TIP**: 更多加载方式请查看 `config.Load*` 相关方法 229 | 230 | ## 从ENV载入数据 231 | 232 | `LoadOSEnvs` 支持从环境变量中读取数据,并解析为配置数据。格式为 `ENV_NAME: config_key` 233 | 234 | - `config_key` 可以是 key path 格式。 eg: `{"DB_USERNAME": "db.username"}` 值将会映射到 `db` 配置的 `username` 235 | 236 | ```go 237 | // os env: APP_NAME=config APP_DEBUG=true 238 | // load ENV info 239 | config.LoadOSEnvs(map[string]string{"APP_NAME": "app_name", "APP_DEBUG": "app_debug"}) 240 | 241 | // read 242 | config.Bool("app_debug") // true 243 | config.String("app_name") // "config" 244 | ``` 245 | 246 | ## 从命令行参数载入数据 247 | 248 | 支持简单的从命令行 `flag` 参数解析,加载数据。 249 | 250 | - 配置参数格式为 `name:type:desc` OR `name:type` OR `name:desc` (type, desc 是可选的) 251 | - `type` 可以设置 `flag` 的类型,支持 `bool`, `int`, `string`(默认) 252 | - `desc` 可以设置 `flag` 的描述信息 253 | - `name` 可以是 key path 格式。 eg: `db.username`, input: `--db.username=someone` 值将会映射到 `db` 配置的 `username` 254 | 255 | ```go 256 | // 'debug' flag is bool type 257 | config.LoadFlags([]string{"env", "debug:bool"}) 258 | // can with flag desc message 259 | config.LoadFlags([]string{"env:set the run env"}) 260 | config.LoadFlags([]string{"debug:bool:set debug mode"}) 261 | // can set value to map key. eg: myapp --map1.sub-key=val 262 | config.LoadFlags([]string{"map1.sub-key"}) 263 | ``` 264 | 265 | Examples: 266 | 267 | ```go 268 | // flags like: --name inhere --env dev --age 99 --debug --map1.sub-key=val 269 | 270 | // load flag info 271 | keys := []string{ 272 | "name", 273 | "env:set the run env", 274 | "age:int", 275 | "debug:bool:set debug mode", 276 | "map1.sub-key", 277 | } 278 | err := config.LoadFlags(keys) 279 | 280 | // read 281 | config.String("name") // "inhere" 282 | config.String("env") // "dev" 283 | config.Int("age") // 99 284 | config.Bool("debug") // true 285 | config.Get("map1") // map[string]any{"sub-key":"val"} 286 | ``` 287 | 288 | ## 创建自定义实例 289 | 290 | 您可以创建自定义配置实例: 291 | 292 | ```go 293 | // create new instance, will auto register JSON driver 294 | myConf := config.New("my-conf") 295 | 296 | // create empty instance 297 | myConf := config.NewEmpty("my-conf") 298 | 299 | // create and with some options 300 | myConf := config.NewWithOptions("my-conf", config.ParseEnv, config.ReadOnly) 301 | ``` 302 | 303 | ## 监听配置更改 304 | 305 | 现在,您可以添加一个钩子函数来监听配置数据更改。然后,您可以执行一些自定义操作, 例如:将数据写入文件 306 | 307 | **在创建配置时添加钩子函数**: 308 | 309 | ```go 310 | hookFn := func(event string, c *Config) { 311 | fmt.Println("fire the:", event) 312 | } 313 | 314 | c := NewWithOptions("test", WithHookFunc(hookFn)) 315 | // for global config 316 | config.WithOptions(WithHookFunc(hookFn)) 317 | ``` 318 | 319 | **之后**, 当调用 `LoadXXX, Set, SetData, ClearData` 等方法时, 就会输出: 320 | 321 | ```text 322 | fire the: load.data 323 | fire the: set.value 324 | fire the: set.data 325 | fire the: clean.data 326 | ``` 327 | 328 | ### 监听载入的配置文件变动 329 | 330 | 想要监听载入的配置文件变动,并在变动时重新加载配置,你需要使用 https://github.com/fsnotify/fsnotify 库。 331 | 使用方法可以参考示例 [./_example/watch_file.go](_examples/watch_file.go) 332 | 333 | 同时,你需要监听 `reload.data` 事件: 334 | 335 | ```go 336 | config.WithOptions(config.WithHookFunc(func(event string, c *config.Config) { 337 | if event == config.OnReloadData { 338 | fmt.Println("config reloaded, you can do something ....") 339 | } 340 | })) 341 | ``` 342 | 343 | 当配置发生变化并重新加载后,你可以做相关的事情,例如:重新绑定配置到你的结构体。 344 | 345 | ## 导出配置到文件 346 | 347 | > 可以使用 `config.DumpTo(out io.Writer, format string)` 将整个配置数据导出到指定的writer, 比如 buffer,file。 348 | 349 | **示例:导出为JSON文件** 350 | 351 | ```go 352 | buf := new(bytes.Buffer) 353 | 354 | _, err := config.DumpTo(buf, config.JSON) 355 | ioutil.WriteFile("my-config.json", buf.Bytes(), 0755) 356 | ``` 357 | 358 | **示例:导出格式化的JSON** 359 | 360 | 可以设置默认变量 `JSONMarshalIndent` 的值 或 自定义新的 JSON 驱动程序。 361 | 362 | ```go 363 | config.JSONMarshalIndent = " " 364 | ``` 365 | 366 | **示例:导出为YAML文件** 367 | 368 | ```go 369 | _, err := config.DumpTo(buf, config.YAML) 370 | ioutil.WriteFile("my-config.yaml", buf.Bytes(), 0755) 371 | ``` 372 | 373 | ## 可用选项 374 | 375 | ```go 376 | // Options config options 377 | type Options struct { 378 | // parse env in string value. like: "${EnvName}" "${EnvName|default}" 379 | ParseEnv bool 380 | // ParseTime parses a duration string to time.Duration 381 | // eg: 10s, 2m 382 | ParseTime bool 383 | // config is readonly. default is False 384 | Readonly bool 385 | // enable config data cache. default is False 386 | EnableCache bool 387 | // parse key, allow find value by key path. default is True eg: 'key.sub' will find `map[key]sub` 388 | ParseKey bool 389 | // the delimiter char for split key, when `FindByPath=true`. default is '.' 390 | Delimiter byte 391 | // default write format. default is JSON 392 | DumpFormat string 393 | // default input format. default is JSON 394 | ReadFormat string 395 | // DecoderConfig setting for binding data to struct 396 | DecoderConfig *mapstructure.DecoderConfig 397 | // HookFunc on data changed. 398 | HookFunc HookFunc 399 | // ParseDefault tag on binding data to struct. tag: default 400 | ParseDefault bool 401 | } 402 | ``` 403 | 404 | > **提示**: 访问 https://pkg.go.dev/github.com/gookit/config/v2#Options 查看最新的选项信息 405 | 406 | Examples for set options: 407 | 408 | ```go 409 | config.WithOptions(config.WithTagName("mytag")) 410 | config.WithOptions(func(opt *Options) { 411 | opt.SetTagNames("config") 412 | }) 413 | ``` 414 | 415 | ### 选项: 解析默认值 416 | 417 | NEW: 支持通过结构标签 `default` 解析并设置默认值,支持嵌套解析处理。 418 | 419 | > 注意 ⚠️ 如果想要解析子结构体,需要对结构体设置 `default:""` 标记,否则不会解析到它的字段。 420 | 421 | ```go 422 | // add option: config.ParseDefault 423 | c := config.New("test").WithOptions(config.ParseDefault) 424 | 425 | // only set name 426 | c.SetData(map[string]any{ 427 | "name": "inhere", 428 | }) 429 | 430 | // age load from default tag 431 | type User struct { 432 | Age int `default:"30"` 433 | Name string 434 | Tags []int 435 | } 436 | 437 | user := &User{} 438 | goutil.MustOk(c.Decode(user)) 439 | dump.Println(user) 440 | ``` 441 | 442 | **Output**: 443 | 444 | ```shell 445 | &config_test.User { 446 | Age: int(30), 447 | Name: string("inhere"), #len=6 448 | Tags: []int [ #len=0 449 | ], 450 | }, 451 | ``` 452 | 453 | ## API方法参考 454 | 455 | ### 载入配置 456 | 457 | - `LoadData(dataSource ...any) (err error)` 从struct或map加载数据 458 | - `LoadFlags(keys []string) (err error)` 从命令行参数载入数据 459 | - `LoadOSEnvs(nameToKeyMap map[string]string)` 从ENV载入配置数据 460 | - `LoadExists(sourceFiles ...string) (err error)` 从存在的配置文件里加载数据,会忽略不存在的文件 461 | - `LoadFiles(sourceFiles ...string) (err error)` 从给定的配置文件里加载数据,有文件不存在则会panic 462 | - `LoadFromDir(dirPath, format string) (err error)` 从给定目录里加载自定格式的文件,文件名会作为 key 463 | - `LoadRemote(format, url string) (err error)` 从远程 URL 加载配置数据 464 | - `LoadSources(format string, src []byte, more ...[]byte) (err error)` 从给定格式的字节数据加载配置 465 | - `LoadStrings(format string, str string, more ...string) (err error)` 从给定格式的字符串配置里加载配置数据 466 | - `LoadFilesByFormat(format string, sourceFiles ...string) (err error)` 从给定格式的文件加载配置 467 | - `LoadExistsByFormat(format string, sourceFiles ...string) error` 从给定格式的文件加载配置,会忽略不存在的文件 468 | 469 | ### 获取值 470 | 471 | - `Bool(key string, defVal ...bool) bool` 472 | - `Int(key string, defVal ...int) int` 473 | - `Uint(key string, defVal ...uint) uint` 474 | - `Int64(key string, defVal ...int64) int64` 475 | - `Ints(key string) (arr []int)` 476 | - `IntMap(key string) (mp map[string]int)` 477 | - `Float(key string, defVal ...float64) float64` 478 | - `String(key string, defVal ...string) string` 479 | - `Strings(key string) (arr []string)` 480 | - `StringMap(key string) (mp map[string]string)` 481 | - `SubDataMap(key string) maputi.Data` 482 | - `Get(key string, findByPath ...bool) (value any)` 483 | 484 | **将数据映射到结构体:** 485 | 486 | - `BindStruct(key string, dst any) error` 487 | - `MapOnExists(key string, dst any) error` 488 | 489 | ### 设置值 490 | 491 | - `Set(key string, val any, setByPath ...bool) (err error)` 492 | 493 | ### 有用的方法 494 | 495 | - `Getenv(name string, defVal ...string) (val string)` 496 | - `AddDriver(driver Driver)` 497 | - `Data() map[string]any` 498 | - `Exists(key string, findByPath ...bool) bool` 499 | - `DumpTo(out io.Writer, format string) (n int64, err error)` 500 | - `SetData(data map[string]any)` 设置数据以覆盖 `Config.Data` 501 | 502 | ## 单元测试 503 | 504 | ```bash 505 | go test -cover 506 | // contains all sub-folder 507 | go test -cover ./... 508 | ``` 509 | 510 | ## 使用Config的项目 511 | 512 | 看看这些使用了 https://github.com/gookit/config 的项目: 513 | 514 | - https://github.com/JanDeDobbeleer/oh-my-posh A prompt theme engine for any shell. 515 | - [+ See More](https://pkg.go.dev/github.com/gookit/config?tab=importedby) 516 | 517 | ## Gookit 工具包 518 | 519 | - [gookit/ini](https://github.com/gookit/ini) INI配置读取管理,支持多文件加载,数据覆盖合并, 解析ENV变量, 解析变量引用 520 | - [gookit/rux](https://github.com/gookit/rux) Simple and fast request router for golang HTTP 521 | - [gookit/gcli](https://github.com/gookit/gcli) Go的命令行应用,工具库,运行CLI命令,支持命令行色彩,用户交互,进度显示,数据格式化显示 522 | - [gookit/event](https://github.com/gookit/event) Go实现的轻量级的事件管理、调度程序库, 支持设置监听器的优先级, 支持对一组事件进行监听 523 | - [gookit/cache](https://github.com/gookit/cache) 通用的缓存使用包装库,通过包装各种常用的驱动,来提供统一的使用API 524 | - [gookit/config](https://github.com/gookit/config) Go应用配置管理,支持多种格式(JSON, YAML, TOML, INI, HCL, ENV, Flags),多文件加载,远程文件加载,数据合并 525 | - [gookit/color](https://github.com/gookit/color) CLI 控制台颜色渲染工具库, 拥有简洁的使用API,支持16色,256色,RGB色彩渲染输出 526 | - [gookit/filter](https://github.com/gookit/filter) 提供对Golang数据的过滤,净化,转换 527 | - [gookit/validate](https://github.com/gookit/validate) Go通用的数据验证与过滤库,使用简单,内置大部分常用验证、过滤器 528 | - [gookit/goutil](https://github.com/gookit/goutil) Go 的一些工具函数,格式化,特殊处理,常用信息获取等 529 | - 更多请查看 https://github.com/gookit 530 | 531 | ## 相关包 532 | 533 | - Ini 解析 [gookit/ini/parser](https://github.com/gookit/ini/tree/master/parser) 534 | - Properties 解析 [gookit/properties](https://github.com/gookit/properties) 535 | - Yaml 解析 [go-yaml](https://github.com/go-yaml/yaml) 536 | - Toml 解析 [go toml](https://github.com/BurntSushi/toml) 537 | - 数据合并 [mergo](https://github.com/imdario/mergo) 538 | - 映射数据到结构体 [mapstructure](https://github.com/mitchellh/mapstructure) 539 | - JSON5 解析 540 | - [yosuke-furukawa/json5](https://github.com/yosuke-furukawa/json5) 541 | - [titanous/json5](https://github.com/titanous/json5) 542 | 543 | ## License 544 | 545 | **MIT** 546 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - remote `etcd` `consul` 4 | - watch changed config files and reload 5 | - [x] set default value on binding struct. use tag `defalut` 6 | -------------------------------------------------------------------------------- /_examples/ini.go: -------------------------------------------------------------------------------- 1 | // These are some sample code for YAML,TOML,JSON,INI,HCL 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/ini" 9 | ) 10 | 11 | // go run ./examples/ini.go 12 | func main() { 13 | config.WithOptions(config.ParseEnv) 14 | 15 | // add Decoder and Encoder 16 | config.AddDriver(ini.Driver) 17 | // Or 18 | // config.SetEncoder(config.Ini, ini.Encoder) 19 | 20 | err := config.LoadFiles("testdata/ini_base.ini") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | fmt.Printf("config data: \n %#v\n", config.Data()) 26 | 27 | err = config.LoadFiles("testdata/ini_other.ini") 28 | // config.LoadFiles("testdata/ini_base.ini", "testdata/ini_other.ini") 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | fmt.Printf("config data: \n %#v\n", config.Data()) 34 | fmt.Print("get config example:\n") 35 | 36 | name := config.String("name") 37 | fmt.Printf("- get string\n val: %v\n", name) 38 | 39 | // NOTICE: ini is not support array 40 | 41 | map1 := config.StringMap("map1") 42 | fmt.Printf("- get map\n val: %#v\n", map1) 43 | 44 | val0 := config.String("map1.key") 45 | fmt.Printf("- get sub-value by path 'map.key'\n val: %v\n", val0) 46 | 47 | // can parse env name(ParseEnv: true) 48 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 49 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 50 | 51 | // set value 52 | _ = config.Set("name", "new name") 53 | name = config.String("name") 54 | fmt.Printf("- set string\n val: %v\n", name) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /_examples/json.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/json" 8 | ) 9 | 10 | // go run ./examples/json.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(json.Driver) 16 | 17 | err := config.LoadFiles("testdata/json_base.json") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/json_other.json") 25 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("get string\n val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("get array\n val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("get sub-value by path 'arr.index'\n val: %#v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("get map\n val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.key") 46 | fmt.Printf("get sub-value by path 'map.key'\n val: %#v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | // set value 53 | _ = config.Set("name", "new name") 54 | name = config.String("name") 55 | fmt.Printf("set string\n val: %v\n", name) 56 | 57 | // if you want export config data 58 | // buf := new(bytes.Buffer) 59 | // _, err = config.DumpTo(buf, config.JSON) 60 | // if err != nil { 61 | // panic(err) 62 | // } 63 | // fmt.Printf("export config:\n%s", buf.String()) 64 | } 65 | -------------------------------------------------------------------------------- /_examples/toml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/toml" 8 | ) 9 | 10 | // go run ./examples/toml.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(toml.Driver) 16 | 17 | err := config.LoadFiles("testdata/toml_base.toml") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/toml_other.toml") 25 | // config.LoadFiles("testdata/toml_base.toml", "testdata/toml_other.toml") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("- get string\n val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("- get array\n val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("- get map\n val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.name") 46 | fmt.Printf("- get sub-value by path 'map.key'\n val: %v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /_examples/watch_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/fsnotify/fsnotify" 5 | "github.com/gookit/config/v2" 6 | "github.com/gookit/config/v2/yaml" 7 | "github.com/gookit/goutil" 8 | "github.com/gookit/goutil/cliutil" 9 | ) 10 | 11 | func main() { 12 | config.AddDriver(yaml.Driver) 13 | config.WithOptions( 14 | config.ParseEnv, 15 | config.WithHookFunc(func(event string, c *config.Config) { 16 | if event == config.OnReloadData { 17 | cliutil.Cyanln("config reloaded, you can do something ....") 18 | } 19 | }), 20 | ) 21 | 22 | // load app config files 23 | err := config.LoadFiles( 24 | "testdata/json_base.json", 25 | "testdata/yml_base.yml", 26 | "testdata/yml_other.yml", 27 | ) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // mock server running 33 | done := make(chan bool) 34 | 35 | // watch loaded config files 36 | err = watchConfigFiles(config.Default()) 37 | goutil.PanicErr(err) 38 | 39 | cliutil.Infoln("loaded config files is watching ...") 40 | <-done 41 | } 42 | 43 | func watchConfigFiles(cfg *config.Config) error { 44 | watcher, err := fsnotify.NewWatcher() 45 | if err != nil { 46 | return err 47 | } 48 | //noinspection GoUnhandledErrorResult 49 | defer watcher.Close() 50 | 51 | // get loaded files 52 | files := cfg.LoadedFiles() 53 | if len(files) == 0 { 54 | return nil 55 | } 56 | 57 | go func() { 58 | for { 59 | select { 60 | case event, ok := <-watcher.Events: 61 | if !ok { // 'Events' channel is closed 62 | cliutil.Infoln("'Events' channel is closed ...", event) 63 | return 64 | } 65 | 66 | // if event.Op > 0 { 67 | cliutil.Infof("file event: %s\n", event) 68 | 69 | if event.Op&fsnotify.Write == fsnotify.Write { 70 | cliutil.Infof("modified file: %s\n", event.Name) 71 | 72 | err := cfg.ReloadFiles() 73 | if err != nil { 74 | cliutil.Errorf("reload config error: %s\n", err.Error()) 75 | } 76 | } 77 | // } 78 | 79 | case err, ok := <-watcher.Errors: 80 | if ok { // 'Errors' channel is not closed 81 | cliutil.Errorf("watch file error: %s\n", err.Error()) 82 | } 83 | if err != nil { 84 | cliutil.Errorf("watch file error2: %s\n", err.Error()) 85 | } 86 | return 87 | } 88 | } 89 | }() 90 | 91 | for _, path := range files { 92 | cliutil.Infof("add watch file: %s\n", path) 93 | if err := watcher.Add(path); err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /_examples/yaml.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/yamlv3" 8 | ) 9 | 10 | // go run ./examples/yaml.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // only add decoder 15 | // config.SetDecoder(config.Yaml, yamlv3.Decoder) 16 | // Or 17 | config.AddDriver(yamlv3.Driver) 18 | 19 | err := config.LoadFiles("testdata/yml_base.yml") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/yml_other.yml") 27 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("- get string\n val: %v\n", name) 37 | 38 | arr1 := config.Strings("arr1") 39 | fmt.Printf("- get array\n val: %#v\n", arr1) 40 | 41 | val0 := config.String("arr1.0") 42 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 43 | 44 | map1 := config.StringMap("map1") 45 | fmt.Printf("- get map\n val: %#v\n", map1) 46 | 47 | val0 = config.String("map1.key") 48 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 49 | 50 | // can parse env name(ParseEnv: true) 51 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 52 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 53 | 54 | // set value 55 | config.Set("name", "new name") 56 | name = config.String("name") 57 | fmt.Printf("- set string\n val: %v\n", name) 58 | 59 | // if you want export config data 60 | // buf := new(bytes.Buffer) 61 | // _, err = config.DumpTo(buf, config.Yaml) 62 | // if err != nil { 63 | // panic(err) 64 | // } 65 | // fmt.Printf("export config:\n%s", buf.String()) 66 | } 67 | -------------------------------------------------------------------------------- /_examples/yamlv2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/yaml" 8 | ) 9 | 10 | // go run ./examples/yamlv2.go 11 | func main() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // only add decoder 15 | // config.SetDecoder(config.Yaml, yaml.Decoder) 16 | // Or 17 | config.AddDriver(yaml.Driver) 18 | 19 | err := config.LoadFiles("testdata/yml_base.yml") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/yml_other.yml") 27 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("- get string\n val: %v\n", name) 37 | 38 | arr1 := config.Strings("arr1") 39 | fmt.Printf("- get array\n val: %#v\n", arr1) 40 | 41 | val0 := config.String("arr1.0") 42 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 43 | 44 | map1 := config.StringMap("map1") 45 | fmt.Printf("- get map\n val: %#v\n", map1) 46 | 47 | val0 = config.String("map1.key") 48 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 49 | 50 | // can parse env name(ParseEnv: true) 51 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 52 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 53 | 54 | // set value 55 | config.Set("name", "new name") 56 | name = config.String("name") 57 | fmt.Printf("- set string\n val: %v\n", name) 58 | 59 | // if you want export config data 60 | // buf := new(bytes.Buffer) 61 | // _, err = config.DumpTo(buf, config.Yaml) 62 | // if err != nil { 63 | // panic(err) 64 | // } 65 | // fmt.Printf("export config:\n%s", buf.String()) 66 | } 67 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package config is a go config management implement. support YAML,TOML,JSON,INI,HCL format. 3 | 4 | Source code and other details for the project are available at GitHub: 5 | 6 | https://github.com/gookit/config 7 | 8 | JSON format content example: 9 | 10 | { 11 | "name": "app", 12 | "debug": false, 13 | "baseKey": "value", 14 | "age": 123, 15 | "envKey": "${SHELL}", 16 | "envKey1": "${NotExist|defValue}", 17 | "map1": { 18 | "key": "val", 19 | "key1": "val1", 20 | "key2": "val2" 21 | }, 22 | "arr1": [ 23 | "val", 24 | "val1", 25 | "val2" 26 | ], 27 | "lang": { 28 | "dir": "res/lang", 29 | "defLang": "en", 30 | "allowed": { 31 | "en": "val", 32 | "zh-CN": "val2" 33 | } 34 | } 35 | } 36 | 37 | Usage please see example(more example please see examples folder in the lib): 38 | */ 39 | package config 40 | 41 | import ( 42 | "fmt" 43 | "sync" 44 | ) 45 | 46 | // There are supported config format 47 | const ( 48 | Ini = "ini" 49 | Hcl = "hcl" 50 | Yml = "yml" 51 | JSON = "json" 52 | Yaml = "yaml" 53 | Toml = "toml" 54 | Prop = "properties" 55 | ) 56 | 57 | const ( 58 | // default delimiter 59 | defaultDelimiter byte = '.' 60 | // default struct tag name for binding data to struct 61 | defaultStructTag = "mapstructure" 62 | // struct tag name for set default-value on binding data 63 | defaultValueTag = "default" 64 | ) 65 | 66 | // internal vars 67 | // type intArr []int 68 | type strArr []string 69 | 70 | // type intMap map[string]int 71 | type strMap map[string]string 72 | 73 | // This is a default config manager instance 74 | var dc = New("default") 75 | 76 | // Config structure definition 77 | type Config struct { 78 | // save the latest error, will clear after read. 79 | err error 80 | // config instance name 81 | name string 82 | lock sync.RWMutex 83 | 84 | // config options 85 | opts *Options 86 | // all config data 87 | data map[string]any 88 | 89 | // loaded config files records 90 | loadedUrls []string 91 | loadedFiles []string 92 | driverNames []string 93 | // driver alias to name map. 94 | aliasMap map[string]string 95 | reloading bool 96 | 97 | // TODO Deprecated decoder and encoder, use driver instead 98 | // drivers map[string]Driver 99 | 100 | // decoders["toml"] = func(blob []byte, v any) (err error){} 101 | // decoders["yaml"] = func(blob []byte, v any) (err error){} 102 | decoders map[string]Decoder 103 | encoders map[string]Encoder 104 | 105 | // cache on got config data 106 | intCache map[string]int 107 | strCache map[string]string 108 | // iArrCache map[string]intArr TODO cache it 109 | // iMapCache map[string]intMap 110 | sArrCache map[string]strArr 111 | sMapCache map[string]strMap 112 | } 113 | 114 | // New config instance with custom options, default with JSON driver 115 | func New(name string, opts ...OptionFn) *Config { 116 | return NewEmpty(name, opts...).WithDriver(JSONDriver) 117 | } 118 | 119 | // NewGeneric create generic config instance with custom options. 120 | // 121 | // - default add options: ParseEnv, ParseDefault, ParseTime 122 | func NewGeneric(name string, opts ...OptionFn) *Config { 123 | return NewEmpty(name, ParseEnv, ParseDefault, ParseTime).WithOptions(opts...).WithDriver(JSONDriver) 124 | } 125 | 126 | // NewEmpty create config instance with custom options 127 | func NewEmpty(name string, opts ...OptionFn) *Config { 128 | c := &Config{ 129 | name: name, 130 | opts: newDefaultOption(), 131 | data: make(map[string]any), 132 | // don't add any drivers 133 | encoders: map[string]Encoder{}, 134 | decoders: map[string]Decoder{}, 135 | aliasMap: make(map[string]string), 136 | } 137 | 138 | return c.WithOptions(opts...) 139 | } 140 | 141 | // NewWith create config instance, and you can call some init func 142 | func NewWith(name string, fn func(c *Config)) *Config { 143 | return New(name).With(fn) 144 | } 145 | 146 | // NewWithOptions config instance. alias of New() 147 | func NewWithOptions(name string, opts ...OptionFn) *Config { 148 | return New(name).WithOptions(opts...) 149 | } 150 | 151 | // Default get the default instance 152 | func Default() *Config { return dc } 153 | 154 | /************************************************************* 155 | * config drivers 156 | *************************************************************/ 157 | 158 | // WithDriver set multi drivers at once. 159 | func WithDriver(drivers ...Driver) { dc.WithDriver(drivers...) } 160 | 161 | // WithDriver set multi drivers at once. 162 | func (c *Config) WithDriver(drivers ...Driver) *Config { 163 | for _, driver := range drivers { 164 | c.AddDriver(driver) 165 | } 166 | return c 167 | } 168 | 169 | // AddDriver set a decoder and encoder driver for a format. 170 | func AddDriver(driver Driver) { dc.AddDriver(driver) } 171 | 172 | // AddDriver set a decoder and encoder driver for a format. 173 | func (c *Config) AddDriver(driver Driver) { 174 | format := driver.Name() 175 | if len(driver.Aliases()) > 0 { 176 | for _, alias := range driver.Aliases() { 177 | c.aliasMap[alias] = format 178 | } 179 | } 180 | 181 | c.driverNames = append(c.driverNames, format) 182 | c.decoders[format] = driver.GetDecoder() 183 | c.encoders[format] = driver.GetEncoder() 184 | } 185 | 186 | // HasDecoder has decoder 187 | func (c *Config) HasDecoder(format string) bool { 188 | format = c.resolveFormat(format) 189 | _, ok := c.decoders[format] 190 | return ok 191 | } 192 | 193 | // HasEncoder has encoder 194 | func (c *Config) HasEncoder(format string) bool { 195 | format = c.resolveFormat(format) 196 | _, ok := c.encoders[format] 197 | return ok 198 | } 199 | 200 | // DelDriver delete driver of the format 201 | func (c *Config) DelDriver(format string) { 202 | format = c.resolveFormat(format) 203 | delete(c.decoders, format) 204 | delete(c.encoders, format) 205 | } 206 | 207 | /************************************************************* 208 | * helper methods 209 | *************************************************************/ 210 | 211 | // Name get config name 212 | func (c *Config) Name() string { return c.name } 213 | 214 | // AddAlias add alias for a format(driver name) 215 | func AddAlias(format, alias string) { dc.AddAlias(format, alias) } 216 | 217 | // AddAlias add alias for a format(driver name) 218 | // 219 | // Example: 220 | // 221 | // config.AddAlias("ini", "conf") 222 | func (c *Config) AddAlias(format, alias string) { 223 | c.aliasMap[alias] = format 224 | } 225 | 226 | // AliasMap get alias map 227 | func (c *Config) AliasMap() map[string]string { return c.aliasMap } 228 | 229 | // Error get last error, will clear after read. 230 | func (c *Config) Error() error { 231 | err := c.err 232 | c.err = nil 233 | return err 234 | } 235 | 236 | // IsEmpty of the config 237 | func (c *Config) IsEmpty() bool { 238 | return len(c.data) == 0 239 | } 240 | 241 | // LoadedUrls get loaded urls list 242 | func (c *Config) LoadedUrls() []string { return c.loadedUrls } 243 | 244 | // LoadedFiles get loaded files name 245 | func (c *Config) LoadedFiles() []string { return c.loadedFiles } 246 | 247 | // DriverNames get loaded driver names 248 | func (c *Config) DriverNames() []string { return c.driverNames } 249 | 250 | // Reset data and caches 251 | func Reset() { dc.ClearAll() } 252 | 253 | // ClearAll data and caches 254 | func ClearAll() { dc.ClearAll() } 255 | 256 | // ClearAll data and caches 257 | func (c *Config) ClearAll() { 258 | c.ClearData() 259 | c.ClearCaches() 260 | 261 | c.aliasMap = make(map[string]string) 262 | // options 263 | c.opts.Readonly = false 264 | } 265 | 266 | // ClearData clear data 267 | func (c *Config) ClearData() { 268 | c.fireHook(OnCleanData) 269 | 270 | c.data = make(map[string]any) 271 | c.loadedUrls = []string{} 272 | c.loadedFiles = []string{} 273 | } 274 | 275 | // ClearCaches clear caches 276 | func (c *Config) ClearCaches() { 277 | if c.opts.EnableCache { 278 | c.intCache = nil 279 | c.strCache = nil 280 | c.sMapCache = nil 281 | c.sArrCache = nil 282 | } 283 | } 284 | 285 | /************************************************************* 286 | * helper methods 287 | *************************************************************/ 288 | 289 | // fire hook 290 | func (c *Config) fireHook(name string) { 291 | if c.opts.HookFunc != nil { 292 | c.opts.HookFunc(name, c) 293 | } 294 | } 295 | 296 | // record error 297 | func (c *Config) addError(err error) { 298 | c.err = err 299 | } 300 | 301 | // format and record error 302 | func (c *Config) addErrorf(format string, a ...any) { 303 | c.err = fmt.Errorf(format, a...) 304 | } 305 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "github.com/gookit/goutil/maputil" 10 | "github.com/gookit/goutil/testutil" 11 | "github.com/gookit/goutil/testutil/assert" 12 | ) 13 | 14 | var jsonStr = `{ 15 | "name": "app", 16 | "debug": true, 17 | "baseKey": "value", 18 | "tagsStr": "php,go", 19 | "age": 123, 20 | "envKey": "${SHELL}", 21 | "envKey1": "${NotExist|defValue}", 22 | "invalidEnvKey": "${noClose", 23 | "map1": { 24 | "key": "val", 25 | "key1": "val1", 26 | "key2": "val2", 27 | "key4": "230", 28 | "key3": "${SHELL}" 29 | }, 30 | "arr1": [ 31 | "val", 32 | "val1", 33 | "val2" 34 | ] 35 | }` 36 | 37 | func Example() { 38 | // WithOptions(ParseEnv) 39 | 40 | // use yaml github.com/gookit/config/yamlv3 41 | // AddDriver(Yaml, yamlv3.Driver) 42 | // use toml github.com/gookit/config/toml 43 | // AddDriver(Toml, toml.Driver) 44 | // use toml github.com/gookit/config/hcl 45 | // AddDriver(Hcl, hcl.Driver) 46 | 47 | err := LoadFiles("testdata/json_base.json") 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | // fmt.Printf("config data: \n %#v\n", Data()) 53 | 54 | err = LoadFiles("testdata/json_other.json") 55 | // LoadFiles("testdata/json_base.json", "testdata/json_other.json") 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | // load from string 61 | err = LoadSources(JSON, []byte(jsonStr)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | // fmt.Printf("config data: \n %#v\n", Data()) 67 | fmt.Print("get config example:\n") 68 | 69 | name := String("name") 70 | fmt.Printf("- get string\n val: %v\n", name) 71 | 72 | arr1 := Strings("arr1") 73 | fmt.Printf("- get array\n val: %#v\n", arr1) 74 | 75 | val0 := String("arr1.0") 76 | fmt.Printf("- get sub-value by path 'arr.index'\n val: %#v\n", val0) 77 | 78 | map1 := StringMap("map1") 79 | fmt.Printf("- get map\n val: %#v\n", map1) 80 | 81 | val0 = String("map1.key") 82 | fmt.Printf("- get sub-value by path 'map.key'\n val: %#v\n", val0) 83 | 84 | // can parse env name(ParseEnv: true) 85 | fmt.Printf("get env 'envKey' val: %s\n", String("envKey", "")) 86 | fmt.Printf("get env 'envKey1' val: %s\n", String("envKey1", "")) 87 | 88 | // set value 89 | _ = Set("name", "new name") 90 | name = String("name") 91 | fmt.Printf("- set string\n val: %v\n", name) 92 | 93 | // if you want export config data 94 | // buf := new(bytes.Buffer) 95 | // _, err = config.DumpTo(buf, config.JSON) 96 | // if err != nil { 97 | // panic(err) 98 | // } 99 | // fmt.Printf("export config:\n%s", buf.String()) 100 | 101 | // Out: 102 | // get config example: 103 | // - get string 104 | // val: app 105 | // - get array 106 | // val: []string{"val", "val1", "val2"} 107 | // - get sub-value by path 'arr.index' 108 | // val: "val" 109 | // - get map 110 | // val: map[string]string{"key":"val", "key1":"val1", "key2":"val2"} 111 | // - get sub-value by path 'map.key' 112 | // val: "val" 113 | // get env 'envKey' val: /bin/zsh 114 | // get env 'envKey1' val: defValue 115 | // - set string 116 | // val: new name 117 | } 118 | 119 | func Example_exportConfig() { 120 | // Notice: before dump please set driver encoder 121 | // SetEncoder(Yaml, yaml.Encoder) 122 | 123 | ClearAll() 124 | // load from string 125 | err := LoadStrings(JSON, `{ 126 | "name": "app", 127 | "age": 34 128 | }`) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | buf := new(bytes.Buffer) 134 | _, err = DumpTo(buf, JSON) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | fmt.Printf("%s", buf.String()) 140 | 141 | // Output: 142 | // {"age":34,"name":"app"} 143 | } 144 | 145 | func BenchmarkGet(b *testing.B) { 146 | err := LoadStrings(JSON, jsonStr) 147 | if err != nil { 148 | panic(err) 149 | } 150 | 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | Get("name") 154 | } 155 | } 156 | 157 | func TestBasic(t *testing.T) { 158 | is := assert.New(t) 159 | 160 | ClearAll() 161 | c := Default() 162 | is.True(c.HasDecoder(JSON)) 163 | is.True(c.HasEncoder(JSON)) 164 | is.Eq("default", c.Name()) 165 | is.NoErr(c.Error()) 166 | 167 | c = NewWithOptions("test", Readonly, WithTagName("mytag")) 168 | opts := c.Options() 169 | is.True(opts.Readonly) 170 | is.Eq(JSON, opts.DumpFormat) 171 | is.Eq(JSON, opts.ReadFormat) 172 | is.Eq("mytag", opts.TagName) 173 | } 174 | 175 | func TestGetEnv(t *testing.T) { 176 | testutil.MockEnvValues(map[string]string{ 177 | "APP_NAME": "config", 178 | "APP_DEBUG": "true", 179 | }, func() { 180 | assert.Eq(t, "config", Getenv("APP_NAME")) 181 | assert.Eq(t, "true", Getenv("APP_DEBUG")) 182 | assert.Eq(t, "defVal", GetEnv("not-exsit", "defVal")) 183 | }) 184 | } 185 | 186 | func TestSetDecoderEncoder(t *testing.T) { 187 | is := assert.New(t) 188 | 189 | c := Default() 190 | c.ClearAll() 191 | 192 | is.True(c.HasDecoder(JSON)) 193 | is.True(c.HasEncoder(JSON)) 194 | 195 | c.DelDriver(JSON) 196 | 197 | is.False(c.HasDecoder(JSON)) 198 | is.False(c.HasEncoder(JSON)) 199 | 200 | SetDecoder(JSON, JSONDecoder) 201 | SetEncoder(JSON, JSONEncoder) 202 | 203 | is.True(c.HasDecoder(JSON)) 204 | is.True(c.HasEncoder(JSON)) 205 | } 206 | 207 | func TestDefault(t *testing.T) { 208 | is := assert.New(t) 209 | 210 | ClearAll() 211 | WithOptions(ParseEnv) 212 | is.True(GetOptions().ParseEnv) 213 | 214 | _ = LoadStrings(JSON, `{"name": "inhere"}`) 215 | 216 | buf := &bytes.Buffer{} 217 | _, err := WriteTo(buf) 218 | is.Nil(err) 219 | 220 | // add alias 221 | AddAlias("ini", "conf") 222 | is.NotEmpty(Default().AliasMap()) 223 | } 224 | 225 | func TestJSONDriver(t *testing.T) { 226 | is := assert.New(t) 227 | is.Eq("json", JSONDriver.Name()) 228 | 229 | // empty 230 | c := NewEmpty("test") 231 | is.False(c.HasDecoder(JSON)) 232 | 233 | c.AddDriver(JSONDriver) 234 | is.True(c.HasDecoder(JSON)) 235 | is.True(c.HasEncoder(JSON)) 236 | is.Len(c.DriverNames(), 1) 237 | 238 | is.Eq(byte('.'), c.Options().Delimiter) 239 | is.Eq(".", string(c.Options().Delimiter)) 240 | c.WithOptions(func(opt *Options) { 241 | opt.Delimiter = 0 242 | }) 243 | is.Eq(byte(0), c.Options().Delimiter) 244 | 245 | err := c.LoadStrings(JSON, `{"key": 1}`) 246 | is.NoErr(err) 247 | is.Eq(1, c.Int("key")) 248 | 249 | c = NewWith("test", func(c *Config) { 250 | err = c.LoadData(map[string]any{"key1": 2}) 251 | is.NoErr(err) 252 | }) 253 | is.Eq(2, c.Int("key1")) 254 | 255 | // test call JSONDriver.Encode 256 | s := c.ToJSON() 257 | is.StrContains(s, `{"key1":2}`) 258 | 259 | // set MarshalIndent 260 | JSONDriver.MarshalIndent = " " 261 | s = c.ToJSON() 262 | is.StrContains(s, ` "key1": 2`) 263 | JSONDriver.MarshalIndent = "" // reset 264 | } 265 | 266 | func TestDriver(t *testing.T) { 267 | is := assert.New(t) 268 | 269 | c := Default() 270 | is.True(c.HasDecoder(JSON)) 271 | is.True(c.HasEncoder(JSON)) 272 | 273 | c.DelDriver(JSON) 274 | is.False(c.HasDecoder(JSON)) 275 | is.False(c.HasEncoder(JSON)) 276 | 277 | AddDriver(JSONDriver) 278 | is.True(c.HasDecoder(JSON)) 279 | is.True(c.HasEncoder(JSON)) 280 | 281 | c.DelDriver(JSON) 282 | is.False(c.HasDecoder(JSON)) 283 | is.False(c.HasEncoder(JSON)) 284 | 285 | WithDriver(JSONDriver) 286 | is.True(c.HasDecoder(JSON)) 287 | is.True(c.HasEncoder(JSON)) 288 | 289 | c.DelDriver(JSON) 290 | 291 | c.SetDecoders(map[string]Decoder{JSON: JSONDecoder}) 292 | c.SetEncoders(map[string]Encoder{JSON: JSONEncoder}) 293 | is.True(c.HasDecoder(JSON)) 294 | is.True(c.HasEncoder(JSON)) 295 | } 296 | 297 | func TestStdDriver_methods(t *testing.T) { 298 | d1 := NewDriver("my001", JSONDecoder, JSONEncoder) 299 | d1.WithAlias("json") 300 | assert.Eq(t, "my001", d1.Name()) 301 | assert.Contains(t, d1.Aliases(), "json") 302 | 303 | s := `{"age": 245}` 304 | m := make(maputil.Map) 305 | err := d1.Decode([]byte(s), &m) 306 | assert.NoErr(t, err) 307 | 308 | bs, err := d1.Encode(m) 309 | assert.NoErr(t, err) 310 | assert.StrContains(t, string(bs), `{"age":245}`) 311 | } 312 | 313 | func TestOptions(t *testing.T) { 314 | is := assert.New(t) 315 | 316 | // options: ParseEnv 317 | c := New("test") 318 | c.WithOptions(ParseEnv) 319 | 320 | is.True(c.Options().ParseEnv) 321 | 322 | err := c.LoadStrings(JSON, jsonStr) 323 | is.Nil(err) 324 | 325 | str := c.String("name") 326 | is.Eq("app", str) 327 | 328 | // test: parse env name 329 | shell := os.Getenv("SHELL") 330 | // ensure env var is exist 331 | if shell == "" { 332 | _ = os.Setenv("SHELL", "/usr/bin/bash") 333 | } 334 | 335 | str = c.String("envKey") 336 | is.NotContains(str, "${") 337 | 338 | // revert 339 | if shell != "" { 340 | _ = os.Setenv("SHELL", shell) 341 | } 342 | 343 | str = c.String("invalidEnvKey") 344 | is.Contains(str, "${") 345 | 346 | str = c.String("envKey1") 347 | is.NotContains(str, "${") 348 | is.Eq("defValue", str) 349 | 350 | // options: Readonly 351 | c = New("test") 352 | c.WithOptions(Readonly) 353 | 354 | is.True(c.Options().Readonly) 355 | 356 | err = c.LoadStrings(JSON, jsonStr) 357 | is.Nil(err) 358 | 359 | str = c.String("name") 360 | is.Eq("app", str) 361 | 362 | err = c.Set("name", "new app") 363 | is.Err(err) 364 | } 365 | 366 | func TestDelimiter(t *testing.T) { 367 | // options: Delimiter 368 | is := assert.New(t) 369 | c := New("test") 370 | c.WithOptions(Delimiter(':')) 371 | is.Eq(byte(':'), c.Options().Delimiter) 372 | 373 | err := c.LoadData(map[string]any{ 374 | "top0": 1, 375 | "top1": map[string]int{"sub0": 2}, 376 | }) 377 | is.NoErr(err) 378 | // is.Eq(1, c.Int("top0")) 379 | is.Eq(2, c.Int("top1:sub0")) 380 | 381 | // load will use defaultDelimiter 382 | c = NewWithOptions("test", Delimiter(0)) 383 | is.Eq(byte(0), c.Options().Delimiter) 384 | 385 | err = c.LoadData(map[string]any{ 386 | "top0": 1, 387 | "top1": map[string]int{"sub0": 2}, 388 | }) 389 | is.NoErr(err) 390 | is.Eq(2, c.Int("top1.sub0")) 391 | } 392 | 393 | func TestEnableCache(t *testing.T) { 394 | is := assert.New(t) 395 | 396 | c := NewWithOptions("test", EnableCache) 397 | err := c.LoadStrings(JSON, jsonStr) 398 | is.Nil(err) 399 | 400 | str := c.String("name") 401 | is.Eq("app", str) 402 | 403 | // re-get, from caches 404 | str = c.String("name") 405 | is.Eq("app", str) 406 | 407 | sArr := c.Strings("arr1") 408 | is.Eq("val1", sArr[1]) 409 | 410 | // re-get, from caches 411 | sArr = c.Strings("arr1") 412 | is.Eq("val1", sArr[1]) 413 | 414 | sMap := c.StringMap("map1") 415 | is.Eq("val1", sMap["key1"]) 416 | sMap = c.StringMap("map1") 417 | is.Eq("val1", sMap["key1"]) 418 | 419 | c.ClearAll() 420 | } 421 | 422 | func TestJSONAllowComments(t *testing.T) { 423 | is := assert.New(t) 424 | 425 | m := struct { 426 | N string 427 | }{} 428 | 429 | // disable clear comments 430 | old := JSONAllowComments 431 | JSONAllowComments = false 432 | err := JSONDecoder([]byte(`{ 433 | // comments 434 | "n":"v"} 435 | `), &m) 436 | is.Err(err) 437 | 438 | JSONAllowComments = true 439 | err = JSONDecoder([]byte(`{ 440 | // comments 441 | "n":"v"} 442 | `), &m) 443 | is.NoErr(err) 444 | JSONAllowComments = old 445 | } 446 | 447 | func TestSaveFileOnSet(t *testing.T) { 448 | old := JSONMarshalIndent 449 | JSONMarshalIndent = " " 450 | defer func() { 451 | JSONMarshalIndent = old 452 | }() 453 | 454 | is := assert.New(t) 455 | c := New("test") 456 | c.WithOptions(SaveFileOnSet("testdata/config.bak.json", JSON)) 457 | 458 | err := c.LoadStrings(JSON, jsonStr) 459 | is.Nil(err) 460 | 461 | is.NoErr(c.Set("new-key", "new-value")) 462 | is.Eq("new-value", c.Get("new-key")) 463 | } 464 | 465 | func TestMapStringStringParseEnv(t *testing.T) { 466 | is := assert.New(t) 467 | c := New("test") 468 | c.WithOptions(ParseEnv) 469 | err := c.LoadStrings(JSON, jsonStr) 470 | is.Nil(err) 471 | 472 | shellVal := "/usr/bin/bash" 473 | testutil.MockEnvValue("SHELL", shellVal, func(_ string) { 474 | sMap := c.StringMap("map1") 475 | is.Eq(shellVal, sMap["key3"]) 476 | }) 477 | } 478 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/gookit/goutil/jsonutil" 7 | ) 8 | 9 | // Driver interface. 10 | // TODO refactor: rename GetDecoder() to Decode(), rename GetEncoder() to Encode() 11 | type Driver interface { 12 | Name() string 13 | Aliases() []string // alias format names, use for resolve format name 14 | GetDecoder() Decoder 15 | GetEncoder() Encoder 16 | } 17 | 18 | // DriverV2 interface. 19 | type DriverV2 interface { 20 | Name() string // driver name, also is format name. 21 | Aliases() []string // alias format names, use for resolve format name 22 | Decode(blob []byte, v any) (err error) 23 | Encode(v any) (out []byte, err error) 24 | } 25 | 26 | // Decoder for decode yml,json,toml format content 27 | type Decoder func(blob []byte, v any) (err error) 28 | 29 | // Encoder for decode yml,json,toml format content 30 | type Encoder func(v any) (out []byte, err error) 31 | 32 | // StdDriver struct 33 | type StdDriver struct { 34 | name string 35 | aliases []string 36 | decoder Decoder 37 | encoder Encoder 38 | } 39 | 40 | // NewDriver new std driver instance. 41 | func NewDriver(name string, dec Decoder, enc Encoder) *StdDriver { 42 | return &StdDriver{name: name, decoder: dec, encoder: enc} 43 | } 44 | 45 | // WithAliases set aliases for driver 46 | func (d *StdDriver) WithAliases(aliases ...string) *StdDriver { 47 | d.aliases = aliases 48 | return d 49 | } 50 | 51 | // WithAlias add alias for driver 52 | func (d *StdDriver) WithAlias(alias string) *StdDriver { 53 | d.aliases = append(d.aliases, alias) 54 | return d 55 | } 56 | 57 | // Name of driver 58 | func (d *StdDriver) Name() string { return d.name } 59 | 60 | // Aliases format name of driver 61 | func (d *StdDriver) Aliases() []string { 62 | return d.aliases 63 | } 64 | 65 | // Decode of driver 66 | func (d *StdDriver) Decode(blob []byte, v any) (err error) { 67 | return d.decoder(blob, v) 68 | } 69 | 70 | // Encode of driver 71 | func (d *StdDriver) Encode(v any) ([]byte, error) { 72 | return d.encoder(v) 73 | } 74 | 75 | // GetDecoder of driver 76 | func (d *StdDriver) GetDecoder() Decoder { 77 | return d.decoder 78 | } 79 | 80 | // GetEncoder of driver 81 | func (d *StdDriver) GetEncoder() Encoder { 82 | return d.encoder 83 | } 84 | 85 | /************************************************************* 86 | * JSON driver 87 | *************************************************************/ 88 | 89 | var ( 90 | // JSONAllowComments support write comments on json file. 91 | JSONAllowComments = true 92 | 93 | // JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data. 94 | // 95 | // Deprecated: please use JSONDriver.MarshalIndent 96 | JSONMarshalIndent string 97 | ) 98 | 99 | // JSONDecoder for json decode 100 | var JSONDecoder Decoder = func(data []byte, v any) (err error) { 101 | JSONDriver.ClearComments = JSONAllowComments 102 | return JSONDriver.Decode(data, v) 103 | } 104 | 105 | // JSONEncoder for json encode 106 | var JSONEncoder Encoder = func(v any) (out []byte, err error) { 107 | JSONDriver.MarshalIndent = JSONMarshalIndent 108 | return JSONDriver.Encode(v) 109 | } 110 | 111 | // JSONDriver instance fot json 112 | var JSONDriver = &jsonDriver{ 113 | driverName: JSON, 114 | ClearComments: JSONAllowComments, 115 | MarshalIndent: JSONMarshalIndent, 116 | } 117 | 118 | // jsonDriver for json format content 119 | type jsonDriver struct { 120 | driverName string 121 | // ClearComments before parse JSON string. 122 | ClearComments bool 123 | // MarshalIndent if not empty, will use json.MarshalIndent for encode data. 124 | MarshalIndent string 125 | } 126 | 127 | // Name of the driver 128 | func (d *jsonDriver) Name() string { 129 | return d.driverName 130 | } 131 | 132 | // Aliases of the driver 133 | func (d *jsonDriver) Aliases() []string { 134 | return nil 135 | } 136 | 137 | // Decode for the driver 138 | func (d *jsonDriver) Decode(data []byte, v any) error { 139 | if d.ClearComments { 140 | str := jsonutil.StripComments(string(data)) 141 | return json.Unmarshal([]byte(str), v) 142 | } 143 | return json.Unmarshal(data, v) 144 | } 145 | 146 | // GetDecoder for the driver 147 | func (d *jsonDriver) GetDecoder() Decoder { 148 | return d.Decode 149 | } 150 | 151 | // Encode for the driver 152 | func (d *jsonDriver) Encode(v any) (out []byte, err error) { 153 | if len(d.MarshalIndent) > 0 { 154 | return json.MarshalIndent(v, "", d.MarshalIndent) 155 | } 156 | return json.Marshal(v) 157 | } 158 | 159 | // GetEncoder for the driver 160 | func (d *jsonDriver) GetEncoder() Encoder { 161 | return d.Encode 162 | } 163 | -------------------------------------------------------------------------------- /export.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/gookit/goutil/structs" 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | // Decode all config data to the dst ptr 15 | // 16 | // Usage: 17 | // 18 | // myConf := &MyConf{} 19 | // config.Decode(myConf) 20 | func Decode(dst any) error { return dc.Decode(dst) } 21 | 22 | // Decode all config data to the dst ptr. 23 | // 24 | // It's equals: 25 | // 26 | // c.Structure("", dst) 27 | func (c *Config) Decode(dst any) error { 28 | return c.Structure("", dst) 29 | } 30 | 31 | // MapStruct alias method of the 'Structure' 32 | // 33 | // Usage: 34 | // 35 | // dbInfo := &Db{} 36 | // config.MapStruct("db", dbInfo) 37 | func MapStruct(key string, dst any) error { return dc.MapStruct(key, dst) } 38 | 39 | // MapStruct alias method of the 'Structure' 40 | func (c *Config) MapStruct(key string, dst any) error { 41 | return c.Structure(key, dst) 42 | } 43 | 44 | // BindStruct alias method of the 'Structure' 45 | func BindStruct(key string, dst any) error { return dc.BindStruct(key, dst) } 46 | 47 | // BindStruct alias method of the 'Structure' 48 | func (c *Config) BindStruct(key string, dst any) error { 49 | return c.Structure(key, dst) 50 | } 51 | 52 | // MapOnExists mapping data to the dst structure only on key exists. 53 | func MapOnExists(key string, dst any) error { 54 | return dc.MapOnExists(key, dst) 55 | } 56 | 57 | // MapOnExists mapping data to the dst structure only on key exists. 58 | // 59 | // - Support ParseEnv on mapping 60 | // - Support ParseDefault on mapping 61 | func (c *Config) MapOnExists(key string, dst any) error { 62 | err := c.Structure(key, dst) 63 | if err != nil && err == ErrNotFound { 64 | return nil 65 | } 66 | 67 | return err 68 | } 69 | 70 | // Structure get config data and binding to the dst structure. 71 | // 72 | // - Support ParseEnv on mapping 73 | // - Support ParseDefault on mapping 74 | // 75 | // Usage: 76 | // 77 | // dbInfo := Db{} 78 | // config.Structure("db", &dbInfo) 79 | func (c *Config) Structure(key string, dst any) (err error) { 80 | var data any 81 | // binding all data on key is empty. 82 | if key == "" { 83 | // fix: if c.data is nil, don't need to apply map structure 84 | if len(c.data) == 0 { 85 | // init default value by tag: default 86 | if c.opts.ParseDefault { 87 | err = structs.InitDefaults(dst, func(opt *structs.InitOptions) { 88 | opt.ParseEnv = c.opts.ParseEnv 89 | }) 90 | } 91 | return 92 | } 93 | 94 | data = c.data 95 | } else { 96 | // binding sub-data of the config 97 | var ok bool 98 | data, ok = c.GetValue(key) 99 | if !ok { 100 | return ErrNotFound 101 | } 102 | } 103 | 104 | // map structure from data 105 | bindConf := c.opts.makeDecoderConfig() 106 | // set result struct ptr 107 | bindConf.Result = dst 108 | decoder, err := mapstructure.NewDecoder(bindConf) 109 | if err == nil { 110 | if err = decoder.Decode(data); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | // init default value by tag: default 116 | if c.opts.ParseDefault { 117 | err = structs.InitDefaults(dst, func(opt *structs.InitOptions) { 118 | opt.ParseEnv = c.opts.ParseEnv 119 | }) 120 | } 121 | return err 122 | } 123 | 124 | // ToJSON string, will ignore error 125 | func (c *Config) ToJSON() string { 126 | buf := &bytes.Buffer{} 127 | 128 | _, err := c.DumpTo(buf, JSON) 129 | if err != nil { 130 | return "" 131 | } 132 | return buf.String() 133 | } 134 | 135 | // WriteTo a writer 136 | func WriteTo(out io.Writer) (int64, error) { return dc.WriteTo(out) } 137 | 138 | // WriteTo Write out config data representing the current state to a writer. 139 | func (c *Config) WriteTo(out io.Writer) (n int64, err error) { 140 | return c.DumpTo(out, c.opts.DumpFormat) 141 | } 142 | 143 | // DumpTo a writer and use format 144 | func DumpTo(out io.Writer, format string) (int64, error) { return dc.DumpTo(out, format) } 145 | 146 | // DumpTo use the format(json,yaml,toml) dump config data to a writer 147 | func (c *Config) DumpTo(out io.Writer, format string) (n int64, err error) { 148 | var ok bool 149 | var encoder Encoder 150 | 151 | format = c.resolveFormat(format) 152 | if encoder, ok = c.encoders[format]; !ok { 153 | err = errors.New("not exists/register encoder for the format: " + format) 154 | return 155 | } 156 | 157 | // is empty 158 | if len(c.data) == 0 { 159 | return 160 | } 161 | 162 | // encode data to string 163 | encoded, err := encoder(c.data) 164 | if err != nil { 165 | return 166 | } 167 | 168 | // write content to out 169 | num, _ := fmt.Fprintln(out, string(encoded)) 170 | return int64(num), nil 171 | } 172 | 173 | // DumpToFile use the format(json,yaml,toml) dump config data to a writer 174 | func (c *Config) DumpToFile(fileName string, format string) (err error) { 175 | fsFlags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC 176 | f, err := os.OpenFile(fileName, fsFlags, os.ModePerm) 177 | if err != nil { 178 | return err 179 | } 180 | 181 | _, err = c.DumpTo(f, format) 182 | if err1 := f.Close(); err1 != nil && err == nil { 183 | err = err1 184 | } 185 | return err 186 | } 187 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/dump" 9 | "github.com/gookit/goutil/jsonutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | func TestExport(t *testing.T) { 14 | is := assert.New(t) 15 | c := New("test") 16 | 17 | str := c.ToJSON() 18 | is.Eq("", str) 19 | 20 | err := c.LoadStrings(JSON, jsonStr) 21 | is.Nil(err) 22 | 23 | str = c.ToJSON() 24 | is.Contains(str, `"name":"app"`) 25 | 26 | buf := &bytes.Buffer{} 27 | _, err = c.WriteTo(buf) 28 | is.Nil(err) 29 | 30 | // test dump 31 | buf = &bytes.Buffer{} 32 | _, err = c.DumpTo(buf, "invalid") 33 | is.Err(err) 34 | _, err = c.DumpTo(buf, Yml) 35 | is.Err(err) 36 | 37 | _, err = c.DumpTo(buf, JSON) 38 | is.Nil(err) 39 | } 40 | 41 | func TestDumpTo_encode_error(t *testing.T) { 42 | is := assert.New(t) 43 | c := NewEmpty("test") 44 | is.NoErr(c.Set("age", 34)) 45 | 46 | drv := NewDriver(JSON, JSONDecoder, func(v any) (out []byte, err error) { 47 | return nil, errors.New("encode data error") 48 | }) 49 | c.WithDriver(drv) 50 | 51 | // encode error 52 | buf := &bytes.Buffer{} 53 | _, err := c.DumpTo(buf, JSON) 54 | is.ErrMsg(err, "encode data error") 55 | 56 | is.Empty(c.ToJSON()) 57 | } 58 | 59 | func TestConfig_Structure(t *testing.T) { 60 | is := assert.New(t) 61 | 62 | cfg := Default() 63 | cfg.ClearAll() 64 | 65 | err := cfg.LoadStrings(JSON, `{ 66 | "age": 28, 67 | "name": "inhere", 68 | "sports": ["pingPong", "跑步"] 69 | }`) 70 | 71 | is.Nil(err) 72 | type User struct { 73 | Age int // always float64 from JSON 74 | Name string 75 | Sports []string 76 | } 77 | 78 | user := &User{} 79 | // map all data 80 | err = MapStruct("", user) 81 | is.Nil(err) 82 | 83 | is.Eq(28, user.Age) 84 | is.Eq("inhere", user.Name) 85 | is.Eq("pingPong", user.Sports[0]) 86 | 87 | // map all data 88 | u1 := &User{} 89 | err = Decode(u1) 90 | is.Nil(err) 91 | 92 | is.Eq(28, u1.Age) 93 | is.Eq("inhere", u1.Name) 94 | is.Eq("pingPong", u1.Sports[0]) 95 | 96 | // - auto convert string to int 97 | // age use string in JSON 98 | cfg1 := New("test") 99 | err = cfg1.LoadStrings(JSON, `{ 100 | "age": "26", 101 | "name": "inhere", 102 | "sports": ["pingPong", "跑步"] 103 | }`) 104 | 105 | is.Nil(err) 106 | 107 | user1 := &User{} 108 | err = cfg1.MapStruct("", user1) 109 | is.Nil(err) 110 | 111 | dump.P(*user1) 112 | 113 | // map some data 114 | err = cfg.LoadStrings(JSON, `{ 115 | "sec": { 116 | "key": "val", 117 | "age": 120, 118 | "tags": [12, 34] 119 | } 120 | }`) 121 | is.Nil(err) 122 | 123 | some := struct { 124 | Age int 125 | Key string 126 | Tags []int 127 | }{} 128 | err = BindStruct("sec", &some) 129 | is.Nil(err) 130 | is.Eq(120, some.Age) 131 | is.Eq(12, some.Tags[0]) 132 | cfg.ClearAll() 133 | 134 | // custom data 135 | cfg = New("test") 136 | err = cfg.LoadData(map[string]any{ 137 | "key": "val", 138 | "age": 120, 139 | "tags": []int{12, 34}, 140 | }) 141 | is.NoErr(err) 142 | 143 | s1 := struct { 144 | Age int 145 | Kye string 146 | Tags []int 147 | }{} 148 | err = cfg.BindStruct("", &s1) 149 | is.Nil(err) 150 | is.Eq(120, s1.Age) 151 | is.Eq(12, s1.Tags[0]) 152 | 153 | // key not exist 154 | err = cfg.BindStruct("not-exist", &s1) 155 | is.Err(err) 156 | is.Eq("this key does not exist in the config", err.Error()) 157 | 158 | // invalid dst 159 | err = cfg.BindStruct("sec", "invalid") 160 | is.Err(err) 161 | 162 | cfg.ClearAll() 163 | } 164 | 165 | func TestMapStruct_embedded_struct_squash_false(t *testing.T) { 166 | loader := NewWithOptions("test", func(options *Options) { 167 | options.DecoderConfig.TagName = "json" 168 | options.DecoderConfig.Squash = false 169 | }) 170 | assert.False(t, loader.Options().DecoderConfig.Squash) 171 | 172 | err := loader.LoadStrings(JSON, `{ 173 | "c": "12", 174 | "test1": { 175 | "b": "34" 176 | } 177 | }`) 178 | assert.NoErr(t, err) 179 | dump.Println(loader.Data()) 180 | assert.Eq(t, 12, loader.Int("c")) 181 | assert.Eq(t, 34, loader.Int("test1.b")) 182 | 183 | type Test1 struct { 184 | B int `json:"b"` 185 | } 186 | type Test2 struct { 187 | Test1 188 | C int `json:"c"` 189 | } 190 | cfg := &Test2{} 191 | 192 | err = loader.MapStruct("", cfg) 193 | assert.NoErr(t, err) 194 | dump.Println(cfg) 195 | assert.Eq(t, 34, cfg.Test1.B) 196 | 197 | type Test3 struct { 198 | *Test1 199 | C int `json:"c"` 200 | } 201 | cfg1 := &Test3{} 202 | err = loader.MapStruct("", cfg1) 203 | assert.NoErr(t, err) 204 | dump.Println(cfg1) 205 | assert.Eq(t, 34, cfg1.Test1.B) 206 | 207 | loader.SetData(map[string]any{ 208 | "c": 120, 209 | "b": 340, 210 | }) 211 | dump.Println(loader.Data()) 212 | 213 | cfg2 := &Test3{} 214 | err = loader.BindStruct("", cfg2) 215 | 216 | cfg3 := &Test3{} 217 | _ = jsonutil.DecodeString(`{"c": 12, "b": 34}`, cfg3) 218 | 219 | dump.Println(cfg2, cfg3) 220 | } 221 | 222 | func TestMapStruct_embedded_struct_squash_true(t *testing.T) { 223 | loader := NewWithOptions("test", func(options *Options) { 224 | options.DecoderConfig.TagName = "json" 225 | options.DecoderConfig.Squash = true 226 | }) 227 | assert.True(t, loader.Options().DecoderConfig.Squash) 228 | 229 | err := loader.LoadStrings(JSON, `{ 230 | "c": "12", 231 | "test1": { 232 | "b": "34" 233 | } 234 | }`) 235 | assert.NoErr(t, err) 236 | dump.Println(loader.Data()) 237 | assert.Eq(t, 12, loader.Int("c")) 238 | assert.Eq(t, 34, loader.Int("test1.b")) 239 | 240 | type Test1 struct { 241 | B int `json:"b"` 242 | } 243 | 244 | // use value - will not set ok 245 | type Test2 struct { 246 | Test1 247 | // Test1 `json:",squash"` 248 | C int `json:"c"` 249 | } 250 | cfg := &Test2{} 251 | 252 | err = loader.MapStruct("", cfg) 253 | assert.NoErr(t, err) 254 | dump.Println(cfg) 255 | assert.Eq(t, 0, cfg.Test1.B) 256 | 257 | // use pointer 258 | type Test3 struct { 259 | *Test1 260 | C int `json:"c"` 261 | } 262 | cfg1 := &Test3{} 263 | err = loader.MapStruct("", cfg1) 264 | assert.NoErr(t, err) 265 | dump.Println(cfg1) 266 | assert.Eq(t, 34, cfg1.B) 267 | assert.Eq(t, 34, cfg1.Test1.B) 268 | 269 | loader.SetData(map[string]any{ 270 | "c": 120, 271 | "b": 340, 272 | }) 273 | dump.Println(loader.Data()) 274 | 275 | cfg2 := &Test3{} 276 | err = loader.BindStruct("", cfg2) 277 | 278 | cfg3 := &Test3{} 279 | _ = jsonutil.DecodeString(`{"c": 12, "b": 34}`, cfg3) 280 | 281 | dump.Println(cfg2, cfg3) 282 | } 283 | 284 | func TestMapOnExists(t *testing.T) { 285 | cfg := Default() 286 | cfg.ClearAll() 287 | 288 | err := cfg.LoadStrings(JSON, `{ 289 | "age": 28, 290 | "name": "inhere", 291 | "sports": ["pingPong", "跑步"] 292 | }`) 293 | assert.NoErr(t, err) 294 | assert.NoErr(t, MapOnExists("not-exists", nil)) 295 | 296 | user := &struct { 297 | Age int 298 | Name string 299 | Sports []string 300 | }{} 301 | assert.NoErr(t, MapOnExists("", user)) 302 | 303 | assert.Eq(t, 28, user.Age) 304 | assert.Eq(t, "inhere", user.Name) 305 | } 306 | 307 | func TestConfig_BindStruct_set_DecoderConfig(t *testing.T) { 308 | cfg := NewWith("test", func(c *Config) { 309 | c.opts.DecoderConfig = nil 310 | }) 311 | err := cfg.LoadStrings(JSON, `{ 312 | "age": 28, 313 | "name": "inhere", 314 | "sports": ["pingPong", "跑步"] 315 | }`) 316 | assert.NoErr(t, err) 317 | 318 | user := &struct { 319 | Age int 320 | Name string 321 | Sports []string 322 | }{} 323 | assert.NoErr(t, cfg.BindStruct("", user)) 324 | 325 | assert.Eq(t, 28, user.Age) 326 | assert.Eq(t, "inhere", user.Name) 327 | 328 | // not use ptr 329 | assert.Err(t, cfg.BindStruct("", *user)) 330 | } 331 | 332 | func TestConfig_BindStruct_error(t *testing.T) { 333 | // cfg := NewEmpty() 334 | } 335 | 336 | func TestConfig_BindStruct_default(t *testing.T) { 337 | type MyConf struct { 338 | Env string `default:"${APP_ENV | dev}"` 339 | Debug bool `default:"${APP_DEBUG | false}"` 340 | } 341 | 342 | cfg := NewWithOptions("test", ParseEnv, ParseDefault) 343 | // cfg.SetData(map[string]any{ 344 | // "env": "prod", 345 | // "debug": "true", 346 | // }) 347 | 348 | mc := &MyConf{} 349 | err := cfg.Decode(mc) 350 | dump.P(mc) 351 | assert.NoErr(t, err) 352 | assert.Eq(t, "dev", mc.Env) 353 | assert.False(t, mc.Debug) 354 | } 355 | 356 | // test DumpToFile 357 | func TestConfig_DumpToFile(t *testing.T) { 358 | cfg := New("test") 359 | err := cfg.LoadStrings(JSON, `{ 360 | "age": 28, 361 | "name": "inhere", 362 | "sports": ["pingPong", "跑步"] 363 | }`) 364 | assert.NoErr(t, err) 365 | 366 | // open file error 367 | err = cfg.DumpToFile("not-exists/some.json", JSON) 368 | assert.Err(t, err) 369 | assert.ErrSubMsg(t, err, "open not-exists/some.json") // TIP: 不同go版本错误不一样 370 | 371 | // encoder error 372 | err = cfg.DumpToFile("./testdata/test_dump_file.yaml", Yaml) 373 | assert.Err(t, err) 374 | assert.ErrMsg(t, err, "not exists/register encoder for the format: yaml") 375 | } 376 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/config/v2 2 | 3 | go 1.19 4 | 5 | require ( 6 | dario.cat/mergo v1.0.1 7 | github.com/BurntSushi/toml v1.5.0 8 | github.com/goccy/go-json v0.10.5 9 | github.com/goccy/go-yaml v1.12.0 10 | github.com/gookit/goutil v0.6.18 11 | github.com/gookit/ini/v2 v2.2.3 12 | github.com/gookit/properties v0.4.0 13 | github.com/hashicorp/hcl v1.0.0 14 | github.com/hashicorp/hcl/v2 v2.22.0 15 | github.com/mitchellh/mapstructure v1.5.0 16 | github.com/titanous/json5 v1.0.0 17 | ) 18 | 19 | require ( 20 | github.com/agext/levenshtein v1.2.3 // indirect 21 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 22 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 23 | github.com/fatih/color v1.14.1 // indirect 24 | github.com/google/go-cmp v0.6.0 // indirect 25 | github.com/gookit/color v1.5.4 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.17 // indirect 28 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 29 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 30 | github.com/zclconf/go-cty v1.13.0 // indirect 31 | golang.org/x/mod v0.17.0 // indirect 32 | golang.org/x/sync v0.10.0 // indirect 33 | golang.org/x/sys v0.28.0 // indirect 34 | golang.org/x/term v0.27.0 // indirect 35 | golang.org/x/text v0.21.0 // indirect 36 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 37 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 4 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 5 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 6 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 7 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 8 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 9 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 10 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= 14 | github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= 15 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 16 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 17 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 18 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 19 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 20 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 21 | github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= 22 | github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= 23 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 24 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 25 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 26 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 27 | github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw= 28 | github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA= 29 | github.com/gookit/ini/v2 v2.2.3 h1:nSbN+x9OfQPcMObTFP+XuHt8ev6ndv/fWWqxFhPMu2E= 30 | github.com/gookit/ini/v2 v2.2.3/go.mod h1:Vu6p7P7xcfmb8KYu3L0ek8bqu/Im63N81q208SCCZY4= 31 | github.com/gookit/properties v0.4.0 h1:e6slZWnIhzU9VSHmkxOYGixLDiFDt8sTmIKP//tP/uA= 32 | github.com/gookit/properties v0.4.0/go.mod h1:2YkGqrCwNubrJIJoMy3RHjkgFygdwSy5yHXYg+upHPM= 33 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 34 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 35 | github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= 36 | github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= 37 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 38 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 39 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 40 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 41 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 42 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 43 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 44 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 45 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 46 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 47 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 48 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 49 | github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= 50 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 51 | github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= 52 | github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= 53 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 54 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 55 | github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= 56 | github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= 57 | github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= 58 | golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= 59 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 60 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 61 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 62 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 63 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 64 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 66 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 67 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 68 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 69 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 70 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 71 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 72 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 73 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= 74 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= 77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 78 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | -------------------------------------------------------------------------------- /hcl/hcl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package hcl is driver use HCL format content as config source 3 | 4 | about HCL, please see https://github.com/hashicorp/hcl 5 | */ 6 | package hcl 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/gookit/config/v2" 12 | "github.com/hashicorp/hcl" 13 | ) 14 | 15 | // Decoder the hcl content decoder 16 | var Decoder config.Decoder = hcl.Unmarshal 17 | 18 | // Encoder the hcl content encoder 19 | var Encoder config.Encoder = func(ptr any) (out []byte, err error) { 20 | err = errors.New("HCL: is not support encode data to HCL") 21 | return 22 | } 23 | 24 | // Driver instance for hcl 25 | var Driver = config.NewDriver(config.Hcl, Decoder, Encoder).WithAlias("conf") 26 | -------------------------------------------------------------------------------- /hcl/hcl_test.go: -------------------------------------------------------------------------------- 1 | package hcl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/goutil/dump" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestDriver(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | is.Eq("hcl", Driver.Name()) 15 | 16 | c := config.NewEmpty("test") 17 | is.False(c.HasDecoder(config.Hcl)) 18 | 19 | c.AddDriver(Driver) 20 | is.True(c.HasDecoder(config.Hcl)) 21 | is.True(c.HasEncoder(config.Hcl)) 22 | 23 | _, err := Encoder("some data") 24 | is.Err(err) 25 | } 26 | 27 | func TestLoadFile(t *testing.T) { 28 | is := assert.New(t) 29 | c := config.NewEmpty("test") 30 | c.AddDriver(Driver) 31 | 32 | err := c.LoadFiles("../testdata/hcl_base.hcl") 33 | is.NoErr(err) 34 | dump.Println(c.Data()) 35 | 36 | err = c.LoadFiles("../testdata/hcl_example.conf") 37 | is.NoErr(err) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /hclv2/hcl_v2.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package hclv2 is driver use HCL format content as config source 3 | 4 | about HCL, please see https://github.com/hashicorp/hcl 5 | docs for HCL v2 https://pkg.go.dev/github.com/hashicorp/hcl/v2 6 | */ 7 | package hclv2 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/gookit/config/v2" 13 | "github.com/hashicorp/hcl/v2/hclsimple" 14 | ) 15 | 16 | // Decoder the hcl content decoder 17 | var Decoder config.Decoder = func(blob []byte, v any) (err error) { 18 | // TODO hcl2 decode data to map ptr will report error 19 | return hclsimple.Decode("hcl2/config.hcl", blob, nil, v) 20 | } 21 | 22 | // Encoder the hcl content encoder 23 | var Encoder config.Encoder = func(ptr any) (out []byte, err error) { 24 | err = errors.New("HCLv2: is not support encode data to HCL") 25 | return 26 | } 27 | 28 | // Driver instance for hcl 29 | var Driver = config.NewDriver(config.Hcl, Decoder, Encoder).WithAlias("conf") 30 | -------------------------------------------------------------------------------- /hclv2/hcl_v2_test.go: -------------------------------------------------------------------------------- 1 | package hclv2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/goutil/dump" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestDriver(t *testing.T) { 12 | is := assert.New(t) 13 | is.Eq("hcl", Driver.Name()) 14 | is.Eq(config.Hcl, Driver.Name()) 15 | 16 | c := config.NewEmpty("test") 17 | is.False(c.HasDecoder(config.Hcl)) 18 | 19 | c.AddDriver(Driver) 20 | is.True(c.HasDecoder(config.Hcl)) 21 | is.True(c.HasEncoder(config.Hcl)) 22 | 23 | _, err := Encoder("some data") 24 | is.Err(err) 25 | } 26 | 27 | func TestHcl2Package(t *testing.T) { 28 | hclStr := `io_mode = "async" 29 | 30 | service "http" "web_proxy" { 31 | listen_addr = "127.0.0.1:8080" 32 | 33 | process "main" { 34 | command = ["/usr/local/bin/awesome-app", "server"] 35 | } 36 | 37 | process "mgmt" { 38 | command = ["/usr/local/bin/awesome-app", "mgmt"] 39 | } 40 | }` 41 | 42 | mp := make(map[string]any) 43 | // mp := make(map[string]cty.Type) 44 | // err := hclsimple.Decode("test.hcl", []byte(hclStr), nil, &mp) 45 | // assert.NoErr(t, err) 46 | dump.P(hclStr, mp) 47 | t.Skip("Not completed") 48 | } 49 | 50 | func TestLoadFile(t *testing.T) { 51 | is := assert.New(t) 52 | c := config.NewEmpty("test") 53 | c.AddDriver(Driver) 54 | is.True(c.HasDecoder(config.Hcl)) 55 | 56 | t.Skip("Not completed") 57 | return 58 | err := c.LoadFiles("../testdata/hcl2_base.hcl") 59 | is.NoErr(err) 60 | dump.Println(c.Data()) 61 | 62 | err = c.LoadFiles("../testdata/hcl2_example.hcl") 63 | is.NoErr(err) 64 | } 65 | -------------------------------------------------------------------------------- /ini/ini.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package ini is driver use INI format content as config source 3 | 4 | about ini parse, please see https://github.com/gookit/ini/parser 5 | */ 6 | package ini 7 | 8 | import ( 9 | "github.com/gookit/config/v2" 10 | "github.com/gookit/ini/v2/parser" 11 | ) 12 | 13 | // Decoder the ini content decoder 14 | var Decoder config.Decoder = parser.Decode 15 | 16 | // Encoder encode data to ini content 17 | var Encoder config.Encoder = parser.Encode 18 | 19 | // Driver for ini 20 | var Driver = config.NewDriver(config.Ini, Decoder, Encoder) 21 | -------------------------------------------------------------------------------- /ini/ini_test.go: -------------------------------------------------------------------------------- 1 | package ini 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func Example() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(Driver) 16 | // Or 17 | // config.SetEncoder(config.Ini, ini.Encoder) 18 | 19 | err := config.LoadFiles("testdata/ini_base.ini") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | // fmt.Printf("config data: \n %#v\n", config.Data()) 25 | 26 | err = config.LoadFiles("testdata/ini_other.ini") 27 | // config.LoadFiles("testdata/ini_base.ini", "testdata/ini_other.ini") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | // fmt.Printf("config data: \n %#v\n", config.Data()) 33 | fmt.Print("get config example:\n") 34 | 35 | name := config.String("name") 36 | fmt.Printf("get string\n - val: %v\n", name) 37 | 38 | // NOTICE: ini is not support array 39 | 40 | map1 := config.StringMap("map1") 41 | fmt.Printf("get map\n - val: %#v\n", map1) 42 | 43 | val0 := config.String("map1.key") 44 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 45 | 46 | // can parse env name(ParseEnv: true) 47 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 48 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 49 | 50 | // set value 51 | _ = config.Set("name", "new name") 52 | name = config.String("name") 53 | fmt.Printf("set string\n - val: %v\n", name) 54 | } 55 | 56 | func TestDriver(t *testing.T) { 57 | st := assert.New(t) 58 | 59 | st.Eq("ini", Driver.Name()) 60 | // st.IsType(new(Encoder), JSONDriver.GetEncoder()) 61 | 62 | c := config.NewEmpty("test") 63 | st.False(c.HasDecoder(config.Ini)) 64 | 65 | c.AddDriver(Driver) 66 | st.True(c.HasDecoder(config.Ini)) 67 | st.True(c.HasEncoder(config.Ini)) 68 | 69 | _, err := Encoder(map[string]any{"k": "v"}) 70 | st.Nil(err) 71 | 72 | _, err = Encoder("invalid") 73 | st.Err(err) 74 | } 75 | -------------------------------------------------------------------------------- /issues_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gookit/config/v2" 13 | "github.com/gookit/config/v2/ini" 14 | "github.com/gookit/config/v2/yaml" 15 | "github.com/gookit/config/v2/yamlv3" 16 | "github.com/gookit/goutil/dump" 17 | "github.com/gookit/goutil/fsutil" 18 | "github.com/gookit/goutil/testutil" 19 | "github.com/gookit/goutil/testutil/assert" 20 | ) 21 | 22 | // https://github.com/gookit/config/issues/37 23 | func TestIssues_37(t *testing.T) { 24 | is := assert.New(t) 25 | 26 | c := config.New("test") 27 | c.AddDriver(yaml.Driver) 28 | 29 | err := c.LoadStrings(config.JSON, `{ 30 | "lang": { 31 | "allowed": { 32 | "en": "ddd" 33 | } 34 | } 35 | } 36 | `) 37 | is.NoErr(err) 38 | dump.Println(c.Data()) 39 | 40 | // update yaml pkg to goccy/go-yaml 41 | is.NotPanics(func() { 42 | _ = c.LoadStrings(config.Yaml, ` 43 | lang: 44 | allowed: 45 | en: "666" 46 | `) 47 | }) 48 | } 49 | 50 | // https://github.com/gookit/config/issues/37 51 | func TestIssues37_yaml_v3(t *testing.T) { 52 | is := assert.New(t) 53 | 54 | c := config.New("test") 55 | c.AddDriver(yamlv3.Driver) 56 | 57 | err := c.LoadStrings(config.JSON, ` 58 | { 59 | "lang": { 60 | "allowed": { 61 | "en": "ddd" 62 | } 63 | } 64 | } 65 | `) 66 | is.NoErr(err) 67 | dump.Println(c.Data()) 68 | 69 | err = c.LoadStrings(config.Yaml, ` 70 | lang: 71 | newKey: hhh 72 | allowed: 73 | en: "666" 74 | `) 75 | is.NoErr(err) 76 | dump.Println(c.Data()) 77 | } 78 | 79 | // BindStruct doesn't seem to work with env var substitution 80 | // https://github.com/gookit/config/issues/46 81 | func TestIssues_46(t *testing.T) { 82 | is := assert.New(t) 83 | 84 | c := config.New("test").WithOptions(config.ParseEnv) 85 | err := c.LoadStrings(config.JSON, ` 86 | { 87 | "http": { 88 | "port": "${HTTP_PORT|8080}" 89 | } 90 | } 91 | `) 92 | 93 | is.NoErr(err) 94 | dump.Println(c.Data()) 95 | 96 | val, _ := c.GetValue("http") 97 | mp := val.(map[string]any) 98 | dump.Println(mp) 99 | is.Eq("${HTTP_PORT|8080}", mp["port"]) 100 | 101 | smp := c.StringMap("http") 102 | dump.Println(smp) 103 | is.Contains(smp, "port") 104 | is.Eq("8080", smp["port"]) 105 | 106 | type Http struct { 107 | Port int 108 | } 109 | 110 | h := &Http{} 111 | err = c.BindStruct("http", h) 112 | is.NoErr(err) 113 | dump.Println(h) 114 | is.Eq(8080, h.Port) 115 | 116 | testutil.MockEnvValue("HTTP_PORT", "19090", func(_ string) { 117 | h := &Http{} 118 | err = c.BindStruct("http", h) 119 | is.NoErr(err) 120 | dump.Println(h) 121 | is.Eq(19090, h.Port) 122 | }) 123 | } 124 | 125 | // https://github.com/gookit/config/issues/46 126 | func TestIssues_59(t *testing.T) { 127 | is := assert.New(t) 128 | 129 | c := config.NewWithOptions("test", config.ParseEnv) 130 | c.AddDriver(ini.Driver) 131 | 132 | err := c.LoadFiles("testdata/ini_base.ini") 133 | is.NoErr(err) 134 | dump.Println(c.Data()) 135 | 136 | dumpfile := "testdata/issues59.ini" 137 | out, err := fsutil.OpenTruncFile(dumpfile, 0666) 138 | is.NoErr(err) 139 | _, err = c.DumpTo(out, config.Ini) 140 | is.NoErr(err) 141 | 142 | str := string(fsutil.MustReadFile(dumpfile)) 143 | is.Contains(str, "name = app") 144 | is.Contains(str, "key1 = val1") 145 | } 146 | 147 | // https://github.com/gookit/config/issues/70 148 | func TestIssues_70(t *testing.T) { 149 | c := config.New("test") 150 | 151 | err := c.LoadStrings(config.JSON, `{ 152 | "parent": { 153 | "child": "Test Var" 154 | } 155 | }`) 156 | 157 | assert.NoErr(t, err) 158 | assert.Eq(t, "Test Var", c.String("parent.child")) 159 | dump.P(c.Data()) 160 | 161 | // cannot this. 162 | err = c.Set("parent.child.grandChild", "New Val") 163 | assert.Err(t, err) 164 | 165 | err = c.Set("parent.child", map[string]any{ 166 | "grandChild": "New Val", 167 | }) 168 | assert.NoErr(t, err) 169 | assert.Eq(t, map[string]any{ 170 | "grandChild": "New Val", 171 | }, c.Get("parent.child")) 172 | 173 | dump.P(c.Data()) 174 | } 175 | 176 | // https://github.com/gookit/config/issues/76 177 | func TestIssues_76(t *testing.T) { 178 | is := assert.New(t) 179 | c := config.New("test") 180 | 181 | err := c.LoadStrings(config.JSON, ` 182 | { 183 | "lang": { 184 | "allowed": { 185 | "en": "ddd" 186 | } 187 | }, 188 | "key0": 234 189 | } 190 | `) 191 | is.NoErr(err) 192 | 193 | ss := c.Strings("key0") 194 | is.Empty(ss) 195 | 196 | lastErr := c.Error() 197 | is.Err(lastErr) 198 | is.Eq("value cannot be convert to []string, key is 'key0'", lastErr.Error()) 199 | is.NoErr(c.Error()) 200 | } 201 | 202 | // https://github.com/gookit/config/issues/81 203 | func TestIssues_81(t *testing.T) { 204 | is := assert.New(t) 205 | c := config.New("test").WithOptions(config.ParseTime, func(options *config.Options) { 206 | options.DecoderConfig.TagName = "json" 207 | }) 208 | 209 | err := c.LoadStrings(config.JSON, ` 210 | { 211 | "key0": "abc", 212 | "age": 12, 213 | "connTime": "10s", 214 | "idleTime": "1m" 215 | } 216 | `) 217 | is.NoErr(err) 218 | 219 | type Options struct { 220 | ConnTime time.Duration `json:"connTime"` 221 | IdleTime time.Duration `json:"idleTime"` 222 | } 223 | 224 | opt := &Options{} 225 | err = c.BindStruct("", opt) 226 | 227 | is.NoErr(err) 228 | is.Eq("10s", c.String("connTime")) 229 | wantTm, err := time.ParseDuration("10s") 230 | is.NoErr(err) 231 | is.Eq(wantTm, opt.ConnTime) 232 | 233 | is.Eq("1m", c.String("idleTime")) 234 | wantTm, err = time.ParseDuration("1m") 235 | is.NoErr(err) 236 | is.Eq(wantTm, opt.IdleTime) 237 | } 238 | 239 | // https://github.com/gookit/config/issues/94 240 | func TestIssues_94(t *testing.T) { 241 | is := assert.New(t) 242 | // add option: config.ParseDefault 243 | c := config.New("test").WithOptions(config.ParseDefault) 244 | 245 | // only set name 246 | c.SetData(map[string]any{ 247 | "name": "inhere", 248 | }) 249 | 250 | // age load from default tag 251 | type User struct { 252 | Age int `json:"age" default:"30"` 253 | Name string 254 | Tags []int 255 | } 256 | 257 | user := &User{} 258 | is.NoErr(c.Decode(user)) 259 | dump.Println(user) 260 | is.Eq("inhere", user.Name) 261 | is.Eq(30, user.Age) 262 | 263 | // field use ptr 264 | type User1 struct { 265 | Age *int `json:"age" default:"30"` 266 | Name string 267 | Tags []int 268 | } 269 | 270 | u1 := &User1{} 271 | is.NoErr(c.Decode(u1)) 272 | dump.Println(u1) 273 | is.Eq("inhere", u1.Name) 274 | is.Eq(30, *u1.Age) 275 | } 276 | 277 | // https://github.com/gookit/config/issues/96 278 | func TestIssues_96(t *testing.T) { 279 | is := assert.New(t) 280 | c := config.New("test") 281 | 282 | err := c.Set("parent.child[0]", "Test1") 283 | is.NoErr(err) 284 | err = c.Set("parent.child[1]", "Test2") 285 | is.NoErr(err) 286 | 287 | dump.Println(c.Data()) 288 | is.NotEmpty(c.Data()) 289 | is.Eq([]string{"Test1", "Test2"}, c.Get("parent.child")) 290 | } 291 | 292 | // https://github.com/gookit/config/issues/114 293 | func TestIssues_114(t *testing.T) { 294 | c := config.NewWithOptions("test", 295 | config.ParseDefault, 296 | config.ParseEnv, 297 | config.Readonly, 298 | ) 299 | 300 | type conf struct { 301 | Name string `mapstructure:"name" default:"${NAME | Bob}"` 302 | Value []string `mapstructure:"value" default:"${VAL | val1}"` 303 | } 304 | 305 | err := c.LoadExists("") 306 | assert.NoErr(t, err) 307 | 308 | var cc conf 309 | err = c.Decode(&cc) 310 | assert.NoErr(t, err) 311 | 312 | assert.Eq(t, "Bob", cc.Name) 313 | assert.Eq(t, []string{"val1"}, cc.Value) 314 | // dump.Println(cc) 315 | } 316 | 317 | // https://github.com/gookit/config/issues/139 318 | func TestIssues_139(t *testing.T) { 319 | c := config.New("issues_139", config.ParseEnv) 320 | c.AddDriver(ini.Driver) 321 | c.AddAlias("ini", "conf") 322 | 323 | err := c.LoadFiles("testdata/issues_139.conf") 324 | assert.NoErr(t, err) 325 | 326 | assert.Eq(t, "app", c.String("name")) 327 | assert.Eq(t, "defValue", c.String("envKey1")) 328 | } 329 | 330 | // https://github.com/gookit/config/issues/141 331 | func TestIssues_141(t *testing.T) { 332 | type Logger struct { 333 | Name string `json:"name"` 334 | LogFile string `json:"logFile"` 335 | MaxSize int `json:"maxSize" default:"1024"` // MB 336 | MaxDays int `json:"maxDays" default:"7"` 337 | Compress bool `json:"compress" default:"true"` 338 | } 339 | 340 | type LogConfig struct { 341 | Loggers []*Logger `default:""` // mark for parse default 342 | } 343 | 344 | c := config.New("issues_141", config.ParseDefault) 345 | err := c.LoadStrings(config.JSON, ` 346 | { 347 | "loggers": [ 348 | { 349 | "name": "error", 350 | "logFile": "logs/error.log" 351 | }, 352 | { 353 | "name": "request", 354 | "logFile": "logs/request.log", 355 | "maxSize": 2048, 356 | "maxDays": 30, 357 | "compress": false 358 | } 359 | ] 360 | } 361 | `) 362 | 363 | assert.NoErr(t, err) 364 | 365 | opt := &LogConfig{} 366 | err = c.Decode(opt) 367 | dump.Println(opt) 368 | assert.NoErr(t, err) 369 | assert.Eq(t, 2, len(opt.Loggers)) 370 | assert.Eq(t, 1024, opt.Loggers[0].MaxSize) 371 | assert.Eq(t, 7, opt.Loggers[0].MaxDays) 372 | assert.Eq(t, true, opt.Loggers[0].Compress) 373 | 374 | assert.Eq(t, 2048, opt.Loggers[1].MaxSize) 375 | assert.Eq(t, 30, opt.Loggers[1].MaxDays) 376 | assert.Eq(t, true, opt.Loggers[1].Compress) 377 | 378 | t.Run("3 elements", func(t *testing.T) { 379 | jsonStr := ` 380 | { 381 | "loggers": [ 382 | { 383 | "name": "info", 384 | "logFile": "logs/info.log" 385 | }, 386 | { 387 | "name": "error", 388 | "logFile": "logs/error.log" 389 | }, 390 | { 391 | "name": "request", 392 | "logFile": "logs/request.log", 393 | "maxSize": 2048, 394 | "maxDays": 30, 395 | "compress": false 396 | } 397 | ] 398 | } 399 | ` 400 | c := config.New("issues_141", config.ParseDefault) 401 | err := c.LoadStrings(config.JSON, jsonStr) 402 | assert.NoErr(t, err) 403 | 404 | opt := &LogConfig{} 405 | err = c.Decode(opt) 406 | dump.Println(opt) 407 | }) 408 | } 409 | 410 | // https://github.com/gookit/config/issues/146 411 | func TestIssues_146(t *testing.T) { 412 | c := config.NewWithOptions("test", 413 | config.ParseDefault, 414 | config.ParseEnv, 415 | config.ParseTime, 416 | ) 417 | 418 | type conf struct { 419 | Env time.Duration 420 | DefaultEnv time.Duration 421 | NoEnv time.Duration 422 | } 423 | 424 | err := os.Setenv("ENV", "5s") 425 | assert.NoError(t, err) 426 | 427 | err = c.LoadStrings(config.JSON, `{ 428 | "env": "${ENV}", 429 | "defaultEnv": "${DEFAULT_ENV| 10s}", 430 | "noEnv": "15s" 431 | }`) 432 | assert.NoErr(t, err) 433 | 434 | var cc conf 435 | err = c.Decode(&cc) 436 | assert.NoErr(t, err) 437 | 438 | assert.Eq(t, 5*time.Second, cc.Env) 439 | assert.Eq(t, 10*time.Second, cc.DefaultEnv) 440 | assert.Eq(t, 15*time.Second, cc.NoEnv) 441 | } 442 | 443 | type DurationStruct struct { 444 | Duration time.Duration 445 | } 446 | 447 | // https://github.com/gookit/config/pull/151 448 | func TestDuration(t *testing.T) { 449 | var ( 450 | err error 451 | str string 452 | ) 453 | 454 | c := config.New("test").WithOptions(config.ParseTime) 455 | is := assert.New(t) 456 | dur := DurationStruct{} 457 | 458 | for _, seconds := range []int{10, 90} { 459 | str = fmt.Sprintf(`{ "Duration": "%ds" }`, seconds) 460 | 461 | err = c.LoadSources(config.JSON, []byte(str)) 462 | is.Nil(err) 463 | err = c.Decode(&dur) 464 | is.Nil(err) 465 | is.Equal(float64(seconds), dur.Duration.Seconds()) 466 | } 467 | } 468 | 469 | // https://github.com/gookit/config/issues/156 470 | func TestIssues_156(t *testing.T) { 471 | c := config.New("test", config.ParseEnv) 472 | c.AddDriver(yaml.Driver) 473 | 474 | type DbConfig struct { 475 | Url string 476 | Type string 477 | Password string 478 | Username string 479 | } 480 | 481 | err := c.LoadStrings(config.Yaml, ` 482 | --- 483 | datasource: 484 | password: ${DATABASE_PASSWORD|?} # use fixed error message 485 | type: postgres 486 | username: ${DATABASE_USERNAME|postgres} 487 | url: ${DATABASE_URL|?error message2} 488 | `) 489 | assert.NoErr(t, err) 490 | // dump.Println(c.Data()) 491 | assert.NotEmpty(t, c.Sub("datasource")) 492 | 493 | // will error 494 | dbConf := &DbConfig{} 495 | err = c.BindStruct("datasource", dbConf) 496 | assert.Err(t, err) 497 | assert.ErrSubMsg(t, err, "decoding 'Password': value is required for var: DATABASE_PASSWORD") 498 | assert.ErrSubMsg(t, err, "error decoding 'Url': error message2") 499 | 500 | testutil.MockEnvValues(map[string]string{ 501 | "DATABASE_PASSWORD": "1234yz56", 502 | "DATABASE_URL": "localhost:5432/postgres?sslmode=disable", 503 | }, func() { 504 | dbConf := &DbConfig{} 505 | err = c.BindStruct("datasource", dbConf) 506 | assert.NoErr(t, err) 507 | dump.Println(dbConf) 508 | assert.Eq(t, "1234yz56", dbConf.Password) 509 | assert.Eq(t, "localhost:5432/postgres?sslmode=disable", dbConf.Url) 510 | }) 511 | } 512 | 513 | // https://github.com/gookit/config/issues/162 514 | func TestIssues_162(t *testing.T) { 515 | type Logger struct { 516 | Name string `json:"name"` 517 | LogFile string `json:"logFile"` 518 | MaxSize int `json:"maxSize" default:"1024"` // MB 519 | MaxDays int `json:"maxDays" default:"7"` 520 | Compress bool `json:"compress" default:"true"` 521 | } 522 | 523 | type LogConfig struct { 524 | Loggers []*Logger `default:""` // mark for parse default 525 | } 526 | 527 | c := config.New("issues_162", config.ParseDefault) 528 | err := c.LoadStrings(config.JSON, `{}`) 529 | assert.NoErr(t, err) 530 | 531 | opt := &LogConfig{} 532 | err = c.Decode(opt) 533 | // dump.Println(opt) 534 | assert.Empty(t, opt.Loggers) 535 | } 536 | 537 | // https://github.com/gookit/goutil/issues/135 538 | func TestGoutil_issues_135(t *testing.T) { 539 | // TIP: not support use JSON as default value 540 | testYml := ` 541 | test: 542 | credentials: > 543 | ${CREDENTIALS|{}} 544 | apiKey: ${API_KEY|AN_APIKEY} 545 | apiUri: ${API_URI|http://localhost:8888/v1/api} 546 | ` 547 | 548 | type Setup struct { 549 | Credentials string `mapstructure:"credentials"` 550 | ApiKey string `mapstructure:"apiKey"` 551 | ApiUri string `mapstructure:"apiUri"` 552 | } 553 | 554 | type Configuration struct { 555 | Details Setup `mapstructure:"test"` 556 | } 557 | 558 | c := config.New("config", config.ParseEnv).WithDriver(yamlv3.Driver) 559 | 560 | err := c.LoadStrings(config.Yaml, testYml) 561 | assert.NoErr(t, err) 562 | 563 | // no env values 564 | t.Run("no env values", func(t *testing.T) { 565 | st := Configuration{} 566 | err = c.Decode(&st) 567 | assert.NoErr(t, err) 568 | dump.Println(st) 569 | }) 570 | 571 | // set value 572 | err = c.Set("test.credentials", `${CREDENTIALS}`) 573 | assert.NoErr(t, err) 574 | 575 | // set value(use JSON as default value) 576 | err = c.Set("test.credentials", `${CREDENTIALS | {}}`) 577 | assert.NoErr(t, err) 578 | 579 | // with env values 580 | t.Run("with env values", func(t *testing.T) { 581 | testutil.MockEnvValues(map[string]string{ 582 | "CREDENTIALS": `{"username":"admin"}`, 583 | }, func() { 584 | st := Configuration{} 585 | err = c.Decode(&st) 586 | assert.NoErr(t, err) 587 | dump.Println(st) 588 | assert.Eq(t, `{"username":"admin"}`, st.Details.Credentials) 589 | }) 590 | }) 591 | } 592 | 593 | // https://github.com/gookit/config/issues/178 594 | func TestIssues_178(t *testing.T) { 595 | type ConferenceConfigure struct { 596 | AuthServerEnable bool `mapstructure:"authServerEnable" default:"true"` 597 | } 598 | 599 | var ENVS = map[string]string{ 600 | "CONF_AUTH_SERVER_ENABLE": "authServerEnable", 601 | } 602 | 603 | config.WithOptions(config.ParseEnv, config.ParseTime, config.ParseDefault) 604 | config.LoadOSEnvs(ENVS) 605 | 606 | cfg := &ConferenceConfigure{} 607 | err := config.Decode(cfg) 608 | assert.NoErr(t, err) 609 | dump.Println(cfg) 610 | } 611 | 612 | // https://github.com/gookit/config/issues/192 613 | func TestIssues_192(t *testing.T) { 614 | s := `{ 615 | "key": 23707729876828933003792990320594511132013137629744363463325945636682800546201191581706241551352734654762086038344743940857801503840360878427584255703013924373301145683882034301334533678253123777083489887967659929148298684008991665609773532863485728577470710590688325197694460521376123072613857785739366064688074459399408762960887169067851291291611970194076234580897060365318108861340336375060983779163595033605894055218557648363640361256922411962394084268360413547861005069585285713253756167043430574673046032573767256949834558011358364591391964981157578571244295339467926678988648036459748021538498339258036608168313 616 | }` 617 | 618 | jsd := config.NewDriver("json", func(blob []byte, v any) (err error) { 619 | jnd := json.NewDecoder(bytes.NewReader(blob)) 620 | jnd.UseNumber() 621 | return jnd.Decode(v) 622 | }, config.JSONEncoder) 623 | 624 | cfg := config.NewEmpty("test1").WithDriver(jsd) 625 | err := cfg.LoadStrings(config.JSON, s) 626 | assert.NoErr(t, err) 627 | dump.P(cfg.Data()) 628 | 629 | // to big.Int 630 | bi := new(big.Int) 631 | _, ok := bi.SetString(cfg.String("key"), 10) 632 | assert.True(t, ok) 633 | } 634 | 635 | // https://github.com/gookit/config/issues/193 Support Environment Variable Overrides 636 | func TestIssues_193(t *testing.T) { 637 | c := config.NewGeneric("test", config.WithTagName("config")) 638 | 639 | err := c.LoadStrings(config.JSON, `{"name": "default"}`) 640 | assert.NoErr(t, err) 641 | assert.Eq(t, "default", c.String("name")) 642 | 643 | err = c.LoadStrings(config.JSON, `{"datasource": {"username": "name in dev"}}`) 644 | assert.NoErr(t, err) 645 | assert.Eq(t, "name in dev", c.String("datasource.username")) 646 | 647 | testutil.MockEnvValue("DATASOURCE_USERNAME", "name in prod", func(val string) { 648 | c.LoadOSEnvs(map[string]string{ 649 | "DATASOURCE_USERNAME": "datasource.username", 650 | }) 651 | }) 652 | 653 | assert.Eq(t, "name in prod", c.String("datasource.username")) 654 | } 655 | 656 | // https://github.com/gookit/config/issues/194 657 | func TestIssues_194(t *testing.T) { 658 | cl := config.New("test", config.ParseDefault) 659 | 660 | type TestConfig struct { 661 | Nested struct { 662 | SimpleValue string 663 | WithDefault string `default:"default-value"` 664 | } `default:""` // <-- add this line 665 | } 666 | 667 | cfg := TestConfig{} 668 | err := cl.BindStruct("", &cfg) 669 | assert.NoErr(t, err) 670 | dump.P(cfg) 671 | assert.Eq(t, "default-value", cfg.Nested.WithDefault) 672 | } 673 | -------------------------------------------------------------------------------- /json/json.go: -------------------------------------------------------------------------------- 1 | // Package json use the https://github.com/json-iterator/go for parse json 2 | package json 3 | 4 | import ( 5 | "github.com/goccy/go-json" 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/goutil/jsonutil" 8 | ) 9 | 10 | var ( 11 | // Decoder for json 12 | Decoder config.Decoder = func(data []byte, v any) (err error) { 13 | if config.JSONAllowComments { 14 | str := jsonutil.StripComments(string(data)) 15 | return json.Unmarshal([]byte(str), v) 16 | } 17 | return json.Unmarshal(data, v) 18 | } 19 | 20 | // Encoder for json 21 | Encoder config.Encoder = json.Marshal 22 | // Driver for json 23 | Driver = config.NewDriver(config.JSON, Decoder, Encoder) 24 | ) 25 | -------------------------------------------------------------------------------- /json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func Example() { 12 | config.WithOptions(config.ParseEnv) 13 | 14 | // add Decoder and Encoder 15 | config.AddDriver(Driver) 16 | 17 | err := config.LoadFiles("testdata/json_base.json") 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | fmt.Printf("config data: \n %#v\n", config.Data()) 23 | 24 | err = config.LoadFiles("testdata/json_other.json") 25 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | fmt.Printf("config data: \n %#v\n", config.Data()) 31 | fmt.Print("get config example:\n") 32 | 33 | name := config.String("name") 34 | fmt.Printf("get string\n - val: %v\n", name) 35 | 36 | arr1 := config.Strings("arr1") 37 | fmt.Printf("get array\n - val: %#v\n", arr1) 38 | 39 | val0 := config.String("arr1.0") 40 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 41 | 42 | map1 := config.StringMap("map1") 43 | fmt.Printf("get map\n - val: %#v\n", map1) 44 | 45 | val0 = config.String("map1.key") 46 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 47 | 48 | // can parse env name(ParseEnv: true) 49 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 50 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 51 | 52 | // set value 53 | _ = config.Set("name", "new name") 54 | name = config.String("name") 55 | fmt.Printf("set string\n - val: %v\n", name) 56 | 57 | // if you want export config data 58 | // buf := new(bytes.Buffer) 59 | // _, err = config.DumpTo(buf, config.JSON) 60 | // if err != nil { 61 | // panic(err) 62 | // } 63 | // fmt.Printf("export config:\n%s", buf.String()) 64 | } 65 | 66 | func TestDriver(t *testing.T) { 67 | is := assert.New(t) 68 | 69 | is.Eq("json", Driver.Name()) 70 | 71 | c := config.NewEmpty("test") 72 | is.False(c.HasDecoder(config.JSON)) 73 | c.AddDriver(Driver) 74 | 75 | is.True(c.HasDecoder(config.JSON)) 76 | is.True(c.HasEncoder(config.JSON)) 77 | 78 | m := struct { 79 | N string 80 | }{} 81 | err := Decoder([]byte(`{ 82 | // comments 83 | "n":"v"} 84 | `), &m) 85 | is.Nil(err) 86 | is.Eq("v", m.N) 87 | 88 | // disable clear comments 89 | old := config.JSONAllowComments 90 | config.JSONAllowComments = false 91 | err = Decoder([]byte(`{ 92 | // comments 93 | "n":"v"} 94 | `), &m) 95 | is.Err(err) 96 | 97 | config.JSONAllowComments = old 98 | } 99 | -------------------------------------------------------------------------------- /json5/json5.go: -------------------------------------------------------------------------------- 1 | // Package json5 support for parse and load json5 2 | package json5 3 | 4 | import ( 5 | "encoding/json" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/titanous/json5" 9 | ) 10 | 11 | // Name for driver 12 | const Name = "json5" 13 | 14 | // NAME for driver 15 | const NAME = Name 16 | 17 | // JSONMarshalIndent if not empty, will use json.MarshalIndent for encode data. 18 | var JSONMarshalIndent string 19 | 20 | var ( 21 | // Decoder for json 22 | Decoder config.Decoder = json5.Unmarshal 23 | 24 | // Encoder for json5 25 | Encoder config.Encoder = func(v any) (out []byte, err error) { 26 | if len(JSONMarshalIndent) == 0 { 27 | return json.Marshal(v) 28 | } 29 | return json.MarshalIndent(v, "", JSONMarshalIndent) 30 | } 31 | 32 | // Driver for json5 33 | Driver = config.NewDriver(Name, Decoder, Encoder) 34 | ) 35 | -------------------------------------------------------------------------------- /json5/json5_test.go: -------------------------------------------------------------------------------- 1 | package json5_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/json5" 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func Example() { 13 | config.WithOptions(config.ParseEnv) 14 | 15 | // add Decoder and Encoder 16 | config.AddDriver(json5.Driver) 17 | 18 | err := config.LoadFiles("testdata/json_base.json5") 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | fmt.Printf("config data: \n %#v\n", config.Data()) 24 | 25 | err = config.LoadFiles("testdata/json_other.json") 26 | // config.LoadFiles("testdata/json_base.json", "testdata/json_other.json") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | fmt.Printf("config data: \n %#v\n", config.Data()) 32 | fmt.Print("get config example:\n") 33 | 34 | name := config.String("name") 35 | fmt.Printf("get string\n - val: %v\n", name) 36 | 37 | arr1 := config.Strings("arr1") 38 | fmt.Printf("get array\n - val: %#v\n", arr1) 39 | 40 | val0 := config.String("arr1.0") 41 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 42 | 43 | map1 := config.StringMap("map1") 44 | fmt.Printf("get map\n - val: %#v\n", map1) 45 | 46 | val0 = config.String("map1.key") 47 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 48 | 49 | // can parse env name(ParseEnv: true) 50 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 51 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 52 | 53 | // set value 54 | _ = config.Set("name", "new name") 55 | name = config.String("name") 56 | fmt.Printf("set string\n - val: %v\n", name) 57 | 58 | // if you want export config data 59 | // buf := new(bytes.Buffer) 60 | // _, err = config.DumpTo(buf, json5.NAME) 61 | // if err != nil { 62 | // panic(err) 63 | // } 64 | // fmt.Printf("export config:\n%s", buf.String()) 65 | } 66 | 67 | func TestDriver(t *testing.T) { 68 | is := assert.New(t) 69 | 70 | is.Eq(json5.Name, json5.Driver.Name()) 71 | 72 | c := config.NewEmpty("test") 73 | is.False(c.HasDecoder(json5.Name)) 74 | c.AddDriver(json5.Driver) 75 | 76 | is.True(c.HasDecoder(json5.Name)) 77 | is.True(c.HasEncoder(json5.Name)) 78 | 79 | // test use 80 | m := struct { 81 | N string 82 | }{} 83 | err := json5.Decoder([]byte(`{ 84 | // comments 85 | "n":"v"} 86 | `), &m) 87 | is.Nil(err) 88 | is.Eq("v", m.N) 89 | 90 | // load file 91 | err = c.LoadFiles("../testdata/json_base.json5") 92 | is.NoErr(err) 93 | is.Eq("app", c.Get("name")) 94 | } 95 | 96 | func TestEncode2JSON5(t *testing.T) { 97 | is := assert.New(t) 98 | 99 | mp := map[string]any{ 100 | "name": "app", 101 | "age": 45, 102 | } 103 | bs, err := json5.Encoder(mp) 104 | is.NoErr(err) 105 | is.StrContains(string(bs), `"name":"app"`) 106 | 107 | json5.JSONMarshalIndent = " " 108 | bs, err = json5.Encoder(mp) 109 | is.NoErr(err) 110 | s := string(bs) 111 | is.StrContains(s, ` "name": "app"`) 112 | } 113 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "dario.cat/mergo" 16 | "github.com/gookit/goutil/errorx" 17 | "github.com/gookit/goutil/fsutil" 18 | ) 19 | 20 | // LoadFiles load one or multi files, will fire OnLoadData event 21 | // 22 | // Usage: 23 | // 24 | // config.LoadFiles(file1, file2, ...) 25 | func LoadFiles(sourceFiles ...string) error { return dc.LoadFiles(sourceFiles...) } 26 | 27 | // LoadFiles load and parse config files, will fire OnLoadData event 28 | func (c *Config) LoadFiles(sourceFiles ...string) (err error) { 29 | for _, file := range sourceFiles { 30 | if err = c.loadFile(file, false, ""); err != nil { 31 | return 32 | } 33 | } 34 | return 35 | } 36 | 37 | // LoadExists load one or multi files, will ignore not exist 38 | // 39 | // Usage: 40 | // 41 | // config.LoadExists(file1, file2, ...) 42 | func LoadExists(sourceFiles ...string) error { return dc.LoadExists(sourceFiles...) } 43 | 44 | // LoadExists load and parse config files, but will ignore not exists file. 45 | func (c *Config) LoadExists(sourceFiles ...string) (err error) { 46 | for _, file := range sourceFiles { 47 | if file == "" { 48 | continue 49 | } 50 | 51 | if err = c.loadFile(file, true, ""); err != nil { 52 | return 53 | } 54 | } 55 | return 56 | } 57 | 58 | // LoadRemote load config data from remote URL. 59 | func LoadRemote(format, url string) error { return dc.LoadRemote(format, url) } 60 | 61 | // LoadRemote load config data from remote URL. 62 | // 63 | // Usage: 64 | // 65 | // c.LoadRemote(config.JSON, "http://abc.com/api-config.json") 66 | func (c *Config) LoadRemote(format, url string) (err error) { 67 | // create http client 68 | client := http.Client{Timeout: 300 * time.Second} 69 | resp, err := client.Get(url) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | //noinspection GoUnhandledErrorResult 75 | defer resp.Body.Close() 76 | if resp.StatusCode != 200 { 77 | return fmt.Errorf("fetch remote config error, reply status code is %d", resp.StatusCode) 78 | } 79 | 80 | // read response content 81 | bts, err := io.ReadAll(resp.Body) 82 | if err == nil { 83 | if err = c.parseSourceCode(format, bts); err != nil { 84 | return 85 | } 86 | c.loadedUrls = append(c.loadedUrls, url) 87 | } 88 | return 89 | } 90 | 91 | // LoadOSEnv load data from OS ENV 92 | // 93 | // Deprecated: please use LoadOSEnvs() 94 | func LoadOSEnv(keys []string, keyToLower bool) { dc.LoadOSEnv(keys, keyToLower) } 95 | 96 | // LoadOSEnv load data from os ENV 97 | // 98 | // Deprecated: please use Config.LoadOSEnvs() 99 | func (c *Config) LoadOSEnv(keys []string, keyToLower bool) { 100 | for _, key := range keys { 101 | // NOTICE: if is Windows os, os.Getenv() Key is not case-sensitive 102 | val := os.Getenv(key) 103 | if keyToLower { 104 | key = strings.ToLower(key) 105 | } 106 | _ = c.Set(key, val) 107 | } 108 | c.fireHook(OnLoadData) 109 | } 110 | 111 | // LoadOSEnvs load data from OS ENVs. see Config.LoadOSEnvs 112 | func LoadOSEnvs(nameToKeyMap map[string]string) { dc.LoadOSEnvs(nameToKeyMap) } 113 | 114 | // LoadOSEnvs load data from os ENVs. format: `{ENV_NAME: config_key}` 115 | // 116 | // - `config_key` allow use key path. eg: `{"DB_USERNAME": "db.username"}` 117 | func (c *Config) LoadOSEnvs(nameToKeyMap map[string]string) { 118 | for name, cfgKey := range nameToKeyMap { 119 | if val := os.Getenv(name); val != "" { 120 | if cfgKey == "" { 121 | cfgKey = strings.ToLower(name) 122 | } 123 | _ = c.Set(cfgKey, val) 124 | } 125 | } 126 | 127 | c.fireHook(OnLoadData) 128 | } 129 | 130 | // support bound types for CLI flags vars 131 | var validTypes = map[string]int{ 132 | "int": 1, 133 | "uint": 1, 134 | "bool": 1, 135 | // string is default 136 | "string": 1, 137 | } 138 | 139 | // LoadFlags load data from cli flags. see Config.LoadFlags 140 | func LoadFlags(defines []string) error { return dc.LoadFlags(defines) } 141 | 142 | // LoadFlags parse command line arguments, based on provide keys. 143 | // 144 | // Usage: 145 | // 146 | // // 'debug' flag is bool type 147 | // c.LoadFlags([]string{"env", "debug:bool"}) 148 | // // can with flag desc message 149 | // c.LoadFlags([]string{"env:set the run env"}) 150 | // c.LoadFlags([]string{"debug:bool:set debug mode"}) 151 | // // can set value to map key. eg: myapp --map1.sub-key=val 152 | // c.LoadFlags([]string{"--map1.sub-key"}) 153 | func (c *Config) LoadFlags(defines []string) (err error) { 154 | hash := map[string]int8{} 155 | 156 | // bind vars 157 | for _, str := range defines { 158 | key, typ, desc := parseVarNameAndType(str) 159 | if desc == "" { 160 | desc = "config flag " + key 161 | } 162 | 163 | switch typ { 164 | case "int": 165 | ptr := new(int) 166 | flag.IntVar(ptr, key, c.Int(key), desc) 167 | hash[key] = 0 168 | case "uint": 169 | ptr := new(uint) 170 | flag.UintVar(ptr, key, c.Uint(key), desc) 171 | hash[key] = 0 172 | case "bool": 173 | ptr := new(bool) 174 | flag.BoolVar(ptr, key, c.Bool(key), desc) 175 | hash[key] = 0 176 | default: // as string 177 | ptr := new(string) 178 | flag.StringVar(ptr, key, c.String(key), desc) 179 | hash[key] = 0 180 | } 181 | } 182 | 183 | // parse and collect 184 | flag.Parse() 185 | flag.Visit(func(f *flag.Flag) { 186 | name := f.Name 187 | // only get name in the keys. 188 | if _, ok := hash[name]; !ok { 189 | return 190 | } 191 | 192 | // if f.Value implement the flag.Getter, read typed value 193 | if gtr, ok := f.Value.(flag.Getter); ok { 194 | _ = c.Set(name, gtr.Get()) 195 | // } else { // TIP: basic type flag always implements Getter interface 196 | // _ = c.Set(name, f.Value.String()) // ignore error 197 | } 198 | }) 199 | 200 | c.fireHook(OnLoadData) 201 | return 202 | } 203 | 204 | // LoadData load one or multi data 205 | func LoadData(dataSource ...any) error { return dc.LoadData(dataSource...) } 206 | 207 | // LoadData load data from map OR struct 208 | // 209 | // The dataSources type allow: 210 | // - map[string]any 211 | // - map[string]string 212 | func (c *Config) LoadData(dataSources ...any) (err error) { 213 | if c.opts.Delimiter == 0 { 214 | c.opts.Delimiter = defaultDelimiter 215 | } 216 | 217 | var loaded bool 218 | for _, ds := range dataSources { 219 | if smp, ok := ds.(map[string]string); ok { 220 | loaded = true 221 | c.LoadSMap(smp) 222 | continue 223 | } 224 | 225 | err = mergo.Merge(&c.data, ds, c.opts.MergeOptions...) 226 | if err != nil { 227 | return errorx.WithStack(err) 228 | } 229 | loaded = true 230 | } 231 | 232 | if loaded { 233 | c.fireHook(OnLoadData) 234 | } 235 | return 236 | } 237 | 238 | // LoadSMap to config 239 | func (c *Config) LoadSMap(smp map[string]string) { 240 | for k, v := range smp { 241 | c.data[k] = v 242 | } 243 | c.fireHook(OnLoadData) 244 | } 245 | 246 | // LoadSources load one or multi byte data 247 | func LoadSources(format string, src []byte, more ...[]byte) error { 248 | return dc.LoadSources(format, src, more...) 249 | } 250 | 251 | // LoadSources load data from byte content. 252 | // 253 | // Usage: 254 | // 255 | // config.LoadSources(config.Yaml, []byte(` 256 | // name: blog 257 | // arr: 258 | // key: val 259 | // 260 | // `)) 261 | func (c *Config) LoadSources(format string, src []byte, more ...[]byte) (err error) { 262 | err = c.parseSourceCode(format, src) 263 | if err != nil { 264 | return 265 | } 266 | 267 | for _, sc := range more { 268 | err = c.parseSourceCode(format, sc) 269 | if err != nil { 270 | return 271 | } 272 | } 273 | return 274 | } 275 | 276 | // LoadStrings load one or multi string 277 | func LoadStrings(format string, str string, more ...string) error { 278 | return dc.LoadStrings(format, str, more...) 279 | } 280 | 281 | // LoadStrings load data from source string content. 282 | func (c *Config) LoadStrings(format string, str string, more ...string) (err error) { 283 | err = c.parseSourceCode(format, []byte(str)) 284 | if err != nil { 285 | return 286 | } 287 | 288 | for _, s := range more { 289 | err = c.parseSourceCode(format, []byte(s)) 290 | if err != nil { 291 | return 292 | } 293 | } 294 | return 295 | } 296 | 297 | // LoadFilesByFormat load one or multi config files by give format, will fire OnLoadData event 298 | func LoadFilesByFormat(format string, configFiles ...string) error { 299 | return dc.LoadFilesByFormat(format, configFiles...) 300 | } 301 | 302 | // LoadFilesByFormat load one or multi files by give format, will fire OnLoadData event 303 | func (c *Config) LoadFilesByFormat(format string, configFiles ...string) (err error) { 304 | for _, file := range configFiles { 305 | if err = c.loadFile(file, false, format); err != nil { 306 | return 307 | } 308 | } 309 | return 310 | } 311 | 312 | // LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event 313 | func LoadExistsByFormat(format string, configFiles ...string) error { 314 | return dc.LoadExistsByFormat(format, configFiles...) 315 | } 316 | 317 | // LoadExistsByFormat load one or multi files by give format, will fire OnLoadData event 318 | func (c *Config) LoadExistsByFormat(format string, configFiles ...string) (err error) { 319 | for _, file := range configFiles { 320 | if err = c.loadFile(file, true, format); err != nil { 321 | return 322 | } 323 | } 324 | return 325 | } 326 | 327 | // LoadOptions for load config from dir. 328 | type LoadOptions struct { 329 | // DataKey use for load config from dir. 330 | // see https://github.com/gookit/config/issues/173 331 | DataKey string 332 | } 333 | 334 | // LoadOptFn type func 335 | type LoadOptFn func(lo *LoadOptions) 336 | 337 | func newLoadOptions(loFns []LoadOptFn) *LoadOptions { 338 | lo := &LoadOptions{} 339 | for _, fn := range loFns { 340 | fn(lo) 341 | } 342 | return lo 343 | } 344 | 345 | // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. 346 | // 347 | // Example: 348 | // 349 | // // file: /somedir/task.json 350 | // LoadFromDir("/somedir", "json") 351 | // 352 | // // after load 353 | // Config.data = map[string]any{"task": file data} 354 | func LoadFromDir(dirPath, format string, loFns ...LoadOptFn) error { 355 | return dc.LoadFromDir(dirPath, format, loFns...) 356 | } 357 | 358 | // LoadFromDir Load custom format files from the given directory, the file name will be used as the key. 359 | // 360 | // NOTE: will not be reloaded on call ReloadFiles(), if data loaded by the method. 361 | // 362 | // Example: 363 | // 364 | // // file: /somedir/task.json , will use filename 'task' as key 365 | // Config.LoadFromDir("/somedir", "json") 366 | // 367 | // // after load, the data will be: 368 | // Config.data = map[string]any{"task": {file data}} 369 | func (c *Config) LoadFromDir(dirPath, format string, loFns ...LoadOptFn) (err error) { 370 | extName := "." + format 371 | extLen := len(extName) 372 | 373 | lo := newLoadOptions(loFns) 374 | dirData := make(map[string]any) 375 | dataList := make([]map[string]any, 0, 8) 376 | 377 | err = fsutil.FindInDir(dirPath, func(fPath string, ent fs.DirEntry) error { 378 | baseName := ent.Name() 379 | if strings.HasSuffix(baseName, extName) { 380 | data, err := c.parseSourceToMap(format, fsutil.MustReadFile(fPath)) 381 | if err != nil { 382 | return err 383 | } 384 | 385 | // filename without ext. 386 | onlyName := baseName[:len(baseName)-extLen] 387 | if lo.DataKey != "" { 388 | dataList = append(dataList, data) 389 | } else { 390 | dirData[onlyName] = data 391 | } 392 | 393 | // TODO use file name as key, it cannot be reloaded. So, cannot append to loadedFiles 394 | // c.loadedFiles = append(c.loadedFiles, fPath) 395 | } 396 | return nil 397 | }) 398 | 399 | if err != nil { 400 | return err 401 | } 402 | if lo.DataKey != "" { 403 | dirData[lo.DataKey] = dataList 404 | } 405 | 406 | if len(dirData) == 0 { 407 | return nil 408 | } 409 | return c.loadDataMap(dirData) 410 | } 411 | 412 | // ReloadFiles reload config data use loaded files 413 | func ReloadFiles() error { return dc.ReloadFiles() } 414 | 415 | // ReloadFiles reload config data use loaded files. use on watching loaded files change 416 | func (c *Config) ReloadFiles() (err error) { 417 | files := c.loadedFiles 418 | if len(files) == 0 { 419 | return 420 | } 421 | 422 | data := c.Data() 423 | c.reloading = true 424 | c.ClearCaches() 425 | 426 | defer func() { 427 | // revert to back up data on error 428 | if err != nil { 429 | c.data = data 430 | } 431 | 432 | c.lock.Unlock() 433 | c.reloading = false 434 | 435 | if err == nil { 436 | c.fireHook(OnReloadData) 437 | } 438 | }() 439 | 440 | // with lock 441 | c.lock.Lock() 442 | 443 | // reload config files 444 | return c.LoadFiles(files...) 445 | } 446 | 447 | // load config file, will fire OnLoadData event 448 | func (c *Config) loadFile(file string, loadExist bool, format string) (err error) { 449 | fd, err := os.Open(file) 450 | if err != nil { 451 | // skip not exist file 452 | if os.IsNotExist(err) && loadExist { 453 | return nil 454 | } 455 | return err 456 | } 457 | //noinspection GoUnhandledErrorResult 458 | defer fd.Close() 459 | 460 | // read file content 461 | bts, err := io.ReadAll(fd) 462 | if err == nil { 463 | // get format for file ext 464 | if format == "" { 465 | format = strings.Trim(filepath.Ext(file), ".") 466 | } 467 | 468 | // parse file content 469 | if err = c.parseSourceCode(format, bts); err != nil { 470 | return 471 | } 472 | 473 | if !c.reloading { 474 | c.loadedFiles = append(c.loadedFiles, file) 475 | } 476 | } 477 | return 478 | } 479 | 480 | // parse config source code to Config. 481 | func (c *Config) parseSourceCode(format string, blob []byte) (err error) { 482 | data, err := c.parseSourceToMap(format, blob) 483 | if err != nil { 484 | return err 485 | } 486 | 487 | return c.loadDataMap(data) 488 | } 489 | 490 | func (c *Config) loadDataMap(data map[string]any) (err error) { 491 | // first: init config data 492 | if len(c.data) == 0 { 493 | c.data = data 494 | } else { 495 | // again ... will merge data 496 | err = mergo.Merge(&c.data, data, c.opts.MergeOptions...) 497 | } 498 | 499 | if !c.reloading && err == nil { 500 | c.fireHook(OnLoadData) 501 | } 502 | return err 503 | } 504 | 505 | // parse config source code to Config. 506 | func (c *Config) parseSourceToMap(format string, blob []byte) (map[string]any, error) { 507 | format = c.resolveFormat(format) 508 | decode := c.decoders[format] 509 | if decode == nil { 510 | return nil, errors.New("not register decoder for the format: " + format) 511 | } 512 | 513 | if c.opts.Delimiter == 0 { 514 | c.opts.Delimiter = defaultDelimiter 515 | } 516 | 517 | // decode content to data 518 | data := make(map[string]any) 519 | 520 | if err := decode(blob, &data); err != nil { 521 | return nil, err 522 | } 523 | return data, nil 524 | } 525 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/gookit/goutil/dump" 11 | "github.com/gookit/goutil/testutil" 12 | "github.com/gookit/goutil/testutil/assert" 13 | ) 14 | 15 | func TestDefaultLoad(t *testing.T) { 16 | is := assert.New(t) 17 | 18 | ClearAll() 19 | err := LoadFiles("testdata/json_base.json", "testdata/json_other.json") 20 | is.Nil(err) 21 | 22 | ClearAll() 23 | err = LoadFilesByFormat(JSON, "testdata/json_base.json", "testdata/json_other.json") 24 | is.Nil(err) 25 | 26 | ClearAll() 27 | err = LoadExists("testdata/json_base.json", "not-exist.json") 28 | is.Nil(err) 29 | 30 | ClearAll() 31 | err = LoadExistsByFormat(JSON, "testdata/json_base.json", "not-exist.json") 32 | is.Nil(err) 33 | 34 | ClearAll() 35 | // load map 36 | err = LoadData(map[string]any{ 37 | "name": "inhere", 38 | "age": 28, 39 | "working": true, 40 | "tags": []string{"a", "b"}, 41 | "info": map[string]string{"k1": "a", "k2": "b"}, 42 | }) 43 | is.NotEmpty(Data()) 44 | is.NotEmpty(Keys()) 45 | is.Empty(Sub("not-exist")) 46 | is.Nil(err) 47 | } 48 | 49 | func TestLoad(t *testing.T) { 50 | is := assert.New(t) 51 | 52 | var name string 53 | c := New("test"). 54 | WithOptions(WithHookFunc(func(event string, c *Config) { 55 | name = event 56 | })) 57 | err := c.LoadExists("testdata/json_base.json", "not-exist.json") 58 | is.Nil(err) 59 | 60 | c.ClearAll() 61 | is.Eq(OnCleanData, name) 62 | 63 | // load map data 64 | err = c.LoadData(map[string]any{ 65 | "name": "inhere", 66 | "age": float64(28), 67 | "working": true, 68 | "tags": []string{"a", "b"}, 69 | "info": map[string]string{"k1": "a", "k2": "b"}, 70 | }, map[string]string{"str-map": "value"}) 71 | 72 | is.Eq(OnLoadData, name) 73 | is.NotEmpty(c.Data()) 74 | is.Nil(err) 75 | is.Eq("value", c.String("str-map")) 76 | 77 | // LoadData 78 | err = c.LoadData("invalid") 79 | is.Err(err) 80 | 81 | is.Panics(func() { 82 | c.WithOptions(ParseEnv) 83 | }) 84 | 85 | err = c.LoadStrings(JSON, `{"name": "inhere"}`, jsonStr) 86 | is.Nil(err) 87 | 88 | // LoadSources 89 | err = c.LoadSources(JSON, []byte(`{"name": "inhere"}`), []byte(jsonStr)) 90 | is.Nil(err) 91 | 92 | err = c.LoadSources(JSON, []byte(`invalid`)) 93 | is.Err(err) 94 | 95 | err = c.LoadSources(JSON, []byte(`{"name": "inhere"}`), []byte(`invalid`)) 96 | is.Err(err) 97 | 98 | c = New("test") 99 | 100 | // LoadFiles 101 | err = c.LoadFiles("not-exist.json") 102 | is.Err(err) 103 | 104 | err = c.LoadFiles("testdata/json_error.json") 105 | is.Err(err) 106 | 107 | err = c.LoadExists("testdata/json_error.json") 108 | is.Err(err) 109 | 110 | // LoadStrings 111 | err = c.LoadStrings("invalid", jsonStr) 112 | is.Err(err) 113 | 114 | err = c.LoadStrings(JSON, "invalid") 115 | is.Err(err) 116 | 117 | err = c.LoadStrings(JSON, `{"name": "inhere"}`, "invalid") 118 | is.Err(err) 119 | } 120 | 121 | func TestLoad_error(t *testing.T) { 122 | is := assert.New(t) 123 | 124 | err := LoadFilesByFormat(Yaml, "testdata/json_base.json") 125 | is.Err(err) 126 | 127 | err = LoadExistsByFormat(Yaml, "testdata/json_base.json") 128 | is.Err(err) 129 | } 130 | 131 | func TestLoadRemote(t *testing.T) { 132 | is := assert.New(t) 133 | 134 | // invalid remote url 135 | url3 := "invalid-url" 136 | err := LoadRemote(JSON, url3) 137 | is.Err(err) 138 | 139 | if runtime.GOOS == "windows" { 140 | t.Skip("skip test load remote on Windows") 141 | return 142 | } 143 | 144 | // load remote config 145 | c := New("remote") 146 | url := "https://raw.githubusercontent.com/gookit/config/master/testdata/json_base.json" 147 | err = c.LoadRemote(JSON, url) 148 | is.Nil(err) 149 | is.Eq("123", c.String("age", "")) 150 | 151 | is.Len(c.LoadedUrls(), 1) 152 | is.Eq(url, c.LoadedUrls()[0]) 153 | 154 | // load invalid remote data 155 | url1 := "https://raw.githubusercontent.com/gookit/config/master/testdata/json_error.json" 156 | err = c.LoadRemote(JSON, url1) 157 | is.Err(err) 158 | 159 | // load not exist 160 | url2 := "https://raw.githubusercontent.com/gookit/config/master/testdata/not-exist.txt" 161 | err = c.LoadRemote(JSON, url2) 162 | is.Err(err) 163 | } 164 | 165 | func TestLoadFlags(t *testing.T) { 166 | is := assert.New(t) 167 | 168 | ClearAll() 169 | c := Default() 170 | bakArgs := os.Args 171 | defer func() { 172 | os.Args = bakArgs 173 | }() 174 | 175 | defines := []string{"name", 176 | "env:Set the run `env`", 177 | "debug:bool", "age:int", "var0:uint", 178 | "unknownTyp:notExist", 179 | "desc:string:This is a custom description: abc", 180 | } 181 | 182 | // custom global flag instance 183 | flag.CommandLine = flag.NewFlagSet("binFile", flag.ContinueOnError) 184 | 185 | t.Run("show help", func(t *testing.T) { 186 | // show help 187 | os.Args = []string{"./binFile", "--help"} 188 | is.Nil(LoadFlags(defines)) 189 | // reset flag instance 190 | flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) 191 | }) 192 | 193 | t.Run("load flags", func(t *testing.T) { 194 | // --name inhere --env dev --age 99 --debug 195 | os.Args = []string{ 196 | "./binFile", 197 | "--env", "dev", 198 | "--age", "99", 199 | "--var0", "12", 200 | "--name", "inhere", 201 | "--unknownTyp", "val", 202 | "--debug", 203 | "--desc=abc", 204 | } 205 | 206 | // load flag info 207 | err := LoadFlags(defines) 208 | is.Nil(err) 209 | is.Eq("inhere", c.String("name", "")) 210 | is.Eq("dev", c.String("env", "")) 211 | is.Eq(99, c.Int("age")) 212 | is.Eq(uint(12), c.Uint("var0")) 213 | is.Eq(uint(20), c.Uint("not-exist", uint(20))) 214 | is.Eq("val", c.Get("unknownTyp")) 215 | is.True(c.Bool("debug", false)) 216 | is.Eq("abc", c.String("desc")) 217 | descFlag := flag.Lookup("desc") 218 | is.NotNil(descFlag) 219 | is.Eq("This is a custom description: abc", descFlag.Usage) 220 | }) 221 | 222 | t.Run("set sub key", func(t *testing.T) { 223 | // set sub key 224 | c = New("flag") 225 | _ = c.LoadStrings(JSON, jsonStr) 226 | os.Args = []string{ 227 | "./binFile", 228 | "--map1.key", "new val", 229 | } 230 | is.Eq("val", c.String("map1.key")) 231 | err := c.LoadFlags([]string{"--map1.key"}) 232 | is.NoErr(err) 233 | is.Eq("new val", c.String("map1.key")) 234 | }) 235 | // fmt.Println(err) 236 | // fmt.Printf("%#v\n", c.Data()) 237 | } 238 | 239 | func TestLoadOSEnv(t *testing.T) { 240 | ClearAll() 241 | 242 | testutil.MockEnvValues(map[string]string{ 243 | "APP_NAME": "config", 244 | "app_debug": "true", 245 | "test_env0": "val0", 246 | "TEST_ENV1": "val1", 247 | }, func() { 248 | assert.Eq(t, "", String("test_env0")) 249 | 250 | LoadOSEnv([]string{"APP_NAME", "app_debug", "test_env0"}, true) 251 | 252 | assert.True(t, Bool("app_debug")) 253 | assert.Eq(t, "config", String("app_name")) 254 | assert.Eq(t, "val0", String("test_env0")) 255 | assert.Eq(t, "", String("test_env1")) 256 | }) 257 | 258 | ClearAll() 259 | } 260 | 261 | func TestLoadOSEnvs(t *testing.T) { 262 | ClearAll() 263 | 264 | testutil.MockEnvValues(map[string]string{ 265 | "APP_NAME": "config", 266 | "APP_DEBUG": "true", 267 | "TEST_ENV0": "val0", 268 | "TEST_ENV1": "val1", 269 | }, func() { 270 | assert.Eq(t, "", String("test_env0")) 271 | assert.Eq(t, "val0", Getenv("TEST_ENV0")) 272 | 273 | LoadOSEnvs(map[string]string{ 274 | "APP_NAME": "", 275 | "APP_DEBUG": "app_debug", 276 | "TEST_ENV0": "test0", 277 | }) 278 | 279 | assert.True(t, Bool("app_debug")) 280 | assert.Eq(t, "config", String("app_name")) 281 | assert.Eq(t, "val0", String("test0")) 282 | assert.Eq(t, "", String("test_env1")) 283 | }) 284 | 285 | ClearAll() 286 | } 287 | 288 | func TestLoadFromDir(t *testing.T) { 289 | ClearAll() 290 | assert.NoErr(t, LoadStrings(JSON, `{ 291 | "topKey": "a value" 292 | }`)) 293 | 294 | assert.NoErr(t, LoadFromDir("testdata/subdir", JSON)) 295 | dump.P(Data()) 296 | assert.Eq(t, "value in sub data", Get("subdata.key01")) 297 | assert.Eq(t, "value in task.json", Get("task.key01")) 298 | 299 | ClearAll() 300 | assert.NoErr(t, LoadFromDir("testdata/emptydir", JSON)) 301 | 302 | // with DataKey option. see https://github.com/gookit/config/issues/173 303 | assert.NoErr(t, LoadFromDir("testdata/subdir", JSON, func(lo *LoadOptions) { 304 | lo.DataKey = "dataList" 305 | })) 306 | dump.P(Data()) 307 | dl := Get("dataList") 308 | assert.NotNil(t, dl) 309 | assert.IsKind(t, reflect.Slice, dl) 310 | ClearAll() 311 | } 312 | 313 | func TestReloadFiles(t *testing.T) { 314 | ClearAll() 315 | c := Default() 316 | // no loaded files 317 | assert.NoErr(t, ReloadFiles()) 318 | 319 | var eventName string 320 | c.WithOptions(WithHookFunc(func(event string, c *Config) { 321 | eventName = event 322 | })) 323 | 324 | // load files 325 | err := LoadFiles("testdata/json_base.json", "testdata/json_other.json") 326 | assert.NoErr(t, err) 327 | assert.Eq(t, OnLoadData, eventName) 328 | assert.NotEmpty(t, c.LoadedFiles()) 329 | assert.Eq(t, "app2", c.String("name")) 330 | 331 | // set value 332 | assert.NoErr(t, c.Set("name", "new value")) 333 | assert.Eq(t, OnSetValue, eventName) 334 | assert.Eq(t, "new value", c.String("name")) 335 | 336 | // reload files 337 | assert.NoErr(t, ReloadFiles()) 338 | assert.Eq(t, OnReloadData, eventName) 339 | 340 | // value is reverted 341 | assert.Eq(t, "app2", c.String("name")) 342 | ClearAll() 343 | } 344 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "dario.cat/mergo" 7 | "github.com/gookit/goutil" 8 | "github.com/mitchellh/mapstructure" 9 | ) 10 | 11 | // there are some event names for config data changed. 12 | const ( 13 | OnSetValue = "set.value" 14 | OnSetData = "set.data" 15 | OnLoadData = "load.data" 16 | OnReloadData = "reload.data" 17 | OnCleanData = "clean.data" 18 | ) 19 | 20 | // HookFunc on config data changed. 21 | type HookFunc func(event string, c *Config) 22 | 23 | // Options config options 24 | type Options struct { 25 | // ParseEnv parse env in string value and default value. default: false 26 | // 27 | // - like: "${EnvName}" "${EnvName|default}" 28 | ParseEnv bool 29 | // ParseTime parses a duration string to `time.Duration`. default: false 30 | // 31 | // eg: 10s, 2m 32 | ParseTime bool 33 | // ParseDefault tag on binding data to struct. default: false 34 | // 35 | // - tag: default 36 | // 37 | // NOTE: If you want to parse a substruct, you need to set the `default:""` flag on the struct, 38 | // otherwise the fields that will not resolve to it will not be resolved. 39 | ParseDefault bool 40 | // Readonly config is readonly. default: false 41 | Readonly bool 42 | // EnableCache enable config data cache. default: false 43 | EnableCache bool 44 | // ParseKey support key path, allow finding value by key path. default: true 45 | // 46 | // - eg: 'key.sub' will find `map[key]sub` 47 | ParseKey bool 48 | // TagName tag name for binding data to struct 49 | // 50 | // Deprecated: please set tag name by DecoderConfig, or use SetTagName() 51 | TagName string 52 | // Delimiter the delimiter char for split key path, on `ParseKey=true`. 53 | // 54 | // - default is '.' 55 | Delimiter byte 56 | // DumpFormat default write format. default is 'json' 57 | DumpFormat string 58 | // ReadFormat default input format. default is 'json' 59 | ReadFormat string 60 | // DecoderConfig setting for binding data to struct. such as: TagName 61 | DecoderConfig *mapstructure.DecoderConfig 62 | // MergeOptions settings for merge two data 63 | MergeOptions []func(*mergo.Config) 64 | // HookFunc on data changed. you can do something... 65 | HookFunc HookFunc 66 | // WatchChange bool 67 | } 68 | 69 | // OptionFn option func 70 | type OptionFn func(*Options) 71 | 72 | func newDefaultOption() *Options { 73 | return &Options{ 74 | ParseKey: true, 75 | TagName: defaultStructTag, 76 | Delimiter: defaultDelimiter, 77 | // for export 78 | DumpFormat: JSON, 79 | ReadFormat: JSON, 80 | // struct decoder config 81 | DecoderConfig: newDefaultDecoderConfig(""), 82 | MergeOptions: []func(*mergo.Config){ 83 | mergo.WithOverride, 84 | mergo.WithTypeCheck, 85 | }, 86 | } 87 | } 88 | 89 | func newDefaultDecoderConfig(tagName string) *mapstructure.DecoderConfig { 90 | if tagName == "" { 91 | tagName = defaultStructTag 92 | } 93 | 94 | return &mapstructure.DecoderConfig{ 95 | // tag name for binding struct 96 | TagName: tagName, 97 | // will auto convert string to int/uint 98 | WeaklyTypedInput: true, 99 | } 100 | } 101 | 102 | // SetTagName for mapping data to struct 103 | func (o *Options) SetTagName(tagName string) { 104 | o.TagName = tagName 105 | o.DecoderConfig.TagName = tagName 106 | } 107 | 108 | func (o *Options) shouldAddHookFunc() bool { 109 | return o.ParseTime || o.ParseEnv 110 | } 111 | 112 | func (o *Options) makeDecoderConfig() *mapstructure.DecoderConfig { 113 | var bindConf *mapstructure.DecoderConfig 114 | if o.DecoderConfig == nil { 115 | bindConf = newDefaultDecoderConfig(o.TagName) 116 | } else { 117 | // copy new config for each binding. 118 | copyConf := *o.DecoderConfig 119 | bindConf = ©Conf 120 | 121 | // compatible with previous settings opts.TagName 122 | if bindConf.TagName == "" { 123 | bindConf.TagName = o.TagName 124 | } 125 | } 126 | 127 | // add hook on decode value to struct 128 | if bindConf.DecodeHook == nil && o.shouldAddHookFunc() { 129 | bindConf.DecodeHook = ValDecodeHookFunc(o.ParseEnv, o.ParseTime) 130 | } 131 | 132 | return bindConf 133 | } 134 | 135 | /************************************************************* 136 | * config setting 137 | *************************************************************/ 138 | 139 | // WithTagName set tag name for export to struct 140 | func WithTagName(tagName string) func(*Options) { 141 | return func(opts *Options) { 142 | opts.SetTagName(tagName) 143 | } 144 | } 145 | 146 | // ParseEnv set parse env value 147 | func ParseEnv(opts *Options) { opts.ParseEnv = true } 148 | 149 | // ParseTime set parse time string. 150 | func ParseTime(opts *Options) { opts.ParseTime = true } 151 | 152 | // ParseDefault tag value on binding data to struct. 153 | func ParseDefault(opts *Options) { opts.ParseDefault = true } 154 | 155 | // Readonly set readonly 156 | func Readonly(opts *Options) { opts.Readonly = true } 157 | 158 | // Delimiter set delimiter char 159 | func Delimiter(sep byte) func(*Options) { 160 | return func(opts *Options) { 161 | opts.Delimiter = sep 162 | } 163 | } 164 | 165 | // SaveFileOnSet set hook func, will panic on save error 166 | func SaveFileOnSet(fileName string, format string) func(options *Options) { 167 | return func(opts *Options) { 168 | opts.HookFunc = func(event string, c *Config) { 169 | if strings.HasPrefix(event, "set.") { 170 | goutil.PanicErr(c.DumpToFile(fileName, format)) 171 | } 172 | } 173 | } 174 | } 175 | 176 | // WithHookFunc set hook func 177 | func WithHookFunc(fn HookFunc) func(*Options) { 178 | return func(opts *Options) { 179 | opts.HookFunc = fn 180 | } 181 | } 182 | 183 | // EnableCache set readonly 184 | func EnableCache(opts *Options) { opts.EnableCache = true } 185 | 186 | // WithOptions with options 187 | func WithOptions(opts ...OptionFn) { dc.WithOptions(opts...) } 188 | 189 | // WithOptions apply some options 190 | func (c *Config) WithOptions(opts ...OptionFn) *Config { 191 | if !c.IsEmpty() { 192 | panic("config: Cannot set options after data has been loaded") 193 | } 194 | 195 | // apply options 196 | for _, opt := range opts { 197 | opt(c.opts) 198 | } 199 | return c 200 | } 201 | 202 | // GetOptions get options 203 | func GetOptions() *Options { return dc.Options() } 204 | 205 | // Options get 206 | func (c *Config) Options() *Options { 207 | return c.opts 208 | } 209 | 210 | // With apply some options 211 | func (c *Config) With(fn func(c *Config)) *Config { 212 | fn(c) 213 | return c 214 | } 215 | 216 | // Readonly disable set data to config. 217 | // 218 | // Usage: 219 | // 220 | // config.LoadFiles(a, b, c) 221 | // config.Readonly() 222 | func (c *Config) Readonly() { 223 | c.opts.Readonly = true 224 | } 225 | -------------------------------------------------------------------------------- /other/other.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package other is an example of a custom driver 3 | */ 4 | package other 5 | 6 | import ( 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/config/v2/ini" 9 | ) 10 | 11 | // DriverName string 12 | const DriverName = "other" 13 | 14 | var ( 15 | // Encoder is the encoder for this driver 16 | Encoder = ini.Encoder 17 | // Decoder is the decoder for this driver 18 | Decoder = ini.Decoder 19 | // Driver is the exported symbol 20 | Driver = config.NewDriver(DriverName, Decoder, Encoder) 21 | ) 22 | -------------------------------------------------------------------------------- /other/other_test.go: -------------------------------------------------------------------------------- 1 | package other 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestOtherDriver(t *testing.T) { 12 | is := assert.New(t) 13 | 14 | is.Eq("other", Driver.Name()) 15 | 16 | c := config.NewEmpty("test") 17 | is.False(c.HasDecoder("other")) 18 | 19 | c.AddDriver(Driver) 20 | is.True(c.HasDecoder("other")) 21 | is.True(c.HasEncoder("other")) 22 | 23 | _, err := Encoder(map[string]any{"k": "v"}) 24 | is.Nil(err) 25 | 26 | _, err = Encoder("invalid") 27 | is.Err(err) 28 | } 29 | 30 | func TestOtherLoader(t *testing.T) { 31 | config.AddDriver(Driver) 32 | 33 | err := config.LoadFiles("../testdata/ini_base.other") 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | fmt.Printf("get config example:\n") 39 | 40 | name := config.String("name") 41 | fmt.Printf("get string\n - val: %v\n", name) 42 | 43 | map1 := config.StringMap("map1") 44 | fmt.Printf("get map\n - val: %#v\n", map1) 45 | 46 | val0 := config.String("map1.key") 47 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 48 | 49 | // can parse env name(ParseEnv: true) 50 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 51 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 52 | 53 | // set value 54 | _ = config.Set("name", "new name") 55 | name = config.String("name") 56 | fmt.Printf("set string\n - val: %v\n", name) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /properties/properties.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package properties is a driver use Java properties format content as config source 3 | 4 | Usage please see readme. 5 | 6 | */ 7 | package properties 8 | 9 | import ( 10 | "github.com/gookit/config/v2" 11 | "github.com/gookit/properties" 12 | ) 13 | 14 | // Name string 15 | const Name = "properties" 16 | 17 | var ( 18 | // Decoder the properties content decoder 19 | Decoder config.Decoder = properties.Decode 20 | 21 | // Encoder the properties content encoder 22 | Encoder config.Encoder = properties.Encode 23 | 24 | // Driver for properties 25 | Driver = config.NewDriver(Name, Decoder, Encoder) 26 | ) 27 | -------------------------------------------------------------------------------- /properties/properties_test.go: -------------------------------------------------------------------------------- 1 | package properties_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/config/v2" 7 | "github.com/gookit/config/v2/properties" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestDriver(t *testing.T) { 12 | is := assert.New(t) 13 | is.Eq(properties.Name, properties.Driver.Name()) 14 | 15 | c := config.NewEmpty("test") 16 | is.False(c.HasDecoder(properties.Name)) 17 | c.AddDriver(properties.Driver) 18 | 19 | is.True(c.HasDecoder(properties.Name)) 20 | is.True(c.HasEncoder(properties.Name)) 21 | 22 | m := struct { 23 | N string 24 | }{} 25 | err := properties.Decoder([]byte(` 26 | // comments 27 | n=value 28 | `), &m) 29 | 30 | is.Nil(err) 31 | is.Eq("value", m.N) 32 | } 33 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/gookit/goutil/envutil" 9 | "github.com/gookit/goutil/maputil" 10 | "github.com/gookit/goutil/mathutil" 11 | "github.com/gookit/goutil/strutil" 12 | ) 13 | 14 | // Exists key exists check 15 | func Exists(key string, findByPath ...bool) bool { return dc.Exists(key, findByPath...) } 16 | 17 | // Exists key exists check 18 | func (c *Config) Exists(key string, findByPath ...bool) (ok bool) { 19 | sep := c.opts.Delimiter 20 | if key = formatKey(key, string(sep)); key == "" { 21 | return 22 | } 23 | 24 | if _, ok = c.data[key]; ok { 25 | return 26 | } 27 | 28 | // disable find by path. 29 | if len(findByPath) > 0 && !findByPath[0] { 30 | return 31 | } 32 | 33 | // has sub key? eg. "lang.dir" 34 | if strings.IndexByte(key, sep) == -1 { 35 | return 36 | } 37 | 38 | keys := strings.Split(key, string(sep)) 39 | topK := keys[0] 40 | 41 | // find top item data based on top key 42 | var item any 43 | if item, ok = c.data[topK]; !ok { 44 | return 45 | } 46 | for _, k := range keys[1:] { 47 | switch typeData := item.(type) { 48 | case map[string]int: // is map(from Set) 49 | if item, ok = typeData[k]; !ok { 50 | return 51 | } 52 | case map[string]string: // is map(from Set) 53 | if item, ok = typeData[k]; !ok { 54 | return 55 | } 56 | case map[string]any: // is map(decode from toml/json/yaml.v3) 57 | if item, ok = typeData[k]; !ok { 58 | return 59 | } 60 | case map[any]any: // is map(decode from yaml.v2) 61 | if item, ok = typeData[k]; !ok { 62 | return 63 | } 64 | case []int: // is array(is from Set) 65 | i, err := strconv.Atoi(k) 66 | 67 | // check slice index 68 | if err != nil || len(typeData) < i { 69 | return false 70 | } 71 | case []string: // is array(is from Set) 72 | i, err := strconv.Atoi(k) 73 | if err != nil || len(typeData) < i { 74 | return false 75 | } 76 | case []any: // is array(load from file) 77 | i, err := strconv.Atoi(k) 78 | if err != nil || len(typeData) < i { 79 | return false 80 | } 81 | default: // error 82 | return false 83 | } 84 | } 85 | return true 86 | } 87 | 88 | /************************************************************* 89 | * read config data 90 | *************************************************************/ 91 | 92 | // Data return all config data 93 | func Data() map[string]any { return dc.Data() } 94 | 95 | // Data get all config data. 96 | // 97 | // Note: will don't apply any options, like ParseEnv 98 | func (c *Config) Data() map[string]any { 99 | return c.data 100 | } 101 | 102 | // Sub return a map config data by key 103 | func Sub(key string) map[string]any { return dc.Sub(key) } 104 | 105 | // Sub get a map config data by key 106 | // 107 | // Note: will don't apply any options, like ParseEnv 108 | func (c *Config) Sub(key string) map[string]any { 109 | if mp, ok := c.GetValue(key); ok { 110 | if mmp, ok := mp.(map[string]any); ok { 111 | return mmp 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | // Keys return all config data 118 | func Keys() []string { return dc.Keys() } 119 | 120 | // Keys get all config data 121 | func (c *Config) Keys() []string { 122 | keys := make([]string, 0, len(c.data)) 123 | for key := range c.data { 124 | keys = append(keys, key) 125 | } 126 | return keys 127 | } 128 | 129 | // Get config value by key string, support get sub-value by key path(eg. 'map.key'), 130 | func Get(key string, findByPath ...bool) any { return dc.Get(key, findByPath...) } 131 | 132 | // Get config value by key, findByPath default is true. 133 | func (c *Config) Get(key string, findByPath ...bool) any { 134 | val, _ := c.GetValue(key, findByPath...) 135 | return val 136 | } 137 | 138 | // GetValue get value by given key string. findByPath default is true. 139 | func GetValue(key string, findByPath ...bool) (any, bool) { 140 | return dc.GetValue(key, findByPath...) 141 | } 142 | 143 | // GetValue get value by given key string. findByPath default is true. 144 | // 145 | // Return: 146 | // - ok is true, find value from config 147 | // - ok is false, not found or error 148 | func (c *Config) GetValue(key string, findByPath ...bool) (value any, ok bool) { 149 | sep := c.opts.Delimiter 150 | if key = formatKey(key, string(sep)); key == "" { 151 | c.addError(ErrKeyIsEmpty) 152 | return 153 | } 154 | 155 | // if not is readonly 156 | if !c.opts.Readonly { 157 | c.lock.RLock() 158 | defer c.lock.RUnlock() 159 | } 160 | 161 | // is top key 162 | if value, ok = c.data[key]; ok { 163 | return 164 | } 165 | 166 | // disable find by path. 167 | if len(findByPath) > 0 && !findByPath[0] { 168 | // c.addError(ErrNotFound) 169 | return 170 | } 171 | 172 | // has sub key? eg. "lang.dir" 173 | if strings.IndexByte(key, sep) == -1 { 174 | // c.addError(ErrNotFound) 175 | return 176 | } 177 | 178 | keys := strings.Split(key, string(sep)) 179 | topK := keys[0] 180 | 181 | // find top item data based on top key 182 | var item any 183 | if item, ok = c.data[topK]; !ok { 184 | // c.addError(ErrNotFound) 185 | return 186 | } 187 | 188 | // find child 189 | // NOTICE: don't merge case, will result in an error. 190 | // e.g. case []int, []string 191 | // OR 192 | // case []int: 193 | // case []string: 194 | for _, k := range keys[1:] { 195 | switch typeData := item.(type) { 196 | case map[string]int: // is map(from Set) 197 | if item, ok = typeData[k]; !ok { 198 | return 199 | } 200 | case map[string]string: // is map(from Set) 201 | if item, ok = typeData[k]; !ok { 202 | return 203 | } 204 | case map[string]any: // is map(decode from toml/json) 205 | if item, ok = typeData[k]; !ok { 206 | return 207 | } 208 | case map[any]any: // is map(decode from yaml) 209 | if item, ok = typeData[k]; !ok { 210 | return 211 | } 212 | case []int: // is array(is from Set) 213 | i, err := strconv.Atoi(k) 214 | 215 | // check slice index 216 | if err != nil || len(typeData) < i { 217 | ok = false 218 | c.addError(err) 219 | return 220 | } 221 | 222 | item = typeData[i] 223 | case []string: // is array(is from Set) 224 | i, err := strconv.Atoi(k) 225 | if err != nil || len(typeData) < i { 226 | ok = false 227 | c.addError(err) 228 | return 229 | } 230 | 231 | item = typeData[i] 232 | case []any: // is array(load from file) 233 | i, err := strconv.Atoi(k) 234 | if err != nil || len(typeData) < i { 235 | ok = false 236 | c.addError(err) 237 | return 238 | } 239 | 240 | item = typeData[i] 241 | default: // error 242 | ok = false 243 | c.addErrorf("cannot get value of the key '%s'", key) 244 | return 245 | } 246 | } 247 | 248 | return item, true 249 | } 250 | 251 | /************************************************************* 252 | * read config (basic data type) 253 | *************************************************************/ 254 | 255 | // String get a string by key 256 | func String(key string, defVal ...string) string { return dc.String(key, defVal...) } 257 | 258 | // String get a string by key, if not found return default value 259 | func (c *Config) String(key string, defVal ...string) string { 260 | value, ok := c.getString(key) 261 | 262 | if !ok && len(defVal) > 0 { // give default value 263 | value = defVal[0] 264 | } 265 | return value 266 | } 267 | 268 | // MustString get a string by key, will panic on empty or not exists 269 | func MustString(key string) string { return dc.MustString(key) } 270 | 271 | // MustString get a string by key, will panic on empty or not exists 272 | func (c *Config) MustString(key string) string { 273 | value, ok := c.getString(key) 274 | if !ok { 275 | panic("config: string value not found, key: " + key) 276 | } 277 | return value 278 | } 279 | 280 | func (c *Config) getString(key string) (value string, ok bool) { 281 | // find from cache 282 | if c.opts.EnableCache && len(c.strCache) > 0 { 283 | value, ok = c.strCache[key] 284 | if ok { 285 | return 286 | } 287 | } 288 | 289 | val, ok := c.GetValue(key) 290 | if !ok { 291 | return 292 | } 293 | 294 | switch typVal := val.(type) { 295 | // from json `int` always is float64 296 | case string: 297 | value = typVal 298 | if c.opts.ParseEnv { 299 | value = envutil.ParseEnvValue(value) 300 | } 301 | default: 302 | var err error 303 | value, err = strutil.AnyToString(val, false) 304 | if err != nil { 305 | return "", false 306 | } 307 | } 308 | 309 | // add cache 310 | if ok && c.opts.EnableCache { 311 | if c.strCache == nil { 312 | c.strCache = make(map[string]string) 313 | } 314 | c.strCache[key] = value 315 | } 316 | return 317 | } 318 | 319 | // Int get an int by key 320 | func Int(key string, defVal ...int) int { return dc.Int(key, defVal...) } 321 | 322 | // Int get a int value, if not found return default value 323 | func (c *Config) Int(key string, defVal ...int) (value int) { 324 | i64, exist := c.tryInt64(key) 325 | 326 | if exist { 327 | value = int(i64) 328 | } else if len(defVal) > 0 { 329 | value = defVal[0] 330 | } 331 | return 332 | } 333 | 334 | // Uint get a uint value, if not found return default value 335 | func Uint(key string, defVal ...uint) uint { return dc.Uint(key, defVal...) } 336 | 337 | // Uint get a int value, if not found return default value 338 | func (c *Config) Uint(key string, defVal ...uint) (value uint) { 339 | i64, exist := c.tryInt64(key) 340 | 341 | if exist { 342 | value = uint(i64) 343 | } else if len(defVal) > 0 { 344 | value = defVal[0] 345 | } 346 | return 347 | } 348 | 349 | // Int64 get a int value, if not found return default value 350 | func Int64(key string, defVal ...int64) int64 { return dc.Int64(key, defVal...) } 351 | 352 | // Int64 get a int value, if not found return default value 353 | func (c *Config) Int64(key string, defVal ...int64) (value int64) { 354 | value, exist := c.tryInt64(key) 355 | 356 | if !exist && len(defVal) > 0 { 357 | value = defVal[0] 358 | } 359 | return 360 | } 361 | 362 | // try to get an int64 value by given key 363 | func (c *Config) tryInt64(key string) (value int64, ok bool) { 364 | strVal, ok := c.getString(key) 365 | if !ok { 366 | return 367 | } 368 | 369 | value, err := strconv.ParseInt(strVal, 10, 0) 370 | if err != nil { 371 | c.addError(err) 372 | } 373 | return 374 | } 375 | 376 | // Duration get a time.Duration type value. if not found return default value 377 | func Duration(key string, defVal ...time.Duration) time.Duration { return dc.Duration(key, defVal...) } 378 | 379 | // Duration get a time.Duration type value. if not found return default value 380 | func (c *Config) Duration(key string, defVal ...time.Duration) time.Duration { 381 | value, exist := c.tryInt64(key) 382 | 383 | if !exist && len(defVal) > 0 { 384 | return defVal[0] 385 | } 386 | return time.Duration(value) 387 | } 388 | 389 | // Float get a float64 value, if not found return default value 390 | func Float(key string, defVal ...float64) float64 { return dc.Float(key, defVal...) } 391 | 392 | // Float get a float64 by key 393 | func (c *Config) Float(key string, defVal ...float64) (value float64) { 394 | str, ok := c.getString(key) 395 | if !ok { 396 | if len(defVal) > 0 { 397 | value = defVal[0] 398 | } 399 | return 400 | } 401 | 402 | value, err := strconv.ParseFloat(str, 64) 403 | if err != nil { 404 | c.addError(err) 405 | } 406 | return 407 | } 408 | 409 | // Bool get a bool value, if not found return default value 410 | func Bool(key string, defVal ...bool) bool { return dc.Bool(key, defVal...) } 411 | 412 | // Bool looks up a value for a key in this section and attempts to parse that value as a boolean, 413 | // along with a boolean result similar to a map lookup. 414 | // 415 | // of following(case insensitive): 416 | // - true 417 | // - yes 418 | // - false 419 | // - no 420 | // - 1 421 | // - 0 422 | // 423 | // The `ok` boolean will be false in the event that the value could not be parsed as a bool 424 | func (c *Config) Bool(key string, defVal ...bool) (value bool) { 425 | rawVal, ok := c.getString(key) 426 | if !ok { 427 | if len(defVal) > 0 { 428 | return defVal[0] 429 | } 430 | return 431 | } 432 | 433 | lowerCase := strings.ToLower(rawVal) 434 | switch lowerCase { 435 | case "", "0", "false", "no": 436 | value = false 437 | case "1", "true", "yes": 438 | value = true 439 | default: 440 | c.addErrorf("the value '%s' cannot be convert to bool", lowerCase) 441 | } 442 | return 443 | } 444 | 445 | /************************************************************* 446 | * read config (complex data type) 447 | *************************************************************/ 448 | 449 | // Ints get config data as an int slice/array 450 | func Ints(key string) []int { return dc.Ints(key) } 451 | 452 | // Ints get config data as an int slice/array 453 | func (c *Config) Ints(key string) (arr []int) { 454 | rawVal, ok := c.GetValue(key) 455 | if !ok { 456 | return 457 | } 458 | 459 | switch typeData := rawVal.(type) { 460 | case []int: 461 | arr = typeData 462 | case []any: 463 | for _, v := range typeData { 464 | iv, err := mathutil.ToInt(v) 465 | // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) 466 | if err != nil { 467 | c.addError(err) 468 | arr = arr[0:0] // reset 469 | return 470 | } 471 | 472 | arr = append(arr, iv) 473 | } 474 | default: 475 | c.addErrorf("value cannot be convert to []int, key is '%s'", key) 476 | } 477 | return 478 | } 479 | 480 | // IntMap get config data as a map[string]int 481 | func IntMap(key string) map[string]int { return dc.IntMap(key) } 482 | 483 | // IntMap get config data as a map[string]int 484 | func (c *Config) IntMap(key string) (mp map[string]int) { 485 | rawVal, ok := c.GetValue(key) 486 | if !ok { 487 | return 488 | } 489 | 490 | switch typeData := rawVal.(type) { 491 | case map[string]int: // from Set 492 | mp = typeData 493 | case map[string]any: // decode from json,toml 494 | mp = make(map[string]int) 495 | for k, v := range typeData { 496 | // iv, err := strconv.Atoi(fmt.Sprintf("%v", v)) 497 | iv, err := mathutil.ToInt(v) 498 | if err != nil { 499 | c.addError(err) 500 | mp = map[string]int{} // reset 501 | return 502 | } 503 | mp[k] = iv 504 | } 505 | case map[any]any: // if decode from yaml 506 | mp = make(map[string]int) 507 | for k, v := range typeData { 508 | // iv, err := strconv.Atoi(fmt.Sprintf( "%v", v)) 509 | iv, err := mathutil.ToInt(v) 510 | if err != nil { 511 | c.addError(err) 512 | mp = map[string]int{} // reset 513 | return 514 | } 515 | 516 | // sk := fmt.Sprintf("%v", k) 517 | sk, _ := strutil.AnyToString(k, false) 518 | mp[sk] = iv 519 | } 520 | default: 521 | c.addErrorf("value cannot be convert to map[string]int, key is '%s'", key) 522 | } 523 | return 524 | } 525 | 526 | // Strings get strings by key 527 | func Strings(key string) []string { return dc.Strings(key) } 528 | 529 | // Strings get config data as a string slice/array 530 | func (c *Config) Strings(key string) (arr []string) { 531 | var ok bool 532 | // find from cache 533 | if c.opts.EnableCache && len(c.sArrCache) > 0 { 534 | arr, ok = c.sArrCache[key] 535 | if ok { 536 | return 537 | } 538 | } 539 | 540 | rawVal, ok := c.GetValue(key) 541 | if !ok { 542 | return 543 | } 544 | 545 | switch typeData := rawVal.(type) { 546 | case []string: 547 | arr = typeData 548 | case []any: 549 | for _, v := range typeData { 550 | // arr = append(arr, fmt.Sprintf("%v", v)) 551 | arr = append(arr, strutil.MustString(v)) 552 | } 553 | default: 554 | c.addErrorf("value cannot be convert to []string, key is '%s'", key) 555 | return 556 | } 557 | 558 | // add cache 559 | if c.opts.EnableCache { 560 | if c.sArrCache == nil { 561 | c.sArrCache = make(map[string]strArr) 562 | } 563 | c.sArrCache[key] = arr 564 | } 565 | return 566 | } 567 | 568 | // StringsBySplit get []string by split a string value. 569 | func StringsBySplit(key, sep string) []string { return dc.StringsBySplit(key, sep) } 570 | 571 | // StringsBySplit get []string by split a string value. 572 | func (c *Config) StringsBySplit(key, sep string) (ss []string) { 573 | if str, ok := c.getString(key); ok { 574 | ss = strutil.Split(str, sep) 575 | } 576 | return 577 | } 578 | 579 | // StringMap get config data as a map[string]string 580 | func StringMap(key string) map[string]string { return dc.StringMap(key) } 581 | 582 | // StringMap get config data as a map[string]string 583 | func (c *Config) StringMap(key string) (mp map[string]string) { 584 | var ok bool 585 | 586 | // find from cache 587 | if c.opts.EnableCache && len(c.sMapCache) > 0 { 588 | mp, ok = c.sMapCache[key] 589 | if ok { 590 | return 591 | } 592 | } 593 | 594 | rawVal, ok := c.GetValue(key) 595 | if !ok { 596 | return 597 | } 598 | 599 | switch typeData := rawVal.(type) { 600 | case map[string]string: // from Set 601 | mp = typeData 602 | case map[string]any: // decode from json,toml,yaml.v3 603 | mp = make(map[string]string, len(typeData)) 604 | 605 | for k, v := range typeData { 606 | switch tv := v.(type) { 607 | case string: 608 | if c.opts.ParseEnv { 609 | mp[k] = envutil.ParseEnvValue(tv) 610 | } else { 611 | mp[k] = tv 612 | } 613 | default: 614 | mp[k] = strutil.QuietString(v) 615 | } 616 | } 617 | case map[any]any: // decode from yaml v2 618 | mp = make(map[string]string, len(typeData)) 619 | 620 | for k, v := range typeData { 621 | sk := strutil.QuietString(k) 622 | 623 | switch typVal := v.(type) { 624 | case string: 625 | if c.opts.ParseEnv { 626 | mp[sk] = envutil.ParseEnvValue(typVal) 627 | } else { 628 | mp[sk] = typVal 629 | } 630 | default: 631 | mp[sk] = strutil.QuietString(v) 632 | } 633 | } 634 | default: 635 | c.addErrorf("value cannot be convert to map[string]string, key is %q", key) 636 | return 637 | } 638 | 639 | // add cache 640 | if c.opts.EnableCache { 641 | if c.sMapCache == nil { 642 | c.sMapCache = make(map[string]strMap) 643 | } 644 | c.sMapCache[key] = mp 645 | } 646 | return 647 | } 648 | 649 | // SubDataMap get sub config data as maputil.Map 650 | func SubDataMap(key string) maputil.Map { return dc.SubDataMap(key) } 651 | 652 | // SubDataMap get sub config data as maputil.Map 653 | // 654 | // TIP: will not enable parse Env and more 655 | func (c *Config) SubDataMap(key string) maputil.Map { 656 | if mp, ok := c.GetValue(key); ok { 657 | if mmp, ok := mp.(map[string]any); ok { 658 | return mmp 659 | } 660 | } 661 | 662 | // keep is not nil 663 | return maputil.Map{} 664 | } 665 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gookit/goutil/testutil" 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func TestConfig_GetValue(t *testing.T) { 13 | is := assert.New(t) 14 | 15 | ClearAll() 16 | err := LoadStrings(JSON, jsonStr) 17 | is.Nil(err) 18 | 19 | c := Default() 20 | 21 | // error on get 22 | _, ok := GetValue("") 23 | is.False(ok) 24 | 25 | _, ok = c.GetValue("notExist") 26 | is.False(ok) 27 | _, ok = c.GetValue("name.sub") 28 | is.False(ok) 29 | is.Err(c.Error()) 30 | 31 | _, ok = c.GetValue("map1.key", false) 32 | is.False(ok) 33 | is.False(Exists("map1.key", false)) 34 | 35 | val, ok := GetValue("map1.notExist") 36 | is.Nil(val) 37 | is.False(ok) 38 | is.False(Exists("map1.notExist")) 39 | 40 | val, ok = GetValue("notExist.sub") 41 | is.False(ok) 42 | is.Nil(val) 43 | is.False(Exists("notExist.sub")) 44 | 45 | val, ok = c.GetValue("arr1.100") 46 | is.Nil(val) 47 | is.False(ok) 48 | is.False(Exists("arr1.100")) 49 | 50 | val, ok = c.GetValue("arr1.notExist") 51 | is.Nil(val) 52 | is.False(ok) 53 | is.False(Exists("arr1.notExist")) 54 | 55 | // load data for tests 56 | err = c.LoadData(map[string]any{ 57 | "setStrMap": map[string]string{ 58 | "k": "v", 59 | }, 60 | "setIntMap": map[string]int{ 61 | "k2": 23, 62 | }, 63 | }) 64 | is.Nil(err) 65 | // -- assert map[string]string 66 | is.True(Exists("setStrMap.k")) 67 | is.Eq("v", Get("setStrMap.k")) 68 | is.False(Exists("setStrMap.k1")) 69 | 70 | // -- assert map[string]int 71 | is.True(Exists("setIntMap.k2")) 72 | is.Eq(23, Get("setIntMap.k2")) 73 | is.False(Exists("setIntMap.k1")) 74 | 75 | ClearAll() 76 | } 77 | 78 | func TestGet(t *testing.T) { 79 | is := assert.New(t) 80 | 81 | ClearAll() 82 | err := LoadStrings(JSON, jsonStr) 83 | is.Nil(err) 84 | 85 | // fmt.Printf("%#v\n", Data()) 86 | c := Default() 87 | 88 | is.False(c.IsEmpty()) 89 | is.True(Exists("age")) 90 | is.True(Exists("map1.key")) 91 | is.True(Exists("arr1.1")) 92 | is.False(Exists("arr1.1", false)) 93 | is.False(Exists("not-exist.sub")) 94 | is.False(Exists("")) 95 | is.False(Exists("not-exist")) 96 | 97 | // get value 98 | val := Get("age") 99 | is.Eq(float64(123), val) 100 | is.Eq("float64", fmt.Sprintf("%T", val)) 101 | 102 | val = Get("not-exist") 103 | is.Nil(val) 104 | 105 | val = Get("name") 106 | is.Eq("app", val) 107 | 108 | is.Eq([]string{"php", "go"}, StringsBySplit("tagsStr", ",")) 109 | 110 | // get string array 111 | arr := Strings("notExist") 112 | is.Empty(arr) 113 | 114 | arr = Strings("map1") 115 | is.Empty(arr) 116 | 117 | arr = Strings("arr1") 118 | is.Eq(`[]string{"val", "val1", "val2"}`, fmt.Sprintf("%#v", arr)) 119 | 120 | val = String("arr1.1") 121 | is.Eq("val1", val) 122 | 123 | err = LoadStrings(JSON, `{ 124 | "iArr": [12, 34, 36], 125 | "iMap": {"k1": 12, "k2": 34, "k3": 36} 126 | }`) 127 | is.Nil(err) 128 | 129 | // Ints: get int arr 130 | iarr := Ints("name") 131 | is.False(Exists("name.1")) 132 | is.Empty(iarr) 133 | 134 | iarr = Ints("notExist") 135 | is.Empty(iarr) 136 | 137 | iarr = Ints("iArr") 138 | is.Eq(`[]int{12, 34, 36}`, fmt.Sprintf("%#v", iarr)) 139 | 140 | iv := Int("iArr.1") 141 | is.Eq(34, iv) 142 | 143 | iv = Int("iArr.100") 144 | is.Eq(0, iv) 145 | 146 | // IntMap: get int map 147 | imp := IntMap("name") 148 | is.Empty(imp) 149 | imp = IntMap("notExist") 150 | is.Empty(imp) 151 | 152 | imp = IntMap("iMap") 153 | is.NotEmpty(imp) 154 | 155 | iv = Int("iMap.k2") 156 | is.Eq(34, iv) 157 | is.True(Exists("iMap.k2")) 158 | 159 | iv = Int("iMap.notExist") 160 | is.Eq(0, iv) 161 | is.False(Exists("iMap.notExist")) 162 | 163 | // set a intMap 164 | err = Set("intMap0", map[string]int{"a": 1, "b": 2}) 165 | is.Nil(err) 166 | 167 | imp = IntMap("intMap0") 168 | is.NotEmpty(imp) 169 | is.Eq(1, imp["a"]) 170 | is.Eq(2, Get("intMap0.b")) 171 | is.True(Exists("intMap0.a")) 172 | is.False(Exists("intMap0.c")) 173 | 174 | // StringMap: get string map 175 | smp := StringMap("map1") 176 | is.Eq("val1", smp["key1"]) 177 | 178 | // like load from yaml content 179 | // c = New("test") 180 | err = c.LoadData(map[string]any{ 181 | "newIArr": []int{2, 3}, 182 | "newSArr": []string{"a", "b"}, 183 | "newIArr1": []any{12, 23}, 184 | "newIArr2": []any{12, "abc"}, 185 | "invalidMap": map[string]int{"k": 1}, 186 | "yMap": map[any]any{ 187 | "k0": "v0", 188 | "k1": 23, 189 | }, 190 | "yMap1": map[any]any{ 191 | "k": "v", 192 | "k1": 23, 193 | "k2": []any{23, 45}, 194 | }, 195 | "yMap10": map[string]any{ 196 | "k": "v", 197 | "k1": 23, 198 | "k2": []any{23, 45}, 199 | }, 200 | "yMap2": map[any]any{ 201 | "k": 2, 202 | "k1": 23, 203 | }, 204 | "yArr": []any{23, 45, "val", map[string]any{"k4": "v4"}}, 205 | }) 206 | is.Nil(err) 207 | 208 | iarr = Ints("newIArr") 209 | is.Eq("[2 3]", fmt.Sprintf("%v", iarr)) 210 | 211 | iarr = Ints("newIArr1") 212 | is.Eq("[12 23]", fmt.Sprintf("%v", iarr)) 213 | iarr = Ints("newIArr2") 214 | is.Empty(iarr) 215 | 216 | iv = Int("newIArr.1") 217 | is.True(Exists("newIArr.1")) 218 | is.Eq(3, iv) 219 | 220 | iv = Int("newIArr.200") 221 | is.False(Exists("newIArr.200")) 222 | is.Eq(0, iv) 223 | 224 | // invalid intMap 225 | imp = IntMap("yMap1") 226 | is.Empty(imp) 227 | 228 | imp = IntMap("yMap10") 229 | is.Empty(imp) 230 | 231 | imp = IntMap("yMap2") 232 | is.Eq(2, imp["k"]) 233 | 234 | val = String("newSArr.1") 235 | is.True(Exists("newSArr.1")) 236 | is.Eq("b", val) 237 | 238 | val = String("newSArr.100") 239 | is.False(Exists("newSArr.100")) 240 | is.Eq("", val) 241 | 242 | smp = StringMap("invalidMap") 243 | is.Nil(smp) 244 | 245 | smp = StringMap("yMap.notExist") 246 | is.Nil(smp) 247 | 248 | smp = StringMap("yMap") 249 | is.True(Exists("yMap.k0")) 250 | is.False(Exists("yMap.k100")) 251 | is.Eq("v0", smp["k0"]) 252 | 253 | iarr = Ints("yMap1.k2") 254 | is.Eq("[23 45]", fmt.Sprintf("%v", iarr)) 255 | } 256 | 257 | func TestInt(t *testing.T) { 258 | is := assert.New(t) 259 | ClearAll() 260 | _ = LoadStrings(JSON, jsonStr) 261 | 262 | is.True(Exists("age")) 263 | 264 | iv := Int("age") 265 | is.Eq(123, iv) 266 | 267 | iv = Int("name") 268 | is.Eq(0, iv) 269 | 270 | iv = Int("notExist", 34) 271 | is.Eq(34, iv) 272 | 273 | c := Default() 274 | iv = c.Int("age") 275 | is.Eq(123, iv) 276 | iv = c.Int("notExist") 277 | is.Eq(0, iv) 278 | 279 | uiv := Uint("age") 280 | is.Eq(uint(123), uiv) 281 | 282 | dur := Duration("age") 283 | is.Eq(time.Duration(123), dur) 284 | is.Eq(time.Duration(340), Duration("not-exist", 340)) 285 | 286 | ClearAll() 287 | } 288 | 289 | func TestInt64(t *testing.T) { 290 | is := assert.New(t) 291 | ClearAll() 292 | _ = LoadStrings(JSON, jsonStr) 293 | 294 | // get int64 295 | iv64 := Int64("age") 296 | is.Eq(int64(123), iv64) 297 | 298 | iv64 = Int64("name") 299 | is.Eq(iv64, int64(0)) 300 | 301 | iv64 = Int64("age", 34) 302 | is.Eq(int64(123), iv64) 303 | iv64 = Int64("notExist", 34) 304 | is.Eq(int64(34), iv64) 305 | 306 | c := Default() 307 | iv64 = c.Int64("age") 308 | is.Eq(int64(123), iv64) 309 | iv64 = c.Int64("notExist") 310 | is.Eq(int64(0), iv64) 311 | 312 | ClearAll() 313 | } 314 | 315 | func TestFloat(t *testing.T) { 316 | is := assert.New(t) 317 | ClearAll() 318 | _ = LoadStrings(JSON, jsonStr) 319 | c := Default() 320 | 321 | // get float 322 | err := c.Set("flVal", 23.45) 323 | is.Nil(err) 324 | flt := c.Float("flVal") 325 | is.Eq(23.45, flt) 326 | 327 | flt = Float("name") 328 | is.Eq(float64(0), flt) 329 | 330 | flt = c.Float("notExists") 331 | is.Eq(float64(0), flt) 332 | 333 | flt = c.Float("notExists", 10) 334 | is.Eq(float64(10), flt) 335 | 336 | flt = Float("flVal", 0) 337 | is.Eq(23.45, flt) 338 | 339 | ClearAll() 340 | } 341 | 342 | func TestString(t *testing.T) { 343 | is := assert.New(t) 344 | ClearAll() 345 | _ = LoadStrings(JSON, jsonStr) 346 | 347 | // get string 348 | val := String("arr1") 349 | is.Eq("[val val1 val2]", val) 350 | 351 | str := String("notExists") 352 | is.Eq("", str) 353 | is.Panics(func() { 354 | MustString("notExists") 355 | }) 356 | 357 | str = String("notExists", "defVal") 358 | is.Eq("defVal", str) 359 | 360 | c := Default() 361 | str = c.String("name") 362 | is.Eq("app", str) 363 | str = c.String("notExist") 364 | is.Eq("", str) 365 | 366 | ClearAll() 367 | } 368 | 369 | func TestBool(t *testing.T) { 370 | is := assert.New(t) 371 | ClearAll() 372 | _ = LoadSources(JSON, []byte(jsonStr)) 373 | 374 | // get bool 375 | val := Get("debug") 376 | is.Eq(true, val) 377 | 378 | bv := Bool("debug") 379 | is.Eq(true, bv) 380 | 381 | bv = Bool("age") 382 | is.Eq(false, bv) 383 | 384 | bv = Bool("debug", false) 385 | is.Eq(true, bv) 386 | 387 | bv = Bool("notExist", false) 388 | is.Eq(false, bv) 389 | 390 | c := Default() 391 | bv = c.Bool("debug") 392 | is.True(bv) 393 | bv = c.Bool("notExist") 394 | is.False(bv) 395 | 396 | ClearAll() 397 | } 398 | 399 | func TestSubDataMap(t *testing.T) { 400 | is := assert.New(t) 401 | ClearAll() 402 | _ = LoadSources(JSON, []byte(jsonStr)) 403 | 404 | mp := SubDataMap("map1") 405 | is.NotEmpty(mp) 406 | is.Eq("230", mp.Get("key4")) 407 | is.Eq(230, mp.Int("key4")) 408 | 409 | mp = SubDataMap("notExist") 410 | is.Empty(mp) 411 | 412 | ClearAll() 413 | } 414 | 415 | func TestParseEnv(t *testing.T) { 416 | is := assert.New(t) 417 | 418 | cfg := NewWithOptions("test", ParseEnv) 419 | err := cfg.LoadStrings(JSON, `{ 420 | "ekey": "${EnvKey}", 421 | "ekey0": "${ EnvKey0 }", 422 | "ekey1": "${EnvKey1|defValue}", 423 | "ekey2": "${ EnvKey2 | defValue1 }", 424 | "ekey3": "${ EnvKey3 | app:run }", 425 | "ekey4": "${FirstEnv}/${ SecondEnv }", 426 | "ekey5": "${TEST_SHELL|/bin/bash}", 427 | "ekey6": "${ EnvKey6 | app=run }", 428 | "ekey7": "${ EnvKey7 | app.run }", 429 | "ekey8": "${ EnvKey8 | app/run }" 430 | }`) 431 | 432 | is.NoErr(err) 433 | 434 | tests := []struct{ EKey, EVal, CKey, CVal string }{ 435 | {"EnvKey", "EnvKey val", "ekey", "EnvKey val"}, 436 | {"EnvKey", "", "ekey", ""}, 437 | {"EnvKey0", "EnvKey0 val", "ekey0", "EnvKey0 val"}, 438 | {"EnvKey3", "EnvKey3 val", "ekey3", "EnvKey3 val"}, 439 | {"EnvKey3", "", "ekey3", "app:run"}, 440 | {"EnvKey6", "", "ekey6", "app=run"}, 441 | {"EnvKey7", "", "ekey7", "app.run"}, 442 | {"EnvKey8", "", "ekey8", "app/run"}, 443 | {"TEST_SHELL", "/bin/zsh", "ekey5", "/bin/zsh"}, 444 | {"TEST_SHELL", "", "ekey5", "/bin/bash"}, 445 | } 446 | 447 | for _, smp := range tests { 448 | is.Eq("", Getenv(smp.EKey)) 449 | 450 | testutil.MockEnvValue(smp.EKey, smp.EVal, func(eVal string) { 451 | is.Eq(smp.EVal, eVal) 452 | is.Eq(smp.CVal, cfg.String(smp.CKey)) 453 | }) 454 | } 455 | 456 | // test multi ENV key 457 | is.Eq("", Getenv("FirstEnv")) 458 | 459 | testutil.MockEnvValues(map[string]string{ 460 | "FirstEnv": "abc", 461 | "SecondEnv": "def", 462 | }, func() { 463 | is.Eq("abc", Getenv("FirstEnv")) 464 | is.Eq("def", Getenv("SecondEnv")) 465 | is.Eq("abc/def", cfg.String("ekey4")) 466 | }) 467 | 468 | testutil.MockEnvValues(map[string]string{ 469 | "FirstEnv": "abc", 470 | }, func() { 471 | is.Eq("abc", Getenv("FirstEnv")) 472 | is.Eq("", Getenv("SecondEnv")) 473 | is.Eq("abc/", cfg.String("ekey4")) 474 | }) 475 | } 476 | -------------------------------------------------------------------------------- /testdata/config.bak.json: -------------------------------------------------------------------------------- 1 | {"age":123,"arr1":["val","val1","val2"],"baseKey":"value","debug":true,"envKey":"${SHELL}","envKey1":"${NotExist|defValue}","invalidEnvKey":"${noClose","map1":{"key":"val","key1":"val1","key2":"val2","key3":"${SHELL}","key4":"230"},"name":"app","new-key":"new-value","tagsStr":"php,go"} 2 | -------------------------------------------------------------------------------- /testdata/emptydir/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/config/80a4a39486908d76ca2eeccde4bb2a8a93d1c966/testdata/emptydir/.keep -------------------------------------------------------------------------------- /testdata/hcl2_base.hcl: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | pkg_name = "config" 3 | 4 | service "http" { 5 | listen_addr = "127.0.0.1:9999" 6 | 7 | process "main" { 8 | command = [ 9 | "/usr/local/bin/awesome-app", 10 | "server"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testdata/hcl2_example.hcl: -------------------------------------------------------------------------------- 1 | io_mode = "async" 2 | 3 | service "http" { 4 | listen_addr = "127.0.0.1:8080" 5 | 6 | process "main" { 7 | command = ["/usr/local/bin/awesome-app", "server"] 8 | } 9 | 10 | process "mgmt" { 11 | command = ["/usr/local/bin/awesome-app", "mgmt"] 12 | } 13 | } 14 | 15 | /* 16 | output like: 17 | { 18 | "io_mode": "async", 19 | "service": { 20 | "http": { 21 | "web_proxy": { 22 | "listen_addr": "127.0.0.1:8080", 23 | "process": { 24 | "main": { 25 | "command": ["/usr/local/bin/awesome-app", "server"] 26 | }, 27 | "mgmt": { 28 | "command": ["/usr/local/bin/awesome-app", "mgmt"] 29 | }, 30 | } 31 | } 32 | } 33 | } 34 | } 35 | */ -------------------------------------------------------------------------------- /testdata/hcl_base.hcl: -------------------------------------------------------------------------------- 1 | 2 | job "binstore-storagelocker" { 3 | group "binsl" { 4 | task "binstore" { 5 | driver = "docker" 6 | 7 | artifact { 8 | source = "http://foo.com/bar" 9 | destination = "" 10 | 11 | options { 12 | foo = "bar" 13 | } 14 | } 15 | 16 | artifact { 17 | source = "http://foo.com/baz" 18 | } 19 | 20 | artifact { 21 | source = "http://foo.com/bam" 22 | destination = "var/foo" 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testdata/hcl_example.conf: -------------------------------------------------------------------------------- 1 | app { 2 | io_mode = "async" 3 | 4 | service "http" { 5 | listen_addr = "127.0.0.1:8080" 6 | listen_addr2 = "127.0.0.1:8090" 7 | 8 | process "main" { 9 | command = ["/usr/local/bin/awesome-app", "server"] 10 | } 11 | 12 | process "mgmt" { 13 | command = ["/usr/local/bin/awesome-app", "mgmt"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /testdata/ini_base.ini: -------------------------------------------------------------------------------- 1 | name = app 2 | debug = false 3 | baseKey = value 4 | age = 123 5 | envKey = ${SHELL} 6 | envKey1 = ${NotExist|defValue} 7 | multiWords = hello world 8 | 9 | [map1] 10 | key = val 11 | key1 = val1 12 | key2 = val2 13 | -------------------------------------------------------------------------------- /testdata/ini_base.other: -------------------------------------------------------------------------------- 1 | name = app 2 | debug = false 3 | baseKey = value 4 | age = 123 5 | envKey = ${SHELL} 6 | envKey1 = ${NotExist|defValue} 7 | 8 | [map1] 9 | key = val 10 | key1 = val1 11 | key2 = val2 12 | -------------------------------------------------------------------------------- /testdata/ini_other.ini: -------------------------------------------------------------------------------- 1 | name = app2 2 | debug = false 3 | age = 12 4 | baseKey = value2 5 | 6 | [map1] 7 | key = val2 8 | key2 = val20 9 | -------------------------------------------------------------------------------- /testdata/issues59.ini: -------------------------------------------------------------------------------- 1 | ; exported at 2025-04-10 12:16:16 2 | 3 | age = 123 4 | baseKey = value 5 | debug = false 6 | envKey = ${SHELL} 7 | envKey1 = ${NotExist|defValue} 8 | multiWords = hello world 9 | name = app 10 | 11 | [map1] 12 | key = val 13 | key1 = val1 14 | key2 = val2 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/issues_139.conf: -------------------------------------------------------------------------------- 1 | ; exported at 2023-06-11 13:30:01 2 | 3 | age = 123 4 | baseKey = value 5 | debug = false 6 | envKey = ${SHELL} 7 | envKey1 = ${NotExist|defValue} 8 | multiWords = hello world 9 | name = app 10 | 11 | [map1] 12 | key1 = val1 13 | key2 = val2 14 | key = val 15 | 16 | 17 | -------------------------------------------------------------------------------- /testdata/json-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[string]interface {} { 3 | "allowed": map[string]interface {} { 4 | "en": string("ddd"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/json_base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "debug": false, 4 | "baseKey": "value", 5 | "key_in_json": "hello jsond", 6 | "age": 123, 7 | "envKey": "${SHELL}", 8 | "envKey1": "${NotExist|defValue}", 9 | "map1": { 10 | "key": "val", 11 | "key1": "val1", 12 | "key2": "val2" 13 | }, 14 | "arr1": [ 15 | "val", 16 | "val1", 17 | "val2" 18 | ], 19 | "lang": { 20 | "dir": "res/lang", 21 | "defLang": "en", 22 | "allowed": { 23 | "en": "val", 24 | "zh-CN": "val2" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/json_base.json5: -------------------------------------------------------------------------------- 1 | { 2 | name: "app", 3 | "debug": false, 4 | "baseKey": "value", 5 | "age": 123, // comments 6 | "envKey": "${SHELL}", 7 | "envKey1": "${NotExist|defValue}", 8 | "map1": { 9 | "key": "val", 10 | "key1": "val1", 11 | "key2": "val2" 12 | }, 13 | arr1: [ 14 | "val", 15 | "val1", 16 | "val2" 17 | ], 18 | "lang": { 19 | "dir": "res/lang", 20 | "defLang": "en", 21 | "allowed": { 22 | "en": "val", 23 | "zh-CN": "val2", // for comment 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /testdata/json_error.json: -------------------------------------------------------------------------------- 1 | "invalid" 2 | -------------------------------------------------------------------------------- /testdata/json_other.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app2", 3 | "debug": false, 4 | "age": 12, 5 | "baseKey": "value2", 6 | "map1": { 7 | "key": "val2", 8 | "key2": "val20" 9 | }, 10 | "arr1": [ 11 | "val1", 12 | "val21" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /testdata/subdir/subdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "key01": "value in sub data", 3 | "key02": 230 4 | } -------------------------------------------------------------------------------- /testdata/subdir/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "key01": "value in task.json", 3 | "key02": 250 4 | } -------------------------------------------------------------------------------- /testdata/test_dump_file.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/config/80a4a39486908d76ca2eeccde4bb2a8a93d1c966/testdata/test_dump_file.yaml -------------------------------------------------------------------------------- /testdata/toml_base.toml: -------------------------------------------------------------------------------- 1 | # This is a TOML document. Boom. 2 | 3 | title = "TOML Example" 4 | name = "app" 5 | 6 | envKey = "${SHELL}" 7 | envKey1 = "${NotExist|defValue}" 8 | 9 | arr1 = [ 10 | "alpha", 11 | "omega" 12 | ] 13 | 14 | [map1] 15 | name = "inhere" 16 | org = "GitHub" 17 | 18 | [owner] 19 | name = "inhere" 20 | organization = "GitHub" 21 | bio = "GitHub Cofounder & CEO\nLikes tater tots and beer." 22 | dob = 1979-05-27T07:32:00Z # First class dates? Why not? 23 | 24 | [database] 25 | server = "192.168.1.1" 26 | ports = [ 8001, 8001, 8002 ] 27 | connection_max = 5000 28 | enabled = true 29 | 30 | [servers] 31 | 32 | # You can indent as you please. Tabs or spaces. TOML don't care. 33 | [servers.alpha] 34 | ip = "10.0.0.1" 35 | dc = "eqdc10" 36 | 37 | [servers.beta] 38 | ip = "10.0.0.2" 39 | dc = "eqdc10" 40 | 41 | [clients] 42 | data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it 43 | 44 | # Line breaks are OK when inside arrays 45 | hosts = [ 46 | "alpha", 47 | "omega" 48 | ] 49 | -------------------------------------------------------------------------------- /testdata/toml_other.toml: -------------------------------------------------------------------------------- 1 | name = "app2" 2 | age = 25 3 | Cats = [ "Cauchy", "Plato" ] 4 | Pi = 3.14 5 | Perfection = [ 6, 28, 496, 8128 ] 6 | DOB = 1987-07-05T05:45:00Z 7 | -------------------------------------------------------------------------------- /testdata/yaml-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[interface {}]interface {} { 3 | "allowed": map[interface {}]interface {} { 4 | "en": string("666"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/yaml-v3-decode-example.txt: -------------------------------------------------------------------------------- 1 | map[string]interface {} { 2 | "lang": map[string]interface {} { 3 | "allowed": map[string]interface {} { 4 | "en": string("ddd"), 5 | }, 6 | }, 7 | }, 8 | -------------------------------------------------------------------------------- /testdata/yml_base.yml: -------------------------------------------------------------------------------- 1 | name: app 2 | debug: false 3 | baseKey: value 4 | age: 123 5 | envKey: ${SHELL} 6 | envKey1: ${NotExist|defValue} 7 | 8 | map1: 9 | key: val 10 | key1: val1 11 | key2: val2 12 | 13 | arr1: 14 | - val 15 | - val1 16 | - val2 17 | 18 | lang: 19 | dir: res/lang 20 | defLang: en 21 | allowed: 22 | en: val 23 | zh-CN: val2 24 | -------------------------------------------------------------------------------- /testdata/yml_other.yml: -------------------------------------------------------------------------------- 1 | name: app2 2 | debug: false 3 | age: 12 4 | baseKey: value2 5 | 6 | map1: 7 | key: val2 8 | key2: val20 9 | 10 | arr1: 11 | - val1 12 | - val21 13 | -------------------------------------------------------------------------------- /toml/toml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package toml is driver use TOML format content as config source. 3 | How to usage please see README and unit tests. 4 | */ 5 | package toml 6 | 7 | // see https://godoc.org/github.com/BurntSushi/toml 8 | import ( 9 | "bytes" 10 | 11 | "github.com/BurntSushi/toml" 12 | "github.com/gookit/config/v2" 13 | ) 14 | 15 | // Decoder the toml content decoder 16 | var Decoder config.Decoder = func(blob []byte, ptr any) (err error) { 17 | _, err = toml.Decode(string(blob), ptr) 18 | return 19 | } 20 | 21 | // Encoder the toml content encoder 22 | var Encoder config.Encoder = func(ptr any) (out []byte, err error) { 23 | buf := new(bytes.Buffer) 24 | err = toml.NewEncoder(buf).Encode(ptr) 25 | return buf.Bytes(), err 26 | } 27 | 28 | // Driver for toml format 29 | var Driver = config.NewDriver(config.Toml, Decoder, Encoder) 30 | -------------------------------------------------------------------------------- /toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/config/v2" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | var tomlStr = ` 12 | title = "TOML Example" 13 | name = "app" 14 | 15 | envKey = "${SHELL}" 16 | envKey1 = "${NotExist|defValue}" 17 | 18 | arr1 = [ 19 | "alpha", 20 | "omega" 21 | ] 22 | 23 | [map1] 24 | name = "inhere" 25 | org = "GitHub" 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add Decoder and Encoder 32 | config.AddDriver(Driver) 33 | 34 | err := config.LoadFiles("../testdata/toml_base.toml") 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // fmt.Printf("config data: \n %#v\n", Data()) 40 | 41 | // load more files 42 | err = config.LoadFiles("../testdata/toml_other.toml") 43 | // can also 44 | // config.LoadFiles("testdata/toml_base.toml", "testdata/toml_other.toml") 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | // load from string 50 | _ = config.LoadSources(config.Toml, []byte(tomlStr)) 51 | 52 | // fmt.Printf("config data: \n %#v\n", Data()) 53 | fmt.Print("get config example:\n") 54 | 55 | name := config.String("name") 56 | fmt.Printf("get string\n - val: %v\n", name) 57 | 58 | arr1 := config.Strings("arr1") 59 | fmt.Printf("get array\n - val: %#v\n", arr1) 60 | 61 | val0 := config.String("arr1.0") 62 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %v\n", val0) 63 | 64 | map1 := config.StringMap("map1") 65 | fmt.Printf("get map\n - val: %#v\n", map1) 66 | 67 | val0 = config.String("map1.name") 68 | fmt.Printf("get sub-value by path 'map.key'\n - val: %v\n", val0) 69 | 70 | // can parse env name(ParseEnv: true) 71 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 72 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 73 | 74 | // Out: 75 | // get config example: 76 | // get string 77 | // - val: app2 78 | // get array 79 | // - val: []string{"alpha", "omega"} 80 | // get sub-value by path 'arr.index' 81 | // - val: alpha 82 | // get map 83 | // - val: map[string]string{"name":"inhere", "org":"GitHub"} 84 | // get sub-value by path 'map.key' 85 | // - val: inhere 86 | // get env 'envKey' val: /bin/zsh 87 | // get env 'envKey1' val: defValue 88 | } 89 | 90 | func TestDriver(t *testing.T) { 91 | is := assert.New(t) 92 | 93 | is.Eq("toml", Driver.Name()) 94 | 95 | c := config.NewEmpty("test") 96 | is.False(c.HasDecoder(config.Toml)) 97 | 98 | c.AddDriver(Driver) 99 | is.True(c.HasDecoder(config.Toml)) 100 | is.True(c.HasEncoder(config.Toml)) 101 | 102 | tg := new(map[string]any) 103 | err := Decoder([]byte("invalid"), tg) 104 | is.Err(err) 105 | 106 | out, err := Encoder("invalid") 107 | is.Eq(`"invalid"`, string(out)) 108 | is.Nil(err) 109 | 110 | out, err = Encoder(map[string]any{"k": "v"}) 111 | is.Nil(err) 112 | is.Contains(string(out), `k = "v"`) 113 | } 114 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "strings" 7 | "time" 8 | 9 | "github.com/gookit/goutil/envutil" 10 | "github.com/mitchellh/mapstructure" 11 | ) 12 | 13 | // ValDecodeHookFunc returns a mapstructure.DecodeHookFunc 14 | // that parse ENV var, and more custom parse 15 | func ValDecodeHookFunc(parseEnv, parseTime bool) mapstructure.DecodeHookFunc { 16 | return func(f reflect.Type, t reflect.Type, data any) (any, error) { 17 | if f.Kind() != reflect.String { 18 | return data, nil 19 | } 20 | 21 | var err error 22 | str := data.(string) 23 | if parseEnv { 24 | // https://docs.docker.com/compose/environment-variables/env-file/ 25 | str, err = envutil.ParseOrErr(str) 26 | if err != nil { 27 | return nil, err 28 | } 29 | } 30 | if len(str) < 2 { 31 | return str, nil 32 | } 33 | 34 | // start char is number(1-9) 35 | if str[0] > '0' && str[0] <= '9' { 36 | // parse time string. eg: 10s 37 | if parseTime && t.Kind() == reflect.Int64 { 38 | dur, err := time.ParseDuration(str) 39 | if err == nil { 40 | return dur, nil 41 | } 42 | } 43 | } 44 | return str, nil 45 | } 46 | } 47 | 48 | // resolve format, check is alias 49 | func (c *Config) resolveFormat(f string) string { 50 | if name, ok := c.aliasMap[f]; ok { 51 | return name 52 | } 53 | return f 54 | } 55 | 56 | /************************************************************* 57 | * Deprecated methods 58 | *************************************************************/ 59 | 60 | // SetDecoder add/set a format decoder 61 | // 62 | // Deprecated: please use driver instead 63 | func SetDecoder(format string, decoder Decoder) { 64 | dc.SetDecoder(format, decoder) 65 | } 66 | 67 | // SetDecoder set decoder 68 | // 69 | // Deprecated: please use driver instead 70 | func (c *Config) SetDecoder(format string, decoder Decoder) { 71 | format = c.resolveFormat(format) 72 | c.decoders[format] = decoder 73 | } 74 | 75 | // SetDecoders set decoders 76 | // 77 | // Deprecated: please use driver instead 78 | func (c *Config) SetDecoders(decoders map[string]Decoder) { 79 | for format, decoder := range decoders { 80 | c.SetDecoder(format, decoder) 81 | } 82 | } 83 | 84 | // SetEncoder set a encoder for the format 85 | // 86 | // Deprecated: please use driver instead 87 | func SetEncoder(format string, encoder Encoder) { 88 | dc.SetEncoder(format, encoder) 89 | } 90 | 91 | // SetEncoder set a encoder for the format 92 | // 93 | // Deprecated: please use driver instead 94 | func (c *Config) SetEncoder(format string, encoder Encoder) { 95 | format = c.resolveFormat(format) 96 | c.encoders[format] = encoder 97 | } 98 | 99 | // SetEncoders set encoders 100 | // 101 | // Deprecated: please use driver instead 102 | func (c *Config) SetEncoders(encoders map[string]Encoder) { 103 | for format, encoder := range encoders { 104 | c.SetEncoder(format, encoder) 105 | } 106 | } 107 | 108 | /************************************************************* 109 | * helper methods/functions 110 | *************************************************************/ 111 | 112 | // LoadENVFiles load 113 | // func LoadENVFiles(filePaths ...string) error { 114 | // return dotenv.LoadFiles(filePaths...) 115 | // } 116 | 117 | // GetEnv get os ENV value by name 118 | func GetEnv(name string, defVal ...string) (val string) { 119 | return Getenv(name, defVal...) 120 | } 121 | 122 | // Getenv get os ENV value by name. like os.Getenv, but support default value 123 | // 124 | // Notice: 125 | // - Key is not case-sensitive when getting 126 | func Getenv(name string, defVal ...string) (val string) { 127 | if val = os.Getenv(name); val != "" { 128 | return 129 | } 130 | 131 | if len(defVal) > 0 { 132 | val = defVal[0] 133 | } 134 | return 135 | } 136 | 137 | func parseVarNameAndType(key string) (string, string, string) { 138 | var desc string 139 | typ := "string" 140 | key = strings.Trim(key, "-") 141 | 142 | // can set var type: int, uint, bool 143 | if strings.IndexByte(key, ':') > 0 { 144 | list := strings.SplitN(key, ":", 3) 145 | key, typ = list[0], list[1] 146 | if len(list) == 3 { 147 | desc = list[2] 148 | } 149 | 150 | // if type is not valid and has multi words, as desc message. 151 | if _, ok := validTypes[typ]; !ok { 152 | if desc == "" && strings.ContainsRune(typ, ' ') { 153 | desc = typ 154 | } 155 | typ = "string" 156 | } 157 | } 158 | return key, typ, desc 159 | } 160 | 161 | // format key 162 | func formatKey(key, sep string) string { 163 | return strings.Trim(strings.TrimSpace(key), sep) 164 | } 165 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/gookit/goutil/maputil" 8 | ) 9 | 10 | // some common errors definitions 11 | var ( 12 | ErrReadonly = errors.New("the config instance in 'readonly' mode") 13 | ErrKeyIsEmpty = errors.New("the config key is cannot be empty") 14 | ErrNotFound = errors.New("this key does not exist in the config") 15 | ) 16 | 17 | // SetData for override the Config.Data 18 | func SetData(data map[string]any) { 19 | dc.SetData(data) 20 | } 21 | 22 | // SetData for override the Config.Data 23 | func (c *Config) SetData(data map[string]any) { 24 | c.lock.Lock() 25 | c.data = data 26 | c.lock.Unlock() 27 | 28 | c.fireHook(OnSetData) 29 | } 30 | 31 | // Set value by key. setByPath default is true 32 | func Set(key string, val any, setByPath ...bool) error { 33 | return dc.Set(key, val, setByPath...) 34 | } 35 | 36 | // Set a value by key string. setByPath default is true 37 | func (c *Config) Set(key string, val any, setByPath ...bool) (err error) { 38 | if c.opts.Readonly { 39 | return ErrReadonly 40 | } 41 | 42 | c.lock.Lock() 43 | defer c.lock.Unlock() 44 | 45 | sep := c.opts.Delimiter 46 | if key = formatKey(key, string(sep)); key == "" { 47 | return ErrKeyIsEmpty 48 | } 49 | 50 | defer c.fireHook(OnSetValue) 51 | if strings.IndexByte(key, sep) == -1 { 52 | c.data[key] = val 53 | return 54 | } 55 | 56 | // disable set by path. 57 | if len(setByPath) > 0 && !setByPath[0] { 58 | c.data[key] = val 59 | return 60 | } 61 | 62 | // set by path 63 | keys := strings.Split(key, string(sep)) 64 | return maputil.SetByKeys(&c.data, keys, val) 65 | } 66 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestSetData(t *testing.T) { 12 | defer func() { 13 | Reset() 14 | }() 15 | 16 | c := Default() 17 | 18 | err := c.LoadStrings(JSON, jsonStr) 19 | assert.NoErr(t, err) 20 | assert.Eq(t, "app", c.String("name")) 21 | assert.True(t, c.Exists("age")) 22 | 23 | SetData(map[string]any{ 24 | "name": "new app", 25 | }) 26 | assert.Eq(t, "new app", c.String("name")) 27 | assert.False(t, c.Exists("age")) 28 | 29 | c.SetData(map[string]any{ 30 | "age": 222, 31 | }) 32 | assert.Eq(t, "", c.String("name")) 33 | assert.False(t, c.Exists("name")) 34 | assert.True(t, c.Exists("age")) 35 | assert.Eq(t, 222, c.Int("age")) 36 | } 37 | 38 | func TestSet(t *testing.T) { 39 | defer func() { 40 | ClearAll() 41 | }() 42 | 43 | is := assert.New(t) 44 | c := Default() 45 | 46 | // clear old 47 | ClearAll() 48 | // err := LoadFiles("testdata/json_base.json") 49 | err := LoadStrings(JSON, jsonStr) 50 | is.Nil(err) 51 | 52 | val := String("name") 53 | is.Eq("app", val) 54 | 55 | // empty key 56 | err = Set("", "val") 57 | is.Err(err) 58 | 59 | // set new value: int 60 | err = Set("newInt", 23) 61 | if is.Nil(err).IsOk() { 62 | iv := Int("newInt") 63 | is.Eq(23, iv) 64 | } 65 | 66 | // set new value: int 67 | err = Set("newBool", false) 68 | if is.Nil(err).IsOk() { 69 | bv := Bool("newBool") 70 | is.False(bv) 71 | } 72 | 73 | // set new value: string 74 | err = Set("newKey", "new val") 75 | if is.Nil(err).IsOk() { 76 | val = String("newKey") 77 | is.Eq("new val", val) 78 | } 79 | 80 | // like yaml.v2 decoded data 81 | err = Set("ymlLike", map[any]any{"k": "v"}) 82 | is.Nil(err) 83 | str := c.String("ymlLike.k") 84 | is.Eq("v", str) 85 | 86 | err = Set("ymlLike.nk", "nv") 87 | is.Nil(err) 88 | str = c.String("ymlLike.nk") 89 | is.Eq("nv", str) 90 | 91 | // disable setByPath 92 | err = Set("some.key", "val", false) 93 | if is.Nil(err).IsOk() { 94 | val = String("some") 95 | is.Eq("", val) 96 | 97 | val = String("some.key") 98 | is.Eq("val", val) 99 | } 100 | // fmt.Printf("%#v\n", c.Data()) 101 | 102 | // set value 103 | err = Set("name", "new name") 104 | if is.Nil(err).IsOk() { 105 | val = String("name") 106 | is.Eq("new name", val) 107 | } 108 | 109 | // set value to arr: by path 110 | err = Set("arr1.1", "new val") 111 | if is.Nil(err).IsOk() { 112 | val = String("arr1.1") 113 | is.Eq("new val", val) 114 | } 115 | 116 | // array only support add 1 level value 117 | err = Set("arr1.1.key", "new val") 118 | is.Err(err) 119 | 120 | // set value to map: by path 121 | err = Set("map1.key", "new val") 122 | if is.Nil(err).IsOk() { 123 | val = String("map1.key") 124 | is.Eq("new val", val) 125 | } 126 | 127 | // more path nodes 128 | err = Set("map1.info.key", "val200") 129 | if is.Nil(err).IsOk() { 130 | // fmt.Printf("%v\n", c.Data()) 131 | smp := StringMap("map1.info") 132 | is.Eq("val200", smp["key"]) 133 | 134 | str = String("map1.info.key") 135 | is.Eq("val200", str) 136 | } 137 | 138 | // new map 139 | err = Set("map2.key", "new val") 140 | if is.Nil(err).IsOk() { 141 | val = String("map2.key") 142 | 143 | is.Eq("new val", val) 144 | } 145 | 146 | // set new value: array(slice) 147 | err = Set("newArr", []string{"a", "b"}) 148 | if is.Nil(err).IsOk() { 149 | arr := Strings("newArr") 150 | 151 | is.Eq(`[]string{"a", "b"}`, fmt.Sprintf("%#v", arr)) 152 | 153 | val = String("newArr.1") 154 | is.Eq("b", val) 155 | 156 | val = String("newArr.100") 157 | is.Eq("", val) 158 | } 159 | 160 | // set new value: map 161 | err = Set("newMap", map[string]string{"k1": "a", "k2": "b"}) 162 | if is.Nil(err).IsOk() { 163 | mp := StringMap("newMap") 164 | is.NotEmpty(mp) 165 | // is.Eq("map[k1:a k2:b]", fmt.Sprintf("%v", mp)) 166 | 167 | val = String("newMap.k1") 168 | is.Eq("a", val) 169 | 170 | val = String("newMap.notExist") 171 | is.Eq("", val) 172 | } 173 | 174 | is.NoErr(Set("name.sub", []int{2}, false)) 175 | ints := Ints("name.sub") 176 | is.Eq([]int{2}, ints) 177 | 178 | // Readonly 179 | Default().Readonly() 180 | is.True(c.Options().Readonly) 181 | is.Err(Set("name", "new name")) 182 | } 183 | 184 | func TestSet_fireEvent(t *testing.T) { 185 | var buf bytes.Buffer 186 | hookFn := func(event string, c *Config) { 187 | buf.WriteString("fire the: ") 188 | buf.WriteString(event) 189 | } 190 | 191 | c := NewWithOptions("test", WithHookFunc(hookFn)) 192 | err := c.LoadData(map[string]any{ 193 | "key": "value", 194 | }) 195 | assert.NoErr(t, err) 196 | assert.Eq(t, "fire the: load.data", buf.String()) 197 | buf.Reset() 198 | 199 | err = c.Set("key", "value2") 200 | assert.NoErr(t, err) 201 | assert.Eq(t, "fire the: set.value", buf.String()) 202 | buf.Reset() 203 | } 204 | -------------------------------------------------------------------------------- /yaml/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package yaml is a driver use YAML format content as config source 3 | 4 | Usage please see example: 5 | */ 6 | package yaml 7 | 8 | import ( 9 | "github.com/goccy/go-yaml" 10 | "github.com/gookit/config/v2" 11 | ) 12 | 13 | // Decoder the yaml content decoder 14 | var Decoder config.Decoder = yaml.Unmarshal 15 | 16 | // Encoder the yaml content encoder 17 | var Encoder config.Encoder = yaml.Marshal 18 | 19 | // Driver for yaml 20 | var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml) 21 | -------------------------------------------------------------------------------- /yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/config/v2" 9 | "github.com/gookit/goutil/testutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | var yamlStr = ` 14 | name: app2 15 | debug: false 16 | age: 23 17 | baseKey: value2 18 | 19 | map1: 20 | key: val2 21 | key2: val20 22 | 23 | arr1: 24 | - val1 25 | - val21 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add yaml decoder 32 | // only add decoder 33 | // config.SetDecoder(config.Yaml, Decoder) 34 | // Or 35 | config.AddDriver(Driver) 36 | 37 | err := config.LoadFiles("../testdata/yml_other.yml") 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // load from string 43 | _ = config.LoadSources(config.Yaml, []byte(yamlStr)) 44 | 45 | fmt.Print("get config example:\n") 46 | 47 | name := config.String("name") 48 | fmt.Printf("get string\n - val: %v\n", name) 49 | 50 | arr1 := config.Strings("arr1") 51 | fmt.Printf("get array\n - val: %#v\n", arr1) 52 | 53 | val0 := config.String("arr1.0") 54 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 55 | 56 | map1 := config.StringMap("map1") 57 | fmt.Printf("get map\n - val: %#v\n", map1) 58 | 59 | val0 = config.String("map1.key") 60 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 61 | 62 | // can parse env name(ParseEnv: true) 63 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 64 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 65 | 66 | // Out: 67 | // get config example: 68 | // age: 23 69 | // get string 70 | // - val: app2 71 | // get array 72 | // - val: []string{"val1", "val21"} 73 | // get sub-value by path 'arr.index' 74 | // - val: "val1" 75 | // get map 76 | // val: map[string]string{"key":"val2", "key2":"val20"} 77 | // get sub-value by path 'map.key' 78 | // - val: "val2" 79 | // get env 'envKey' val: /bin/zsh 80 | // get env 'envKey1' val: defValue 81 | } 82 | 83 | func TestDumpConfig(t *testing.T) { 84 | is := assert.New(t) 85 | c := config.NewEmpty("test") 86 | // Notice: before dump please set driver encoder 87 | c.AddDriver(Driver) 88 | err := c.LoadStrings(config.Yaml, yamlStr) 89 | is.NoErr(err) 90 | 91 | buf := new(bytes.Buffer) 92 | _, err = c.DumpTo(buf, config.Yaml) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | fmt.Printf("export config:\n%s", buf.String()) 98 | } 99 | 100 | func TestLoadFile(t *testing.T) { 101 | c := config.NewEmpty("test") 102 | c.AddDriver(Driver) 103 | c.WithOptions(config.ParseEnv) 104 | 105 | err := c.LoadFiles("../testdata/yml_base.yml") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fmt.Printf("config data: \n %#v\n", c.Data()) 111 | assert.Eq(t, "app", c.String("name")) 112 | 113 | err = c.LoadFiles("../testdata/yml_other.yml") 114 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | fmt.Printf("config data: \n %#v\n", c.Data()) 120 | assert.Eq(t, "app2", c.String("name")) 121 | } 122 | 123 | func TestDriver(t *testing.T) { 124 | is := assert.New(t) 125 | 126 | is.Eq("yaml", Driver.Name()) 127 | // is.IsType(new(Encoder), JSONDriver.GetEncoder()) 128 | 129 | c := config.NewEmpty("test") 130 | is.False(c.HasDecoder(config.Yaml)) 131 | c.AddDriver(Driver) 132 | is.True(c.HasDecoder(config.Yaml)) 133 | is.True(c.HasEncoder(config.Yaml)) 134 | } 135 | 136 | // Support "=", ":", "." characters for default values 137 | // see https://github.com/gookit/config/issues/9 138 | func TestIssue2(t *testing.T) { 139 | is := assert.New(t) 140 | 141 | c := config.NewEmpty("test") 142 | c.AddDriver(Driver) 143 | c.WithOptions(config.ParseEnv) 144 | 145 | err := c.LoadStrings(config.Yaml, ` 146 | command: ${APP_COMMAND|app:run} 147 | `) 148 | is.NoErr(err) 149 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 150 | is.Eq("new val", nv) 151 | is.Eq("new val", c.String("command")) 152 | }) 153 | 154 | is.Eq("", config.Getenv("APP_COMMAND")) 155 | is.Eq("app:run", c.String("command")) 156 | 157 | c.ClearAll() 158 | err = c.LoadStrings(config.Yaml, ` 159 | command: ${ APP_COMMAND | app:run } 160 | `) 161 | is.NoErr(err) 162 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 163 | is.Eq("new val", nv) 164 | is.Eq("new val", c.String("command")) 165 | }) 166 | is.Eq("", config.Getenv("APP_COMMAND")) 167 | is.Eq("app:run", c.String("command")) 168 | } 169 | -------------------------------------------------------------------------------- /yamlv3/yamlv3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package yamlv3 is a driver use YAML format content as config source 3 | 4 | Usage please see example: 5 | */ 6 | package yamlv3 7 | 8 | import ( 9 | "github.com/goccy/go-yaml" 10 | "github.com/gookit/config/v2" 11 | ) 12 | 13 | // Decoder the yaml content decoder 14 | var Decoder config.Decoder = yaml.Unmarshal 15 | 16 | // Encoder the yaml content encoder 17 | var Encoder config.Encoder = yaml.Marshal 18 | 19 | // Driver for yaml 20 | var Driver = config.NewDriver(config.Yaml, Decoder, Encoder).WithAliases(config.Yml) 21 | -------------------------------------------------------------------------------- /yamlv3/yamlv3_test.go: -------------------------------------------------------------------------------- 1 | package yamlv3 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/config/v2" 9 | "github.com/gookit/goutil/testutil" 10 | "github.com/gookit/goutil/testutil/assert" 11 | ) 12 | 13 | var yamlStr = ` 14 | name: app2 15 | debug: false 16 | age: 23 17 | baseKey: value2 18 | 19 | map1: 20 | key: val2 21 | key2: val20 22 | 23 | arr1: 24 | - val1 25 | - val21 26 | ` 27 | 28 | func Example() { 29 | config.WithOptions(config.ParseEnv) 30 | 31 | // add yaml decoder 32 | // only add decoder 33 | // config.SetDecoder(config.Yaml, Decoder) 34 | // Or 35 | config.AddDriver(Driver) 36 | 37 | err := config.LoadFiles("../testdata/yml_other.yml") 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | // load from string 43 | _ = config.LoadSources(config.Yaml, []byte(yamlStr)) 44 | 45 | fmt.Print("get config example:\n") 46 | 47 | name := config.String("name") 48 | fmt.Printf("get string\n - val: %v\n", name) 49 | 50 | arr1 := config.Strings("arr1") 51 | fmt.Printf("get array\n - val: %#v\n", arr1) 52 | 53 | val0 := config.String("arr1.0") 54 | fmt.Printf("get sub-value by path 'arr.index'\n - val: %#v\n", val0) 55 | 56 | map1 := config.StringMap("map1") 57 | fmt.Printf("get map\n - val: %#v\n", map1) 58 | 59 | val0 = config.String("map1.key") 60 | fmt.Printf("get sub-value by path 'map.key'\n - val: %#v\n", val0) 61 | 62 | // can parse env name(ParseEnv: true) 63 | fmt.Printf("get env 'envKey' val: %s\n", config.String("envKey", "")) 64 | fmt.Printf("get env 'envKey1' val: %s\n", config.String("envKey1", "")) 65 | 66 | // Out: 67 | // get config example: 68 | // age: 23 69 | // get string 70 | // - val: app2 71 | // get array 72 | // - val: []string{"val1", "val21"} 73 | // get sub-value by path 'arr.index' 74 | // - val: "val1" 75 | // get map 76 | // val: map[string]string{"key":"val2", "key2":"val20"} 77 | // get sub-value by path 'map.key' 78 | // - val: "val2" 79 | // get env 'envKey' val: /bin/zsh 80 | // get env 'envKey1' val: defValue 81 | } 82 | 83 | func TestDumpConfig(t *testing.T) { 84 | is := assert.New(t) 85 | c := config.NewEmpty("test") 86 | // Notice: before dump please set driver encoder 87 | c.AddDriver(Driver) 88 | err := c.LoadStrings(config.Yaml, yamlStr) 89 | is.NoErr(err) 90 | 91 | buf := new(bytes.Buffer) 92 | _, err = c.DumpTo(buf, config.Yaml) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | fmt.Printf("export config:\n%s", buf.String()) 98 | } 99 | 100 | func TestLoadFile(t *testing.T) { 101 | c := config.NewEmpty("test") 102 | c.AddDriver(Driver) 103 | c.WithOptions(config.ParseEnv) 104 | 105 | err := c.LoadFiles("../testdata/yml_base.yml") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | fmt.Printf("config data: \n %#v\n", c.Data()) 111 | assert.Eq(t, "app", c.String("name")) 112 | 113 | err = c.LoadFiles("../testdata/yml_other.yml") 114 | // config.LoadFiles("testdata/yml_base.yml", "testdata/yml_other.yml") 115 | if err != nil { 116 | panic(err) 117 | } 118 | 119 | fmt.Printf("config data: \n %#v\n", c.Data()) 120 | assert.Eq(t, "app2", c.String("name")) 121 | } 122 | 123 | func TestDriver(t *testing.T) { 124 | is := assert.New(t) 125 | 126 | is.Eq("yaml", Driver.Name()) 127 | // is.IsType(new(Encoder), JSONDriver.GetEncoder()) 128 | 129 | c := config.NewEmpty("test") 130 | is.False(c.HasDecoder(config.Yaml)) 131 | c.AddDriver(Driver) 132 | is.True(c.HasDecoder(config.Yaml)) 133 | is.True(c.HasEncoder(config.Yaml)) 134 | } 135 | 136 | // Support "=", ":", "." characters for default values 137 | // see https://github.com/gookit/config/issues/9 138 | func TestIssue2(t *testing.T) { 139 | is := assert.New(t) 140 | 141 | c := config.NewEmpty("test") 142 | c.AddDriver(Driver) 143 | c.WithOptions(config.ParseEnv) 144 | 145 | err := c.LoadStrings(config.Yaml, ` 146 | command: ${APP_COMMAND|app:run} 147 | `) 148 | is.NoErr(err) 149 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 150 | is.Eq("new val", nv) 151 | is.Eq("new val", c.String("command")) 152 | }) 153 | 154 | is.Eq("", config.Getenv("APP_COMMAND")) 155 | is.Eq("app:run", c.String("command")) 156 | 157 | c.ClearAll() 158 | err = c.LoadStrings(config.Yaml, ` 159 | command: ${ APP_COMMAND | app:run } 160 | `) 161 | is.NoErr(err) 162 | testutil.MockEnvValue("APP_COMMAND", "new val", func(nv string) { 163 | is.Eq("new val", nv) 164 | is.Eq("new val", c.String("command")) 165 | }) 166 | is.Eq("", config.Getenv("APP_COMMAND")) 167 | is.Eq("app:run", c.String("command")) 168 | } 169 | --------------------------------------------------------------------------------