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}`)