elliotchance/gedcom

View on GitHub
q/accessor_expr.go

Summary

Maintainability
A
35 mins
Test Coverage
package q

import (
    "fmt"
    "reflect"
    "runtime/debug"
)

// AccessorExpr is used to fetch the value of a property or to invoke a method.
//
// The simplest form is ".Foo" where Foo could be a property or method.
//
// When an accessor is used on a slice the accessor is performed on each
// element, generating a new slice of that returned type.
type AccessorExpr struct {
    Query string
}

// Evaluate  will automatically handle conversions between pointer and
// non-pointers to find the property or method and return the value. However, if
// it is a method it must not take any arguments.
//
// It will return an error if a property or method could not be found by that
// name.
func (e *AccessorExpr) Evaluate(engine *Engine, input interface{}, args []*Statement) (interface{}, error) {
    in := reflect.ValueOf(input)
    accessor := e.Query[1:]

    if input == nil {
        return nil, nil
    }

    // If it is a slice we need to Evaluate each one.
    if in.Kind() == reflect.Slice {
        t := TypeOfSliceElement(input)
        if t.Kind() == reflect.Ptr {
            t = t.Elem()
        }
        returnType := e.getReturnType(accessor, reflect.New(t).Interface())

        results := reflect.MakeSlice(reflect.SliceOf(returnType), 0, 0)

        for i := 0; i < in.Len(); i++ {
            result, err := e.Evaluate(engine, in.Index(i).Interface(), nil)
            if err != nil {
                return nil, err
            }

            results = reflect.Append(results, reflect.ValueOf(result))
        }

        return results.Interface(), nil
    }

    var err error
    input, err = e.evaluateAccessor(accessor, input)

    if err != nil {
        return nil, err
    }

    return input, nil
}

func (e *AccessorExpr) evaluateAccessor(accessor string, input interface{}) (r interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            stackTrace := string(debug.Stack())
            err = fmt.Errorf("panic %s.%s: %v\n%s", getType(input), accessor, r,
                stackTrace)
        }
    }()

    method, field := e.getMethodOrField(accessor, input)

    switch {
    case method != nil:
        return callMethod(*method)

    case field != nil:
        return field.Interface(), nil
    }

    return nil, fmt.Errorf(`%s does not have a method or property named "%s"`,
        getType(input), accessor)
}

func (e *AccessorExpr) getReturnType(accessor string, input interface{}) reflect.Type {
    method, field := e.getMethodOrField(accessor, input)

    switch {
    case method != nil:
        return (*method).Type().Out(0)

    case field != nil:
        return field.Type()
    }

    return nil
}

func (e *AccessorExpr) getMethodOrField(accessor string, input interface{}) (*reflect.Value, *reflect.Value) {
    method := e.getMethod(accessor, input)

    if method != nil {
        return method, nil
    }

    field := e.getField(accessor, input)

    return nil, field
}

func (e *AccessorExpr) getMethod(accessor string, input interface{}) *reflect.Value {
    defer func() {
        // The nil return value will be handled higher up.
        recover()
    }()

    in := ValueToPointer(reflect.ValueOf(input))

    s := in.String()
    s += ""

    // Try the method on the pointer.
    methodByName := in.MethodByName(accessor)
    if methodByName.IsValid() {
        return &methodByName
    }

    // If that doesn't work, try calling it on the dereferenced value.
    methodByName = in.Elem().MethodByName(accessor)
    if methodByName.IsValid() {
        return &methodByName
    }

    return nil
}

func (e *AccessorExpr) getField(accessor string, input interface{}) *reflect.Value {
    defer func() {
        // The nil return value will be handled higher up.
        recover()
    }()

    in := ValueToPointer(reflect.ValueOf(input))

    // Try the property on the pointer.
    fieldByName := in.Elem().FieldByName(accessor)
    if fieldByName.IsValid() {
        return &fieldByName
    }

    // Try the property on the struct.
    if in.Kind() == reflect.Struct {
        fieldByName = in.FieldByName(accessor)
        if fieldByName.IsValid() {
            return &fieldByName
        }
    }

    return nil
}

func callMethod(methodByName reflect.Value) (interface{}, error) {
    result := methodByName.Call([]reflect.Value{})

    return result[0].Interface(), nil
}