Initial Version

This commit is contained in:
Kevin Fries 2024-08-09 14:05:53 -06:00
parent 631d2986b3
commit 6e5331dfe9
11 changed files with 900 additions and 1 deletions

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
golang 1.22.6

214
README.md
View File

@ -1,2 +1,214 @@
# config
# GO Config Module
## Common golang config module for all programs
### Purpose
There are several config management libraries for every programming language. Keeping them straight and consistent can be a challenge. This library uses a very common library for the GO programming language, and wraps it up with some standard practices. The goal is to ensure that all programs written in GO will behave similarly from the command line.
### Underlying libraries
This package is a wrapper around 3 libraries:
* [Viper](https://pkg.go.dev/github.com/spf13/viper) - For storing and retrieving configuration items
* [PFlag](https://pkg.go.dev/github.com/spf13/pflag) - For enhanced command line configuration
* [SemVer](https://pkg.go.dev/github.com/Masterminds/semver) - For Semantic Versioning
The config item in this package is a Viper object, so anything my convenience methods do not support, you still can operate directly on Viper and PFlag objects. You can look those APIs up on your own.
The remainder of this README file will discuss the value add from the plain vanilla libraries
### Multiple levels of Config
This package is built to allow multiple methods to config your application depending on your use case. But do so in a way that makes it consistent across multiple applications. A config item can be defined in any one of the following locations:
* A Default Value
* Inside a config file (YAML format by default, but TOML, and JSON also supported)
* An Environment Variable
* A command line flag
* Code level override
### Workflow
Configuration items are designed so that there is a hierarchy to them. If you look at the list above, they are from most static source, to most dynamic source. IF an item is defined in multiple locations, the more dynamic source will override any more static settings for the item.
* A config item is created by defining it a default value.
* If a value exists in a config file, the config item will then take that value
* If an environment variable exists, it will now set the config item
* If a command line flag is defined, it will override all others
The final level is setting the value in code. Think of this as a "running config"
### Set application level parameters
This library at this point only contains 3 application level parameters:
* Application Name: a name for your application, shows up in help
* Application Prefix: a short one word name used to path environment variables and config file
* Application Version: The semantic version value for this application
To set the application name and prefix:
~~~
SetAppName("My Awesome App", "myapp")
~~~
To set the application version:
~~~
SetAppVersion("1.2.3")
~~~
This will define the following config items:
* app.name
* app.prefix
* app.version
These are all queryable from within your program
It will also set the default locations to look for the config file to:
* /etc/{app.prefix}/config
* $HOME/.{app.prefix}/config
* $PWD/etc/config
### Overriding the config filename or format
The library contains a convenience method to set the file name and format:
~~~
SetConfigFile("config", "yaml")
~~~
If you do nothing, this is the default, otherwise set it as your use case dictates
### Automatic Command Line Flags
By default, this package will automatically define two command line flags for you.
* -h/--help to get help from the command line
* -v/--version to get the application version from the command line
To stop this automatic behavior, simply set the variable autoHelpAndVersion to false
### Defining Configuration Items
To define configuration items, there is a simple convenience method to do so:
~~~
DefineConfigItem("myString", "s", "cool saying", "A string to show how cool I am")
DefineConfigItem("myInt", "i", 1500, "An int just because")
DefineConfigItem("myFloat", "f", 35.99, "Great for holding a price")
DefineConfigItem("myBool", "b", true, "Yep, supports bools too")
~~~
The first line of this code defines:
* A config item "myString"
* a command line flag "--myString"
* a short flag "-s"
* a help message "A string to show how cool I am"
* sets the value to "cool saying"
* will look in the config file for a setting named myString
* Will look for an environment variable named "<app.prefix>_MYSTRING"
lots of stuff for a single line of code. The other three items will do similar things with their appropriate data types.
***Note:** if you set the short flag parameter to nil, it will not create a short flag, only a long one
***Note2:** Environment variable will be uppercased as is convention, and prefixed with the application prefix to keep this programs env vars separate from other applications running on the same box.*
### Hierarchical config items
Sometimes you desire to group certain config items together when it makes sense to do so. For example, if your application were to connect to a remote database. It would make sense that the database config items should be grouped together. Items such as:
* URL to reach the server
* Port the server is listening to
* Username to access the database
* Password to access the database
* Database name
* and Table name
to do this, you can separate the parts of the name with periods. So, I could name each of the items above as follows:
* db.url
* db.port
* db.username
* db.password
* db.database
* db.table
If I use the yaml config file to set these values, the yaml will also reflect the hierarchical nature of this data. Add these values to the config file like so:
~~~yaml
---
db:
url: localhost
port: 5432
username: dbGuru
password: YouWillNeverGuessThis
database: employees
table: users
~~~
Environment variables will replace the period with underscores, so I can set the password in an environment variable for an application with a prefix of "myapp" as:
MYAPP_DB_PASSWORD=OkYouGuessedTheLastOneButYouWillNeverGuessThisOne
### Main Method Call
Once your application and config items are defined, you can have the library retrieve all your configs with a single call from your main():
GetConfigs()
## Example
**db.go**
~~~
...
func init() {
DefineConfigItem("db.url", nil, "localhost", "Database URL")
DefineConfigItem("db.port", nil, 5432, "Database port")
DefineConfigItem("db.username", nil, "dbGuru")
DefineConfigItem("db.password", nil, "StopGuessingMyPasswords")
DefineConfigItem("db.database", nil, "Inventory")
}
// My database manipulation code goes here
// dbConnect(config.getString("db.url"), config.getString("db.username"), config.getString("db.password")
~~~
**web.go**
~~~
...
func init() {
DefineConfigItem("web.bindaddr", "a", "localhost", "Web Bind Address")
DefineConfigItem("web.port", "p", 443, "Web Bind Port")
}
// My web server definition code goes here
~~~
**main.go**
~~~
...
func init() {
SetAppName("My App", "myapp")
SetAppVersion("1.0.5")
DefineConfigItem("debug", "D", false, "debug node")
}
func main() {
GetCongig()
if config.GetBool("debug") {
// debug mode is on
}
...
}
~~~

126
config.go Normal file
View File

@ -0,0 +1,126 @@
package config
import (
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/Masterminds/semver"
"github.com/spf13/afero"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
var (
Config = viper.GetViper()
autoHelpAndVersion = true
configFS = afero.NewOsFs()
)
func init() {
SetConfigFile("config", "yaml")
}
func SetConfigFile(name string, format string) {
viper.SetDefault("Config.filename", name)
viper.SetDefault("Config.format", format)
}
func AddHelpAndVersion(helpRequested *bool, versionRequested *bool) {
if autoHelpAndVersion {
if !viper.IsSet("help") {
pflag.BoolVarP(helpRequested, "help", "h", false, "This message")
}
if !viper.IsSet("version") {
pflag.BoolVarP(versionRequested, "version", "v", false, "Get Program Version")
}
}
}
func DefineConfigItem(key string, short string, def interface{}, helpString string) {
switch t := def.(type) {
case bool:
viper.SetDefault(key, t)
pflag.BoolP(key, short, viper.GetBool(key), helpString)
case float64:
viper.SetDefault(key, t)
pflag.Float64P(key, short, viper.GetFloat64(key), helpString)
case int:
viper.SetDefault(key, t)
pflag.IntP(key, short, viper.GetInt(key), helpString)
case string:
viper.SetDefault(key, t)
pflag.StringP(key, short, viper.GetString(key), helpString)
default:
log.Fatalln("Config Type Unsupported")
}
}
func SetAppName(appName, appPrefix string) {
viper.SetDefault("app.name", appName)
viper.SetDefault("app.prefix", appPrefix)
}
func SetAppVersion(verStr string) {
var appVersion *semver.Version
var err error
appVersion, err = semver.NewVersion(verStr)
if err != nil {
log.Fatalln("Error setting app version")
}
viper.SetDefault("app.version", appVersion)
}
func GetConfigs() {
var helpRequested bool
var versionRequested bool
viper.SetFs(configFS)
viper.SetConfigName(viper.GetString("Config.filename"))
viper.SetConfigType(viper.GetString("Config.format"))
viper.AddConfigPath(fmt.Sprintf("/etc/%s/", viper.GetString("app.prefix")))
viper.AddConfigPath(fmt.Sprintf("$HOME/.%s/", viper.GetString("app.prefix")))
viper.AddConfigPath("./etc/")
viper.WatchConfig()
viper.SetEnvPrefix(viper.GetString("app.prefix"))
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
if autoHelpAndVersion {
AddHelpAndVersion(&helpRequested, &versionRequested)
autoHelpAndVersion = false // insure you do not add these twice, or you will experience a failure
}
if err := viper.ReadInConfig(); err != nil {
var configFileNotFoundError viper.ConfigFileNotFoundError
if errors.As(err, &configFileNotFoundError) {
} else {
log.Fatal("invalid Config file format", err)
}
}
pflag.Parse()
_ = viper.BindPFlags(pflag.CommandLine)
if helpRequested {
fmt.Printf("Usage: %s:\n", os.Args[0])
pflag.PrintDefaults()
os.Exit(0)
}
if versionRequested {
fmt.Printf("%s: %s\n", os.Args[0], viper.Get("app.version"))
os.Exit(0)
}
}

281
config_test.go Normal file
View File

@ -0,0 +1,281 @@
package config
import (
"context"
"fmt"
"github.com/Masterminds/semver"
"github.com/spf13/afero"
"os"
filepath2 "path/filepath"
"strconv"
"strings"
"testing"
"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
"github.com/spf13/pflag"
a "github.com/stretchr/testify/assert"
)
var (
SupportedDataTypes = [4]string{"string", "integer", "float", "boolean"}
assertTest *a.Assertions
assertError = fmt.Errorf("assert error")
opts = godog.Options{
Output: colors.Colored(os.Stdout),
Format: "pretty",
Paths: []string{"features"},
}
originalCommandLine = os.Args
)
const (
StringDefaultValue = "default value"
StringConfigValue = "config file value"
StringEnvironmentValue = "environment variable value"
StringFlagValue = "command line value"
IntegerDefaultValue = 123
IntegerConfigValue = 234
IntegerEnvironmentValue = "345"
IntegerFlagValue = 456
FloatDefaultValue = 1.99
FloatConfigValue = 2.99
FloatEnvironmentValue = "3.99"
FloatFlagValue = 4.99
BoolDefaultValue = false
BoolConfigValue = true
BoolEnvironmentValue = "false"
BoolFlagValue = true
)
func init() {
godog.BindCommandLineFlags("godog.", &opts)
configFS = afero.NewMemMapFs()
}
func TestCucumber(t *testing.T) {
pflag.Parse()
assertTest = a.New(t)
opts.Paths = pflag.Args()
opts.TestingT = t
suite := godog.TestSuite{
Name: "pretty",
ScenarioInitializer: InitializeScenario,
TestSuiteInitializer: InitializeTestSuite,
Options: &opts,
}
if suite.Run() != 0 {
t.Fatal("cucumber tests failed")
}
}
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
ctx.BeforeSuite(func() {
os.Args = []string{originalCommandLine[0]}
})
ctx.AfterSuite(func() {
os.Args = originalCommandLine
})
}
func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Given(`^an application with a name of "([^"]*)" and a prefix of "([^"]*)"$`, setAppNameAndPrefix)
ctx.Given(`^a version of "([^"]*)"$`, setAppVersion)
ctx.Given(`^a "([^"]*)" configuration item defines as$`, setupConfigItem)
ctx.When(`^I call GetConfig$`, callGetConfig)
ctx.Then(`^the "([^"]*)" result should return "([^"]*)"$`, checkConfigValue)
ctx.Then(`^the app name should return "([^"]*)"$`, theAppNameShouldReturn)
ctx.Then(`^the app prefix should return "([^"]*)"$`, theAppPrefixShouldReturn)
ctx.Then(`^the app version should pass the following constraint: "([^"]*)"$`, theAppVersionShouldPassTheFollowingConstraint)
}
// Given Callbacks
func setAppNameAndPrefix(ctx context.Context, name, prefix string) context.Context {
SetAppName(name, prefix)
return context.WithValue(ctx, "appPrefix", prefix)
}
func setAppVersion(_ context.Context, versionString string) {
SetAppVersion(versionString)
}
func setupConfigItem(ctx context.Context, itemType string, itemDefinition *godog.Table) (context.Context, error) {
if assertTest.Containsf(SupportedDataTypes, itemType, "config items of type '%s' are not yet supported", itemType) {
var (
data = make(map[string]string)
name string
shortFlag = ""
helpText = ""
fileContents = ""
defVal any
configVal any
envVal string
flagVal any
)
for idx, key := range itemDefinition.Rows[0].Cells {
data[key.Value] = itemDefinition.Rows[1].Cells[idx].Value
}
ctx = context.WithValue(ctx, "data", data)
name = data["name"]
switch itemType {
case "string":
defVal = StringDefaultValue
configVal = StringConfigValue
envVal = StringEnvironmentValue
flagVal = StringFlagValue
helpText = "String Config Item Test"
case "integer":
defVal = IntegerDefaultValue
configVal = IntegerConfigValue
envVal = IntegerEnvironmentValue
flagVal = IntegerFlagValue
helpText = "Integer Config Item Test"
case "float":
defVal = FloatDefaultValue
configVal = FloatConfigValue
envVal = FloatEnvironmentValue
flagVal = FloatFlagValue
helpText = "Float Config Item Test"
case "boolean":
defVal = BoolDefaultValue
configVal = BoolConfigValue
envVal = BoolEnvironmentValue
flagVal = BoolFlagValue
helpText = "Bool Config Item Test"
}
if data["has_file_entry"] == "true" {
var dirPath = filepath2.Join("/", "etc", Config.GetString("app.prefix"))
var filename = filepath2.Join(dirPath, "config.yaml")
fileContents = fmt.Sprintf("---\n%s: %v\n", name, configVal)
err := configFS.MkdirAll(dirPath, 0755)
if !assertTest.NoError(err, "unable to create test config folder") {
if !assertTest.DirExists(dirPath, "unable to create test config folder") {
return ctx, assertError
}
}
err = afero.WriteFile(configFS, filename, []byte(fileContents), 0755)
if !assertTest.NoError(err, "unable to write file contents") {
return ctx, fmt.Errorf(err.Error())
}
}
if data["has_env_var"] == "true" {
if data["has_env_var"] == "true" {
key := strings.ToUpper(
strings.ReplaceAll(
strings.ReplaceAll(
fmt.Sprintf("test_%s", name),
"-",
"_",
),
".",
"_",
),
)
_ = os.Setenv(key, envVal)
}
}
if data["commandline_flag"] != "" {
os.Args = append(os.Args, fmt.Sprintf("%s=%v", data["commandline_flag"], flagVal))
tagName := strings.Trim(data["commandline_flag"], "-")
if len(tagName) == 1 {
shortFlag = tagName
}
}
DefineConfigItem(name, shortFlag, defVal, helpText)
return ctx, nil
} else {
return ctx, assertError
}
}
// When Callbacks
func callGetConfig(_ context.Context) {
GetConfigs()
}
// Then Callbacks
func checkConfigValue(ctx context.Context, itemType, expectedString string) error {
var (
// key = ctx.Value("itemName").(string)
data = ctx.Value("data").(map[string]string)
key = data["name"]
err error
)
if assertTest.Containsf(SupportedDataTypes, itemType, "config items of type '%s' are not yet supported", itemType) {
var actual, expected interface{}
switch itemType {
case "string":
actual = strings.Trim(Config.GetString(key), "\"")
expected = expectedString
case "integer":
actual = Config.GetInt64(key)
expected, _ = strconv.ParseInt(expectedString, 10, 64)
case "float":
actual = Config.GetFloat64(key)
expected, _ = strconv.ParseFloat(expectedString, 64)
case "boolean":
actual = Config.GetBool(key)
expected, _ = strconv.ParseBool(expectedString)
}
if !assertTest.Equal(expected, actual, "config item should equal expected", os.Args, data) {
err = assertError
}
} else {
err = assertError
}
return err
}
func theAppNameShouldReturn(expected string) error {
if !assertTest.Equal(expected, Config.GetString("app.name")) {
return assertError
}
return nil
}
func theAppPrefixShouldReturn(expected string) error {
if !assertTest.Equal(expected, Config.GetString("app.prefix")) {
return assertError
}
return nil
}
func theAppVersionShouldPassTheFollowingConstraint(constraintString string) error {
var (
constraint, _ = semver.NewConstraint(constraintString)
actual = Config.Get("app.version").(*semver.Version)
)
if !assertTest.True(constraint.Check(actual), "version does not pass check") {
return assertError
}
return nil
}

20
features/app.feature Normal file
View File

@ -0,0 +1,20 @@
#noinspection CucumberUndefinedStep
Feature: String configuration items
In order to be able to create and retrieve application leven config items
As a developer
I want to develop code that will make this simple, efficient, and consistent
Scenario: Check app name and prefix
Given an application with a name of "Testing App" and a prefix of "test"
When I call GetConfig
Then the app name should return "Testing App"
And the app prefix should return "test"
Scenario: Check app version
Given an application with a name of "Testing App" and a prefix of "test"
And a version of "1.2.3"
When I call GetConfig
Then the app version should pass the following constraint: "=1.2.3"
And the app version should pass the following constraint: "~1.2"
And the app version should pass the following constraint: ">1.1"
And the app version should pass the following constraint: "<2.0"

29
features/bool.feature Normal file
View File

@ -0,0 +1,29 @@
#noinspection CucumberUndefinedStep
Feature: String configuration items
In order to be able to create and retrieve boolean configuration items
As a developer
I want to develop code that will make this simple, efficient, and consistent
Scenario Outline: Integer config items
Given an application with a name of "Testing App" and a prefix of "test"
And a version of "1.2.3"
And a "boolean" configuration item defines as
| name | commandline_flag | has_file_entry | has_env_var |
| <name> | <flag> | <configfile> | <environment> |
When I call GetConfig
Then the "boolean" result should return "<result>"
Examples:
| name | flag | configfile | environment | result |
| boolean_xxxd | | false | false | false |
| boolean_xxcd | | true | false | true |
| boolean_xexd | | false | true | false |
| boolean_xecd | | true | true | false |
| boolean_lxxd | --boolean_lxxd | false | false | true |
| boolean_lxcd | --boolean_lxcd | true | false | true |
| boolean_lexd | --boolean_lexd | false | true | true |
| boolean_lecd | --boolean_lecd | true | true | true |
| boolean_sxxd | -M | false | false | true |
| boolean_sxcd | -N | true | false | true |
| boolean_sexd | -O | false | true | true |
| boolean_secd | -P | true | true | true |

29
features/float.feature Normal file
View File

@ -0,0 +1,29 @@
#noinspection CucumberUndefinedStep
Feature: String configuration items
In order to be able to create and retrieve float configuration items
As a developer
I want to develop code that will make this simple, efficient, and consistent
Scenario Outline: Integer config items
Given an application with a name of "Testing App" and a prefix of "test"
And a version of "1.2.3"
And a "float" configuration item defines as
| name | commandline_flag | has_file_entry | has_env_var |
| <name> | <flag> | <configfile> | <environment> |
When I call GetConfig
Then the "float" result should return "<result>"
Examples:
| name | flag | configfile | environment | result |
| float_xxxd | | false | false | 1.99 |
| float_xxcd | | true | false | 2.99 |
| float_xexd | | false | true | 3.99 |
| float_xecd | | true | true | 3.99 |
| float_lxxd | --float_lxxd | false | false | 4.99 |
| float_lxcd | --float_lxcd | true | false | 4.99 |
| float_lexd | --float_lexd | false | true | 4.99 |
| float_lecd | --float_lecd | true | true | 4.99 |
| float_sxxd | -I | false | false | 4.99 |
| float_sxcd | -J | true | false | 4.99 |
| float_sexd | -K | false | true | 4.99 |
| float_secd | -L | true | true | 4.99 |

29
features/integer.feature Normal file
View File

@ -0,0 +1,29 @@
#noinspection CucumberUndefinedStep
Feature: String configuration items
In order to be able to create and retrieve integer configuration items
As a developer
I want to develop code that will make this simple, efficient, and consistent
Scenario Outline: Integer config items
Given an application with a name of "Testing App" and a prefix of "test"
And a version of "1.2.3"
And a "integer" configuration item defines as
| name | commandline_flag | has_file_entry | has_env_var |
| <name> | <flag> | <configfile> | <environment> |
When I call GetConfig
Then the "integer" result should return "<result>"
Examples:
| name | flag | configfile | environment | result |
| integer_xxxd | | false | false | 123 |
| integer_xxcd | | true | false | 234 |
| integer_xexd | | false | true | 345 |
| integer_xecd | | true | true | 345 |
| integer_lxxd | --integer_lxxd | false | false | 456 |
| integer_lxcd | --integer_lxcd | true | false | 456 |
| integer_lexd | --integer_lexd | false | true | 456 |
| integer_lecd | --integer_lecd | true | true | 456 |
| integer_sxxd | -E | false | false | 456 |
| integer_sxcd | -F | true | false | 456 |
| integer_sexd | -G | false | true | 456 |
| integer_secd | -H | true | true | 456 |

29
features/string.feature Normal file
View File

@ -0,0 +1,29 @@
#noinspection CucumberUndefinedStep
Feature: String configuration items
In order to be able to create and retrieve string configuration items
As a developer
I want to develop code that will make this simple, efficient, and consistent
Scenario Outline: String config items
Given an application with a name of "Testing App" and a prefix of "test"
And a version of "1.2.3"
And a "string" configuration item defines as
| name | commandline_flag | has_file_entry | has_env_var |
| <name> | <flag> | <configfile> | <environment> |
When I call GetConfig
Then the "string" result should return "<result>"
Examples:
| name | flag | configfile | environment | result |
| string_xxxd | | false | false | default value |
| string_xxcd | | true | false | config file value |
| string_xexd | | false | true | environment variable value |
| string_xecd | | true | true | environment variable value |
| string_lxxd | --string_lxxd | false | false | command line value |
| string_lxcd | --string_lxcd | true | false | command line value |
| string_lexd | --string_lexd | false | true | command line value |
| string_lecd | --string_lecd | true | true | command line value |
| string_sxxd | -A | false | false | command line value |
| string_sxcd | -B | true | false | command line value |
| string_sexd | -C | false | true | command line value |
| string_secd | -D | true | true | command line value |

40
go.mod Normal file
View File

@ -0,0 +1,40 @@
module config
go 1.22.6
require (
github.com/Masterminds/semver v1.5.0
github.com/cucumber/godog v0.14.1
github.com/spf13/afero v1.11.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
)
require (
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gofrs/uuid v4.3.1+incompatible // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

103
go.sum Normal file
View File

@ -0,0 +1,103 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M=
github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=