wstl1/mapping_engine/projector/projector.go (187 lines of code) (raw):

// Copyright 2020 Google LLC. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package projector contains methods and mechanisms for creating and calling projectors. package projector import ( "fmt" "math" "reflect" "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/errors" /* copybara-comment: errors */ "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/mapping" /* copybara-comment: mapping */ "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/types" /* copybara-comment: types */ "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/util/jsonutil" /* copybara-comment: jsonutil */ mappb "github.com/GoogleCloudPlatform/healthcare-data-harmonization/mapping_engine/proto" /* copybara-comment: mapping_go_proto */ ) // FromDef creates a projector from a proto definition. This will not register it. func FromDef(definition *mappb.ProjectorDefinition, e mapping.Engine) types.Projector { return func(arguments []jsonutil.JSONMetaNode, pctx *types.Context) (jsonutil.JSONToken, error) { pctx.Variables.Push() errLocation := errors.NewProtoLocation(definition, nil) if err := pctx.PushProjectorToStack(definition.Name); err != nil { return nil, errors.Wrap(errLocation, err) } var merged jsonutil.JSONToken // TODO(): Sort in dependency order if err := e.ProcessMappings(definition.Mapping, definition.Name, arguments, &merged, pctx); err != nil { return nil, errors.Wrap(errLocation, err) } pctx.PopProjectorFromStack(definition.Name) if _, err := pctx.Variables.Pop(); err != nil { return nil, errors.Wrap(errLocation, err) } return merged, nil } } // FromFunction creates a projector from a given function. The function must have a return type of // (JSONObject, error) and all arguments must be assignable to JSONObject. This will not register // the projector. func FromFunction(fn interface{}, name string) (types.Projector, error) { tokenType := reflect.TypeOf((*jsonutil.JSONToken)(nil)).Elem() f := reflect.ValueOf(fn) if f.Kind() != reflect.Func { return nil, fmt.Errorf("projector must be a function") } // Check args are our JSON types. ft := reflect.TypeOf(fn) for i := 0; i < ft.NumIn(); i++ { isObj := ft.In(i).AssignableTo(tokenType) isSliceOfObj := ft.In(i).Kind() == reflect.Slice && ft.In(i).Elem().AssignableTo(tokenType) if !isObj && !isSliceOfObj { return nil, fmt.Errorf("parameter %d is of type %v which is not supported", i, ft.In(i)) } } // Check return type is what we expect. if ft.NumOut() != 2 { return nil, fmt.Errorf("incorrect return type, expected (jsonutil.JSONToken, error) got %d return values", ft.NumOut()) } if !ft.Out(0).AssignableTo(tokenType) || !ft.Out(1).AssignableTo(reflect.TypeOf((*error)(nil)).Elem()) { return nil, fmt.Errorf("incorrect return type, expected (jsonutil.JSONToken, error) got (%v, %v)", ft.Out(0), ft.Out(1)) } // Build wrapper closure. return func(metaArgs []jsonutil.JSONMetaNode, pctx *types.Context) (jsonutil.JSONToken, error) { errLocation := errors.FnLocationf("Native Function Preamble %q", name) if err := pctx.PushProjectorToStack(name); err != nil { return nil, errors.Wrap(errLocation, err) } // Lose the meta. args := make([]jsonutil.JSONToken, len(metaArgs)) for i, metaArg := range metaArgs { node, err := jsonutil.NodeToToken(metaArg) if err != nil { return nil, errors.Wrap(errLocation, fmt.Errorf("error converting args: %v", err)) } args[i] = node } if ft.IsVariadic() && len(args) < ft.NumIn()-1 { return nil, errors.Wrap(errLocation, fmt.Errorf("expected at least %d parameters (could be more, function is variadic), got %d", ft.NumIn()-1, len(args))) } if !ft.IsVariadic() && len(args) != ft.NumIn() { return nil, errors.Wrap(errLocation, fmt.Errorf("expected %d parameters, got %d", ft.NumIn(), len(args))) } argvs := make([]reflect.Value, 0, len(args)) for i, arg := range args { if ft.IsVariadic() && i == ft.NumIn()-1 { a, err := extractVariadic(ft.In(i).Elem(), args[i:]) if err != nil { return nil, errors.Wrap(errLocation, fmt.Errorf("error extracting variadic argument %d: %v", i, err)) } argvs = append(argvs, a...) break } if ft.In(i).Kind() == reflect.Slice { a, err := extractSlice(ft.In(i).Elem(), arg) if err != nil { return nil, errors.Wrap(errLocation, fmt.Errorf("error extracting slice argument %d: %v", i, err)) } argvs = append(argvs, a) continue } a, err := extractSimple(ft.In(i), arg) if err != nil { return nil, errors.Wrap(errLocation, fmt.Errorf("error extracting argument %d: %v", i, err)) } argvs = append(argvs, a) } result := f.Call(argvs) var r jsonutil.JSONToken var err error if ri := result[0].Interface(); ri != nil { r = ri.(jsonutil.JSONToken) } if ri := result[1].Interface(); ri != nil { err = ri.(error) } pctx.PopProjectorFromStack(name) if err != nil { return nil, errors.Wrap(errors.FnLocationf("Native Function %q", name), err) } return r, nil }, nil } func extractVariadic(elemType reflect.Type, args []jsonutil.JSONToken) ([]reflect.Value, error) { if arr, ok := args[0].(jsonutil.JSONArr); len(args) == 1 && ok { args = arr } vals := make([]reflect.Value, 0, len(args)) for _, arg := range args { v, err := extractSimple(elemType, arg) if err != nil { return nil, fmt.Errorf("variadic argument error: %v", err) } vals = append(vals, v) } return vals, nil } func extractSimple(elemType reflect.Type, arg jsonutil.JSONToken) (reflect.Value, error) { if arg != nil && !reflect.TypeOf(arg).AssignableTo(elemType) { return reflect.ValueOf(nil), fmt.Errorf("got %T, expected %v", arg, elemType) } if arg == nil { return reflect.New(elemType).Elem(), nil } return reflect.ValueOf(arg), nil } func extractSlice(elemType reflect.Type, arg jsonutil.JSONToken) (reflect.Value, error) { if arg == nil { return reflect.MakeSlice(reflect.SliceOf(elemType), 0, 0), nil } arrArg, ok := arg.(jsonutil.JSONArr) if !ok { return reflect.ValueOf(nil), fmt.Errorf("got %T but expected a JSONArr (whose elements are all %v)", arg, elemType) } ret := reflect.MakeSlice(reflect.SliceOf(elemType), len(arrArg), len(arrArg)) for i, t := range arrArg { tv := reflect.ValueOf(t) if t == nil { tv = reflect.Zero(elemType) } if !IsZero(tv) && !tv.Type().AssignableTo(elemType) { return reflect.ValueOf(nil), fmt.Errorf("array element %d is a %T but must be a %v", i, t, elemType) } ret.Index(i).Set(tv) } return ret, nil } // IsZero reports whether v is the zero value for its type. // It panics if the argument is invalid. This is to backfill for the lack of this method in older // versions of Go. func IsZero(v reflect.Value) bool { switch v.Kind() { case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return math.Float64bits(v.Float()) == 0 case reflect.Complex64, reflect.Complex128: c := v.Complex() return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0 case reflect.Array: for i := 0; i < v.Len(); i++ { if !IsZero(v.Index(i)) { return false } } return true case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: return v.IsNil() case reflect.String: return v.Len() == 0 case reflect.Struct: for i := 0; i < v.NumField(); i++ { if !IsZero(v.Field(i)) { return false } } return true default: // This should never happens, but will act as a safeguard for // later, as a default value doesn't makes sense here. panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()}) } }