feat: Improve error handling with custom error types and detailed messages

This commit is contained in:
2024-11-11 15:01:20 +02:00
parent 6346c9a829
commit 2bb8589eb7
3 changed files with 148 additions and 22 deletions

View File

@@ -39,13 +39,21 @@ func (c *Config) String() string {
var CONFIG Config var CONFIG Config
// parsePositiveInt parses and validates positive integer values // parsePositiveInt parses and validates positive integer values
func parsePositiveInt(value string) (int, error) { func parsePositiveInt(param, value string) (int, error) {
val, err := strconv.Atoi(value) val, err := strconv.Atoi(value)
if err != nil { if err != nil {
return 0, err return 0, &ValidationError{
Param: param,
Value: value,
Reason: "must be a valid integer",
}
} }
if val <= 0 { if val <= 0 {
return 0, fmt.Errorf("value must be positive") return 0, &ValidationError{
Param: param,
Value: value,
Reason: "must be positive",
}
} }
return val, nil return val, nil
} }
@@ -59,46 +67,53 @@ func GetConfig() *Config {
EnvLogLevel: func(v string) error { EnvLogLevel: func(v string) error {
level, err := zerolog.ParseLevel(v) level, err := zerolog.ParseLevel(v)
if err != nil { if err != nil {
return fmt.Errorf("invalid log level: %w", err) return &ValidationError{
Param: EnvLogLevel,
Value: v,
Reason: "must be one of: debug, info, warn, error",
}
} }
config.LogLevel = level config.LogLevel = level
return nil return nil
}, },
EnvRootPath: func(v string) error { EnvRootPath: func(v string) error {
if _, err := os.Stat(v); err != nil { if _, err := os.Stat(v); err != nil {
return fmt.Errorf("invalid root path: %w", err) return &ConfigError{
Param: EnvRootPath,
Err: err,
}
} }
config.RootPath = v config.RootPath = v
return nil return nil
}, },
EnvNumWorkers: func(v string) error { EnvNumWorkers: func(v string) error {
val, err := parsePositiveInt(v) val, err := parsePositiveInt(EnvNumWorkers, v)
if err != nil { if err != nil {
return fmt.Errorf("invalid number of workers: %w", err) return err
} }
config.NumOfWorkers = val config.NumOfWorkers = val
return nil return nil
}, },
EnvWorkerBatchSize: func(v string) error { EnvWorkerBatchSize: func(v string) error {
val, err := parsePositiveInt(v) val, err := parsePositiveInt(EnvWorkerBatchSize, v)
if err != nil { if err != nil {
return fmt.Errorf("invalid worker batch size: %w", err) return err
} }
config.WorkerBatchSize = val config.WorkerBatchSize = val
return nil return nil
}, },
EnvMaxResponseSize: func(v string) error { EnvMaxResponseSize: func(v string) error {
val, err := parsePositiveInt(v) val, err := parsePositiveInt(EnvMaxResponseSize, v)
if err != nil { if err != nil {
return fmt.Errorf("invalid max response size: %w", err) return err
} }
config.MaxResponseSize = val config.MaxResponseSize = val
return nil return nil
}, },
EnvResponseTimeout: func(v string) error { EnvResponseTimeout: func(v string) error {
val, err := parsePositiveInt(v) val, err := parsePositiveInt(EnvResponseTimeout, v)
if err != nil { if err != nil {
return fmt.Errorf("invalid response timeout: %w", err) return err
} }
config.ResponseTimeout = val config.ResponseTimeout = val
return nil return nil

View File

@@ -38,22 +38,56 @@ func TestGetConfig(t *testing.T) {
func TestParsePositiveInt(t *testing.T) { func TestParsePositiveInt(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string param string
want int input string
wantErr bool want int
wantErr bool
errType interface{}
errMessage string
}{ }{
{"valid positive", "42", 42, false}, {
{"zero", "0", 0, true}, name: "valid positive",
{"negative", "-1", 0, true}, param: "TEST_PARAM",
{"invalid", "abc", 0, true}, input: "42",
want: 42,
wantErr: false,
},
{
name: "zero",
param: "TEST_PARAM",
input: "0",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value '0' for TEST_PARAM: must be positive",
},
{
name: "negative",
param: "TEST_PARAM",
input: "-1",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value '-1' for TEST_PARAM: must be positive",
},
{
name: "invalid",
param: "TEST_PARAM",
input: "abc",
wantErr: true,
errType: &ValidationError{},
errMessage: "invalid value 'abc' for TEST_PARAM: must be a valid integer",
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := parsePositiveInt(tt.input) got, err := parsePositiveInt(tt.param, tt.input)
if tt.wantErr { if tt.wantErr {
assert.Error(t, err) assert.Error(t, err)
assert.IsType(t, tt.errType, err)
if tt.errMessage != "" {
assert.Equal(t, tt.errMessage, err.Error())
}
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.want, got) assert.Equal(t, tt.want, got)
@@ -61,3 +95,52 @@ func TestParsePositiveInt(t *testing.T) {
}) })
} }
} }
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
envVars map[string]string
wantErr bool
errMessage string
}{
{
name: "invalid log level",
envVars: map[string]string{
EnvLogLevel: "invalid",
},
wantErr: true,
errMessage: "invalid value 'invalid' for LOG_LEVEL: must be one of: debug, info, warn, error",
},
{
name: "invalid worker count",
envVars: map[string]string{
EnvLogLevel: "debug",
EnvRootPath: ".",
EnvNumWorkers: "-1",
},
wantErr: true,
errMessage: "invalid value '-1' for NUM_OF_WORKERS: must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Clear environment
os.Clearenv()
// Set required environment variables
for k, v := range tt.envVars {
os.Setenv(k, v)
}
// Defer cleanup
defer os.Clearenv()
if tt.wantErr {
assert.PanicsWithError(t, tt.errMessage, func() {
GetConfig()
})
}
})
}
}

28
config/errors.go Normal file
View File

@@ -0,0 +1,28 @@
package config
import "fmt"
// ConfigError represents a configuration error
type ConfigError struct {
Param string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("configuration error for %s: %v", e.Param, e.Err)
}
func (e *ConfigError) Unwrap() error {
return e.Err
}
// ValidationError represents a validation error
type ValidationError struct {
Param string
Value string
Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("invalid value '%s' for %s: %s", e.Value, e.Param, e.Reason)
}