pkg/confidence/utils.go (310 lines of code) (raw):

package confidence import ( "encoding/json" "errors" "fmt" "reflect" "strings" ) const ErrorReason Reason = "ERROR" const TargetingMatchReason Reason = "TARGETING_MATCH" const DefaultReason Reason = "DEFAULT" func splitFlagString(flag string) (string, string) { splittedFlag := strings.SplitN(flag, ".", 2) if len(splittedFlag) == 2 { return splittedFlag[0], splittedFlag[1] } return splittedFlag[0], "" } func extractPropertyValue(path string, values map[string]interface{}) (interface{}, error) { if path == "" { return values, nil } firstPartAndRest := strings.SplitN(path, ".", 2) if len(firstPartAndRest) == 1 { value := values[firstPartAndRest[0]] return value, nil } childMap, ok := values[firstPartAndRest[0]].(map[string]interface{}) if ok { return extractPropertyValue(firstPartAndRest[1], childMap) } return false, fmt.Errorf("unable to find property in path %s", path) } func getTypeForPath(schema map[string]interface{}, path string) (reflect.Kind, error) { if path == "" { return reflect.Map, nil } firstPartAndRest := strings.SplitN(path, ".", 2) if len(firstPartAndRest) == 1 { value, ok := schema[firstPartAndRest[0]].(map[string]interface{}) if !ok { return 0, fmt.Errorf("schema was not in the expected format") } if _, isBool := value["boolSchema"]; isBool { return reflect.Bool, nil } else if _, isString := value["stringSchema"]; isString { return reflect.String, nil } else if _, isInt := value["intSchema"]; isInt { return reflect.Int64, nil } else if _, isFloat := value["doubleSchema"]; isFloat { return reflect.Float64, nil } else if _, isMap := value["structSchema"]; isMap { return reflect.Map, nil } return 0, fmt.Errorf("unable to find property type in schema %s", path) } // If we are here, the property path contains multiple entries -> this must be a struct -> recurse down the tree. childMap, ok := schema[firstPartAndRest[0]].(map[string]interface{}) if !ok { return 0, fmt.Errorf("unexpected error when parsing resolve response schema") } if structMap, isStruct := childMap["structSchema"]; isStruct { structSchema, _ := structMap.(map[string]interface{})["schema"].(map[string]interface{}) return getTypeForPath(structSchema, firstPartAndRest[1]) } return 0, fmt.Errorf("unable to find property in schema %s", path) } func processResolveError(err error, defaultValue interface{}) InterfaceResolutionDetail { switch { case errors.Is(err, errFlagNotFound): return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: FlagNotFoundCode, ErrorMessage: "error when resolving, flag not found", FlagMetadata: nil, }, } default: return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: GeneralCode, ErrorMessage: "error when resolving, returning default value", FlagMetadata: nil, }} } } func processResolvedFlag(resolvedFlag resolvedFlag, defaultValue interface{}, expectedKind reflect.Kind, propertyPath string) InterfaceResolutionDetail { if len(resolvedFlag.Value) == 0 { return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Reason: DefaultReason}} } actualKind, schemaErr := getTypeForPath(resolvedFlag.FlagSchema.Schema, propertyPath) if schemaErr != nil || actualKind != expectedKind { err := NewTypeMismatchResolutionError( fmt.Sprintf("schema for property %s does not match the expected type", propertyPath)) return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }} } updatedMap, err := replaceNumbers("", resolvedFlag.Value, resolvedFlag.FlagSchema.Schema) if err != nil { return typeMismatchError(defaultValue) } extractedValue, extractValueError := extractPropertyValue(propertyPath, updatedMap) if extractValueError != nil { return typeMismatchError(defaultValue) } return InterfaceResolutionDetail{ Value: extractedValue, ResolutionDetail: ResolutionDetail{ Reason: TargetingMatchReason, Variant: resolvedFlag.Variant}} } func replaceNumbers(basePath string, input map[string]interface{}, schema map[string]interface{}) (map[string]interface{}, error) { updatedMap := make(map[string]interface{}) for key, value := range input { kind, typeErr := getTypeForPath(schema, fmt.Sprintf("%s%s", basePath, key)) if typeErr != nil { return updatedMap, fmt.Errorf("unable to get type for path %w", typeErr) } switch kind { case reflect.Float64: floatValue, err := value.(json.Number).Float64() if err != nil { return updatedMap, fmt.Errorf("unable to convert to float") } updatedMap[key] = floatValue case reflect.Int64: intValue, err := value.(json.Number).Int64() if err != nil { return updatedMap, fmt.Errorf("unable to convert to int") } updatedMap[key] = intValue case reflect.Map: asMap, ok := value.(map[string]interface{}) if !ok { return updatedMap, fmt.Errorf("unable to convert map") } childMap, err := replaceNumbers(fmt.Sprintf("%s%s.", basePath, key), asMap, schema) if err != nil { return updatedMap, fmt.Errorf("unable to convert map") } updatedMap[key] = childMap default: updatedMap[key] = value } } return updatedMap, nil } func typeMismatchError(defaultValue interface{}) InterfaceResolutionDetail { err := NewTypeMismatchResolutionError( "Unable to extract property value from resolve response") return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }} } func ToBoolResolutionDetail(res InterfaceResolutionDetail, defaultValue bool) BoolResolutionDetail { if res.ResolutionDetail.Reason == TargetingMatchReason { v, ok := res.Value.(bool) if ok { return BoolResolutionDetail{ Value: v, ResolutionDetail: res.ResolutionDetail, } } err := NewTypeMismatchResolutionError("Unable to convert response property to boolean") return BoolResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil}} } return BoolResolutionDetail{ Value: defaultValue, ResolutionDetail: res.ResolutionDetail, } } func ToStringResolutionDetail(res InterfaceResolutionDetail, defaultValue string) StringResolutionDetail { if res.ResolutionDetail.Reason == TargetingMatchReason { v, ok := res.Value.(string) if ok { return StringResolutionDetail{ Value: v, ResolutionDetail: res.ResolutionDetail, } } err := NewTypeMismatchResolutionError("Unable to convert response property to boolean") return StringResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }, } } return StringResolutionDetail{ Value: defaultValue, ResolutionDetail: res.ResolutionDetail, } } func ToFloatResolutionDetail(res InterfaceResolutionDetail, defaultValue float64) FloatResolutionDetail { if res.ResolutionDetail.Reason == TargetingMatchReason { v, ok := res.Value.(float64) if ok { return FloatResolutionDetail{ Value: v, ResolutionDetail: res.ResolutionDetail, } } err := NewTypeMismatchResolutionError("Unable to convert response property to float") return FloatResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }, } } return FloatResolutionDetail{ Value: defaultValue, ResolutionDetail: res.ResolutionDetail, } } func ToObjectResolutionDetail(res InterfaceResolutionDetail, defaultValue interface{}) InterfaceResolutionDetail { if res.ResolutionDetail.Reason == TargetingMatchReason { v, ok := res.Value.(interface{}) if ok { return InterfaceResolutionDetail{ Value: v, ResolutionDetail: res.ResolutionDetail, } } err := NewTypeMismatchResolutionError("Unable to convert response property to float") return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }, } } return InterfaceResolutionDetail{ Value: defaultValue, ResolutionDetail: res.ResolutionDetail, } } func ToIntResolutionDetail(res InterfaceResolutionDetail, defaultValue int64) IntResolutionDetail { if res.ResolutionDetail.Reason == TargetingMatchReason { v, ok := res.Value.(int64) if ok { return IntResolutionDetail{ Value: v, ResolutionDetail: res.ResolutionDetail, } } err := NewTypeMismatchResolutionError("Unable to convert response property to int") return IntResolutionDetail{ Value: defaultValue, ResolutionDetail: ResolutionDetail{ Variant: "", Reason: ErrorReason, ErrorCode: err.code, ErrorMessage: err.message, FlagMetadata: nil, }, } } return IntResolutionDetail{ Value: defaultValue, ResolutionDetail: res.ResolutionDetail, } }