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  }