package struct_utils import ( "github.com/samber/lo" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "reflect" "strings" ) // KeyValuePair defines a key/value pair derived from form data type KeyValuePair struct { Key string `json:"key"` Value string `json:"value"` } // FormToKeyValuePairs returns a string-based map of strings as derived from posted form keys and values. // e.g. oauth_consumer_key=mlhgs&oauth_consumer_secret=x240ar&oauth_verifier=b0qjbx&store_base_url=http%3A%2F%2Flocalhost.com%2Fstore func FormToKeyValuePairs(body string) []KeyValuePair { out := []KeyValuePair{} parts := strings.Split(body, "&") for _, p := range parts { split := strings.Split(p, "=") var key string if len(split) > 0 { key = split[0] } var value string if len(split) > 1 { value = split[1] } kv := KeyValuePair{ Key: key, Value: value, } out = append(out, kv) } return out } // GetValue returns the value for the given key from a KeyValuePair slice. func GetValue(key string, kv []KeyValuePair) string { for _, v := range kv { if v.Key == key { return v.Value } } return "" } // ValidateRequiredFields checks the required tag on struct fields and returns an error if any are set to zero. func ValidateRequiredFields(object any, parentFieldKeys ...string) error { objectValue := reflect.ValueOf(object) if objectValue.Kind() == reflect.Interface { objectValue = objectValue.Elem() } if objectValue.Kind() == reflect.Ptr { objectValue = objectValue.Elem() } if objectValue.Kind() != reflect.Struct { if objectValue.IsZero() { errorKey := strings.Join(parentFieldKeys, ".") return errors.Error(errorKey + " is required") } return nil } for i := 0; i < objectValue.NumField(); i++ { field := objectValue.Field(i) fieldType := objectValue.Type().Field(i) requiredTag := fieldType.Tag.Get("required") requiredOptions := strings.Split(requiredTag, ",") required := lo.Contains(requiredOptions, "true") notNil := lo.Contains(requiredOptions, "not-nil") deep := lo.Contains(requiredOptions, "deep") thisFieldKey := getRequiredKey(fieldType) err := validateFieldByKind(field, required, notNil, deep, thisFieldKey, parentFieldKeys...) if err != nil { return err } } return nil } func validateFieldByKind(field reflect.Value, required bool, notNil bool, deep bool, thisFieldKey string, parentFieldKeys ...string) error { switch field.Kind() { case reflect.Struct: if required && field.IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } if deep { parentFieldKeys = append(parentFieldKeys, thisFieldKey) err := ValidateRequiredFields(field.Interface(), parentFieldKeys...) if err != nil { return err } } case reflect.Slice: if (required || notNil) && field.IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } if required && field.Len() == 0 { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " may not be empty") } if deep { for j := 0; j < field.Len(); j++ { sliceField := field.Index(j) if sliceField.Kind() == reflect.Ptr { sliceField = sliceField.Elem() } if sliceField.Kind() == reflect.Struct { err := ValidateRequiredFields(sliceField.Interface(), append(parentFieldKeys, thisFieldKey)...) if err != nil { return err } } else { err := validateFieldByKind(sliceField, true, notNil, deep, thisFieldKey, parentFieldKeys...) if err != nil { return err } } } } case reflect.Map: if (required || notNil) && field.IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } if required && field.Len() == 0 { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " may not be empty") } if deep { for _, key := range field.MapKeys() { mapField := field.MapIndex(key) if mapField.Kind() == reflect.Ptr { mapField = mapField.Elem() } if mapField.Kind() == reflect.Struct { err := ValidateRequiredFields(mapField.Interface(), append(parentFieldKeys, thisFieldKey)...) if err != nil { return err } } else { err := validateFieldByKind(mapField, required, notNil, deep, thisFieldKey, parentFieldKeys...) if err != nil { return err } } } } case reflect.Ptr: if (required || notNil) && field.IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } if field.Elem().Kind() == reflect.Struct { if required && field.Elem().IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } if deep { parentFieldKeys = append(parentFieldKeys, thisFieldKey) err := ValidateRequiredFields(field.Interface(), parentFieldKeys...) if err != nil { return err } } } else if required { return validateFieldByKind(field.Elem(), required, notNil, deep, thisFieldKey, parentFieldKeys...) } default: if required && field.IsZero() { return errors.Error(getErrorKey(thisFieldKey, parentFieldKeys) + " is required") } } return nil } func getRequiredKey(fieldType reflect.StructField) string { if fieldType.Anonymous { return "" } jsonTag := fieldType.Tag.Get("json") if jsonTag != "" && jsonTag != "-" { commaIdx := strings.Index(jsonTag, ",") if commaIdx > 0 { // Tag is in the format "key,omitempty" return jsonTag[:commaIdx] } else { return jsonTag } } return fieldType.Name } func getErrorKey(thisKey string, parentKeys []string) string { parentKeys = append(parentKeys, thisKey) parentKeys = lo.Filter(parentKeys, func(key string, _ int) bool { return key != "" }) return strings.Join(parentKeys, ".") }