github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/config/struct_keys.go (about)

     1  package config
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  )
     7  
     8  const sep = "."
     9  
    10  // GetStructKeys returns all keys in a nested struct type, taking the name from the tag name or
    11  // the field name.  It handles an additional suffix squashValue like mapstructure does: if
    12  // present on an embedded struct, name components for that embedded struct should not be
    13  // included.  It does not handle maps, does chase pointers, but does not check for loops in
    14  // nesting.
    15  func GetStructKeys(typ reflect.Type, tag, squashValue string) []string {
    16  	return appendStructKeys(typ, tag, ","+squashValue, nil, nil)
    17  }
    18  
    19  // appendStructKeys recursively appends to keys all keys of nested struct type typ, taking tag
    20  // and squashValue from GetStructKeys.  prefix holds all components of the path from the typ
    21  // passed to GetStructKeys down to this typ.
    22  func appendStructKeys(typ reflect.Type, tag, squashValue string, prefix []string, keys []string) []string {
    23  	// Dereference any pointers.  This is a finite loop: Go types are well-founded.
    24  	for ; typ.Kind() == reflect.Ptr; typ = typ.Elem() {
    25  	}
    26  
    27  	// Handle only struct containers; terminate the recursion on anything else.
    28  	if typ.Kind() != reflect.Struct {
    29  		return append(keys, strings.Join(prefix, sep))
    30  	}
    31  
    32  	for i := 0; i < typ.NumField(); i++ {
    33  		fieldType := typ.Field(i)
    34  		var (
    35  			// fieldName is the name to use for the field.
    36  			fieldName string
    37  			// If squash is true, squash the sub-struct no additional accessor.
    38  			squash bool
    39  			ok     bool
    40  		)
    41  		if fieldName, ok = fieldType.Tag.Lookup(tag); ok {
    42  			if strings.HasSuffix(fieldName, squashValue) {
    43  				squash = true
    44  				fieldName = strings.TrimSuffix(fieldName, squashValue)
    45  			}
    46  		} else {
    47  			fieldName = strings.ToLower(fieldType.Name)
    48  		}
    49  		// Update prefix to recurse into this field.
    50  		if !squash {
    51  			prefix = append(prefix, fieldName)
    52  		}
    53  		keys = appendStructKeys(fieldType.Type, tag, squashValue, prefix, keys)
    54  		// Restore prefix.
    55  		if !squash {
    56  			prefix = prefix[:len(prefix)-1]
    57  		}
    58  	}
    59  	return keys
    60  }
    61  
    62  // ValidateMissingRequiredKeys returns all keys of value in GetStructKeys format that have an
    63  // additional required tag set but are unset.
    64  func ValidateMissingRequiredKeys(value interface{}, tag, squashValue string) []string {
    65  	return appendStructKeysIfZero(reflect.ValueOf(value), tag, ","+squashValue, "validate", "required", nil, nil)
    66  }
    67  
    68  // isScalar returns true if kind is "scalar", i.e. has no Elem().  This
    69  // recites the list from reflect/type.go and may start to give incorrect
    70  // results if new kinds are added to the language.
    71  func isScalar(kind reflect.Kind) bool {
    72  	switch kind {
    73  	case reflect.Array:
    74  	case reflect.Chan:
    75  	case reflect.Map:
    76  	case reflect.Ptr:
    77  	case reflect.Slice:
    78  		return false
    79  	}
    80  	return true
    81  }
    82  
    83  func appendStructKeysIfZero(value reflect.Value, tag, squashValue, validateTag, requiredValue string, prefix []string, keys []string) []string {
    84  	// finite loop: Go types are well-founded.
    85  	for value.Kind() == reflect.Ptr {
    86  		if value.IsZero() { // If required, would already have errored out.
    87  			return keys
    88  		}
    89  		value = value.Elem()
    90  	}
    91  
    92  	if !isScalar(value.Kind()) {
    93  		// Why use Type().Elem() when reflect.Value provides a perfectly good Elem()
    94  		// method?  The two are *not* the same, e.g. for nil pointers value.Elem() is
    95  		// invalid and has no Kind().  (See https://play.golang.org/p/M3ZV19AZAW0)
    96  		if !isScalar(value.Type().Elem().Kind()) {
    97  			// TODO(ariels): Possible to add, but need to define the semantics.  One
    98  			//     way might be to validate each field according to its type.
    99  			panic("No support for detecting required keys inside " + value.Kind().String() + " of structs")
   100  		}
   101  	}
   102  
   103  	// Handle only struct containers; terminate the recursion on anything else.
   104  	if value.Kind() != reflect.Struct {
   105  		return keys
   106  	}
   107  
   108  	for i := 0; i < value.NumField(); i++ {
   109  		fieldType := value.Type().Field(i)
   110  		fieldValue := value.Field(i)
   111  
   112  		var (
   113  			// fieldName is the name to use for the field.
   114  			fieldName string
   115  			// If squash is true, squash the sub-struct no additional accessor.
   116  			squash bool
   117  			ok     bool
   118  		)
   119  		if fieldName, ok = fieldType.Tag.Lookup(tag); ok {
   120  			if strings.HasSuffix(fieldName, squashValue) {
   121  				squash = true
   122  				fieldName = strings.TrimSuffix(fieldName, squashValue)
   123  			}
   124  		} else {
   125  			fieldName = strings.ToLower(fieldType.Name)
   126  		}
   127  
   128  		// Perform any needed validations.
   129  		if validationsString, ok := fieldType.Tag.Lookup(validateTag); ok {
   130  			for _, validation := range strings.Split(validationsString, ",") {
   131  				// Validate "required" field.
   132  				if validation == requiredValue && fieldValue.IsZero() {
   133  					keys = append(keys, strings.Join(append(prefix, fieldName), sep))
   134  				}
   135  			}
   136  		}
   137  
   138  		// Update prefix to recurse into this field.
   139  		if !squash {
   140  			prefix = append(prefix, fieldName)
   141  		}
   142  		keys = appendStructKeysIfZero(fieldValue, tag, squashValue, validateTag, requiredValue, prefix, keys)
   143  		// Restore prefix.
   144  		if !squash {
   145  			prefix = prefix[:len(prefix)-1]
   146  		}
   147  	}
   148  	return keys
   149  }