5 Commits

27 changed files with 2858 additions and 183 deletions

View File

@@ -59,7 +59,6 @@ linters:
- inamedparam - inamedparam
- interfacebloat - interfacebloat
- intrange - intrange
- ireturn
- loggercheck - loggercheck
- makezero - makezero
- mirror - mirror
@@ -110,6 +109,7 @@ linters:
- perfsprint - perfsprint
- errcheck - errcheck
- gosec - gosec
- gocyclo
- path: _test\.go$ - path: _test\.go$
linters: linters:

View File

@@ -4,16 +4,27 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/aykhans/dodo/pkg/config" "github.com/aykhans/dodo/pkg/config/parser"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils" "github.com/aykhans/dodo/pkg/utils"
"github.com/jedib0t/go-pretty/v6/text" "github.com/jedib0t/go-pretty/v6/text"
"github.com/k0kubun/pp/v3"
) )
func main() { func main() {
cliParser := config.NewConfigCLIParser(os.Args) envParser := parser.NewConfigENVParser("DODO")
cfg, err := cliParser.Parse() envConfig, err := envParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.FieldParseErrors) error {
printValidationErrors("ENV", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
cliParser := parser.NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse()
_ = utils.HandleErrorOrDie(err, _ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error { utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp() cliParser.PrintHelp()
@@ -35,11 +46,16 @@ func main() {
}), }),
) )
fmt.Println(cfg) envConfig.Merge(cliConf)
pp.Println(cliConf) //nolint
pp.Println(envConfig) //nolint
} }
func printValidationErrors(parserName string, errors ...types.FieldParseError) { func printValidationErrors(parserName string, errors ...types.FieldParseError) {
for _, fieldErr := range errors { for _, fieldErr := range errors {
utils.PrintErr(text.FgRed, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err) if fieldErr.Value == "" {
utils.PrintErr(text.FgYellow, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err)
}
utils.PrintErr(text.FgYellow, "[%s] Field '%s' (%s): %v", parserName, fieldErr.Field, fieldErr.Value, fieldErr.Err)
} }
} }

5
go.mod
View File

@@ -4,15 +4,18 @@ go 1.25
require ( require (
github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/k0kubun/pp/v3 v3.5.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

11
go.sum
View File

@@ -2,6 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w=
github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -11,10 +17,11 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -29,6 +29,7 @@ var Defaults = struct {
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"} var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
type Config struct { type Config struct {
Files []types.ConfigFile
Method *string Method *string
URL *url.URL URL *url.URL
Timeout *time.Duration Timeout *time.Duration
@@ -48,7 +49,8 @@ func NewConfig() *Config {
return &Config{} return &Config{}
} }
func (config *Config) MergeConfig(newConfig *Config) { func (config *Config) Merge(newConfig *Config) {
config.Files = append(config.Files, newConfig.Files...)
if newConfig.Method != nil { if newConfig.Method != nil {
config.Method = newConfig.Method config.Method = newConfig.Method
} }
@@ -74,19 +76,19 @@ func (config *Config) MergeConfig(newConfig *Config) {
config.SkipVerify = newConfig.SkipVerify config.SkipVerify = newConfig.SkipVerify
} }
if len(newConfig.Params) != 0 { if len(newConfig.Params) != 0 {
config.Params = newConfig.Params config.Params.Append(newConfig.Params...)
} }
if len(newConfig.Headers) != 0 { if len(newConfig.Headers) != 0 {
config.Headers = newConfig.Headers config.Headers.Append(newConfig.Headers...)
} }
if len(newConfig.Cookies) != 0 { if len(newConfig.Cookies) != 0 {
config.Cookies = newConfig.Cookies config.Cookies.Append(newConfig.Cookies...)
} }
if len(newConfig.Bodies) != 0 { if len(newConfig.Bodies) != 0 {
config.Bodies = newConfig.Bodies config.Bodies.Append(newConfig.Bodies...)
} }
if len(newConfig.Proxies) != 0 { if len(newConfig.Proxies) != 0 {
config.Proxies = newConfig.Proxies config.Proxies.Append(newConfig.Proxies...)
} }
} }

View File

@@ -23,6 +23,7 @@ func TestMergeConfig(t *testing.T) {
newDuration := 2 * time.Minute newDuration := 2 * time.Minute
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("GET"), Method: utils.ToPtr("GET"),
URL: originalURL, URL: originalURL,
Timeout: &originalTimeout, Timeout: &originalTimeout,
@@ -39,6 +40,7 @@ func TestMergeConfig(t *testing.T) {
} }
newConfig := &Config{ newConfig := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("POST"), Method: utils.ToPtr("POST"),
URL: newURL, URL: newURL,
Timeout: &newTimeout, Timeout: &newTimeout,
@@ -54,7 +56,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method) assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL) assert.Equal(t, newURL, config.URL)
@@ -64,10 +66,10 @@ func TestMergeConfig(t *testing.T) {
assert.Equal(t, newDuration, *config.Duration) assert.Equal(t, newDuration, *config.Duration)
assert.True(t, *config.Yes) assert.True(t, *config.Yes)
assert.True(t, *config.SkipVerify) assert.True(t, *config.SkipVerify)
assert.Equal(t, types.Params{{Key: "new", Value: []string{"value"}}}, config.Params) assert.Equal(t, types.Params{{Key: "old", Value: []string{"value"}}, {Key: "new", Value: []string{"value"}}}, config.Params)
assert.Equal(t, types.Headers{{Key: "New-Header", Value: []string{"new"}}}, config.Headers) assert.Equal(t, types.Headers{{Key: "Old-Header", Value: []string{"old"}}, {Key: "New-Header", Value: []string{"new"}}}, config.Headers)
assert.Equal(t, types.Cookies{{Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies) assert.Equal(t, types.Cookies{{Key: "oldCookie", Value: []string{"oldValue"}}, {Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies)
assert.Equal(t, types.Bodies{types.Body("new body")}, config.Bodies) assert.Equal(t, types.Bodies{types.Body("old body"), types.Body("new body")}, config.Bodies)
assert.Empty(t, config.Proxies) assert.Empty(t, config.Proxies)
}) })
@@ -76,6 +78,7 @@ func TestMergeConfig(t *testing.T) {
originalTimeout := 5 * time.Second originalTimeout := 5 * time.Second
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("GET"), Method: utils.ToPtr("GET"),
URL: originalURL, URL: originalURL,
Timeout: &originalTimeout, Timeout: &originalTimeout,
@@ -85,11 +88,12 @@ func TestMergeConfig(t *testing.T) {
newURL, _ := url.Parse("https://new.example.com") newURL, _ := url.Parse("https://new.example.com")
newConfig := &Config{ newConfig := &Config{
Files: []types.ConfigFile{},
URL: newURL, URL: newURL,
DodosCount: utils.ToPtr(uint(10)), DodosCount: utils.ToPtr(uint(10)),
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged") assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
assert.Equal(t, newURL, config.URL, "URL should be updated") assert.Equal(t, newURL, config.URL, "URL should be updated")
@@ -103,6 +107,7 @@ func TestMergeConfig(t *testing.T) {
originalTimeout := 5 * time.Second originalTimeout := 5 * time.Second
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("GET"), Method: utils.ToPtr("GET"),
URL: originalURL, URL: originalURL,
Timeout: &originalTimeout, Timeout: &originalTimeout,
@@ -112,6 +117,7 @@ func TestMergeConfig(t *testing.T) {
} }
newConfig := &Config{ newConfig := &Config{
Files: []types.ConfigFile{},
Method: nil, Method: nil,
URL: nil, URL: nil,
Timeout: nil, Timeout: nil,
@@ -121,7 +127,7 @@ func TestMergeConfig(t *testing.T) {
} }
originalConfigCopy := *config originalConfigCopy := *config
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, originalConfigCopy.Method, config.Method) assert.Equal(t, originalConfigCopy.Method, config.Method)
assert.Equal(t, originalConfigCopy.URL, config.URL) assert.Equal(t, originalConfigCopy.URL, config.URL)
@@ -132,7 +138,9 @@ func TestMergeConfig(t *testing.T) {
}) })
t.Run("MergeConfig with empty slices", func(t *testing.T) { t.Run("MergeConfig with empty slices", func(t *testing.T) {
configFile, _ := types.ParseConfigFile("original.yml")
config := &Config{ config := &Config{
Files: []types.ConfigFile{*configFile},
Params: types.Params{{Key: "original", Value: []string{"value"}}}, Params: types.Params{{Key: "original", Value: []string{"value"}}},
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
Cookies: types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, Cookies: types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}},
@@ -141,6 +149,7 @@ func TestMergeConfig(t *testing.T) {
} }
newConfig := &Config{ newConfig := &Config{
Files: []types.ConfigFile{},
Params: types.Params{}, Params: types.Params{},
Headers: types.Headers{}, Headers: types.Headers{},
Cookies: types.Cookies{}, Cookies: types.Cookies{},
@@ -148,8 +157,9 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override")
assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override") assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override")
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Empty Headers should not override") assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Empty Headers should not override")
assert.Equal(t, types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, config.Cookies, "Empty Cookies should not override") assert.Equal(t, types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, config.Cookies, "Empty Cookies should not override")
@@ -157,6 +167,28 @@ func TestMergeConfig(t *testing.T) {
assert.Equal(t, types.Proxies{}, config.Proxies, "Empty Proxies should not override") assert.Equal(t, types.Proxies{}, config.Proxies, "Empty Proxies should not override")
}) })
t.Run("MergeConfig with Files field", func(t *testing.T) {
configFile1, _ := types.ParseConfigFile("config1.yml")
configFile2, _ := types.ParseConfigFile("config2.yaml")
config := &Config{
Files: []types.ConfigFile{*configFile1},
Method: utils.ToPtr("GET"),
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
}
newConfig := &Config{
Files: []types.ConfigFile{*configFile2},
Method: utils.ToPtr("POST"),
}
config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method, "Method should be updated")
assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended")
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Headers should remain unchanged")
})
t.Run("MergeConfig on empty original config", func(t *testing.T) { t.Run("MergeConfig on empty original config", func(t *testing.T) {
config := &Config{} config := &Config{}
@@ -165,6 +197,7 @@ func TestMergeConfig(t *testing.T) {
newDuration := 2 * time.Minute newDuration := 2 * time.Minute
newConfig := &Config{ newConfig := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("POST"), Method: utils.ToPtr("POST"),
URL: newURL, URL: newURL,
Timeout: &newTimeout, Timeout: &newTimeout,
@@ -180,7 +213,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method) assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL) assert.Equal(t, newURL, config.URL)
@@ -246,6 +279,7 @@ func TestSetDefaults(t *testing.T) {
t.Run("SetDefaults adds User-Agent when missing", func(t *testing.T) { t.Run("SetDefaults adds User-Agent when missing", func(t *testing.T) {
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}}, Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}},
} }
@@ -268,6 +302,7 @@ func TestSetDefaults(t *testing.T) {
t.Run("SetDefaults with partial config", func(t *testing.T) { t.Run("SetDefaults with partial config", func(t *testing.T) {
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Method: utils.ToPtr("PUT"), Method: utils.ToPtr("PUT"),
Yes: utils.ToPtr(true), Yes: utils.ToPtr(true),
} }
@@ -306,6 +341,7 @@ func TestSetDefaults(t *testing.T) {
t.Run("SetDefaults with empty Headers initializes correctly", func(t *testing.T) { t.Run("SetDefaults with empty Headers initializes correctly", func(t *testing.T) {
config := &Config{ config := &Config{
Files: []types.ConfigFile{},
Headers: types.Headers{}, Headers: types.Headers{},
} }
@@ -316,3 +352,176 @@ func TestSetDefaults(t *testing.T) {
assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0]) assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0])
}) })
} }
func TestMergeConfig_AppendBehavior(t *testing.T) {
t.Run("MergeConfig appends params with same key", func(t *testing.T) {
config := &Config{
Params: types.Params{{Key: "filter", Value: []string{"active"}}},
}
newConfig := &Config{
Params: types.Params{{Key: "filter", Value: []string{"verified"}}},
}
config.Merge(newConfig)
assert.Len(t, config.Params, 1)
paramValue := config.Params.GetValue("filter")
require.NotNil(t, paramValue)
assert.Equal(t, []string{"active", "verified"}, *paramValue)
})
t.Run("MergeConfig appends headers with same key", func(t *testing.T) {
config := &Config{
Headers: types.Headers{{Key: "Accept", Value: []string{"text/html"}}},
}
newConfig := &Config{
Headers: types.Headers{{Key: "Accept", Value: []string{"application/json"}}},
}
config.Merge(newConfig)
assert.Len(t, config.Headers, 1)
headerValue := config.Headers.GetValue("Accept")
require.NotNil(t, headerValue)
assert.Equal(t, []string{"text/html", "application/json"}, *headerValue)
})
t.Run("MergeConfig appends cookies with same key", func(t *testing.T) {
config := &Config{
Cookies: types.Cookies{{Key: "session", Value: []string{"old_token"}}},
}
newConfig := &Config{
Cookies: types.Cookies{{Key: "session", Value: []string{"new_token"}}},
}
config.Merge(newConfig)
assert.Len(t, config.Cookies, 1)
cookieValue := config.Cookies.GetValue("session")
require.NotNil(t, cookieValue)
assert.Equal(t, []string{"old_token", "new_token"}, *cookieValue)
})
t.Run("MergeConfig appends bodies", func(t *testing.T) {
config := &Config{
Bodies: types.Bodies{types.Body("first body")},
}
newConfig := &Config{
Bodies: types.Bodies{types.Body("second body"), types.Body("third body")},
}
config.Merge(newConfig)
assert.Len(t, config.Bodies, 3)
assert.Equal(t, types.Body("first body"), config.Bodies[0])
assert.Equal(t, types.Body("second body"), config.Bodies[1])
assert.Equal(t, types.Body("third body"), config.Bodies[2])
})
t.Run("MergeConfig appends proxies", func(t *testing.T) {
proxy1URL, _ := url.Parse("http://proxy1.example.com:8080")
proxy2URL, _ := url.Parse("http://proxy2.example.com:8080")
proxy3URL, _ := url.Parse("https://proxy3.example.com:443")
config := &Config{
Proxies: types.Proxies{types.Proxy(*proxy1URL)},
}
newConfig := &Config{
Proxies: types.Proxies{types.Proxy(*proxy2URL), types.Proxy(*proxy3URL)},
}
config.Merge(newConfig)
assert.Len(t, config.Proxies, 3)
assert.Equal(t, "http://proxy1.example.com:8080", config.Proxies[0].String())
assert.Equal(t, "http://proxy2.example.com:8080", config.Proxies[1].String())
assert.Equal(t, "https://proxy3.example.com:443", config.Proxies[2].String())
})
t.Run("MergeConfig appends mixed content", func(t *testing.T) {
config := &Config{
Params: types.Params{{Key: "limit", Value: []string{"10"}}},
Headers: types.Headers{{Key: "Authorization", Value: []string{"Bearer token1"}}},
Cookies: types.Cookies{{Key: "theme", Value: []string{"dark"}}},
Bodies: types.Bodies{types.Body("original")},
}
newConfig := &Config{
Params: types.Params{{Key: "offset", Value: []string{"0"}}, {Key: "limit", Value: []string{"20"}}},
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}, {Key: "Authorization", Value: []string{"Bearer token2"}}},
Cookies: types.Cookies{{Key: "lang", Value: []string{"en"}}, {Key: "theme", Value: []string{"light"}}},
Bodies: types.Bodies{types.Body("updated")},
}
config.Merge(newConfig)
// Check params
assert.Len(t, config.Params, 2)
limitValue := config.Params.GetValue("limit")
require.NotNil(t, limitValue)
assert.Equal(t, []string{"10", "20"}, *limitValue)
offsetValue := config.Params.GetValue("offset")
require.NotNil(t, offsetValue)
assert.Equal(t, []string{"0"}, *offsetValue)
// Check headers
assert.Len(t, config.Headers, 2)
authValue := config.Headers.GetValue("Authorization")
require.NotNil(t, authValue)
assert.Equal(t, []string{"Bearer token1", "Bearer token2"}, *authValue)
contentTypeValue := config.Headers.GetValue("Content-Type")
require.NotNil(t, contentTypeValue)
assert.Equal(t, []string{"application/json"}, *contentTypeValue)
// Check cookies
assert.Len(t, config.Cookies, 2)
themeValue := config.Cookies.GetValue("theme")
require.NotNil(t, themeValue)
assert.Equal(t, []string{"dark", "light"}, *themeValue)
langValue := config.Cookies.GetValue("lang")
require.NotNil(t, langValue)
assert.Equal(t, []string{"en"}, *langValue)
// Check bodies
assert.Len(t, config.Bodies, 2)
assert.Equal(t, types.Body("original"), config.Bodies[0])
assert.Equal(t, types.Body("updated"), config.Bodies[1])
})
t.Run("MergeConfig with empty slices does not append", func(t *testing.T) {
config := &Config{
Params: types.Params{{Key: "existing", Value: []string{"value"}}},
Headers: types.Headers{{Key: "Existing-Header", Value: []string{"value"}}},
Cookies: types.Cookies{{Key: "existing", Value: []string{"value"}}},
Bodies: types.Bodies{types.Body("existing")},
Proxies: types.Proxies{},
}
originalParams := len(config.Params)
originalHeaders := len(config.Headers)
originalCookies := len(config.Cookies)
originalBodies := len(config.Bodies)
originalProxies := len(config.Proxies)
newConfig := &Config{
Params: types.Params{},
Headers: types.Headers{},
Cookies: types.Cookies{},
Bodies: types.Bodies{},
Proxies: types.Proxies{},
}
config.Merge(newConfig)
assert.Len(t, config.Params, originalParams, "Empty params should not change existing params")
assert.Len(t, config.Headers, originalHeaders, "Empty headers should not change existing headers")
assert.Len(t, config.Cookies, originalCookies, "Empty cookies should not change existing cookies")
assert.Len(t, config.Bodies, originalBodies, "Empty bodies should not change existing bodies")
assert.Len(t, config.Proxies, originalProxies, "Empty proxies should not change existing proxies")
})
}

View File

@@ -0,0 +1,7 @@
package parser
import "github.com/aykhans/dodo/pkg/config"
type IParser interface {
Parse() (*config.Config, error)
}

View File

@@ -1,4 +1,4 @@
package config package parser
import ( import (
"errors" "errors"
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils" "github.com/aykhans/dodo/pkg/utils"
) )
@@ -52,9 +53,10 @@ Flags:
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080") -x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
-skip-verify bool Skip SSL/TLS certificate verification (default %v)` -skip-verify bool Skip SSL/TLS certificate verification (default %v)`
var _ IParser = ConfigCLIParser{}
type ConfigCLIParser struct { type ConfigCLIParser struct {
args []string args []string
configFile *types.ConfigFile
} }
func NewConfigCLIParser(args []string) *ConfigCLIParser { func NewConfigCLIParser(args []string) *ConfigCLIParser {
@@ -64,10 +66,6 @@ func NewConfigCLIParser(args []string) *ConfigCLIParser {
return &ConfigCLIParser{args: args} return &ConfigCLIParser{args: args}
} }
func (parser ConfigCLIParser) GetConfigFile() *types.ConfigFile {
return parser.configFile
}
type stringSliceArg []string type stringSliceArg []string
func (arg *stringSliceArg) String() string { func (arg *stringSliceArg) String() string {
@@ -84,14 +82,14 @@ func (arg *stringSliceArg) Set(value string) error {
// - types.ErrCLINoArgs // - types.ErrCLINoArgs
// - types.CLIUnexpectedArgsError // - types.CLIUnexpectedArgsError
// - types.FieldParseErrors // - types.FieldParseErrors
func (parser *ConfigCLIParser) Parse() (*Config, error) { func (parser ConfigCLIParser) Parse() (*config.Config, error) {
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError) flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
flagSet.Usage = func() { parser.PrintHelp() } flagSet.Usage = func() { parser.PrintHelp() }
var ( var (
config = &Config{} config = &config.Config{}
configFile string configFiles = stringSliceArg{}
yes bool yes bool
skipVerify bool skipVerify bool
method string method string
@@ -108,8 +106,8 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
) )
{ {
flagSet.StringVar(&configFile, "config-file", "", "Config file") flagSet.Var(&configFiles, "config-file", "Config file")
flagSet.StringVar(&configFile, "f", "", "Config file") flagSet.Var(&configFiles, "f", "Config file")
flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions") flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions")
flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions") flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions")
@@ -171,28 +169,50 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
flagSet.Visit(func(flagVar *flag.Flag) { flagSet.Visit(func(flagVar *flag.Flag) {
switch flagVar.Name { switch flagVar.Name {
case "config-file", "f": case "config-file", "f":
var err error for i, configFile := range configFiles {
parser.configFile, err = types.ParseConfigFile(configFile) configFileParsed, err := types.ParseConfigFile(configFile)
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error { _ = utils.HandleErrorOrDie(err,
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", errors.New("file extension not found"))) utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
return nil fieldParseErrors = append(
}), fieldParseErrors,
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error { *types.NewFieldParseError(
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", fmt.Errorf("parse error: %w", err))) fmt.Sprintf("config-file[%d]", i),
return nil configFile,
}), errors.New("file extension not found"),
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error { ),
fieldParseErrors = append( )
fieldParseErrors, return nil
*types.NewFieldParseError( }),
"config-file", utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML.String()), fieldParseErrors = append(
), fieldParseErrors,
) *types.NewFieldParseError(
return nil fmt.Sprintf("config-file[%d]", i),
}), configFile,
) fmt.Errorf("parse error: %w", err),
),
)
return nil
}),
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
fmt.Sprintf("config-file[%d]", i),
configFile,
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML),
),
)
return nil
}),
)
if err == nil {
config.Files = append(config.Files, *configFileParsed)
}
}
case "yes", "y": case "yes", "y":
config.Yes = utils.ToPtr(yes) config.Yes = utils.ToPtr(yes)
case "skip-verify": case "skip-verify":
@@ -202,7 +222,7 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
case "url", "u": case "url", "u":
urlParsed, err := url.Parse(urlInput) urlParsed, err := url.Parse(urlInput)
if err != nil { if err != nil {
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", err)) fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", urlInput, err))
} else { } else {
config.URL = urlParsed config.URL = urlParsed
} }
@@ -228,7 +248,7 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
if err != nil { if err != nil {
fieldParseErrors = append( fieldParseErrors = append(
fieldParseErrors, fieldParseErrors,
*types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), err), *types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err),
) )
} }
} }
@@ -242,14 +262,14 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
return config, nil return config, nil
} }
func (parser *ConfigCLIParser) PrintHelp() { func (parser ConfigCLIParser) PrintHelp() {
fmt.Printf( fmt.Printf(
cliUsageText+"\n", cliUsageText+"\n",
Defaults.Yes, config.Defaults.Yes,
Defaults.DodosCount, config.Defaults.DodosCount,
Defaults.RequestTimeout, config.Defaults.RequestTimeout,
Defaults.Method, config.Defaults.Method,
Defaults.SkipVerify, config.Defaults.SkipVerify,
) )
} }

View File

@@ -1,4 +1,4 @@
package config package parser
import ( import (
"bytes" "bytes"
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -20,7 +21,6 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser) require.NotNil(t, parser)
assert.Equal(t, args, parser.args) assert.Equal(t, args, parser.args)
assert.Nil(t, parser.configFile)
}) })
t.Run("NewConfigCLIParser with nil args", func(t *testing.T) { t.Run("NewConfigCLIParser with nil args", func(t *testing.T) {
@@ -28,7 +28,6 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser) require.NotNil(t, parser)
assert.Equal(t, []string{}, parser.args) assert.Equal(t, []string{}, parser.args)
assert.Nil(t, parser.configFile)
}) })
t.Run("NewConfigCLIParser with empty args", func(t *testing.T) { t.Run("NewConfigCLIParser with empty args", func(t *testing.T) {
@@ -37,21 +36,6 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser) require.NotNil(t, parser)
assert.Equal(t, args, parser.args) assert.Equal(t, args, parser.args)
assert.Nil(t, parser.configFile)
})
}
func TestConfigCLIParser_GetConfigFile(t *testing.T) {
t.Run("GetConfigFile returns nil when no config file is set", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo"})
assert.Nil(t, parser.GetConfigFile())
})
t.Run("GetConfigFile returns config file when set", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo"})
expectedConfigFile := &types.ConfigFile{}
parser.configFile = expectedConfigFile
assert.Equal(t, expectedConfigFile, parser.GetConfigFile())
}) })
} }
@@ -130,6 +114,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.ErrorAs(t, err, &fieldErr) require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1) assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "url", fieldErr.Errors[0].Field) assert.Equal(t, "url", fieldErr.Errors[0].Field)
assert.Equal(t, "://invalid-url", fieldErr.Errors[0].Value)
}) })
t.Run("Parse with method flag", func(t *testing.T) { t.Run("Parse with method flag", func(t *testing.T) {
@@ -272,6 +257,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.ErrorAs(t, err, &fieldErr) require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1) assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "proxy[0]", fieldErr.Errors[0].Field) assert.Equal(t, "proxy[0]", fieldErr.Errors[0].Field)
assert.Equal(t, "://invalid-proxy", fieldErr.Errors[0].Value)
}) })
t.Run("Parse with mixed valid and invalid proxies", func(t *testing.T) { t.Run("Parse with mixed valid and invalid proxies", func(t *testing.T) {
@@ -283,6 +269,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.ErrorAs(t, err, &fieldErr) require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1) assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "proxy[1]", fieldErr.Errors[0].Field) assert.Equal(t, "proxy[1]", fieldErr.Errors[0].Field)
assert.Equal(t, "://invalid", fieldErr.Errors[0].Value)
}) })
t.Run("Parse with long flag names", func(t *testing.T) { t.Run("Parse with long flag names", func(t *testing.T) {
@@ -318,7 +305,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, config) require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile()) assert.Len(t, config.Files, 1)
}) })
t.Run("Parse with config-file flag using long form", func(t *testing.T) { t.Run("Parse with config-file flag using long form", func(t *testing.T) {
@@ -327,7 +314,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, config) require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile()) assert.Len(t, config.Files, 1)
}) })
t.Run("Parse with config-file flag invalid extension", func(t *testing.T) { t.Run("Parse with config-file flag invalid extension", func(t *testing.T) {
@@ -338,7 +325,8 @@ func TestConfigCLIParser_Parse(t *testing.T) {
var fieldErr types.FieldParseErrors var fieldErr types.FieldParseErrors
require.ErrorAs(t, err, &fieldErr) require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1) assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "config-file", fieldErr.Errors[0].Field) assert.Equal(t, "config-file[0]", fieldErr.Errors[0].Field)
assert.Equal(t, "/path/to/config", fieldErr.Errors[0].Value)
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file extension not found") assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file extension not found")
}) })
@@ -350,7 +338,8 @@ func TestConfigCLIParser_Parse(t *testing.T) {
var fieldErr types.FieldParseErrors var fieldErr types.FieldParseErrors
require.ErrorAs(t, err, &fieldErr) require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1) assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "config-file", fieldErr.Errors[0].Field) assert.Equal(t, "config-file[0]", fieldErr.Errors[0].Field)
assert.Equal(t, "/path/to/config.json", fieldErr.Errors[0].Value)
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file type") assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file type")
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported") assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported")
}) })
@@ -361,7 +350,16 @@ func TestConfigCLIParser_Parse(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, config) require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile()) assert.Len(t, config.Files, 1)
})
t.Run("Parse with multiple config files", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/config1.yaml", "-f", "/path/config2.yml"})
config, err := parser.Parse()
require.NoError(t, err)
require.NotNil(t, config)
assert.Len(t, config.Files, 2)
}) })
t.Run("Parse with all flags combined", func(t *testing.T) { t.Run("Parse with all flags combined", func(t *testing.T) {
@@ -412,7 +410,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
assert.Len(t, config.Proxies, 1) assert.Len(t, config.Proxies, 1)
assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String()) assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String())
assert.NotNil(t, parser.GetConfigFile()) assert.Len(t, config.Files, 1)
}) })
t.Run("Parse with multiple field parse errors", func(t *testing.T) { t.Run("Parse with multiple field parse errors", func(t *testing.T) {
@@ -437,6 +435,15 @@ func TestConfigCLIParser_Parse(t *testing.T) {
assert.True(t, fields["url"]) assert.True(t, fields["url"])
assert.True(t, fields["proxy[0]"]) assert.True(t, fields["proxy[0]"])
assert.True(t, fields["proxy[1]"]) assert.True(t, fields["proxy[1]"])
// Check error values
values := make(map[string]string)
for _, parseErr := range fieldErr.Errors {
values[parseErr.Field] = parseErr.Value
}
assert.Equal(t, "://invalid-url", values["url"])
assert.Equal(t, "://invalid-proxy1", values["proxy[0]"])
assert.Equal(t, "://invalid-proxy2", values["proxy[1]"])
}) })
} }
@@ -479,7 +486,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) {
assert.Contains(t, output, "-f, -config-file") assert.Contains(t, output, "-f, -config-file")
// Verify default values are included // Verify default values are included
assert.Contains(t, output, Defaults.Method) assert.Contains(t, output, config.Defaults.Method)
assert.Contains(t, output, "1") // DodosCount default assert.Contains(t, output, "1") // DodosCount default
assert.Contains(t, output, "10s") // RequestTimeout default assert.Contains(t, output, "10s") // RequestTimeout default
assert.Contains(t, output, "false") // Yes default assert.Contains(t, output, "false") // Yes default

236
pkg/config/parser/env.go Normal file
View File

@@ -0,0 +1,236 @@
package parser
import (
"errors"
"fmt"
"net/url"
"os"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
)
var _ IParser = ConfigENVParser{}
type ConfigENVParser struct {
envPrefix string
}
func NewConfigENVParser(envPrefix string) *ConfigENVParser {
return &ConfigENVParser{envPrefix}
}
// Parse parses env arguments into a Config object.
// It can return the following errors:
// - types.FieldParseErrors
func (parser ConfigENVParser) Parse() (*config.Config, error) {
var (
config = &config.Config{}
fieldParseErrors []types.FieldParseError
)
if configFile := parser.getEnv("CONFIG_FILE"); configFile != "" {
configFileParsed, err := types.ParseConfigFile(configFile)
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
errors.New("file extension not found"),
),
)
return nil
}),
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
fmt.Errorf("parse error: %w", err),
),
)
return nil
}),
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML),
),
)
return nil
}),
)
if err == nil {
config.Files = append(config.Files, *configFileParsed)
}
}
if yes := parser.getEnv("YES"); yes != "" {
yesParsed, err := utils.ParseString[bool](yes)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("YES"),
yes,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
),
)
} else {
config.Yes = &yesParsed
}
}
if skipVerify := parser.getEnv("SKIP_VERIFY"); skipVerify != "" {
skipVerifyParsed, err := utils.ParseString[bool](skipVerify)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("SKIP_VERIFY"),
skipVerify,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
),
)
} else {
config.SkipVerify = &skipVerifyParsed
}
}
if method := parser.getEnv("METHOD"); method != "" {
config.Method = &method
}
if urlEnv := parser.getEnv("URL"); urlEnv != "" {
urlEnvParsed, err := url.Parse(urlEnv)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(parser.getFullEnvName("URL"), urlEnv, err),
)
} else {
config.URL = urlEnvParsed
}
}
if dodos := parser.getEnv("DODOS"); dodos != "" {
dodosParsed, err := utils.ParseString[uint](dodos)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("DODOS"),
dodos,
errors.New("invalid value for unsigned integer"),
),
)
} else {
config.DodosCount = &dodosParsed
}
}
if requests := parser.getEnv("REQUESTS"); requests != "" {
requestsParsed, err := utils.ParseString[uint](requests)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("REQUESTS"),
requests,
errors.New("invalid value for unsigned integer"),
),
)
} else {
config.RequestCount = &requestsParsed
}
}
if duration := parser.getEnv("DURATION"); duration != "" {
durationParsed, err := utils.ParseString[time.Duration](duration)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("DURATION"),
duration,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
),
)
} else {
config.Duration = &durationParsed
}
}
if timeout := parser.getEnv("TIMEOUT"); timeout != "" {
timeoutParsed, err := utils.ParseString[time.Duration](timeout)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("TIMEOUT"),
timeout,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
),
)
} else {
config.Timeout = &timeoutParsed
}
}
if param := parser.getEnv("PARAM"); param != "" {
config.Params.Parse(param)
}
if header := parser.getEnv("HEADER"); header != "" {
config.Headers.Parse(header)
}
if cookie := parser.getEnv("COOKIE"); cookie != "" {
config.Cookies.Parse(cookie)
}
if body := parser.getEnv("BODY"); body != "" {
config.Bodies.Parse(body)
}
if proxy := parser.getEnv("PROXY"); proxy != "" {
err := config.Proxies.Parse(proxy)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("PROXY"),
proxy,
err,
),
)
}
}
if len(fieldParseErrors) > 0 {
return nil, types.NewFieldParseErrors(fieldParseErrors)
}
return config, nil
}
func (parser ConfigENVParser) getFullEnvName(envName string) string {
if parser.envPrefix == "" {
return envName
}
return parser.envPrefix + "_" + envName
}
func (parser ConfigENVParser) getEnv(envName string) string {
return os.Getenv(parser.getFullEnvName(envName))
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ func (body Body) String() string {
type Bodies []Body type Bodies []Body
func (bodies *Bodies) Append(body Body) { func (bodies *Bodies) Append(body ...Body) {
*bodies = append(*bodies, body) *bodies = append(*bodies, body...)
} }
func (bodies *Bodies) Parse(rawValues ...string) { func (bodies *Bodies) Parse(rawValues ...string) {

View File

@@ -49,6 +49,16 @@ func TestBodies_Append(t *testing.T) {
assert.Equal(t, Body("third"), (*bodies)[2]) assert.Equal(t, Body("third"), (*bodies)[2])
}) })
t.Run("Append multiple bodies in single call", func(t *testing.T) {
bodies := &Bodies{}
bodies.Append(Body("first"), Body("second"), Body("third"))
assert.Len(t, *bodies, 3)
assert.Equal(t, Body("first"), (*bodies)[0])
assert.Equal(t, Body("second"), (*bodies)[1])
assert.Equal(t, Body("third"), (*bodies)[2])
})
t.Run("Append to existing bodies", func(t *testing.T) { t.Run("Append to existing bodies", func(t *testing.T) {
bodies := &Bodies{Body("existing")} bodies := &Bodies{Body("existing")}
bodies.Append(Body("new")) bodies.Append(Body("new"))
@@ -58,6 +68,17 @@ func TestBodies_Append(t *testing.T) {
assert.Equal(t, Body("new"), (*bodies)[1]) assert.Equal(t, Body("new"), (*bodies)[1])
}) })
t.Run("Append multiple to existing bodies", func(t *testing.T) {
bodies := &Bodies{Body("existing")}
bodies.Append(Body("new1"), Body("new2"), Body("new3"))
assert.Len(t, *bodies, 4)
assert.Equal(t, Body("existing"), (*bodies)[0])
assert.Equal(t, Body("new1"), (*bodies)[1])
assert.Equal(t, Body("new2"), (*bodies)[2])
assert.Equal(t, Body("new3"), (*bodies)[3])
})
t.Run("Append empty body", func(t *testing.T) { t.Run("Append empty body", func(t *testing.T) {
bodies := &Bodies{} bodies := &Bodies{}
bodies.Append(Body("")) bodies.Append(Body(""))
@@ -65,6 +86,29 @@ func TestBodies_Append(t *testing.T) {
assert.Len(t, *bodies, 1) assert.Len(t, *bodies, 1)
assert.Empty(t, (*bodies)[0].String()) assert.Empty(t, (*bodies)[0].String())
}) })
t.Run("Append no bodies", func(t *testing.T) {
bodies := &Bodies{Body("existing")}
bodies.Append()
assert.Len(t, *bodies, 1)
assert.Equal(t, Body("existing"), (*bodies)[0])
})
t.Run("Append mixed bodies", func(t *testing.T) {
bodies := &Bodies{}
bodies.Append(Body("first"), Body(""))
bodies.Append(Body("second"))
bodies.Append(Body("third"), Body("fourth"), Body("fifth"))
assert.Len(t, *bodies, 6)
assert.Equal(t, Body("first"), (*bodies)[0])
assert.Equal(t, Body(""), (*bodies)[1])
assert.Equal(t, Body("second"), (*bodies)[2])
assert.Equal(t, Body("third"), (*bodies)[3])
assert.Equal(t, Body("fourth"), (*bodies)[4])
assert.Equal(t, Body("fifth"), (*bodies)[5])
})
} }
func TestBodies_Parse(t *testing.T) { func TestBodies_Parse(t *testing.T) {

View File

@@ -6,39 +6,19 @@ import (
"strings" "strings"
) )
type ConfigFileType int type ConfigFileType string
const ( const (
ConfigFileTypeYAML ConfigFileType = iota ConfigFileTypeYAML ConfigFileType = "yaml/yml"
) )
func (t ConfigFileType) String() string { type ConfigFileLocationType string
switch t {
case ConfigFileTypeYAML:
return "yaml/yml"
default:
return "unknown"
}
}
type ConfigFileLocationType int
const ( const (
ConfigFileLocationLocal ConfigFileLocationType = iota ConfigFileLocationLocal ConfigFileLocationType = "local"
ConfigFileLocationRemote ConfigFileLocationRemote ConfigFileLocationType = "remote"
) )
func (l ConfigFileLocationType) String() string {
switch l {
case ConfigFileLocationLocal:
return "local"
case ConfigFileLocationRemote:
return "remote"
default:
return "unknown"
}
}
type ConfigFile struct { type ConfigFile struct {
path string path string
_type ConfigFileType _type ConfigFileType

View File

@@ -7,35 +7,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestConfigFileType_String(t *testing.T) {
t.Run("ConfigFileTypeYAML returns yaml", func(t *testing.T) {
configType := ConfigFileTypeYAML
assert.Equal(t, "yaml/yml", configType.String())
})
t.Run("Unknown config file type returns unknown", func(t *testing.T) {
configType := ConfigFileType(999)
assert.Equal(t, "unknown", configType.String())
})
}
func TestConfigFileLocationType_String(t *testing.T) {
t.Run("ConfigFileLocationLocal returns local", func(t *testing.T) {
locationType := ConfigFileLocationLocal
assert.Equal(t, "local", locationType.String())
})
t.Run("ConfigFileLocationRemote returns remote", func(t *testing.T) {
locationType := ConfigFileLocationRemote
assert.Equal(t, "remote", locationType.String())
})
t.Run("Unknown location type returns unknown", func(t *testing.T) {
locationType := ConfigFileLocationType(999)
assert.Equal(t, "unknown", locationType.String())
})
}
func TestConfigFile_String(t *testing.T) { func TestConfigFile_String(t *testing.T) {
t.Run("String returns the file path", func(t *testing.T) { t.Run("String returns the file path", func(t *testing.T) {
configFile := ConfigFile{path: "/path/to/config.yaml"} configFile := ConfigFile{path: "/path/to/config.yaml"}

View File

@@ -15,11 +15,13 @@ func (cookies Cookies) GetValue(key string) *[]string {
return nil return nil
} }
func (cookies *Cookies) Append(cookie Cookie) { func (cookies *Cookies) Append(cookie ...Cookie) {
if item := cookies.GetValue(cookie.Key); item != nil { for _, c := range cookie {
*item = append(*item, cookie.Value...) if item := cookies.GetValue(c.Key); item != nil {
} else { *item = append(*item, c.Value...)
*cookies = append(*cookies, cookie) } else {
*cookies = append(*cookies, c)
}
} }
} }

View File

@@ -88,6 +88,39 @@ func TestCookies_Append(t *testing.T) {
assert.Len(t, *cookies, 3) assert.Len(t, *cookies, 3)
}) })
t.Run("Append multiple cookies in single call", func(t *testing.T) {
cookies := &Cookies{}
cookies.Append(
Cookie{Key: "session", Value: []string{"abc123"}},
Cookie{Key: "user", Value: []string{"john"}},
Cookie{Key: "token", Value: []string{"xyz789"}},
)
assert.Len(t, *cookies, 3)
assert.Equal(t, "session", (*cookies)[0].Key)
assert.Equal(t, []string{"abc123"}, (*cookies)[0].Value)
assert.Equal(t, "user", (*cookies)[1].Key)
assert.Equal(t, []string{"john"}, (*cookies)[1].Value)
assert.Equal(t, "token", (*cookies)[2].Key)
assert.Equal(t, []string{"xyz789"}, (*cookies)[2].Value)
})
t.Run("Append multiple with existing keys", func(t *testing.T) {
cookies := &Cookies{
{Key: "prefs", Value: []string{"theme=dark"}},
}
cookies.Append(
Cookie{Key: "prefs", Value: []string{"lang=en"}},
Cookie{Key: "prefs", Value: []string{"tz=UTC"}},
Cookie{Key: "session", Value: []string{"abc123"}},
)
assert.Len(t, *cookies, 2)
assert.Equal(t, []string{"theme=dark", "lang=en", "tz=UTC"}, (*cookies)[0].Value)
assert.Equal(t, "session", (*cookies)[1].Key)
assert.Equal(t, []string{"abc123"}, (*cookies)[1].Value)
})
t.Run("Append multiple values at once", func(t *testing.T) { t.Run("Append multiple values at once", func(t *testing.T) {
cookies := &Cookies{ cookies := &Cookies{
{Key: "tags", Value: []string{"tag1"}}, {Key: "tags", Value: []string{"tag1"}},
@@ -105,6 +138,88 @@ func TestCookies_Append(t *testing.T) {
assert.Len(t, *cookies, 1) assert.Len(t, *cookies, 1)
assert.Equal(t, []string{""}, (*cookies)[0].Value) assert.Equal(t, []string{""}, (*cookies)[0].Value)
}) })
t.Run("Append no cookies", func(t *testing.T) {
cookies := &Cookies{
{Key: "existing", Value: []string{"value"}},
}
cookies.Append()
assert.Len(t, *cookies, 1)
assert.Equal(t, "existing", (*cookies)[0].Key)
assert.Equal(t, []string{"value"}, (*cookies)[0].Value)
})
t.Run("Append mixed new and existing cookies", func(t *testing.T) {
cookies := &Cookies{
{Key: "session", Value: []string{"old_session"}},
{Key: "theme", Value: []string{"dark"}},
}
cookies.Append(
Cookie{Key: "session", Value: []string{"new_session"}},
Cookie{Key: "user", Value: []string{"john"}},
Cookie{Key: "theme", Value: []string{"light"}},
Cookie{Key: "lang", Value: []string{"en"}},
)
assert.Len(t, *cookies, 4)
sessionValue := cookies.GetValue("session")
require.NotNil(t, sessionValue)
assert.Equal(t, []string{"old_session", "new_session"}, *sessionValue)
themeValue := cookies.GetValue("theme")
require.NotNil(t, themeValue)
assert.Equal(t, []string{"dark", "light"}, *themeValue)
userValue := cookies.GetValue("user")
require.NotNil(t, userValue)
assert.Equal(t, []string{"john"}, *userValue)
langValue := cookies.GetValue("lang")
require.NotNil(t, langValue)
assert.Equal(t, []string{"en"}, *langValue)
})
t.Run("Append cookies with multiple values", func(t *testing.T) {
cookies := &Cookies{}
cookies.Append(
Cookie{Key: "permissions", Value: []string{"read", "write", "delete"}},
Cookie{Key: "features", Value: []string{"advanced", "beta"}},
)
assert.Len(t, *cookies, 2)
assert.Equal(t, []string{"read", "write", "delete"}, (*cookies)[0].Value)
assert.Equal(t, []string{"advanced", "beta"}, (*cookies)[1].Value)
})
t.Run("Append typical session cookies", func(t *testing.T) {
cookies := &Cookies{}
cookies.Append(
Cookie{Key: "JSESSIONID", Value: []string{"A1B2C3D4E5F6"}},
Cookie{Key: "PHPSESSID", Value: []string{"abcdef123456"}},
Cookie{Key: "session_token", Value: []string{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}},
Cookie{Key: "__Secure-next-auth.session-token", Value: []string{"secure_token_123"}},
)
assert.Len(t, *cookies, 4)
jsessionValue := cookies.GetValue("JSESSIONID")
require.NotNil(t, jsessionValue)
assert.Equal(t, []string{"A1B2C3D4E5F6"}, *jsessionValue)
phpValue := cookies.GetValue("PHPSESSID")
require.NotNil(t, phpValue)
assert.Equal(t, []string{"abcdef123456"}, *phpValue)
sessionValue := cookies.GetValue("session_token")
require.NotNil(t, sessionValue)
assert.Equal(t, []string{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}, *sessionValue)
secureValue := cookies.GetValue("__Secure-next-auth.session-token")
require.NotNil(t, secureValue)
assert.Equal(t, []string{"secure_token_123"}, *secureValue)
})
} }
func TestCookies_Parse(t *testing.T) { func TestCookies_Parse(t *testing.T) {

View File

@@ -22,14 +22,15 @@ var (
type FieldParseError struct { type FieldParseError struct {
Field string Field string
Value string
Err error Err error
} }
func NewFieldParseError(field string, err error) *FieldParseError { func NewFieldParseError(field string, value string, err error) *FieldParseError {
if err == nil { if err == nil {
err = ErrNoError err = ErrNoError
} }
return &FieldParseError{field, err} return &FieldParseError{field, value, err}
} }
func (e FieldParseError) Error() string { func (e FieldParseError) Error() string {

View File

@@ -10,7 +10,7 @@ import (
func TestFieldParseError_Error(t *testing.T) { func TestFieldParseError_Error(t *testing.T) {
t.Run("Error returns formatted message", func(t *testing.T) { t.Run("Error returns formatted message", func(t *testing.T) {
originalErr := errors.New("invalid value") originalErr := errors.New("invalid value")
fieldErr := NewFieldParseError("username", originalErr) fieldErr := NewFieldParseError("username", "testuser", originalErr)
expected := "Field 'username' parse failed: invalid value" expected := "Field 'username' parse failed: invalid value"
assert.Equal(t, expected, fieldErr.Error()) assert.Equal(t, expected, fieldErr.Error())
@@ -18,14 +18,14 @@ func TestFieldParseError_Error(t *testing.T) {
t.Run("Error with empty field name", func(t *testing.T) { t.Run("Error with empty field name", func(t *testing.T) {
originalErr := errors.New("test error") originalErr := errors.New("test error")
fieldErr := NewFieldParseError("", originalErr) fieldErr := NewFieldParseError("", "somevalue", originalErr)
expected := "Field '' parse failed: test error" expected := "Field '' parse failed: test error"
assert.Equal(t, expected, fieldErr.Error()) assert.Equal(t, expected, fieldErr.Error())
}) })
t.Run("Error with nil underlying error", func(t *testing.T) { t.Run("Error with nil underlying error", func(t *testing.T) {
fieldErr := NewFieldParseError("field", nil) fieldErr := NewFieldParseError("field", "value123", nil)
expected := "Field 'field' parse failed: no error (internal)" expected := "Field 'field' parse failed: no error (internal)"
assert.Equal(t, expected, fieldErr.Error()) assert.Equal(t, expected, fieldErr.Error())
@@ -35,13 +35,13 @@ func TestFieldParseError_Error(t *testing.T) {
func TestFieldParseError_Unwrap(t *testing.T) { func TestFieldParseError_Unwrap(t *testing.T) {
t.Run("Unwrap returns original error", func(t *testing.T) { t.Run("Unwrap returns original error", func(t *testing.T) {
originalErr := errors.New("original error") originalErr := errors.New("original error")
fieldErr := NewFieldParseError("field", originalErr) fieldErr := NewFieldParseError("field", "value", originalErr)
assert.Equal(t, originalErr, fieldErr.Unwrap()) assert.Equal(t, originalErr, fieldErr.Unwrap())
}) })
t.Run("Unwrap with nil error", func(t *testing.T) { t.Run("Unwrap with nil error", func(t *testing.T) {
fieldErr := NewFieldParseError("field", nil) fieldErr := NewFieldParseError("field", "value", nil)
assert.Equal(t, ErrNoError, fieldErr.Unwrap()) assert.Equal(t, ErrNoError, fieldErr.Unwrap())
}) })
@@ -50,16 +50,18 @@ func TestFieldParseError_Unwrap(t *testing.T) {
func TestNewFieldParseError(t *testing.T) { func TestNewFieldParseError(t *testing.T) {
t.Run("Creates FieldParseError with correct values", func(t *testing.T) { t.Run("Creates FieldParseError with correct values", func(t *testing.T) {
originalErr := errors.New("test error") originalErr := errors.New("test error")
fieldErr := NewFieldParseError("testField", originalErr) fieldErr := NewFieldParseError("testField", "testValue", originalErr)
assert.Equal(t, "testField", fieldErr.Field) assert.Equal(t, "testField", fieldErr.Field)
assert.Equal(t, "testValue", fieldErr.Value)
assert.Equal(t, originalErr, fieldErr.Err) assert.Equal(t, originalErr, fieldErr.Err)
}) })
t.Run("Creates FieldParseError with ErrNoError when nil passed", func(t *testing.T) { t.Run("Creates FieldParseError with ErrNoError when nil passed", func(t *testing.T) {
fieldErr := NewFieldParseError("testField", nil) fieldErr := NewFieldParseError("testField", "testValue", nil)
assert.Equal(t, "testField", fieldErr.Field) assert.Equal(t, "testField", fieldErr.Field)
assert.Equal(t, "testValue", fieldErr.Value)
assert.Equal(t, ErrNoError, fieldErr.Err) assert.Equal(t, ErrNoError, fieldErr.Err)
}) })
} }
@@ -72,7 +74,7 @@ func TestFieldParseErrors_Error(t *testing.T) {
}) })
t.Run("Error with single error returns single error message", func(t *testing.T) { t.Run("Error with single error returns single error message", func(t *testing.T) {
fieldErr := *NewFieldParseError("field1", errors.New("error1")) fieldErr := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr}) fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr})
expected := "Field 'field1' parse failed: error1" expected := "Field 'field1' parse failed: error1"
@@ -80,9 +82,9 @@ func TestFieldParseErrors_Error(t *testing.T) {
}) })
t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) { t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("field1", errors.New("error1")) fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := *NewFieldParseError("field2", errors.New("error2")) fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErr3 := *NewFieldParseError("field3", errors.New("error3")) fieldErr3 := *NewFieldParseError("field3", "value3", errors.New("error3"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2, fieldErr3}) fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2, fieldErr3})
expected := "Field 'field1' parse failed: error1\nField 'field2' parse failed: error2\nField 'field3' parse failed: error3" expected := "Field 'field1' parse failed: error1\nField 'field2' parse failed: error2\nField 'field3' parse failed: error3"
@@ -90,8 +92,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
}) })
t.Run("Error with two errors", func(t *testing.T) { t.Run("Error with two errors", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("username", errors.New("too short")) fieldErr1 := *NewFieldParseError("username", "john", errors.New("too short"))
fieldErr2 := *NewFieldParseError("email", errors.New("invalid format")) fieldErr2 := *NewFieldParseError("email", "invalid", errors.New("invalid format"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2}) fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
expected := "Field 'username' parse failed: too short\nField 'email' parse failed: invalid format" expected := "Field 'username' parse failed: too short\nField 'email' parse failed: invalid format"
@@ -101,8 +103,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
func TestNewFieldParseErrors(t *testing.T) { func TestNewFieldParseErrors(t *testing.T) {
t.Run("Creates FieldParseErrors with correct values", func(t *testing.T) { t.Run("Creates FieldParseErrors with correct values", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("field1", errors.New("error1")) fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := *NewFieldParseError("field2", errors.New("error2")) fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2}) fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
assert.Len(t, fieldErrors.Errors, 2) assert.Len(t, fieldErrors.Errors, 2)
@@ -258,7 +260,7 @@ func TestErrorConstants(t *testing.T) {
func TestErrorImplementsErrorInterface(t *testing.T) { func TestErrorImplementsErrorInterface(t *testing.T) {
t.Run("FieldParseError implements error interface", func(t *testing.T) { t.Run("FieldParseError implements error interface", func(t *testing.T) {
var err error = NewFieldParseError("field", errors.New("test")) var err error = NewFieldParseError("field", "value", errors.New("test"))
assert.Error(t, err) assert.Error(t, err)
}) })

View File

@@ -25,11 +25,13 @@ func (headers Headers) GetValue(key string) *[]string {
return nil return nil
} }
func (headers *Headers) Append(header Header) { func (headers *Headers) Append(header ...Header) {
if item := headers.GetValue(header.Key); item != nil { for _, h := range header {
*item = append(*item, header.Value...) if item := headers.GetValue(h.Key); item != nil {
} else { *item = append(*item, h.Value...)
*headers = append(*headers, header) } else {
*headers = append(*headers, h)
}
} }
} }

View File

@@ -125,6 +125,39 @@ func TestHeaders_Append(t *testing.T) {
assert.Len(t, *headers, 3) assert.Len(t, *headers, 3)
}) })
t.Run("Append multiple headers in single call", func(t *testing.T) {
headers := &Headers{}
headers.Append(
Header{Key: "Content-Type", Value: []string{"application/json"}},
Header{Key: "Authorization", Value: []string{"Bearer token"}},
Header{Key: "Accept", Value: []string{"*/*"}},
)
assert.Len(t, *headers, 3)
assert.Equal(t, "Content-Type", (*headers)[0].Key)
assert.Equal(t, []string{"application/json"}, (*headers)[0].Value)
assert.Equal(t, "Authorization", (*headers)[1].Key)
assert.Equal(t, []string{"Bearer token"}, (*headers)[1].Value)
assert.Equal(t, "Accept", (*headers)[2].Key)
assert.Equal(t, []string{"*/*"}, (*headers)[2].Value)
})
t.Run("Append multiple with existing keys", func(t *testing.T) {
headers := &Headers{
{Key: "Accept", Value: []string{"text/html"}},
}
headers.Append(
Header{Key: "Accept", Value: []string{"application/json"}},
Header{Key: "Accept", Value: []string{"application/xml"}},
Header{Key: "Content-Type", Value: []string{"text/plain"}},
)
assert.Len(t, *headers, 2)
assert.Equal(t, []string{"text/html", "application/json", "application/xml"}, (*headers)[0].Value)
assert.Equal(t, "Content-Type", (*headers)[1].Key)
assert.Equal(t, []string{"text/plain"}, (*headers)[1].Value)
})
t.Run("Append multiple values at once", func(t *testing.T) { t.Run("Append multiple values at once", func(t *testing.T) {
headers := &Headers{ headers := &Headers{
{Key: "Accept-Language", Value: []string{"en"}}, {Key: "Accept-Language", Value: []string{"en"}},
@@ -142,6 +175,78 @@ func TestHeaders_Append(t *testing.T) {
assert.Len(t, *headers, 1) assert.Len(t, *headers, 1)
assert.Equal(t, []string{""}, (*headers)[0].Value) assert.Equal(t, []string{""}, (*headers)[0].Value)
}) })
t.Run("Append no headers", func(t *testing.T) {
headers := &Headers{
{Key: "Existing", Value: []string{"value"}},
}
headers.Append()
assert.Len(t, *headers, 1)
assert.Equal(t, "Existing", (*headers)[0].Key)
assert.Equal(t, []string{"value"}, (*headers)[0].Value)
})
t.Run("Append mixed new and existing headers", func(t *testing.T) {
headers := &Headers{
{Key: "Cache-Control", Value: []string{"no-cache"}},
{Key: "Accept", Value: []string{"text/html"}},
}
headers.Append(
Header{Key: "Cache-Control", Value: []string{"no-store"}},
Header{Key: "User-Agent", Value: []string{"Mozilla/5.0"}},
Header{Key: "Accept", Value: []string{"application/json"}},
Header{Key: "X-Custom", Value: []string{"custom-value"}},
)
assert.Len(t, *headers, 4)
cacheValue := headers.GetValue("Cache-Control")
require.NotNil(t, cacheValue)
assert.Equal(t, []string{"no-cache", "no-store"}, *cacheValue)
acceptValue := headers.GetValue("Accept")
require.NotNil(t, acceptValue)
assert.Equal(t, []string{"text/html", "application/json"}, *acceptValue)
userAgentValue := headers.GetValue("User-Agent")
require.NotNil(t, userAgentValue)
assert.Equal(t, []string{"Mozilla/5.0"}, *userAgentValue)
customValue := headers.GetValue("X-Custom")
require.NotNil(t, customValue)
assert.Equal(t, []string{"custom-value"}, *customValue)
})
t.Run("Append headers with multiple values", func(t *testing.T) {
headers := &Headers{}
headers.Append(
Header{Key: "Accept", Value: []string{"text/html", "application/json", "application/xml"}},
Header{Key: "Accept-Encoding", Value: []string{"gzip", "deflate"}},
)
assert.Len(t, *headers, 2)
assert.Equal(t, []string{"text/html", "application/json", "application/xml"}, (*headers)[0].Value)
assert.Equal(t, []string{"gzip", "deflate"}, (*headers)[1].Value)
})
t.Run("Append common HTTP headers", func(t *testing.T) {
headers := &Headers{}
headers.Append(
Header{Key: "Content-Type", Value: []string{"application/json"}},
Header{Key: "Authorization", Value: []string{"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"}},
Header{Key: "User-Agent", Value: []string{"Go-http-client/1.1"}},
Header{Key: "Accept", Value: []string{"*/*"}},
Header{Key: "X-Requested-With", Value: []string{"XMLHttpRequest"}},
)
assert.Len(t, *headers, 5)
assert.True(t, headers.Has("Content-Type"))
assert.True(t, headers.Has("Authorization"))
assert.True(t, headers.Has("User-Agent"))
assert.True(t, headers.Has("Accept"))
assert.True(t, headers.Has("X-Requested-With"))
})
} }
func TestHeaders_Parse(t *testing.T) { func TestHeaders_Parse(t *testing.T) {

View File

@@ -15,11 +15,13 @@ func (params Params) GetValue(key string) *[]string {
return nil return nil
} }
func (params *Params) Append(param Param) { func (params *Params) Append(param ...Param) {
if item := params.GetValue(param.Key); item != nil { for _, p := range param {
*item = append(*item, param.Value...) if item := params.GetValue(p.Key); item != nil {
} else { *item = append(*item, p.Value...)
*params = append(*params, param) } else {
*params = append(*params, p)
}
} }
} }

View File

@@ -88,6 +88,39 @@ func TestParams_Append(t *testing.T) {
assert.Len(t, *params, 3) assert.Len(t, *params, 3)
}) })
t.Run("Append multiple parameters in single call", func(t *testing.T) {
params := &Params{}
params.Append(
Param{Key: "name", Value: []string{"john"}},
Param{Key: "age", Value: []string{"25"}},
Param{Key: "city", Value: []string{"NYC"}},
)
assert.Len(t, *params, 3)
assert.Equal(t, "name", (*params)[0].Key)
assert.Equal(t, []string{"john"}, (*params)[0].Value)
assert.Equal(t, "age", (*params)[1].Key)
assert.Equal(t, []string{"25"}, (*params)[1].Value)
assert.Equal(t, "city", (*params)[2].Key)
assert.Equal(t, []string{"NYC"}, (*params)[2].Value)
})
t.Run("Append multiple with existing keys", func(t *testing.T) {
params := &Params{
{Key: "tags", Value: []string{"go"}},
}
params.Append(
Param{Key: "tags", Value: []string{"test"}},
Param{Key: "tags", Value: []string{"api"}},
Param{Key: "version", Value: []string{"1.0"}},
)
assert.Len(t, *params, 2)
assert.Equal(t, []string{"go", "test", "api"}, (*params)[0].Value)
assert.Equal(t, "version", (*params)[1].Key)
assert.Equal(t, []string{"1.0"}, (*params)[1].Value)
})
t.Run("Append multiple values at once", func(t *testing.T) { t.Run("Append multiple values at once", func(t *testing.T) {
params := &Params{ params := &Params{
{Key: "colors", Value: []string{"red"}}, {Key: "colors", Value: []string{"red"}},
@@ -105,6 +138,60 @@ func TestParams_Append(t *testing.T) {
assert.Len(t, *params, 1) assert.Len(t, *params, 1)
assert.Equal(t, []string{""}, (*params)[0].Value) assert.Equal(t, []string{""}, (*params)[0].Value)
}) })
t.Run("Append no parameters", func(t *testing.T) {
params := &Params{
{Key: "existing", Value: []string{"value"}},
}
params.Append()
assert.Len(t, *params, 1)
assert.Equal(t, "existing", (*params)[0].Key)
assert.Equal(t, []string{"value"}, (*params)[0].Value)
})
t.Run("Append mixed new and existing keys", func(t *testing.T) {
params := &Params{
{Key: "filter", Value: []string{"active"}},
{Key: "sort", Value: []string{"name"}},
}
params.Append(
Param{Key: "filter", Value: []string{"verified"}},
Param{Key: "limit", Value: []string{"10"}},
Param{Key: "sort", Value: []string{"date"}},
Param{Key: "offset", Value: []string{"0"}},
)
assert.Len(t, *params, 4)
filterValue := params.GetValue("filter")
require.NotNil(t, filterValue)
assert.Equal(t, []string{"active", "verified"}, *filterValue)
sortValue := params.GetValue("sort")
require.NotNil(t, sortValue)
assert.Equal(t, []string{"name", "date"}, *sortValue)
limitValue := params.GetValue("limit")
require.NotNil(t, limitValue)
assert.Equal(t, []string{"10"}, *limitValue)
offsetValue := params.GetValue("offset")
require.NotNil(t, offsetValue)
assert.Equal(t, []string{"0"}, *offsetValue)
})
t.Run("Append parameters with multiple values", func(t *testing.T) {
params := &Params{}
params.Append(
Param{Key: "ids", Value: []string{"1", "2", "3"}},
Param{Key: "types", Value: []string{"A", "B"}},
)
assert.Len(t, *params, 2)
assert.Equal(t, []string{"1", "2", "3"}, (*params)[0].Value)
assert.Equal(t, []string{"A", "B"}, (*params)[1].Value)
})
} }
func TestParams_Parse(t *testing.T) { func TestParams_Parse(t *testing.T) {

View File

@@ -13,8 +13,8 @@ func (proxy Proxy) String() string {
type Proxies []Proxy type Proxies []Proxy
func (proxies *Proxies) Append(proxy Proxy) { func (proxies *Proxies) Append(proxy ...Proxy) {
*proxies = append(*proxies, proxy) *proxies = append(*proxies, proxy...)
} }
func (proxies *Proxies) Parse(rawValue string) error { func (proxies *Proxies) Parse(rawValue string) error {

View File

@@ -63,7 +63,7 @@ func TestProxies_Append(t *testing.T) {
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String()) assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
}) })
t.Run("Append multiple proxies", func(t *testing.T) { t.Run("Append multiple proxies sequentially", func(t *testing.T) {
proxies := &Proxies{} proxies := &Proxies{}
url1, err := url.Parse("http://proxy1.example.com:8080") url1, err := url.Parse("http://proxy1.example.com:8080")
@@ -83,6 +83,24 @@ func TestProxies_Append(t *testing.T) {
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String()) assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
}) })
t.Run("Append multiple proxies in single call", func(t *testing.T) {
proxies := &Proxies{}
url1, err := url.Parse("http://proxy1.example.com:8080")
require.NoError(t, err)
url2, err := url.Parse("http://proxy2.example.com:8081")
require.NoError(t, err)
url3, err := url.Parse("https://proxy3.example.com:443")
require.NoError(t, err)
proxies.Append(Proxy(*url1), Proxy(*url2), Proxy(*url3))
assert.Len(t, *proxies, 3)
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
assert.Equal(t, "http://proxy2.example.com:8081", (*proxies)[1].String())
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
})
t.Run("Append to existing proxies", func(t *testing.T) { t.Run("Append to existing proxies", func(t *testing.T) {
existingURL, err := url.Parse("http://existing.example.com:8080") existingURL, err := url.Parse("http://existing.example.com:8080")
require.NoError(t, err) require.NoError(t, err)
@@ -98,6 +116,77 @@ func TestProxies_Append(t *testing.T) {
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String()) assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
assert.Equal(t, "http://new.example.com:8081", (*proxies)[1].String()) assert.Equal(t, "http://new.example.com:8081", (*proxies)[1].String())
}) })
t.Run("Append multiple to existing proxies", func(t *testing.T) {
existingURL, err := url.Parse("http://existing.example.com:8080")
require.NoError(t, err)
proxies := &Proxies{Proxy(*existingURL)}
url1, err := url.Parse("http://new1.example.com:8081")
require.NoError(t, err)
url2, err := url.Parse("http://new2.example.com:8082")
require.NoError(t, err)
url3, err := url.Parse("https://new3.example.com:443")
require.NoError(t, err)
proxies.Append(Proxy(*url1), Proxy(*url2), Proxy(*url3))
assert.Len(t, *proxies, 4)
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
assert.Equal(t, "http://new1.example.com:8081", (*proxies)[1].String())
assert.Equal(t, "http://new2.example.com:8082", (*proxies)[2].String())
assert.Equal(t, "https://new3.example.com:443", (*proxies)[3].String())
})
t.Run("Append no proxies", func(t *testing.T) {
existingURL, err := url.Parse("http://existing.example.com:8080")
require.NoError(t, err)
proxies := &Proxies{Proxy(*existingURL)}
proxies.Append()
assert.Len(t, *proxies, 1)
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
})
t.Run("Append mixed proxies", func(t *testing.T) {
proxies := &Proxies{}
url1, err := url.Parse("http://proxy1.example.com:8080")
require.NoError(t, err)
url2, err := url.Parse("socks5://proxy2.example.com:1080")
require.NoError(t, err)
url3, err := url.Parse("https://proxy3.example.com:443")
require.NoError(t, err)
url4, err := url.Parse("http://proxy4.example.com:3128")
require.NoError(t, err)
proxies.Append(Proxy(*url1), Proxy(*url2))
proxies.Append(Proxy(*url3))
proxies.Append(Proxy(*url4))
assert.Len(t, *proxies, 4)
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
assert.Equal(t, "socks5://proxy2.example.com:1080", (*proxies)[1].String())
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
assert.Equal(t, "http://proxy4.example.com:3128", (*proxies)[3].String())
})
t.Run("Append proxies with authentication", func(t *testing.T) {
proxies := &Proxies{}
url1, err := url.Parse("http://user1:pass1@proxy1.example.com:8080")
require.NoError(t, err)
url2, err := url.Parse("https://user2:pass2@proxy2.example.com:443")
require.NoError(t, err)
proxies.Append(Proxy(*url1), Proxy(*url2))
assert.Len(t, *proxies, 2)
assert.Equal(t, "http://user1:pass1@proxy1.example.com:8080", (*proxies)[0].String())
assert.Equal(t, "https://user2:pass2@proxy2.example.com:443", (*proxies)[1].String())
})
} }
func TestProxies_Parse(t *testing.T) { func TestProxies_Parse(t *testing.T) {

111
pkg/utils/parse.go Normal file
View File

@@ -0,0 +1,111 @@
package utils
import (
"fmt"
"net/url"
"strconv"
"time"
)
// ParseString attempts to parse the input string `s` into a value of the specified type T.
// If parsing the string `s` fails for a supported type, it returns the zero value of T
// and the parsing error.
// /nolint:forcetypeassert,wrapcheck
func ParseString[
T string | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float64 | bool | time.Duration | url.URL,
](rawValue string) (T, error) {
var value T
switch any(value).(type) {
case int:
i, err := strconv.Atoi(rawValue)
if err != nil {
return value, err
}
value = any(i).(T)
case int8:
i, err := strconv.ParseInt(rawValue, 10, 8)
if err != nil {
return value, err
}
value = any(int8(i)).(T)
case int16:
i, err := strconv.ParseInt(rawValue, 10, 16)
if err != nil {
return value, err
}
value = any(int16(i)).(T)
case int32:
i, err := strconv.ParseInt(rawValue, 10, 32)
if err != nil {
return value, err
}
value = any(int32(i)).(T)
case int64:
i, err := strconv.ParseInt(rawValue, 10, 64)
if err != nil {
return value, err
}
value = any(i).(T)
case uint:
u, err := strconv.ParseUint(rawValue, 10, 0)
if err != nil {
return value, err
}
value = any(uint(u)).(T)
case uint8:
u, err := strconv.ParseUint(rawValue, 10, 8)
if err != nil {
return value, err
}
value = any(uint8(u)).(T)
case uint16:
u, err := strconv.ParseUint(rawValue, 10, 16)
if err != nil {
return value, err
}
value = any(uint16(u)).(T)
case uint32:
u, err := strconv.ParseUint(rawValue, 10, 32)
if err != nil {
return value, err
}
value = any(uint32(u)).(T)
case uint64:
u, err := strconv.ParseUint(rawValue, 10, 64)
if err != nil {
return value, err
}
value = any(u).(T)
case float64:
f, err := strconv.ParseFloat(rawValue, 64)
if err != nil {
return value, err
}
value = any(f).(T)
case bool:
b, err := strconv.ParseBool(rawValue)
if err != nil {
return value, err
}
value = any(b).(T)
case string:
value = any(rawValue).(T)
case time.Duration:
d, err := time.ParseDuration(rawValue)
if err != nil {
return value, err
}
value = any(d).(T)
case url.URL:
u, err := url.Parse(rawValue)
if err != nil {
return value, err
}
value = any(*u).(T)
default:
return value, fmt.Errorf("unsupported type: %T", value)
}
return value, nil
}

528
pkg/utils/parse_test.go Normal file
View File

@@ -0,0 +1,528 @@
package utils
import (
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseString(t *testing.T) {
t.Run("ParseString to string", func(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"empty string", "", ""},
{"simple string", "hello", "hello"},
{"string with spaces", "hello world", "hello world"},
{"numeric string", "123", "123"},
{"special characters", "!@#$%^&*()", "!@#$%^&*()"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[string](test.input)
require.NoError(t, err)
assert.Equal(t, test.expected, result)
})
}
})
t.Run("ParseString to int", func(t *testing.T) {
tests := []struct {
name string
input string
expected int
expectError bool
}{
{"positive int", "42", 42, false},
{"negative int", "-42", -42, false},
{"zero", "0", 0, false},
{"invalid int", "abc", 0, true},
{"float string", "3.14", 0, true},
{"empty string", "", 0, true},
{"overflow string", "99999999999999999999", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int8", func(t *testing.T) {
tests := []struct {
name string
input string
expected int8
expectError bool
}{
{"valid int8", "127", 127, false},
{"min int8", "-128", -128, false},
{"overflow int8", "128", 0, true},
{"underflow int8", "-129", 0, true},
{"invalid", "abc", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int8](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int16", func(t *testing.T) {
tests := []struct {
name string
input string
expected int16
expectError bool
}{
{"valid int16", "32767", 32767, false},
{"min int16", "-32768", -32768, false},
{"overflow int16", "32768", 0, true},
{"underflow int16", "-32769", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int16](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int32", func(t *testing.T) {
tests := []struct {
name string
input string
expected int32
expectError bool
}{
{"valid int32", "2147483647", 2147483647, false},
{"min int32", "-2147483648", -2147483648, false},
{"overflow int32", "2147483648", 0, true},
{"underflow int32", "-2147483649", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int32](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to int64", func(t *testing.T) {
tests := []struct {
name string
input string
expected int64
expectError bool
}{
{"valid int64", "9223372036854775807", 9223372036854775807, false},
{"min int64", "-9223372036854775808", -9223372036854775808, false},
{"large number", "123456789012345", 123456789012345, false},
{"invalid", "not a number", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[int64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint
expectError bool
}{
{"valid uint", "42", 42, false},
{"zero", "0", 0, false},
{"large uint", "4294967295", 4294967295, false},
{"negative", "-1", 0, true},
{"invalid", "abc", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint8", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint8
expectError bool
}{
{"valid uint8", "255", 255, false},
{"min uint8", "0", 0, false},
{"overflow uint8", "256", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint8](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint16", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint16
expectError bool
}{
{"valid uint16", "65535", 65535, false},
{"min uint16", "0", 0, false},
{"overflow uint16", "65536", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint16](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint32", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint32
expectError bool
}{
{"valid uint32", "4294967295", 4294967295, false},
{"min uint32", "0", 0, false},
{"overflow uint32", "4294967296", 0, true},
{"negative", "-1", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint32](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to uint64", func(t *testing.T) {
tests := []struct {
name string
input string
expected uint64
expectError bool
}{
{"valid uint64", "18446744073709551615", 18446744073709551615, false},
{"min uint64", "0", 0, false},
{"large number", "123456789012345", 123456789012345, false},
{"negative", "-1", 0, true},
{"invalid", "not a number", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[uint64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to float64", func(t *testing.T) {
tests := []struct {
name string
input string
expected float64
expectError bool
}{
{"integer", "42", 42.0, false},
{"decimal", "3.14159", 3.14159, false},
{"negative", "-2.5", -2.5, false},
{"scientific notation", "1.23e10", 1.23e10, false},
{"zero", "0", 0.0, false},
{"invalid", "not a number", 0, true},
{"empty", "", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[float64](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.InDelta(t, test.expected, result, 0.0001)
}
})
}
})
t.Run("ParseString to bool", func(t *testing.T) {
tests := []struct {
name string
input string
expected bool
expectError bool
}{
{"true lowercase", "true", true, false},
{"True mixed case", "True", true, false},
{"TRUE uppercase", "TRUE", true, false},
{"1 as true", "1", true, false},
{"false lowercase", "false", false, false},
{"False mixed case", "False", false, false},
{"FALSE uppercase", "FALSE", false, false},
{"0 as false", "0", false, false},
{"invalid", "yes", false, true},
{"empty", "", false, true},
{"numeric non-binary", "2", false, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[bool](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to time.Duration", func(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
expectError bool
}{
{"seconds", "10s", 10 * time.Second, false},
{"minutes", "5m", 5 * time.Minute, false},
{"hours", "2h", 2 * time.Hour, false},
{"combined", "1h30m45s", time.Hour + 30*time.Minute + 45*time.Second, false},
{"milliseconds", "500ms", 500 * time.Millisecond, false},
{"microseconds", "100us", 100 * time.Microsecond, false},
{"nanoseconds", "50ns", 50 * time.Nanosecond, false},
{"negative", "-5s", -5 * time.Second, false},
{"invalid", "5x", 0, true},
{"empty", "", 0, true},
{"no unit", "100", 0, true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[time.Duration](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, test.expected, result)
}
})
}
})
t.Run("ParseString to url.URL", func(t *testing.T) {
tests := []struct {
name string
input string
checkFunc func(t *testing.T, u url.URL)
expectError bool
}{
{
name: "http URL",
input: "http://example.com",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "http", u.Scheme)
assert.Equal(t, "example.com", u.Host)
},
expectError: false,
},
{
name: "https URL with path",
input: "https://example.com/path/to/resource",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/path/to/resource", u.Path)
},
expectError: false,
},
{
name: "URL with query parameters",
input: "https://example.com/search?q=test&page=1",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/search", u.Path)
assert.Equal(t, "q=test&page=1", u.RawQuery)
},
expectError: false,
},
{
name: "URL with port",
input: "http://localhost:8080/api",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "http", u.Scheme)
assert.Equal(t, "localhost:8080", u.Host)
assert.Equal(t, "/api", u.Path)
},
expectError: false,
},
{
name: "URL with fragment",
input: "https://example.com/page#section",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "example.com", u.Host)
assert.Equal(t, "/page", u.Path)
assert.Equal(t, "section", u.Fragment)
},
expectError: false,
},
{
name: "relative path",
input: "/path/to/resource",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Empty(t, u.Scheme)
assert.Empty(t, u.Host)
assert.Equal(t, "/path/to/resource", u.Path)
},
expectError: false,
},
{
name: "empty string",
input: "",
checkFunc: func(t *testing.T, u url.URL) {
t.Helper()
assert.Empty(t, u.String())
},
expectError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ParseString[url.URL](test.input)
if test.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
if test.checkFunc != nil {
test.checkFunc(t, result)
}
}
})
}
})
t.Run("Edge cases", func(t *testing.T) {
t.Run("whitespace handling for numeric types", func(t *testing.T) {
result, err := ParseString[int](" 42 ")
require.Error(t, err)
assert.Equal(t, 0, result)
})
t.Run("leading zeros for int", func(t *testing.T) {
result, err := ParseString[int]("007")
require.NoError(t, err)
assert.Equal(t, 7, result)
})
t.Run("plus sign for positive numbers", func(t *testing.T) {
result, err := ParseString[int]("+42")
require.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("case sensitivity for bool", func(t *testing.T) {
testCases := []string{"t", "T", "f", "F"}
for _, tc := range testCases {
result, err := ParseString[bool](tc)
require.NoError(t, err)
if tc == "t" || tc == "T" {
assert.True(t, result)
} else {
assert.False(t, result)
}
}
})
})
}