diff --git a/confidence/.fleet/run.json b/confidence/.fleet/run.json
new file mode 100644
index 0000000..12303d3
--- /dev/null
+++ b/confidence/.fleet/run.json
@@ -0,0 +1,5 @@
+{
+ "configurations": [
+
+ ]
+}
\ No newline at end of file
diff --git a/confidence/confidence.go b/confidence/confidence.go
new file mode 100644
index 0000000..7c39d71
--- /dev/null
+++ b/confidence/confidence.go
@@ -0,0 +1,170 @@
+package confidence
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "reflect"
+ "strings"
+)
+
+type FlagResolver interface {
+ resolveFlag(ctx context.Context, flag string, defaultValue interface{},
+ evalCtx map[string]interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail
+}
+
+type ContextProvider interface {
+ GetContext() map[string]interface{}
+}
+
+var (
+ SDK_ID = "SDK_ID_GO_PROVIDER"
+ SDK_VERSION = "0.1.8" // x-release-please-version
+)
+
+type Confidence struct {
+ parent ContextProvider
+ contextMap map[string]interface{}
+ Config APIConfig
+ ResolveClient ResolveClient
+}
+
+func (e Confidence) GetContext() map[string]interface{} {
+ currentMap := e.contextMap
+ parentMap := make(map[string]interface{})
+ if e.parent != nil {
+ parentMap = e.parent.GetContext()
+ }
+ for key, value := range parentMap {
+ currentMap[key] = value
+ }
+ return currentMap
+}
+
+type ConfidenceBuilder struct {
+ confidence Confidence
+}
+
+func (e ConfidenceBuilder) SetAPIConfig(config APIConfig) ConfidenceBuilder {
+ e.confidence.Config = config
+ return e
+}
+
+func (e ConfidenceBuilder) SetResolveClient(client ResolveClient) ConfidenceBuilder {
+ e.confidence.ResolveClient = client
+ return e
+}
+
+func (e ConfidenceBuilder) Build() Confidence {
+ if e.confidence.ResolveClient == nil {
+ e.confidence.ResolveClient = HttpResolveClient{Client: &http.Client{}, Config: e.confidence.Config}
+ }
+ e.confidence.contextMap = make(map[string]interface{})
+ return e.confidence
+}
+
+func NewConfidenceBuilder() ConfidenceBuilder {
+ return ConfidenceBuilder{
+ confidence: Confidence{},
+ }
+}
+
+func (e Confidence) putContext(key string, value interface{}) {
+ e.contextMap[key] = value
+}
+
+func (e Confidence) WithContext(context map[string]interface{}) Confidence {
+ return Confidence{
+ parent: &e,
+ contextMap: context,
+ Config: e.Config,
+ ResolveClient: e.ResolveClient,
+ }
+}
+
+func (e Confidence) GetBoolFlag(ctx context.Context, flag string, defaultValue bool) BoolResolutionDetail {
+ resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Bool)
+ return ToBoolResolutionDetail(resp, defaultValue)
+}
+
+func (e Confidence) GetBoolValue(ctx context.Context, flag string, defaultValue bool) bool {
+ return e.GetBoolFlag(ctx, flag, defaultValue).Value
+}
+
+func (e Confidence) GetIntFlag(ctx context.Context, flag string, defaultValue int64) IntResolutionDetail {
+ resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Int64)
+ return ToIntResolutionDetail(resp, defaultValue)
+}
+
+func (e Confidence) GetIntValue(ctx context.Context, flag string, defaultValue int64) int64 {
+ return e.GetIntFlag(ctx, flag, defaultValue).Value
+}
+
+func (e Confidence) GetDoubleFlag(ctx context.Context, flag string, defaultValue float64) FloatResolutionDetail {
+ resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Float64)
+ return ToFloatResolutionDetail(resp, defaultValue)
+}
+
+func (e Confidence) GetDoubleValue(ctx context.Context, flag string, defaultValue float64) float64 {
+ return e.GetDoubleFlag(ctx, flag, defaultValue).Value
+}
+
+func (e Confidence) GetStringFlag(ctx context.Context, flag string, defaultValue string) StringResolutionDetail {
+ resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.String)
+ return ToStringResolutionDetail(resp, defaultValue)
+}
+
+func (e Confidence) GetStringValue(ctx context.Context, flag string, defaultValue string) string {
+ return e.GetStringFlag(ctx, flag, defaultValue).Value
+}
+
+func (e Confidence) GetObjectFlag(ctx context.Context, flag string, defaultValue string) InterfaceResolutionDetail {
+ resp := e.ResolveFlag(ctx, flag, defaultValue, reflect.Map)
+ return resp
+}
+
+func (e Confidence) GetObjectValue(ctx context.Context, flag string, defaultValue string) interface{} {
+ return e.GetObjectFlag(ctx, flag, defaultValue).Value
+}
+
+func (e Confidence) ResolveFlag(ctx context.Context, flag string, defaultValue interface{}, expectedKind reflect.Kind) InterfaceResolutionDetail {
+ flagName, propertyPath := splitFlagString(flag)
+
+ requestFlagName := fmt.Sprintf("flags/%s", flagName)
+ resp, err := e.ResolveClient.SendResolveRequest(ctx,
+ ResolveRequest{ClientSecret: e.Config.APIKey,
+ Flags: []string{requestFlagName}, Apply: true, EvaluationContext: e.contextMap,
+ Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}})
+
+ if err != nil {
+ return processResolveError(err, defaultValue)
+ }
+ if len(resp.ResolvedFlags) == 0 {
+ return InterfaceResolutionDetail{
+ Value: defaultValue,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: FlagNotFoundCode,
+ ErrorMessage: "Flag not found",
+ FlagMetadata: nil,
+ },
+ }
+ }
+
+ resolvedFlag := resp.ResolvedFlags[0]
+ if resolvedFlag.Flag != requestFlagName {
+ return InterfaceResolutionDetail{
+ Value: defaultValue,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: FlagNotFoundCode,
+ ErrorMessage: fmt.Sprintf("unexpected flag '%s' from remote", strings.TrimPrefix(resolvedFlag.Flag, "flags/")),
+ FlagMetadata: nil,
+ },
+ }
+ }
+
+ return processResolvedFlag(resolvedFlag, defaultValue, expectedKind, propertyPath)
+}
diff --git a/confidence/confidence_internal_test.go b/confidence/confidence_internal_test.go
new file mode 100644
index 0000000..9b71b0f
--- /dev/null
+++ b/confidence/confidence_internal_test.go
@@ -0,0 +1,260 @@
+package confidence
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "reflect"
+ "fmt"
+ "testing"
+ "github.com/stretchr/testify/assert"
+)
+
+type MockResolveClient struct {
+ MockedResponse ResolveResponse
+ MockedError error
+ TestingT *testing.T
+}
+
+func (r MockResolveClient) SendResolveRequest(_ context.Context,
+ request ResolveRequest) (ResolveResponse, error) {
+ assert.Equal(r.TestingT, "user1", request.EvaluationContext["targeting_key"])
+ return r.MockedResponse, r.MockedError
+}
+
+func TestResolveBoolValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.boolean-key", false)
+
+ assert.Equal(t, true, evalDetails.Value)
+ assert.Equal(t, TargetingMatchReason, evalDetails.Reason)
+ assert.Equal(t, "flags/test-flag/variants/treatment", evalDetails.Variant)
+}
+
+func TestResolveIntValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetIntFlag(context.Background(), "test-flag.integer-key", 99)
+
+ assert.Equal(t, int64(40), evalDetails.Value)
+}
+
+func TestResolveDoubleValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetDoubleFlag(context.Background(), "test-flag.double-key", 99.99)
+
+ assert.Equal(t, 20.203, evalDetails.Value)
+}
+
+func TestResolveStringValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetStringFlag(context.Background(), "test-flag.string-key", "default")
+
+ assert.Equal(t, "treatment", evalDetails.Value)
+}
+
+func TestResolveObjectValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetObjectFlag(context.Background(), "test-flag.struct-key", "default")
+ _, ok := evalDetails.Value.(map[string]interface{})
+ assert.True(t, ok)
+}
+
+func TestResolveNestedValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.struct-key.boolean-key", true)
+ assert.Equal(t, false, evalDetails.Value)
+}
+
+func TestResolveDoubleNestedValue(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.struct-key.nested-struct-key.nested-boolean-key", true)
+ assert.Equal(t, false, evalDetails.Value)
+}
+
+func TestResolveWholeFlagAsObject(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetObjectFlag(context.Background(), "test-flag", "default")
+ _, ok := evalDetails.Value.(map[string]interface{})
+ assert.True(t, ok)
+}
+
+func TestResolveWholeFlagAsObjectWithInts(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetObjectFlag(context.Background(), "test-flag", "default")
+
+ value, _ := evalDetails.Value.(map[string]interface{})
+ rootIntValue := value["integer-key"]
+
+ assert.Equal(t, reflect.Int64, reflect.ValueOf(rootIntValue).Kind())
+ assert.Equal(t, int64(40), rootIntValue)
+
+ nestedIntValue := value["struct-key"].(map[string]interface{})["integer-key"]
+
+ assert.Equal(t, reflect.Int64, reflect.ValueOf(nestedIntValue).Kind())
+ assert.Equal(t, int64(23), nestedIntValue)
+}
+
+func TestResolveWithWrongType(t *testing.T) {
+ client := client(t, templateResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.integer-key", false)
+
+ assert.Equal(t, false, evalDetails.Value)
+ assert.Equal(t, ErrorReason, evalDetails.Reason)
+ assert.Equal(t, TypeMismatchCode, evalDetails.ErrorCode)
+}
+
+func TestResolveWithUnexpectedFlag(t *testing.T) {
+ client := client(t, templateResponseWithFlagName("wrong-flag"), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.boolean-key", true)
+
+ assert.Equal(t, true, evalDetails.Value)
+ assert.Equal(t, ErrorReason, evalDetails.Reason)
+ assert.Equal(t, FlagNotFoundCode, evalDetails.ErrorCode)
+ assert.Equal(t, "unexpected flag 'wrong-flag' from remote", evalDetails.ErrorMessage)
+}
+
+func TestResolveWithNonExistingFlag(t *testing.T) {
+ client := client(t, emptyResponse(), nil)
+ client.putContext("targeting_key", "user1")
+
+ evalDetails := client.GetBoolFlag(context.Background(), "test-flag.boolean-key", true)
+
+ assert.Equal(t, true, evalDetails.Value)
+ assert.Equal(t, ErrorReason, evalDetails.Reason)
+ assert.Equal(t, FlagNotFoundCode, evalDetails.ErrorCode)
+ assert.Equal(t, "Flag not found", evalDetails.ErrorMessage)
+}
+
+func client(t *testing.T, response ResolveResponse, errorToReturn error) *Confidence {
+ confidence := newConfidence("apiKey", MockResolveClient{MockedResponse: response, MockedError: errorToReturn, TestingT: t})
+ return confidence
+}
+
+func templateResponse() ResolveResponse {
+ return templateResponseWithFlagName("test-flag")
+}
+
+func templateResponseWithFlagName(flagName string) ResolveResponse {
+ templateResolveResponse := fmt.Sprintf(`
+{
+ "resolvedFlags": [
+ {
+ "flag": "flags/%[1]s",
+ "variant": "flags/%[1]s/variants/treatment",
+ "value": {
+ "struct-key": {
+ "boolean-key": false,
+ "string-key": "treatment-struct",
+ "double-key": 123.23,
+ "integer-key": 23,
+ "nested-struct-key": {
+ "nested-boolean-key": false
+ }
+ },
+ "boolean-key": true,
+ "string-key": "treatment",
+ "double-key": 20.203,
+ "integer-key": 40
+ },
+ "flagSchema": {
+ "schema": {
+ "struct-key": {
+ "structSchema": {
+ "schema": {
+ "boolean-key": {
+ "boolSchema": {}
+ },
+ "string-key": {
+ "stringSchema": {}
+ },
+ "double-key": {
+ "doubleSchema": {}
+ },
+ "integer-key": {
+ "intSchema": {}
+ },
+ "nested-struct-key": {
+ "structSchema": {
+ "schema": {
+ "nested-boolean-key": {
+ "boolSchema": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "boolean-key": {
+ "boolSchema": {}
+ },
+ "string-key": {
+ "stringSchema": {}
+ },
+ "double-key": {
+ "doubleSchema": {}
+ },
+ "integer-key": {
+ "intSchema": {}
+ }
+ }
+ },
+ "reason": "RESOLVE_REASON_MATCH"
+ }],
+ "resolveToken": ""
+}
+`, flagName)
+ var result ResolveResponse
+ decoder := json.NewDecoder(bytes.NewBuffer([]byte(templateResolveResponse)))
+ decoder.UseNumber()
+ _ = decoder.Decode(&result)
+ return result
+}
+
+func emptyResponse() ResolveResponse {
+ templateResolveResponse :=
+ `
+{
+ "resolvedFlags": [],
+ "resolveToken": ""
+}
+`
+ var result ResolveResponse
+ decoder := json.NewDecoder(bytes.NewBuffer([]byte(templateResolveResponse)))
+ decoder.UseNumber()
+ _ = decoder.Decode(&result)
+ return result
+}
+
+func newConfidence(apiKey string, client ResolveClient) *Confidence {
+ config := APIConfig{
+ APIKey: apiKey,
+ Region: APIRegionGlobal,
+ }
+ return &Confidence{
+ Config: config,
+ ResolveClient: client,
+ contextMap: make(map[string]interface{}),
+ }
+}
diff --git a/confidence/go.mod b/confidence/go.mod
new file mode 100644
index 0000000..463dc1c
--- /dev/null
+++ b/confidence/go.mod
@@ -0,0 +1,11 @@
+module github.com/spotify/confidence-openfeature-provider-go/confidence
+
+go 1.22.2
+
+require github.com/stretchr/testify v1.9.0
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/confidence/go.sum b/confidence/go.sum
new file mode 100644
index 0000000..266857e
--- /dev/null
+++ b/confidence/go.sum
@@ -0,0 +1,11 @@
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/provider/http_resolve_client.go b/confidence/http_resolve_client.go
similarity index 71%
rename from pkg/provider/http_resolve_client.go
rename to confidence/http_resolve_client.go
index c283cfe..45a36bd 100644
--- a/pkg/provider/http_resolve_client.go
+++ b/confidence/http_resolve_client.go
@@ -1,4 +1,4 @@
-package provider
+package confidence
import (
"bytes"
@@ -9,7 +9,7 @@ import (
"net/http"
)
-type httpResolveClient struct {
+type HttpResolveClient struct {
Client *http.Client
Config APIConfig
}
@@ -25,37 +25,37 @@ func parseErrorMessage(body io.ReadCloser) string {
return resolveError.Message
}
-func (client httpResolveClient) sendResolveRequest(ctx context.Context,
- request resolveRequest) (resolveResponse, error) {
+func (client HttpResolveClient) SendResolveRequest(ctx context.Context,
+ request ResolveRequest) (ResolveResponse, error) {
jsonRequest, err := json.Marshal(request)
if err != nil {
- return resolveResponse{}, fmt.Errorf("error when serializing request to the resolver service: %w", err)
+ return ResolveResponse{}, fmt.Errorf("error when serializing request to the resolver service: %w", err)
}
payload := bytes.NewBuffer(jsonRequest)
req, err := http.NewRequestWithContext(ctx,
http.MethodPost, fmt.Sprintf("%s/flags:resolve", client.Config.Region.apiURL()), payload)
if err != nil {
- return resolveResponse{}, err
+ return ResolveResponse{}, err
}
resp, err := client.Client.Do(req)
if err != nil {
- return resolveResponse{}, fmt.Errorf("error when calling the resolver service: %w", err)
+ return ResolveResponse{}, fmt.Errorf("error when calling the resolver service: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return resolveResponse{},
+ return ResolveResponse{},
fmt.Errorf("got '%s' error from the resolver service: %s", resp.Status, parseErrorMessage(resp.Body))
}
- var result resolveResponse
+ var result ResolveResponse
decoder := json.NewDecoder(resp.Body)
decoder.UseNumber()
err = decoder.Decode(&result)
if err != nil {
- return resolveResponse{}, fmt.Errorf("error when deserializing response from the resolver service: %w", err)
+ return ResolveResponse{}, fmt.Errorf("error when deserializing response from the resolver service: %w", err)
}
return result, nil
}
diff --git a/pkg/provider/models.go b/confidence/models.go
similarity index 53%
rename from pkg/provider/models.go
rename to confidence/models.go
index 912704e..6cb4d25 100644
--- a/pkg/provider/models.go
+++ b/confidence/models.go
@@ -1,4 +1,4 @@
-package provider
+package confidence
import (
"context"
@@ -7,6 +7,57 @@ import (
type APIRegion int64
+func NewFlagNotFoundResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: FlagNotFoundCode,
+ message: msg,
+ }
+}
+
+func NewParseErrorResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: ParseErrorCode,
+ message: msg,
+ }
+}
+
+// NewTypeMismatchResolutionError constructs a resolution error with code TYPE_MISMATCH
+//
+// Explanation - The type of the flag value does not match the expected type.
+func NewTypeMismatchResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: TypeMismatchCode,
+ message: msg,
+ }
+}
+
+// NewTargetingKeyMissingResolutionError constructs a resolution error with code TARGETING_KEY_MISSING
+//
+// Explanation - The provider requires a targeting key and one was not provided in the evaluation context.
+func NewTargetingKeyMissingResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: TargetingKeyMissingCode,
+ message: msg,
+ }
+}
+
+func NewInvalidContextResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: InvalidContextCode,
+ message: msg,
+ }
+}
+
+// NewGeneralResolutionError constructs a resolution error with code GENERAL
+//
+// Explanation - The error was for a reason not enumerated above.
+func NewGeneralResolutionError(msg string) ResolutionError {
+ return ResolutionError{
+ code: GeneralCode,
+ message: msg,
+ }
+}
+
type APIConfig struct {
APIKey string
Region APIRegion
@@ -42,7 +93,7 @@ func (r APIRegion) apiURL() string {
return ""
}
-func (c APIConfig) validate() error {
+func (c APIConfig) Validate() error {
if c.APIKey == "" {
return errors.New("api key needs to be set")
}
@@ -52,13 +103,13 @@ func (c APIConfig) validate() error {
return nil
}
-type resolveClient interface {
- sendResolveRequest(ctx context.Context, request resolveRequest) (resolveResponse, error)
+type ResolveClient interface {
+ SendResolveRequest(ctx context.Context, request ResolveRequest) (ResolveResponse, error)
}
var errFlagNotFound = errors.New("flag not found")
-type resolveRequest struct {
+type ResolveRequest struct {
ClientSecret string `json:"client_secret"`
Apply bool `json:"apply"`
EvaluationContext map[string]interface{} `json:"evaluation_context"`
@@ -71,7 +122,7 @@ type sdk struct {
Version string `json:"version"`
}
-type resolveResponse struct {
+type ResolveResponse struct {
ResolvedFlags []resolvedFlag `json:"resolvedFlags"`
ResolveToken string `json:"resolveToken"`
}
diff --git a/confidence/resolution_details.go b/confidence/resolution_details.go
new file mode 100644
index 0000000..244294a
--- /dev/null
+++ b/confidence/resolution_details.go
@@ -0,0 +1,84 @@
+package confidence
+
+import (
+ "fmt"
+)
+
+type ResolutionError struct {
+ // fields are unexported, this means providers are forced to create structs of this type using one of the constructors below.
+ // this effectively emulates an enum
+ code ErrorCode
+ message string
+}
+
+func (r ResolutionError) Error() string {
+ return fmt.Sprintf("%s: %s", r.code, r.message)
+}
+
+type ResolutionDetail struct {
+ Variant string
+ Reason Reason
+ ErrorCode ErrorCode
+ ErrorMessage string
+ FlagMetadata FlagMetadata
+}
+
+// FlagMetadata is a structure which supports definition of arbitrary properties, with keys of type string, and values
+// of type boolean, string, int64 or float64. This structure is populated by a provider for use by an Application
+// Author (via the Evaluation API) or an Application Integrator (via hooks).
+type FlagMetadata map[string]interface{}
+type Reason string
+
+type ErrorCode string
+
+const (
+ // ProviderNotReadyCode - the value was resolved before the provider was ready.
+ ProviderNotReadyCode ErrorCode = "PROVIDER_NOT_READY"
+ // FlagNotFoundCode - the flag could not be found.
+ FlagNotFoundCode ErrorCode = "FLAG_NOT_FOUND"
+ // ParseErrorCode - an error was encountered parsing data, such as a flag configuration.
+ ParseErrorCode ErrorCode = "PARSE_ERROR"
+ // TypeMismatchCode - the type of the flag value does not match the expected type.
+ TypeMismatchCode ErrorCode = "TYPE_MISMATCH"
+ // TargetingKeyMissingCode - the provider requires a targeting key and one was not provided in the evaluation context.
+ TargetingKeyMissingCode ErrorCode = "TARGETING_KEY_MISSING"
+ // InvalidContextCode - the evaluation context does not meet provider requirements.
+ InvalidContextCode ErrorCode = "INVALID_CONTEXT"
+ // GeneralCode - the error was for a reason not enumerated above.
+ GeneralCode ErrorCode = "GENERAL"
+)
+
+// BoolResolutionDetail provides a resolution detail with boolean type
+type BoolResolutionDetail struct {
+ Value bool
+ ResolutionDetail
+}
+
+// StringResolutionDetail provides a resolution detail with string type
+type StringResolutionDetail struct {
+ Value string
+ ResolutionDetail
+}
+
+// FloatResolutionDetail provides a resolution detail with float64 type
+type FloatResolutionDetail struct {
+ Value float64
+ ResolutionDetail
+}
+
+// IntResolutionDetail provides a resolution detail with int64 type
+type IntResolutionDetail struct {
+ Value int64
+ ResolutionDetail
+}
+
+// InterfaceResolutionDetail provides a resolution detail with interface{} type
+type InterfaceResolutionDetail struct {
+ Value interface{}
+ ResolutionDetail
+}
+
+// Metadata provides provider name
+type Metadata struct {
+ Name string
+}
diff --git a/confidence/utils.go b/confidence/utils.go
new file mode 100644
index 0000000..0ec3b0d
--- /dev/null
+++ b/confidence/utils.go
@@ -0,0 +1,360 @@
+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,
+ }
+}
diff --git a/pkg/provider/utils_test.go b/confidence/utils_test.go
similarity index 59%
rename from pkg/provider/utils_test.go
rename to confidence/utils_test.go
index 35acd0b..38b2f90 100644
--- a/pkg/provider/utils_test.go
+++ b/confidence/utils_test.go
@@ -1,9 +1,8 @@
-package provider
+package confidence
import (
"encoding/json"
"errors"
- "github.com/open-feature/go-sdk/openfeature"
"github.com/stretchr/testify/assert"
"reflect"
"testing"
@@ -161,22 +160,20 @@ func TestProcessResolveError(t *testing.T) {
t.Run("FlagNotFoundError", func(t *testing.T) {
res := processResolveError(errFlagNotFound, defaultValue)
assert.Equal(t, defaultValue, res.Value)
- assert.IsType(t, openfeature.ResolutionError{}, res.ProviderResolutionDetail.ResolutionError)
- resDetails := res.ProviderResolutionDetail.ResolutionDetail()
- assert.Equal(t, openfeature.FlagNotFoundCode, resDetails.ErrorCode)
- assert.Equal(t, openfeature.ErrorReason, resDetails.Reason)
+ resDetails := res.ResolutionDetail
+ assert.Equal(t, FlagNotFoundCode, resDetails.ErrorCode)
+ assert.Equal(t, ErrorReason, resDetails.Reason)
})
t.Run("GeneralError", func(t *testing.T) {
err := errors.New("unknown error")
res := processResolveError(err, defaultValue)
assert.Equal(t, defaultValue, res.Value)
- assert.IsType(t, openfeature.ResolutionError{}, res.ProviderResolutionDetail.ResolutionError)
- resDetails := res.ProviderResolutionDetail.ResolutionDetail()
- assert.Equal(t, openfeature.GeneralCode, resDetails.ErrorCode)
- assert.Equal(t, openfeature.ErrorReason, resDetails.Reason)
+ resDetails := res.ResolutionDetail
+ assert.Equal(t, GeneralCode, resDetails.ErrorCode)
+ assert.Equal(t, ErrorReason, resDetails.Reason)
})
}
@@ -188,10 +185,10 @@ func TestProcessResolvedFlag(t *testing.T) {
FlagSchema: flagSchema{Schema: map[string]interface{}{}},
}
- expected := openfeature.InterfaceResolutionDetail{
+ expected := InterfaceResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.DefaultReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: DefaultReason,
},
}
@@ -205,12 +202,14 @@ func TestProcessResolvedFlag(t *testing.T) {
FlagSchema: flagSchema{Schema: map[string]interface{}{"key": "wrongType"}},
}
- expected := openfeature.InterfaceResolutionDetail{
+ expected := InterfaceResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError(
- "schema for property key does not match the expected type"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "schema for property key does not match the expected type",
+ FlagMetadata: nil,
},
}
@@ -225,8 +224,13 @@ func TestProcessResolvedFlag(t *testing.T) {
}
expected := typeMismatchError(defaultValue)
- expected.ProviderResolutionDetail.ResolutionError =
- openfeature.NewTypeMismatchResolutionError("schema for property key.missing does not match the expected type")
+ expected.ResolutionDetail = ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "schema for property key.missing does not match the expected type",
+ FlagMetadata: nil,
+ }
assert.Equal(t, expected, processResolvedFlag(rf, defaultValue, reflect.String, "key.missing"))
})
@@ -238,10 +242,10 @@ func TestProcessResolvedFlag(t *testing.T) {
FlagSchema: flagSchema{Schema: map[string]interface{}{"key": map[string]interface{}{"stringSchema": "value"}}},
}
- expected := openfeature.InterfaceResolutionDetail{
+ expected := InterfaceResolutionDetail{
Value: "value", // Success case excludes default value
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
assert.Equal(t, expected, processResolvedFlag(rf, defaultValue, reflect.String, "key"))
@@ -349,12 +353,14 @@ func TestReplaceNumbers(t *testing.T) {
func TestTypeMismatchError(t *testing.T) {
t.Run("WithStringValue", func(t *testing.T) {
defaultValue := "my default value"
- expected := openfeature.InterfaceResolutionDetail{
+ expected := InterfaceResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError(
- "Unable to extract property value from resolve response"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to extract property value from resolve response",
+ FlagMetadata: nil,
},
}
@@ -363,12 +369,14 @@ func TestTypeMismatchError(t *testing.T) {
t.Run("WithIntValue", func(t *testing.T) {
defaultValue := 123
- expected := openfeature.InterfaceResolutionDetail{
+ expected := InterfaceResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError(
- "Unable to extract property value from resolve response"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to extract property value from resolve response",
+ FlagMetadata: nil,
},
}
@@ -380,58 +388,61 @@ func TestToBoolResolutionDetail(t *testing.T) {
defaultValue := false
t.Run("WhenValueIsBool", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: true,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.BoolResolutionDetail{
+ expected := BoolResolutionDetail{
Value: true,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- assert.Equal(t, expected, toBoolResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToBoolResolutionDetail(res, defaultValue))
})
t.Run("WhenValueIsNotBool", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: "not a bool",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.BoolResolutionDetail{
+ expected := BoolResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to boolean"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to convert response property to boolean",
+ FlagMetadata: nil,
},
}
- assert.Equal(t, expected, toBoolResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToBoolResolutionDetail(res, defaultValue))
})
t.Run("WhenReasonIsNotTargetingMatchReason", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: true,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- expected := openfeature.BoolResolutionDetail{
+ expected := BoolResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- assert.Equal(t, expected, toBoolResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToBoolResolutionDetail(res, defaultValue))
})
}
@@ -439,58 +450,61 @@ func TestToStringResolutionDetail(t *testing.T) {
defaultValue := "default"
t.Run("WhenValueIsString", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: "hello",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.StringResolutionDetail{
+ expected := StringResolutionDetail{
Value: "hello",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- assert.Equal(t, expected, toStringResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToStringResolutionDetail(res, defaultValue))
})
t.Run("WhenValueIsNotString", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: 123,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.StringResolutionDetail{
+ expected := StringResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to boolean"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to convert response property to boolean",
+ FlagMetadata: nil,
},
}
- assert.Equal(t, expected, toStringResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToStringResolutionDetail(res, defaultValue))
})
t.Run("WhenReasonIsNotTargetingMatchReason", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: "hello",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- expected := openfeature.StringResolutionDetail{
+ expected := StringResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- assert.Equal(t, expected, toStringResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToStringResolutionDetail(res, defaultValue))
})
}
@@ -498,115 +512,121 @@ func TestToFloatResolutionDetail(t *testing.T) {
defaultValue := 42.0
t.Run("WhenValueIsFloat", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: 24.0,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.FloatResolutionDetail{
+ expected := FloatResolutionDetail{
Value: 24.0,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- assert.Equal(t, expected, toFloatResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToFloatResolutionDetail(res, defaultValue))
})
t.Run("WhenValueIsNotFloat", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: "not a float",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.FloatResolutionDetail{
+ expected := FloatResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to float"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to convert response property to float",
+ FlagMetadata: nil,
},
}
- assert.Equal(t, expected, toFloatResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToFloatResolutionDetail(res, defaultValue))
})
t.Run("WhenReasonIsNotTargetingMatchReason", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: 24.0,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- expected := openfeature.FloatResolutionDetail{
+ expected := FloatResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- assert.Equal(t, expected, toFloatResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToFloatResolutionDetail(res, defaultValue))
})
}
func TestToIntResolutionDetail(t *testing.T) {
defaultValue := int64(123)
t.Run("WhenValueIsInt", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: int64(456),
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.IntResolutionDetail{
+ expected := IntResolutionDetail{
Value: int64(456),
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- assert.Equal(t, expected, toIntResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToIntResolutionDetail(res, defaultValue))
})
t.Run("WhenValueIsNotInt", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: "not an int",
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.TargetingMatchReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: TargetingMatchReason,
},
}
- expected := openfeature.IntResolutionDetail{
+ expected := IntResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to int"),
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Variant: "",
+ Reason: ErrorReason,
+ ErrorCode: TypeMismatchCode,
+ ErrorMessage: "Unable to convert response property to int",
+ FlagMetadata: nil,
},
}
- assert.Equal(t, expected, toIntResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToIntResolutionDetail(res, defaultValue))
})
t.Run("WhenReasonIsNotTargetingMatchReason", func(t *testing.T) {
- res := openfeature.InterfaceResolutionDetail{
+ res := InterfaceResolutionDetail{
Value: int64(456),
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- expected := openfeature.IntResolutionDetail{
+ expected := IntResolutionDetail{
Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.ErrorReason,
+ ResolutionDetail: ResolutionDetail{
+ Reason: ErrorReason,
},
}
- assert.Equal(t, expected, toIntResolutionDetail(res, defaultValue))
+ assert.Equal(t, expected, ToIntResolutionDetail(res, defaultValue))
})
}
diff --git a/demo/GoDemoApp.go b/demo/GoDemoApp.go
index b0e9c2f..253842e 100644
--- a/demo/GoDemoApp.go
+++ b/demo/GoDemoApp.go
@@ -6,18 +6,17 @@ import (
"github.com/google/uuid"
"github.com/open-feature/go-sdk/openfeature"
- confidence "github.com/spotify/confidence-openfeature-provider-go/pkg/provider"
+ c "github.com/spotify/confidence-openfeature-provider-go/confidence"
+ p "github.com/spotify/confidence-openfeature-provider-go/provider"
)
func main() {
clientSecret := "CLIENT_SECRET"
fmt.Println("Fetching the flags...")
- provider, err := confidence.NewFlagProvider(*confidence.NewAPIConfig(clientSecret))
+ confidence := c.NewConfidenceBuilder().SetAPIConfig(c.APIConfig{APIKey: clientSecret}).Build()
- if err != nil {
- // handle error
- }
+ provider := p.NewFlagProvider(confidence)
openfeature.SetProvider(provider)
client := openfeature.NewClient("testApp")
diff --git a/demo/go.mod b/demo/go.mod
index 6d9a544..1c53a5d 100644
--- a/demo/go.mod
+++ b/demo/go.mod
@@ -1,14 +1,17 @@
module demo
-go 1.19
+go 1.22.2
require (
github.com/google/uuid v1.6.0
github.com/open-feature/go-sdk v1.10.0
- github.com/spotify/confidence-openfeature-provider-go v0.1.7
+ github.com/spotify/confidence-openfeature-provider-go/confidence v1.0.0
+ github.com/spotify/confidence-openfeature-provider-go/provider v0.1.7
)
-replace github.com/spotify/confidence-openfeature-provider-go v0.1.7 => ../
+replace github.com/spotify/confidence-openfeature-provider-go/provider v0.1.7 => ../provider
+
+replace github.com/spotify/confidence-openfeature-provider-go/confidence v1.0.0 => ../confidence
require (
github.com/go-logr/logr v1.4.1 // indirect
diff --git a/demo/go.sum b/demo/go.sum
index dfd5b5f..39c4ad6 100644
--- a/demo/go.sum
+++ b/demo/go.sum
@@ -1,14 +1,20 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM=
github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go.sum b/go.sum
deleted file mode 100644
index a2c86c1..0000000
--- a/go.sum
+++ /dev/null
@@ -1,20 +0,0 @@
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM=
-github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
-golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
deleted file mode 100755
index 5634a09..0000000
--- a/pkg/provider/provider.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package provider
-
-import (
- "context"
- "fmt"
- "net/http"
- "reflect"
- "strings"
-
- "github.com/open-feature/go-sdk/openfeature"
-)
-
-type FlagProvider struct {
- Config APIConfig
- ResolveClient resolveClient
-}
-
-var (
- SDK_ID = "SDK_ID_GO_PROVIDER"
- SDK_VERSION = "0.1.7" // x-release-please-version
-)
-
-func NewFlagProvider(config APIConfig) (*FlagProvider, error) {
- validationError := config.validate()
- if validationError != nil {
- return nil, validationError
- }
- return &FlagProvider{Config: config,
- ResolveClient: httpResolveClient{Client: &http.Client{}, Config: config}}, nil
-}
-
-func (e FlagProvider) Metadata() openfeature.Metadata {
- return openfeature.Metadata{Name: "ConfidenceFlagProvider"}
-}
-
-func (e FlagProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool,
- evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
- res := e.resolveFlag(ctx, flag, defaultValue, evalCtx, reflect.Bool)
- return toBoolResolutionDetail(res, defaultValue)
-}
-
-func (e FlagProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string,
- evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
- res := e.resolveFlag(ctx, flag, defaultValue, evalCtx, reflect.String)
- return toStringResolutionDetail(res, defaultValue)
-}
-
-func (e FlagProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64,
- evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
- res := e.resolveFlag(ctx, flag, defaultValue, evalCtx, reflect.Float64)
- return toFloatResolutionDetail(res, defaultValue)
-}
-
-func (e FlagProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64,
- evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
- res := e.resolveFlag(ctx, flag, defaultValue, evalCtx, reflect.Int64)
- return toIntResolutionDetail(res, defaultValue)
-}
-
-func (e FlagProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{},
- evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
- return e.resolveFlag(ctx, flag, defaultValue, evalCtx, reflect.Map)
-}
-
-func (e FlagProvider) resolveFlag(ctx context.Context, flag string, defaultValue interface{},
- evalCtx openfeature.FlattenedContext, expectedKind reflect.Kind) openfeature.InterfaceResolutionDetail {
- flagName, propertyPath := splitFlagString(flag)
-
- requestFlagName := fmt.Sprintf("flags/%s", flagName)
- resp, err := e.ResolveClient.sendResolveRequest(ctx,
- resolveRequest{ClientSecret: e.Config.APIKey,
- Flags: []string{requestFlagName}, Apply: true, EvaluationContext: processTargetingKey(evalCtx),
- Sdk: sdk{Id: SDK_ID, Version: SDK_VERSION}})
-
- if err != nil {
- return processResolveError(err, defaultValue)
- }
-
- if len(resp.ResolvedFlags) == 0 {
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("no active flag '%s' was found", flagName)),
- Reason: openfeature.ErrorReason}}
- }
-
- resolvedFlag := resp.ResolvedFlags[0]
- if resolvedFlag.Flag != requestFlagName {
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("unexpected flag '%s' from remote", strings.TrimPrefix(resolvedFlag.Flag, "flags/"))),
- Reason: openfeature.ErrorReason}}
- }
-
- return processResolvedFlag(resolvedFlag, defaultValue, expectedKind, propertyPath)
-}
-
-func processTargetingKey(evalCtx openfeature.FlattenedContext) openfeature.FlattenedContext {
- newEvalContext := openfeature.FlattenedContext{}
- newEvalContext = evalCtx
- if targetingKey, exists := evalCtx["targetingKey"]; exists {
- newEvalContext["targeting_key"] = targetingKey
- }
- return newEvalContext
-}
-
-func (e FlagProvider) Hooks() []openfeature.Hook {
- return []openfeature.Hook{}
-}
diff --git a/pkg/provider/utils.go b/pkg/provider/utils.go
deleted file mode 100644
index 5138e1c..0000000
--- a/pkg/provider/utils.go
+++ /dev/null
@@ -1,291 +0,0 @@
-package provider
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "reflect"
- "strings"
-
- "github.com/open-feature/go-sdk/openfeature"
-)
-
-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{}) openfeature.InterfaceResolutionDetail {
- switch {
- case errors.Is(err, errFlagNotFound):
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewFlagNotFoundResolutionError("error when resolving, flag not found"),
- Reason: openfeature.ErrorReason}}
- default:
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewGeneralResolutionError("error when resolving, returning default value"),
- Reason: openfeature.ErrorReason}}
- }
-}
-
-func processResolvedFlag(resolvedFlag resolvedFlag, defaultValue interface{},
- expectedKind reflect.Kind, propertyPath string) openfeature.InterfaceResolutionDetail {
- if len(resolvedFlag.Value) == 0 {
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.DefaultReason}}
- }
-
- actualKind, schemaErr := getTypeForPath(resolvedFlag.FlagSchema.Schema, propertyPath)
- if schemaErr != nil || actualKind != expectedKind {
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError(
- fmt.Sprintf("schema for property %s does not match the expected type",
- propertyPath)),
- Reason: openfeature.ErrorReason}}
- }
-
- 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 openfeature.InterfaceResolutionDetail{
- Value: extractedValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- Reason: openfeature.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{}) openfeature.InterfaceResolutionDetail {
- return openfeature.InterfaceResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError(
- "Unable to extract property value from resolve response"),
- Reason: openfeature.ErrorReason}}
-}
-
-func toBoolResolutionDetail(res openfeature.InterfaceResolutionDetail,
- defaultValue bool) openfeature.BoolResolutionDetail {
- if res.ProviderResolutionDetail.Reason == openfeature.TargetingMatchReason {
- v, ok := res.Value.(bool)
- if ok {
- return openfeature.BoolResolutionDetail{
- Value: v,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
- }
-
- return openfeature.BoolResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to boolean"),
- Reason: openfeature.ErrorReason,
- },
- }
- }
-
- return openfeature.BoolResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
-}
-
-func toStringResolutionDetail(res openfeature.InterfaceResolutionDetail,
- defaultValue string) openfeature.StringResolutionDetail {
- if res.ProviderResolutionDetail.Reason == openfeature.TargetingMatchReason {
- v, ok := res.Value.(string)
- if ok {
- return openfeature.StringResolutionDetail{
- Value: v,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
- }
-
- return openfeature.StringResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to boolean"),
- Reason: openfeature.ErrorReason,
- },
- }
- }
-
- return openfeature.StringResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
-}
-
-func toFloatResolutionDetail(res openfeature.InterfaceResolutionDetail,
- defaultValue float64) openfeature.FloatResolutionDetail {
- if res.ProviderResolutionDetail.Reason == openfeature.TargetingMatchReason {
- v, ok := res.Value.(float64)
- if ok {
- return openfeature.FloatResolutionDetail{
- Value: v,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
- }
-
- return openfeature.FloatResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to float"),
- Reason: openfeature.ErrorReason,
- },
- }
- }
-
- return openfeature.FloatResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
-}
-
-func toIntResolutionDetail(res openfeature.InterfaceResolutionDetail,
- defaultValue int64) openfeature.IntResolutionDetail {
- if res.ProviderResolutionDetail.Reason == openfeature.TargetingMatchReason {
- v, ok := res.Value.(int64)
- if ok {
- return openfeature.IntResolutionDetail{
- Value: v,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
- }
-
- return openfeature.IntResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: openfeature.ProviderResolutionDetail{
- ResolutionError: openfeature.NewTypeMismatchResolutionError("Unable to convert response property to int"),
- Reason: openfeature.ErrorReason,
- },
- }
- }
-
- return openfeature.IntResolutionDetail{
- Value: defaultValue,
- ProviderResolutionDetail: res.ProviderResolutionDetail,
- }
-}
diff --git a/provider/.gitignore b/provider/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/provider/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/go.mod b/provider/go.mod
similarity index 51%
rename from go.mod
rename to provider/go.mod
index cea4a4e..625b8b9 100644
--- a/go.mod
+++ b/provider/go.mod
@@ -1,16 +1,18 @@
-module github.com/spotify/confidence-openfeature-provider-go
+module github.com/spotify/confidence-openfeature-provider-go/provider
-go 1.19
+go 1.22.2
+
+replace github.com/spotify/confidence-openfeature-provider-go/confidence v1.0.0 => ../confidence
require (
github.com/open-feature/go-sdk v1.10.0
- github.com/stretchr/testify v1.8.4
+ github.com/spotify/confidence-openfeature-provider-go/confidence v1.0.0
+ github.com/stretchr/testify v1.9.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
- github.com/google/uuid v1.6.0
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/provider/go.sum b/provider/go.sum
new file mode 100644
index 0000000..ad92d30
--- /dev/null
+++ b/provider/go.sum
@@ -0,0 +1,32 @@
+github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
+github.com/cucumber/godog v0.14.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
+github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/open-feature/go-sdk v1.10.0 h1:druQtYOrN+gyz3rMsXp0F2jW1oBXJb0V26PVQnUGLbM=
+github.com/open-feature/go-sdk v1.10.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
+golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/provider/google-java-format.xml b/provider/google-java-format.xml
new file mode 100644
index 0000000..2aa056d
--- /dev/null
+++ b/provider/google-java-format.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/provider/misc.xml b/provider/misc.xml
new file mode 100644
index 0000000..3d3ab27
--- /dev/null
+++ b/provider/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/provider/modules.xml b/provider/modules.xml
new file mode 100644
index 0000000..1f2e99d
--- /dev/null
+++ b/provider/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/provider/provider.go b/provider/provider.go
new file mode 100644
index 0000000..c71579f
--- /dev/null
+++ b/provider/provider.go
@@ -0,0 +1,133 @@
+package provider
+
+import (
+ "context"
+ "github.com/open-feature/go-sdk/openfeature"
+ c "github.com/spotify/confidence-openfeature-provider-go/confidence"
+ "reflect"
+)
+
+type FlagProvider struct {
+ confidence c.Confidence
+}
+
+func NewFlagProvider(confidence c.Confidence) *FlagProvider {
+ return &FlagProvider{
+ confidence: confidence,
+ }
+}
+
+func (e FlagProvider) Metadata() openfeature.Metadata {
+ return openfeature.Metadata{Name: "ConfidenceFlagProvider"}
+}
+
+func (e FlagProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool,
+ evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail {
+ confidence := e.confidence.WithContext(processTargetingKey(evalCtx))
+ res := confidence.ResolveFlag(ctx, flag, defaultValue, reflect.Bool)
+ boolDetail := c.ToBoolResolutionDetail(res, defaultValue)
+ return openfeature.BoolResolutionDetail{
+ Value: boolDetail.Value,
+ ProviderResolutionDetail: toOFResolutionDetail(boolDetail.ResolutionDetail),
+ }
+}
+
+func (e FlagProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string,
+ evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail {
+ confidence := e.confidence.WithContext(processTargetingKey(evalCtx))
+ res := confidence.ResolveFlag(ctx, flag, defaultValue, reflect.String)
+ detail := c.ToStringResolutionDetail(res, defaultValue)
+ return openfeature.StringResolutionDetail{
+ Value: detail.Value,
+ ProviderResolutionDetail: toOFResolutionDetail(detail.ResolutionDetail),
+ }
+}
+
+func (e FlagProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64,
+ evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail {
+ confidence := e.confidence.WithContext(processTargetingKey(evalCtx))
+ res := confidence.ResolveFlag(ctx, flag, defaultValue, reflect.Float64)
+ detail := c.ToFloatResolutionDetail(res, defaultValue)
+ return openfeature.FloatResolutionDetail{
+ Value: detail.Value,
+ ProviderResolutionDetail: toOFResolutionDetail(detail.ResolutionDetail),
+ }
+}
+
+func (e FlagProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64,
+ evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail {
+ confidence := e.confidence.WithContext(processTargetingKey(evalCtx))
+ res := confidence.ResolveFlag(ctx, flag, defaultValue, reflect.Int64)
+ detail := c.ToIntResolutionDetail(res, defaultValue)
+ return openfeature.IntResolutionDetail{
+ Value: detail.Value,
+ ProviderResolutionDetail: toOFResolutionDetail(detail.ResolutionDetail),
+ }
+}
+
+func (e FlagProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{},
+ evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail {
+ confidence := e.confidence.WithContext(processTargetingKey(evalCtx))
+ res := confidence.ResolveFlag(ctx, flag, defaultValue, reflect.Map)
+ detail := c.ToObjectResolutionDetail(res, defaultValue)
+ return openfeature.InterfaceResolutionDetail{
+ Value: detail.Value,
+ ProviderResolutionDetail: toOFResolutionDetail(detail.ResolutionDetail),
+ }
+}
+
+func (e FlagProvider) Hooks() []openfeature.Hook {
+ return []openfeature.Hook{}
+}
+
+func toOFResolutionDetail(detail c.ResolutionDetail) openfeature.ProviderResolutionDetail {
+ return openfeature.ProviderResolutionDetail{
+ ResolutionError: toOFResolutionError(detail.ErrorCode, detail.ErrorMessage),
+ Reason: toOFReason(detail.Reason),
+ Variant: detail.Variant,
+ FlagMetadata: toOFFlagMetadata(detail.FlagMetadata),
+ }
+}
+
+func toOFResolutionError(code c.ErrorCode, message string) openfeature.ResolutionError {
+ switch code {
+ case c.TypeMismatchCode:
+ return openfeature.NewTypeMismatchResolutionError(message)
+ case c.FlagNotFoundCode:
+ return openfeature.NewFlagNotFoundResolutionError(message)
+ case c.GeneralCode:
+ return openfeature.NewGeneralResolutionError(message)
+ case c.InvalidContextCode:
+ return openfeature.NewInvalidContextResolutionError(message)
+ case c.ProviderNotReadyCode:
+ return openfeature.NewProviderNotReadyResolutionError(message)
+ case c.ParseErrorCode:
+ return openfeature.NewParseErrorResolutionError(message)
+ }
+ return openfeature.ResolutionError{}
+}
+
+func processTargetingKey(evalCtx openfeature.FlattenedContext) openfeature.FlattenedContext {
+ newEvalContext := openfeature.FlattenedContext{}
+ newEvalContext = evalCtx
+ if targetingKey, exists := evalCtx["targetingKey"]; exists {
+ newEvalContext["targeting_key"] = targetingKey
+ }
+ delete(newEvalContext, "targetingKey")
+ return newEvalContext
+}
+
+func toOFFlagMetadata(metadata c.FlagMetadata) map[string]interface{} {
+ return metadata
+}
+
+func toOFReason(reason c.Reason) openfeature.Reason {
+ switch reason {
+ case c.TargetingMatchReason:
+ return openfeature.TargetingMatchReason
+ case c.DefaultReason:
+ return openfeature.DefaultReason
+ default:
+ return openfeature.ErrorReason
+ }
+}
diff --git a/provider/provider.iml b/provider/provider.iml
new file mode 100644
index 0000000..25ed3f6
--- /dev/null
+++ b/provider/provider.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pkg/provider/provider_internal_test.go b/provider/provider_internal_test.go
similarity index 77%
rename from pkg/provider/provider_internal_test.go
rename to provider/provider_internal_test.go
index 7a51fda..9a77e44 100644
--- a/pkg/provider/provider_internal_test.go
+++ b/provider/provider_internal_test.go
@@ -5,23 +5,23 @@ import (
"context"
"encoding/json"
"fmt"
- "reflect"
- "testing"
-
"github.com/open-feature/go-sdk/openfeature"
+ confidence "github.com/spotify/confidence-openfeature-provider-go/confidence"
"github.com/stretchr/testify/assert"
+ "reflect"
+ "testing"
)
type MockResolveClient struct {
- MockedResponse resolveResponse
+ MockedResponse confidence.ResolveResponse
MockedError error
TestingT *testing.T
}
-func (r MockResolveClient) sendResolveRequest(_ context.Context,
- request resolveRequest) (resolveResponse, error) {
+func (r MockResolveClient) SendResolveRequest(_ context.Context,
+ request confidence.ResolveRequest) (confidence.ResolveResponse, error) {
assert.Equal(r.TestingT, "user1", request.EvaluationContext["targeting_key"])
- return r.MockedResponse, r.MockedError
+ return r.MockedResponse, r.MockedError
}
func TestResolveBoolValue(t *testing.T) {
@@ -187,60 +187,63 @@ func TestResolveWithNonExistingFlag(t *testing.T) {
assert.Equal(t, true, evalDetails.Value)
assert.Equal(t, openfeature.ErrorReason, evalDetails.Reason)
assert.Equal(t, openfeature.FlagNotFoundCode, evalDetails.ErrorCode)
- assert.Equal(t, "no active flag 'test-flag' was found", evalDetails.ErrorMessage)
+ assert.Equal(t, "Flag not found", evalDetails.ErrorMessage)
}
-func client(t *testing.T, response resolveResponse, errorToReturn error) *openfeature.Client {
- provider := FlagProvider{Config: APIConfig{APIKey: "apikey",
- Region: APIRegionGlobal}, ResolveClient: MockResolveClient{MockedResponse: response, MockedError: errorToReturn, TestingT: t}}
+func client(t *testing.T, response confidence.ResolveResponse, errorToReturn error) *openfeature.Client {
+ resolveClient := MockResolveClient{MockedResponse: response, MockedError: errorToReturn, TestingT: t}
+ conf := confidence.NewConfidenceBuilder().SetAPIConfig(confidence.APIConfig{APIKey: "apiKey"}).SetResolveClient(resolveClient).Build()
+ provider := FlagProvider{
+ confidence: conf,
+ }
openfeature.SetProvider(provider)
return openfeature.NewClient("testApp")
}
-func templateResponse() resolveResponse {
+func templateResponse() confidence.ResolveResponse {
return templateResponseWithFlagName("test-flag")
}
-func templateResponseWithFlagName(flagName string) resolveResponse {
+func templateResponseWithFlagName(flagName string) confidence.ResolveResponse {
templateResolveResponse := fmt.Sprintf(`
{
- "resolvedFlags": [
- {
- "flag": "flags/%[1]s",
- "variant": "flags/%[1]s/variants/treatment",
- "value": {
- "struct-key": {
- "boolean-key": false,
- "string-key": "treatment-struct",
- "double-key": 123.23,
- "integer-key": 23,
+"resolvedFlags": [
+{
+"flag": "flags/%[1]s",
+"variant": "flags/%[1]s/variants/treatment",
+"value": {
+"struct-key": {
+"boolean-key": false,
+"string-key": "treatment-struct",
+"double-key": 123.23,
+"integer-key": 23,
"nested-struct-key": {
"nested-boolean-key": false
}
- },
- "boolean-key": true,
- "string-key": "treatment",
- "double-key": 20.203,
- "integer-key": 40
- },
- "flagSchema": {
- "schema": {
- "struct-key": {
- "structSchema": {
- "schema": {
- "boolean-key": {
- "boolSchema": {}
- },
- "string-key": {
- "stringSchema": {}
- },
- "double-key": {
- "doubleSchema": {}
- },
- "integer-key": {
- "intSchema": {}
- },
- "nested-struct-key": {
+},
+"boolean-key": true,
+"string-key": "treatment",
+"double-key": 20.203,
+"integer-key": 40
+},
+"flagSchema": {
+"schema": {
+"struct-key": {
+"structSchema": {
+"schema": {
+"boolean-key": {
+"boolSchema": {}
+},
+"string-key": {
+"stringSchema": {}
+},
+"double-key": {
+"doubleSchema": {}
+},
+"integer-key": {
+"intSchema": {}
+},
+ "nested-struct-key": {
"structSchema": {
"schema": {
"nested-boolean-key": {
@@ -248,45 +251,45 @@ func templateResponseWithFlagName(flagName string) resolveResponse {
}
}
}
- }
- }
- }
- },
- "boolean-key": {
- "boolSchema": {}
- },
- "string-key": {
- "stringSchema": {}
- },
- "double-key": {
- "doubleSchema": {}
- },
- "integer-key": {
- "intSchema": {}
- }
- }
- },
- "reason": "RESOLVE_REASON_MATCH"
- }],
- "resolveToken": ""
+ }
+}
+}
+},
+"boolean-key": {
+"boolSchema": {}
+},
+"string-key": {
+"stringSchema": {}
+},
+"double-key": {
+"doubleSchema": {}
+},
+"integer-key": {
+"intSchema": {}
+}
+}
+},
+"reason": "RESOLVE_REASON_MATCH"
+}],
+"resolveToken": ""
}
`, flagName)
- var result resolveResponse
+ var result confidence.ResolveResponse
decoder := json.NewDecoder(bytes.NewBuffer([]byte(templateResolveResponse)))
decoder.UseNumber()
_ = decoder.Decode(&result)
return result
}
-func emptyResponse() resolveResponse {
+func emptyResponse() confidence.ResolveResponse {
templateResolveResponse :=
`
{
- "resolvedFlags": [],
- "resolveToken": ""
+"resolvedFlags": [],
+"resolveToken": ""
}
`
- var result resolveResponse
+ var result confidence.ResolveResponse
decoder := json.NewDecoder(bytes.NewBuffer([]byte(templateResolveResponse)))
decoder.UseNumber()
_ = decoder.Decode(&result)
diff --git a/provider/vcs.xml b/provider/vcs.xml
new file mode 100644
index 0000000..b2bdec2
--- /dev/null
+++ b/provider/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file