package ffclient import ( "fmt" "maps" "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/model" ) const ( errorFlagNotAvailable = "flag %v is not present or disabled" errorWrongVariation = "wrong variation used for flag %v" ) // BoolVariation return the value of the flag in boolean. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func BoolVariation(flagKey string, ctx ffcontext.Context, defaultValue bool) (bool, error) { return ff.BoolVariation(flagKey, ctx, defaultValue) } // BoolVariation return the value of the flag in boolean. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) BoolVariation(flagKey string, ctx ffcontext.Context, defaultValue bool) (bool, error) { res, err := g.BoolVariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // BoolVariationDetails return the details of the evaluation for boolean flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func BoolVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue bool) ( model.VariationResult[bool], error) { return ff.BoolVariationDetails(flagKey, ctx, defaultValue) } // BoolVariationDetails return the details of the evaluation for boolean flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) BoolVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue bool, ) (model.VariationResult[bool], error) { res, err := getVariation[bool](g, flagKey, ctx, defaultValue, "bool") notifyVariation(g, flagKey, ctx, res) return res, err } // IntVariation return the value of the flag in int. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func IntVariation(flagKey string, ctx ffcontext.Context, defaultValue int) (int, error) { return ff.IntVariation(flagKey, ctx, defaultValue) } // IntVariation return the value of the flag in int. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) IntVariation(flagKey string, ctx ffcontext.Context, defaultValue int) (int, error) { res, err := g.IntVariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // IntVariationDetails return the details of the evaluation for int flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func IntVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue int) (model.VariationResult[int], error) { return ff.IntVariationDetails(flagKey, ctx, defaultValue) } // IntVariationDetails return the details of the evaluation for int flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) IntVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue int, ) (model.VariationResult[int], error) { res, err := getVariation[int](g, flagKey, ctx, defaultValue, "int") notifyVariation(g, flagKey, ctx, res) return res, err } // Float64Variation return the value of the flag in float64. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func Float64Variation(flagKey string, ctx ffcontext.Context, defaultValue float64) (float64, error) { return ff.Float64Variation(flagKey, ctx, defaultValue) } // Float64Variation return the value of the flag in float64. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) Float64Variation(flagKey string, ctx ffcontext.Context, defaultValue float64) (float64, error) { res, err := g.Float64VariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // Float64VariationDetails return the details of the evaluation for float64 flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func Float64VariationDetails(flagKey string, ctx ffcontext.Context, defaultValue float64, ) (model.VariationResult[float64], error) { return ff.Float64VariationDetails(flagKey, ctx, defaultValue) } // Float64VariationDetails return the details of the evaluation for float64 flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) Float64VariationDetails(flagKey string, ctx ffcontext.Context, defaultValue float64, ) (model.VariationResult[float64], error) { res, err := getVariation[float64](g, flagKey, ctx, defaultValue, "float64") notifyVariation(g, flagKey, ctx, res) return res, err } // StringVariation return the value of the flag in string. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func StringVariation(flagKey string, ctx ffcontext.Context, defaultValue string) (string, error) { return ff.StringVariation(flagKey, ctx, defaultValue) } // StringVariation return the value of the flag in string. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) StringVariation(flagKey string, ctx ffcontext.Context, defaultValue string) (string, error) { res, err := g.StringVariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // StringVariationDetails return the details of the evaluation for string flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func StringVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue string, ) (model.VariationResult[string], error) { return ff.StringVariationDetails(flagKey, ctx, defaultValue) } // StringVariationDetails return the details of the evaluation for string flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) StringVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue string, ) (model.VariationResult[string], error) { res, err := getVariation[string](g, flagKey, ctx, defaultValue, "string") notifyVariation(g, flagKey, ctx, res) return res, err } // JSONArrayVariation return the value of the flag in []interface{}. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func JSONArrayVariation(flagKey string, ctx ffcontext.Context, defaultValue []interface{}) ([]interface{}, error) { return ff.JSONArrayVariation(flagKey, ctx, defaultValue) } // JSONArrayVariation return the value of the flag in []interface{}. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) JSONArrayVariation( flagKey string, ctx ffcontext.Context, defaultValue []interface{}, ) ([]interface{}, error) { res, err := g.JSONArrayVariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // JSONArrayVariationDetails return the details of the evaluation for []interface{} flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func JSONArrayVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue []interface{}, ) (model.VariationResult[[]interface{}], error) { return ff.JSONArrayVariationDetails(flagKey, ctx, defaultValue) } // JSONArrayVariationDetails return the details of the evaluation for []interface{} flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) JSONArrayVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue []interface{}, ) (model.VariationResult[[]interface{}], error) { res, err := getVariation[[]interface{}](g, flagKey, ctx, defaultValue, "[]interface{}") notifyVariation(g, flagKey, ctx, res) return res, err } // JSONVariation return the value of the flag in map[string]interface{}. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func JSONVariation( flagKey string, ctx ffcontext.Context, defaultValue map[string]interface{}, ) (map[string]interface{}, error) { return ff.JSONVariation(flagKey, ctx, defaultValue) } // JSONVariation return the value of the flag in map[string]interface{}. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) JSONVariation( flagKey string, ctx ffcontext.Context, defaultValue map[string]interface{}, ) (map[string]interface{}, error) { res, err := g.JSONVariationDetails(flagKey, ctx, defaultValue) return res.Value, err } // JSONVariationDetails return the details of the evaluation for map[string]interface{} flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. func JSONVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue map[string]interface{}, ) (model.VariationResult[map[string]interface{}], error) { return ff.JSONVariationDetails(flagKey, ctx, defaultValue) } // JSONVariationDetails return the details of the evaluation for []interface{} flag. // An error is return if you don't have init the library before calling the function. // If the key does not exist we return the default value. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) JSONVariationDetails(flagKey string, ctx ffcontext.Context, defaultValue map[string]interface{}, ) (model.VariationResult[map[string]interface{}], error) { res, err := getVariation[map[string]interface{}](g, flagKey, ctx, defaultValue, "bool") notifyVariation(g, flagKey, ctx, res) return res, err } // RawVariation return the raw value of the flag (without any types). // This raw result is mostly used by software built on top of go-feature-flag such as // go-feature-flag relay proxy. // If you are using directly the library you should avoid calling this function. // Note: Use this function only if you are using multiple go-feature-flag instances. func (g *GoFeatureFlag) RawVariation(flagKey string, ctx ffcontext.Context, sdkDefaultValue interface{}, ) (model.RawVarResult, error) { res, err := getVariation[interface{}](g, flagKey, ctx, sdkDefaultValue, "interface{}") notifyVariation(g, flagKey, ctx, res) return model.RawVarResult(res), err } // getFlagFromCache try to get the flag from the cache. // It returns an error if the cache is not init or if the flag is not present or disabled. func (g *GoFeatureFlag) getFlagFromCache(flagKey string) (flag.Flag, error) { f, err := g.cache.GetFlag(flagKey) if err != nil { return f, fmt.Errorf(errorFlagNotAvailable, flagKey) } return f, nil } // CollectEventData is collecting events and sending them to the data exporter to be stored. func (g *GoFeatureFlag) CollectEventData(event exporter.FeatureEvent) { if g != nil && g.dataExporter != nil { // Add event in the exporter g.dataExporter.AddEvent(event) } } // notifyVariation is logging the evaluation result for a flag // if no logger is provided in the configuration we are not logging anything. func notifyVariation[T model.JSONType]( g *GoFeatureFlag, flagKey string, ctx ffcontext.Context, result model.VariationResult[T], ) { if result.TrackEvents { event := exporter.NewFeatureEvent(ctx, flagKey, result.Value, result.VariationType, result.Failed, result.Version, "SERVER") g.CollectEventData(event) } } // getVariation is the internal generic func that handle the logic of a variation the result will always // contain a valid model.VariationResult // nolint:funlen func getVariation[T model.JSONType]( g *GoFeatureFlag, flagKey string, evaluationCtx ffcontext.Context, sdkDefaultValue T, expectedType string, ) (model.VariationResult[T], error) { if g == nil { return model.VariationResult[T]{ Value: sdkDefaultValue, VariationType: flag.VariationSDKDefault, Failed: true, Reason: flag.ReasonError, ErrorCode: flag.ErrorCodeProviderNotReady, Cacheable: false, }, fmt.Errorf("go-feature-flag is not initialised, default value is used") } if g.config.Offline { return model.VariationResult[T]{ Value: sdkDefaultValue, VariationType: flag.VariationSDKDefault, Failed: false, Reason: flag.ReasonOffline, Cacheable: false, }, nil } f, err := g.getFlagFromCache(flagKey) if err != nil { varResult := model.VariationResult[T]{ Value: sdkDefaultValue, VariationType: flag.VariationSDKDefault, ErrorCode: flag.ErrorCodeFlagNotFound, Failed: true, Reason: flag.ReasonError, Cacheable: false, } if f != nil { varResult.TrackEvents = f.IsTrackEvents() varResult.Version = f.GetVersion() varResult.Metadata = f.GetMetadata() } return varResult, err } flagCtx := flag.Context{ DefaultSdkValue: sdkDefaultValue, EvaluationContextEnrichment: maps.Clone(g.config.EvaluationContextEnrichment), } if g.config.Environment != "" { flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) } flagValue, resolutionDetails := f.Value(flagKey, evaluationCtx, flagCtx) var convertedValue interface{} switch value := flagValue.(type) { case float64: // this part ensures that we convert float64 value into int if we call IntVariation on a float64 value. if expectedType == "int" { convertedValue = int(value) } else { convertedValue = value } default: convertedValue = value } var v T switch val := convertedValue.(type) { case T: v = val default: if val != nil { return model.VariationResult[T]{ Value: sdkDefaultValue, VariationType: flag.VariationSDKDefault, Reason: flag.ReasonError, ErrorCode: flag.ErrorCodeTypeMismatch, Failed: true, TrackEvents: f.IsTrackEvents(), Version: f.GetVersion(), Metadata: f.GetMetadata(), }, fmt.Errorf(errorWrongVariation, flagKey) } } return model.VariationResult[T]{ Value: v, VariationType: resolutionDetails.Variant, Reason: resolutionDetails.Reason, ErrorCode: resolutionDetails.ErrorCode, ErrorDetails: resolutionDetails.ErrorMessage, Failed: resolutionDetails.ErrorCode != "", TrackEvents: f.IsTrackEvents(), Version: f.GetVersion(), Cacheable: resolutionDetails.Cacheable, Metadata: constructMetadata(f, resolutionDetails), }, nil } // constructMetadata is the internal generic func used to enhance model.VariationResult adding // the targeting.rule's name (from configuration) to the Metadata. // That way, it is possible to see when a targeting rule is match during the evaluation process. func constructMetadata(f flag.Flag, resolutionDetails flag.ResolutionDetails) map[string]interface{} { metadata := maps.Clone(f.GetMetadata()) if resolutionDetails.RuleName == nil || *resolutionDetails.RuleName == "" { return metadata } if metadata == nil { metadata = make(map[string]interface{}) } metadata["evaluatedRuleName"] = *resolutionDetails.RuleName return metadata }