-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds configutils package to validate and assign config
finally we add a package to replace code in settings for validation and defaults with a generic func, using json tag for defaulting. and for figuring out which field it is from configmap.
- Loading branch information
Showing
8 changed files
with
383 additions
and
502 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package configutil | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strconv" | ||
"strings" | ||
|
||
"go.uber.org/zap" | ||
) | ||
|
||
func ValidateAndAssignValues(logger *zap.SugaredLogger, configData map[string]string, configStruct interface{}, customValidations map[string]func(string) error) error { | ||
structValue := reflect.ValueOf(configStruct).Elem() | ||
structType := reflect.TypeOf(configStruct).Elem() | ||
|
||
var errors []error | ||
|
||
for i := 0; i < structType.NumField(); i++ { | ||
field := structType.Field(i) | ||
fieldName := field.Name | ||
|
||
jsonTag := field.Tag.Get("json") | ||
// Skip field which doesn't have json tag | ||
if jsonTag == "" { | ||
continue | ||
} | ||
|
||
// Read value from ConfigMap | ||
fieldValue := configData[strings.ToLower(jsonTag)] | ||
|
||
// If value is missing in ConfigMap, use default value from struct tag | ||
if fieldValue == "" { | ||
fieldValue = field.Tag.Get("default") | ||
if fieldValue == "" { | ||
// Skip field if default value is not provided | ||
continue | ||
} | ||
} | ||
|
||
fieldValueKind := field.Type.Kind() | ||
|
||
//nolint | ||
switch fieldValueKind { | ||
case reflect.String: | ||
if validator, ok := customValidations[fieldName]; ok { | ||
if err := validator(fieldValue); err != nil { | ||
errors = append(errors, fmt.Errorf("custom validation failed for field %s: %w", fieldName, err)) | ||
continue | ||
} | ||
} | ||
oldValue := structValue.FieldByName(fieldName).String() | ||
if oldValue != fieldValue { | ||
logger.Infof("updating value for field %s: from '%s' to '%s'", fieldName, oldValue, fieldValue) | ||
} | ||
structValue.FieldByName(fieldName).SetString(fieldValue) | ||
|
||
case reflect.Bool: | ||
boolValue, err := strconv.ParseBool(fieldValue) | ||
if err != nil { | ||
errors = append(errors, fmt.Errorf("invalid value for bool field %s: %w", fieldName, err)) | ||
continue | ||
} | ||
oldValue := structValue.FieldByName(fieldName).Bool() | ||
if oldValue != boolValue { | ||
logger.Infof("updating value for field %s: from '%v' to '%v'", fieldName, oldValue, boolValue) | ||
} | ||
structValue.FieldByName(fieldName).SetBool(boolValue) | ||
|
||
case reflect.Int: | ||
if validator, ok := customValidations[fieldName]; ok { | ||
if err := validator(fieldValue); err != nil { | ||
errors = append(errors, fmt.Errorf("custom validation failed for field %s: %w", fieldName, err)) | ||
continue | ||
} | ||
} | ||
intValue, err := strconv.ParseInt(fieldValue, 10, 64) | ||
if err != nil { | ||
errors = append(errors, fmt.Errorf("invalid value for int field %s: %w", fieldName, err)) | ||
continue | ||
} | ||
oldValue := structValue.FieldByName(fieldName).Int() | ||
if oldValue != intValue { | ||
logger.Infof("updating value for field %s: from '%d' to '%d'", fieldName, oldValue, intValue) | ||
} | ||
structValue.FieldByName(fieldName).SetInt(intValue) | ||
|
||
default: | ||
// Skip unsupported field types | ||
continue | ||
} | ||
} | ||
|
||
if len(errors) > 0 { | ||
return fmt.Errorf("validation errors: %v", errors) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package configutil | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/openshift-pipelines/pipelines-as-code/pkg/test/logger" | ||
"gotest.tools/v3/assert" | ||
) | ||
|
||
type testStruct struct { | ||
ApplicationName string `default:"app-app" json:"application-name"` | ||
BoolField bool `default:"true" json:"bool-field"` | ||
IntField int `default:"43" json:"int-field"` | ||
WithoutDefault string `json:"without-default"` | ||
IgnoredField string | ||
} | ||
|
||
func TestValidateAndAssignValues(t *testing.T) { | ||
logger, _ := logger.GetLogger() | ||
|
||
testCases := []struct { | ||
name string | ||
configMap map[string]string | ||
expectedStruct testStruct | ||
customValidations map[string]func(string) error | ||
expectedError string | ||
}{ | ||
{ | ||
name: "With all default values", | ||
configMap: map[string]string{}, | ||
expectedStruct: testStruct{ | ||
ApplicationName: "app-app", | ||
BoolField: true, | ||
IntField: 43, | ||
WithoutDefault: "", | ||
}, | ||
customValidations: map[string]func(string) error{}, | ||
}, | ||
{ | ||
name: "override default values", | ||
configMap: map[string]string{ | ||
"application-name": "pac-pac", | ||
"bool-field": "false", | ||
"int-field": "101", | ||
"without-default": "random", | ||
}, | ||
expectedStruct: testStruct{ | ||
ApplicationName: "pac-pac", | ||
BoolField: false, | ||
IntField: 101, | ||
WithoutDefault: "random", | ||
}, | ||
customValidations: map[string]func(string) error{}, | ||
}, | ||
{ | ||
name: "custom validator for name to start with pac", | ||
configMap: map[string]string{ | ||
"application-name": "invalid-name", | ||
}, | ||
expectedStruct: testStruct{ | ||
ApplicationName: "throw-error", | ||
BoolField: false, | ||
IntField: 101, | ||
}, | ||
customValidations: map[string]func(string) error{ | ||
"ApplicationName": func(s string) error { | ||
if !strings.HasPrefix(s, "pac") { | ||
return fmt.Errorf("name should start with pac") | ||
} | ||
return nil | ||
}, | ||
}, | ||
expectedError: "custom validation failed for field ApplicationName: name should start with pac", | ||
}, | ||
{ | ||
name: "invalid value for bool field", | ||
configMap: map[string]string{ | ||
"bool-field": "invalid", | ||
}, | ||
expectedError: "invalid value for bool field BoolField: strconv.ParseBool: parsing \"invalid\": invalid syntax", | ||
}, | ||
{ | ||
name: "invalid value for int field", | ||
configMap: map[string]string{ | ||
"int-field": "abcd", | ||
}, | ||
expectedError: "invalid value for int field IntField: strconv.ParseInt: parsing \"abcd\": invalid syntax", | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
var test testStruct | ||
|
||
err := ValidateAndAssignValues(logger, tc.configMap, &test, tc.customValidations) | ||
|
||
if tc.expectedError != "" { | ||
assert.ErrorContains(t, err, tc.expectedError) | ||
return | ||
} | ||
assert.NilError(t, err) | ||
|
||
if !reflect.DeepEqual(test, tc.expectedStruct) { | ||
t.Errorf("failure, actual and expected struct:\nActual: %#v\nExpected: %#v", test, tc.expectedStruct) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.