Add config file support to CLI parser

Add -f/--config-file flag for loading YAML configs from local or remote sources. Fix error handling to return unmatched errors.
This commit is contained in:
2025-08-28 23:57:00 +04:00
parent 42335c1178
commit 29b85d5b83
9 changed files with 138 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
package config
import (
"errors"
"flag"
"fmt"
"net/url"
@@ -52,14 +53,19 @@ Flags:
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
type ConfigCLIParser struct {
args []string
args []string
configFile *types.ConfigFile
}
func NewConfigCLIParser(args []string) *ConfigCLIParser {
if args == nil {
args = []string{}
}
return &ConfigCLIParser{args}
return &ConfigCLIParser{args: args}
}
func (parser ConfigCLIParser) GetConfigFile() *types.ConfigFile {
return parser.configFile
}
type stringSliceArg []string
@@ -85,6 +91,7 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
var (
config = &Config{}
configFile string
yes bool
skipVerify bool
method string
@@ -101,6 +108,9 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
)
{
flagSet.StringVar(&configFile, "config-file", "", "Config file")
flagSet.StringVar(&configFile, "f", "", "Config file")
flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions")
flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions")
@@ -160,6 +170,29 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
// Iterate over flags that were explicitly set on the command line.
flagSet.Visit(func(flagVar *flag.Flag) {
switch flagVar.Name {
case "config-file", "f":
var err error
parser.configFile, err = types.ParseConfigFile(configFile)
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", errors.New("file extension not found")))
return nil
}),
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", fmt.Errorf("parse error: %w", err)))
return nil
}),
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
"config-file",
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML.String()),
),
)
return nil
}),
)
case "yes", "y":
config.Yes = utils.ToPtr(yes)
case "skip-verify":

View File

@@ -20,6 +20,7 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser)
assert.Equal(t, args, parser.args)
assert.Nil(t, parser.configFile)
})
t.Run("NewConfigCLIParser with nil args", func(t *testing.T) {
@@ -27,6 +28,7 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser)
assert.Equal(t, []string{}, parser.args)
assert.Nil(t, parser.configFile)
})
t.Run("NewConfigCLIParser with empty args", func(t *testing.T) {
@@ -35,6 +37,21 @@ func TestNewConfigCLIParser(t *testing.T) {
require.NotNil(t, parser)
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())
})
}
@@ -295,6 +312,58 @@ func TestConfigCLIParser_Parse(t *testing.T) {
assert.Equal(t, 10*time.Second, *config.Timeout)
})
t.Run("Parse with config-file flag valid YAML", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config.yaml"})
config, err := parser.Parse()
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile())
})
t.Run("Parse with config-file flag using long form", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "--config-file", "/path/to/config.yml"})
config, err := parser.Parse()
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile())
})
t.Run("Parse with config-file flag invalid extension", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config"})
config, err := parser.Parse()
assert.Nil(t, config)
var fieldErr types.FieldParseErrors
require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "config-file", fieldErr.Errors[0].Field)
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file extension not found")
})
t.Run("Parse with config-file flag unsupported file type", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config.json"})
config, err := parser.Parse()
assert.Nil(t, config)
var fieldErr types.FieldParseErrors
require.ErrorAs(t, err, &fieldErr)
assert.Len(t, fieldErr.Errors, 1)
assert.Equal(t, "config-file", fieldErr.Errors[0].Field)
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file type")
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported")
})
t.Run("Parse with config-file flag remote URL", func(t *testing.T) {
parser := NewConfigCLIParser([]string{"dodo", "-f", "https://example.com/config.yaml"})
config, err := parser.Parse()
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, parser.GetConfigFile())
})
t.Run("Parse with all flags combined", func(t *testing.T) {
parser := NewConfigCLIParser([]string{
"dodo",
@@ -311,6 +380,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
"-c", "session=token123",
"-b", `{"data": "test"}`,
"-x", "http://proxy.example.com:3128",
"-f", "/path/to/config.yaml",
})
config, err := parser.Parse()
@@ -341,6 +411,8 @@ func TestConfigCLIParser_Parse(t *testing.T) {
assert.Len(t, config.Proxies, 1)
assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String())
assert.NotNil(t, parser.GetConfigFile())
})
t.Run("Parse with multiple field parse errors", func(t *testing.T) {
@@ -404,6 +476,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) {
assert.Contains(t, output, "-x, -proxy")
assert.Contains(t, output, "-skip-verify")
assert.Contains(t, output, "-y, -yes")
assert.Contains(t, output, "-f, -config-file")
// Verify default values are included
assert.Contains(t, output, Defaults.Method)