Skip to content

Commit

Permalink
Adds configutils package to validate and assign config
Browse files Browse the repository at this point in the history
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
sm43 authored and chmouel committed Apr 3, 2024
1 parent 3274759 commit d9750b2
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 502 deletions.
98 changes: 98 additions & 0 deletions pkg/configutil/config.go
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
}
111 changes: 111 additions & 0 deletions pkg/configutil/config_test.go
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)
}
})
}
}
Loading

0 comments on commit d9750b2

Please sign in to comment.