goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/config/entry.go (about) 1 package config 2 3 import ( 4 "os" 5 "reflect" 6 "strconv" 7 "strings" 8 9 "github.com/samber/lo" 10 "goyave.dev/goyave/v5/util/errors" 11 ) 12 13 // Entry is the internal reprensentation of a config entry. 14 // It contains the entry value, its expected type (for validation) 15 // and a slice of authorized values (for validation too). If this slice 16 // is empty, it means any value can be used, provided it is of the correct type. 17 type Entry struct { 18 Value any 19 AuthorizedValues []any // Leave empty for "any" 20 Type reflect.Kind 21 IsSlice bool 22 } 23 24 func makeEntryFromValue(value any) *Entry { 25 isSlice := false 26 t := reflect.TypeOf(value) 27 kind := t.Kind() 28 if kind == reflect.Slice { 29 kind = t.Elem().Kind() 30 isSlice = true 31 } 32 return &Entry{value, []any{}, kind, isSlice} 33 } 34 35 func (e *Entry) validate(key string) error { 36 if e.Value == nil { // nil values means unset 37 return nil 38 } 39 40 if err := e.tryEnvVarConversion(key); err != nil { 41 return err 42 } 43 t := reflect.TypeOf(e.Value) 44 kind := t.Kind() 45 if e.IsSlice && kind == reflect.Slice { 46 kind = t.Elem().Kind() 47 } 48 if kind != e.Type && !e.tryConversion(kind) { 49 var message string 50 if e.IsSlice { 51 message = "%q must be a slice of %s" 52 } else { 53 message = "%q type must be %s" 54 } 55 56 return errors.Errorf(message, key, e.Type) 57 } 58 59 if len(e.AuthorizedValues) > 0 { 60 if e.IsSlice { 61 // Accepted values for slices define the values that can be used inside the slice 62 // It doesn't represent the value of the slice itself (content and order) 63 list := reflect.ValueOf(e.Value) 64 length := list.Len() 65 for i := 0; i < length; i++ { 66 if !lo.Contains(e.AuthorizedValues, list.Index(i).Interface()) { 67 return errors.Errorf("%q elements must have one of the following values: %v", key, e.AuthorizedValues) 68 } 69 } 70 } else if !lo.Contains(e.AuthorizedValues, e.Value) { 71 return errors.Errorf("%q must have one of the following values: %v", key, e.AuthorizedValues) 72 } 73 } 74 75 return nil 76 } 77 78 func (e *Entry) tryConversion(kind reflect.Kind) bool { 79 if !e.IsSlice && kind == reflect.Float64 && e.Type == reflect.Int { 80 intVal, ok := convertInt(e.Value.(float64)) 81 if ok { 82 e.Value = intVal 83 return true 84 } 85 } else if e.IsSlice && kind == reflect.Interface { 86 original := e.Value.([]any) 87 var newValue any 88 var ok bool 89 switch e.Type { 90 case reflect.Int: 91 newValue, ok = convertIntSlice(original) 92 case reflect.Float64: 93 newValue, ok = convertSlice[float64](original) 94 case reflect.String: 95 newValue, ok = convertSlice[string](original) 96 case reflect.Bool: 97 newValue, ok = convertSlice[bool](original) 98 } 99 if ok { 100 e.Value = newValue 101 return true 102 } 103 } 104 105 return false 106 } 107 108 func convertSlice[T any](slice []any) ([]T, bool) { 109 result := make([]T, len(slice)) 110 for k, v := range slice { 111 value, ok := v.(T) 112 if !ok { 113 return nil, false 114 } 115 result[k] = value 116 } 117 return result, true 118 } 119 120 func convertInt(value any) (int, bool) { 121 switch val := value.(type) { 122 case int: 123 return val, true 124 case float64: 125 intVal := int(val) 126 if val == float64(intVal) { 127 return intVal, true 128 } 129 } 130 return 0, false 131 } 132 133 func convertIntSlice(original []any) ([]int, bool) { 134 slice := make([]int, len(original)) 135 for k, v := range original { 136 intVal, ok := convertInt(v) 137 if !ok { 138 return nil, false 139 } 140 slice[k] = intVal 141 } 142 return slice, true 143 } 144 145 func (e *Entry) tryEnvVarConversion(key string) error { 146 str, ok := e.Value.(string) 147 if ok { 148 val, err := e.convertEnvVar(str, key) 149 if err == nil && val != nil { 150 151 if e.IsSlice { 152 return errors.Errorf("%q is a slice entry, it cannot be loaded from env", key) 153 } 154 155 e.Value = val 156 } 157 return err 158 } 159 160 return nil 161 } 162 163 func (e *Entry) convertEnvVar(str, key string) (any, error) { 164 if strings.HasPrefix(str, "${") && strings.HasSuffix(str, "}") { 165 varName := str[2 : len(str)-1] 166 value, set := os.LookupEnv(varName) 167 if !set { 168 return nil, errors.Errorf("%q: %q environment variable is not set", key, varName) 169 } 170 171 switch e.Type { 172 case reflect.Int: 173 if i, err := strconv.Atoi(value); err == nil { 174 return i, nil 175 } 176 return nil, errors.Errorf("%q could not be converted to int from environment variable %q of value %q", key, varName, value) 177 case reflect.Float64: 178 if f, err := strconv.ParseFloat(value, 64); err == nil { 179 return f, nil 180 } 181 return nil, errors.Errorf("%q could not be converted to float64 from environment variable %q of value %q", key, varName, value) 182 case reflect.Bool: 183 if b, err := strconv.ParseBool(value); err == nil { 184 return b, nil 185 } 186 return nil, errors.Errorf("%q could not be converted to bool from environment variable %q of value %q", key, varName, value) 187 default: 188 // Keep value as string if type is not supported and let validation do its job 189 return value, nil 190 } 191 } 192 193 return nil, nil 194 }