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 }