github.com/openshift-online/ocm-sdk-go@v0.1.473/data/digger.go (about) 1 /* 2 Copyright (c) 2021 Red Hat, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // This file contains the implementation of an object that knows how to extract data from objects 18 // using paths. 19 20 package data 21 22 import ( 23 "context" 24 "reflect" 25 "regexp" 26 "strings" 27 "sync" 28 "unicode" 29 "unicode/utf8" 30 ) 31 32 // DiggerBuilder contains the information and logic needed to build a digger. 33 type DiggerBuilder struct { 34 } 35 36 // Digger is an object that knows how to extract information from objects using paths. 37 type Digger struct { 38 methodCache map[cacheKey]reflect.Method 39 methodCacheLock *sync.Mutex 40 fieldCache map[cacheKey]int 41 fieldCacheLock *sync.Mutex 42 } 43 44 // cacheKey is used as the key for the methods and fields caches. 45 type cacheKey struct { 46 class reflect.Type 47 field string 48 } 49 50 // NewDigger creates a builder that can then be used to configure and create diggers. 51 func NewDigger() *DiggerBuilder { 52 return &DiggerBuilder{} 53 } 54 55 // Build uses the configuration stored in the builder to create a new digger. 56 func (b *DiggerBuilder) Build(ctx context.Context) (result *Digger, err error) { 57 // Create and populate the object: 58 result = &Digger{ 59 methodCache: map[cacheKey]reflect.Method{}, 60 methodCacheLock: &sync.Mutex{}, 61 fieldCache: map[cacheKey]int{}, 62 fieldCacheLock: &sync.Mutex{}, 63 } 64 65 return 66 } 67 68 // Dig extracts from the given object the field that corresponds to the given path. The path should 69 // be a sequence of field names separated by dots. 70 func (d *Digger) Dig(object interface{}, path string) interface{} { 71 path = strings.TrimSpace(path) 72 if path == "" { 73 return object 74 } 75 segments := strings.Split(path, ".") 76 if len(segments) == 0 { 77 return object 78 } 79 for i, field := range segments { 80 segments[i] = strings.TrimSpace(field) 81 } 82 return d.digPath(object, segments) 83 } 84 85 func (d *Digger) digPath(object interface{}, path []string) interface{} { 86 if len(path) == 0 { 87 return object 88 } 89 head := path[0] 90 next := d.digField(object, head) 91 if next == nil { 92 return nil 93 } 94 tail := path[1:] 95 return d.digPath(next, tail) 96 } 97 98 func (d *Digger) digField(object interface{}, field string) interface{} { 99 value := reflect.ValueOf(object) 100 return d.digFieldFromValue(value, field) 101 } 102 103 func (d *Digger) digFieldFromValue(value reflect.Value, field string) interface{} { 104 switch value.Kind() { 105 case reflect.Ptr: 106 return d.digFieldFromPtr(value, field) 107 case reflect.Struct: 108 return d.digFieldFromStruct(value, field) 109 case reflect.Map: 110 return d.digFieldFromMap(value, field) 111 default: 112 return nil 113 } 114 } 115 116 func (d *Digger) digFieldFromPtr(value reflect.Value, name string) interface{} { 117 // Try to find a matching method: 118 method, ok := d.lookupMethod(value.Type(), name) 119 if ok { 120 return d.digFieldFromMethod(value, method) 121 } 122 123 // If no matching method was found, but the target of the pointer is a struct then we should 124 // try to extract the field from the public methods of the struct. 125 if value.Type().Elem().Kind() == reflect.Struct { 126 return d.digFieldFromStruct(value.Elem(), name) 127 } 128 129 // If we are here we didn't find any match: 130 return nil 131 } 132 133 func (d *Digger) digFieldFromStruct(value reflect.Value, name string) interface{} { 134 // First try to find a matching method: 135 method, ok := d.lookupMethod(value.Type(), name) 136 if ok { 137 return d.digFieldFromMethod(value, method) 138 } 139 140 // If no matching method was found, try to find a matching public field: 141 index, ok := d.lookupField(value.Type(), name) 142 if ok { 143 return value.Field(index).Interface() 144 } 145 146 // If we are here we didn't find any match: 147 return nil 148 } 149 150 func (d *Digger) digFieldFromMethod(value reflect.Value, method reflect.Method) interface{} { 151 var result reflect.Value 152 153 // Call the method: 154 inArgs := []reflect.Value{ 155 value, 156 } 157 outArgs := method.Func.Call(inArgs) 158 159 // If the method has one output parameter then we assume it is the value. If it has two 160 // output parameters then we assume that the first is the value and the second is a boolean 161 // flag indicating if there is actually a value. 162 switch len(outArgs) { 163 case 1: 164 result = outArgs[0] 165 case 2: 166 if outArgs[1].Bool() { 167 result = outArgs[0] 168 } 169 } 170 171 // Return the result: 172 if !result.IsValid() { 173 return nil 174 } 175 return result.Interface() 176 } 177 178 func (d *Digger) digFieldFromMap(value reflect.Value, name string) interface{} { 179 key := reflect.ValueOf(name) 180 result := value.MapIndex(key) 181 if !result.IsValid() { 182 return nil 183 } 184 return result.Interface() 185 } 186 187 // lookupMethod tries to find a public method of the given value that matches the given path 188 // segment. For example, if the path segment is `my_field` it will look for a method named 189 // `GetMyField` or `MyField`. Only methods that don't have input parameters will be considered. 190 func (d *Digger) lookupMethod(class reflect.Type, field string) (result reflect.Method, ok bool) { 191 // Acquire the method cache lock: 192 d.methodCacheLock.Lock() 193 defer d.methodCacheLock.Unlock() 194 195 // Try to find the method in the cache: 196 key := cacheKey{ 197 class: class, 198 field: field, 199 } 200 result, ok = d.methodCache[key] 201 if ok { 202 return 203 } 204 205 // Get the number of methods: 206 count := class.NumMethod() 207 208 // Try to find a method that returns a value and a boolean flag indicating if there is 209 // actually a value. We try this first because this gives more information and allows us to 210 // return nil when the field isn't present, instead of returning the zero value of the type. 211 for i := 0; i < count; i++ { 212 method := class.Method(i) 213 if !d.isPublic(method.Name) { 214 continue 215 } 216 if method.Type.NumIn() != 1 || method.Type.NumOut() != 2 { 217 continue 218 } 219 if method.Type.Out(1).Kind() != reflect.Bool { 220 continue 221 } 222 if !d.methodNameMatches(method.Name, field) { 223 continue 224 } 225 d.methodCache[key] = method 226 result = method 227 ok = true 228 return 229 } 230 231 // Try now to find a method that returns only the value. 232 for i := 0; i < count; i++ { 233 method := class.Method(i) 234 if method.Type.NumIn() != 1 || method.Type.NumOut() != 1 { 235 continue 236 } 237 if !d.methodNameMatches(method.Name, field) { 238 continue 239 } 240 d.methodCache[key] = method 241 result = method 242 ok = true 243 return 244 } 245 246 // If we are here then we didn't find any matching method. 247 ok = false 248 return 249 } 250 251 // methodNameMatches checks if the name of a Go method matches a path segment. 252 func (d *Digger) methodNameMatches(method, segment string) bool { 253 // If there is a `Get` prefix remove it: 254 name := method 255 if getMethodRE.MatchString(method) { 256 name = name[3:] 257 } 258 259 // Check if the method name matches the segment: 260 return d.nameMatches(name, segment) 261 } 262 263 // lookupField tries to find a public field of the given value that matches the given path segment. 264 // For example, if the path segment is `my_field` it will look for a field named `MyField`. 265 func (d *Digger) lookupField(class reflect.Type, name string) (result int, ok bool) { 266 // Acquire the field cache lock: 267 d.fieldCacheLock.Lock() 268 defer d.fieldCacheLock.Unlock() 269 270 // Try to find the field in the cache: 271 key := cacheKey{ 272 class: class, 273 field: name, 274 } 275 result, ok = d.fieldCache[key] 276 if ok { 277 return 278 } 279 280 // Try now to find a field that matches the name: 281 count := class.NumField() 282 for i := 0; i < count; i++ { 283 field := class.Field(i) 284 if !d.isPublic(field.Name) { 285 continue 286 } 287 if !d.fieldNameMatches(field.Name, name) { 288 continue 289 } 290 d.fieldCache[key] = i 291 result = i 292 ok = true 293 return 294 } 295 296 // If we are here then we didn't find any matching method. 297 ok = false 298 return 299 } 300 301 // fieldNameMatches checks if the name of a Go fields matches a path segment. 302 func (d *Digger) fieldNameMatches(field, segment string) bool { 303 return d.nameMatches(field, segment) 304 } 305 306 // nameMatches checks if a Go name matches with a path segment. 307 func (d *Digger) nameMatches(name, segment string) bool { 308 // Conver the strings to arrays of runes so that we can compare runes one by one easily: 309 nameRunes := []rune(name) 310 nameLen := len(nameRunes) 311 segmentRunes := []rune(segment) 312 segmentLen := len(segmentRunes) 313 314 // Start at the beginning of both arrays of runes, and advance while the runes in both the 315 // name and the path segment are compatible. Two runes are compatible if they are equal 316 // ignoring case. An underscore in the path segment is compatible if there is a transition 317 // from lower case to upper case in the name at that point. 318 nameI, segmentI := 0, 0 319 for nameI < nameLen && segmentI < segmentLen { 320 if unicode.ToLower(nameRunes[nameI]) == unicode.ToLower(segmentRunes[segmentI]) { 321 nameI++ 322 segmentI++ 323 continue 324 } 325 if nameI > 0 && segmentRunes[segmentI] == '_' { 326 previousLower := unicode.IsLower(nameRunes[nameI-1]) 327 currentUpper := unicode.IsUpper(nameRunes[nameI]) 328 if previousLower && currentUpper { 329 segmentI++ 330 continue 331 } 332 } 333 return false 334 } 335 336 // If we have consumed all the runes of both names then there is a match: 337 return nameI == nameLen && segmentI == segmentLen 338 } 339 340 // isPublic checks if the given name is public according to the Go rules. 341 func (d *Digger) isPublic(name string) bool { 342 first, _ := utf8.DecodeRuneInString(name) 343 if first == utf8.RuneError { 344 return false 345 } 346 return unicode.IsUpper(first) 347 } 348 349 // getMethodRE is a regular expression used to check if a method name starts with `Get`. Note that 350 // checking if the string starts with `Get` is not enough as it would fails for methods with names 351 // like `Getaway`. 352 var getMethodRE = regexp.MustCompile(`^Get\p{Lu}`)