From ec66647e25b0bd9e71a3bf5a8d284585a36ed316 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Fri, 2 Aug 2024 17:44:26 +0100 Subject: [PATCH] Add evaluation context support --- Makefile | 8 ++ client.go | 78 ++++++++--- client_test.go | 254 ++++++++++++++++++++++++++--------- evaluationcontext.go | 26 ++++ evaluationcontext_static.go | 33 +++++ internal/flaghttp/client.go | 3 + internal/flaghttp/request.go | 12 ++ utils.go | 31 +++++ 8 files changed, 366 insertions(+), 79 deletions(-) create mode 100644 Makefile create mode 100644 evaluationcontext.go create mode 100644 evaluationcontext_static.go create mode 100644 utils.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1ea9c02 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +.EXPORT_ALL_VARIABLES: + +EVALUATION_CONTEXT_SCHEMA_URL ?= https://raw.githubusercontent.com/Flagsmith/flagsmith/feat/evaluation-context-schema/sdk/evaluation-context.json + + +.PHONY: generate-evaluation-context +generate-evaluation-context: + npx quicktype ${EVALUATION_CONTEXT_SCHEMA_URL} --src-lang schema --lang go --package flagsmith --omit-empty --just-types-and-package > evaluationcontext.go diff --git a/client.go b/client.go index 6646237..6cc1be4 100644 --- a/client.go +++ b/client.go @@ -14,9 +14,13 @@ import ( "github.com/Flagsmith/flagsmith-go-client/v3/flagengine/segments" "github.com/Flagsmith/flagsmith-go-client/v3/internal/flaghttp" - . "github.com/Flagsmith/flagsmith-go-client/v3/flagengine/identities/traits" + enginetraits "github.com/Flagsmith/flagsmith-go-client/v3/flagengine/identities/traits" ) +type contextKey string + +var contextKeyEvaluationContext = contextKey("evaluationContext") + // Client provides various methods to query Flagsmith API. type Client struct { apiKey string @@ -44,8 +48,8 @@ func NewClient(apiKey string, options ...Option) *Client { } c.client.SetHeaders(map[string]string{ - "Accept": "application/json", - "X-Environment-Key": c.apiKey, + "Accept": "application/json", + flaghttp.EnvironmentKeyHeader: c.apiKey, }) c.client.SetTimeout(c.config.timeout) c.log = createLogger() @@ -87,9 +91,34 @@ func NewClient(apiKey string, options ...Option) *Client { // Returns `Flags` struct holding all the flags for the current environment. // +// Provide `EvaluationContext` to evaluate flags for a specific environment or identity. +// // If local evaluation is enabled this function will not call the Flagsmith API // directly, but instead read the asynchronously updated local environment or // use the default flag handler in case it has not yet been updated. +// +// Notes: +// +// * `EvaluationContext.Environment` is ignored in local evaluation mode. +// +// * `EvaluationContext.Feature` is not yet supported. +func (c *Client) GetFlags(ctx context.Context, ec *EvaluationContext) (f Flags, err error) { + if ec != nil { + ctx = context.WithValue(ctx, contextKeyEvaluationContext, ec) + if ec.Identity != nil { + return c.GetIdentityFlags(ctx, ec.Identity.Identifier, mapIdentityEvaluationContextToTraits(*ec.Identity)) + } + } + return c.GetEnvironmentFlags(ctx) +} + +// Returns `Flags` struct holding all the flags for the current environment. +// +// If local evaluation is enabled this function will not call the Flagsmith API +// directly, but instead read the asynchronously updated local environment or +// use the default flag handler in case it has not yet been updated. +// +// Deprecated: Use `GetFlags` instead. func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) { if c.config.localEvaluation || c.config.offlineMode { if f, err = c.getEnvironmentFlagsFromEnvironment(); err == nil { @@ -108,10 +137,6 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) { return Flags{}, &FlagsmithClientError{msg: fmt.Sprintf("Failed to fetch flags with error: %s", err)} } -type GetIdentityFlagsOpts struct { - Transient bool `json:"transient,omitempty"` -} - // Returns `Flags` struct holding all the flags for the current environment for // a given identity. // @@ -122,13 +147,15 @@ type GetIdentityFlagsOpts struct { // If local evaluation is enabled this function will not call the Flagsmith API // directly, but instead read the asynchronously updated local environment or // use the default flag handler in case it has not yet been updated. -func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (f Flags, err error) { +// +// Deprecated: Use `GetFlags` providing `EvaluationContext.Identity` instead. +func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait) (f Flags, err error) { if c.config.localEvaluation || c.config.offlineMode { if f, err = c.getIdentityFlagsFromEnvironment(identifier, traits); err == nil { return f, nil } } else { - if f, err = c.GetIdentityFlagsFromAPI(ctx, identifier, traits, opts); err == nil { + if f, err = c.GetIdentityFlagsFromAPI(ctx, identifier, traits); err == nil { return f, nil } } @@ -180,7 +207,15 @@ func (c *Client) BulkIdentify(ctx context.Context, batch []*IdentityTraits) erro // GetEnvironmentFlagsFromAPI tries to contact the Flagsmith API to get the latest environment data. // Will return an error in case of failure or unexpected response. func (c *Client) GetEnvironmentFlagsFromAPI(ctx context.Context) (Flags, error) { - resp, err := c.client.NewRequest(). + req := c.client.NewRequest() + maybeEc := ctx.Value(contextKeyEvaluationContext) + if maybeEc != nil { + envCtx := maybeEc.(*EvaluationContext).Environment + if envCtx != nil { + req.SetHeader(flaghttp.EnvironmentKeyHeader, envCtx.APIKey) + } + } + resp, err := req. SetContext(ctx). ForceContentType("application/json"). Get(c.config.baseURL + "flags/") @@ -195,16 +230,27 @@ func (c *Client) GetEnvironmentFlagsFromAPI(ctx context.Context) (Flags, error) // GetIdentityFlagsFromAPI tries to contact the Flagsmith API to get the latest identity flags. // Will return an error in case of failure or unexpected response. -func (c *Client) GetIdentityFlagsFromAPI(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (Flags, error) { +func (c *Client) GetIdentityFlagsFromAPI(ctx context.Context, identifier string, traits []*Trait) (Flags, error) { body := struct { Identifier string `json:"identifier"` Traits []*Trait `json:"traits,omitempty"` - GetIdentityFlagsOpts + Transient *bool `json:"transient,omitempty"` }{Identifier: identifier, Traits: traits} - if opts != nil { - body.Transient = opts.Transient + req := c.client.NewRequest() + maybeEc := ctx.Value(contextKeyEvaluationContext) + if maybeEc != nil { + ec := maybeEc.(*EvaluationContext) + envCtx := ec.Environment + if envCtx != nil { + req.SetHeader(flaghttp.EnvironmentKeyHeader, envCtx.APIKey) + } + idCtx := ec.Identity + if idCtx != nil { + // `Identifier` and `Traits` had been set by `GetFlags` earlier. + body.Transient = &idCtx.Transient + } } - resp, err := c.client.NewRequest(). + resp, err := req. SetBody(&body). SetContext(ctx). ForceContentType("application/json"). @@ -295,7 +341,7 @@ func (c *Client) UpdateEnvironment(ctx context.Context) error { } func (c *Client) getIdentityModel(identifier string, apiKey string, traits []*Trait) identities.IdentityModel { - identityTraits := make([]*TraitModel, len(traits)) + identityTraits := make([]*enginetraits.TraitModel, len(traits)) for i, trait := range traits { identityTraits[i] = trait.ToTraitModel() } diff --git a/client_test.go b/client_test.go index 5f75e0b..8b73fa1 100644 --- a/client_test.go +++ b/client_test.go @@ -16,6 +16,27 @@ import ( "github.com/stretchr/testify/assert" ) +func getTestHttpServer(t *testing.T, expectedPath string, expectedEnvKey string, expectedRequestBody *string, responseFixture string) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.URL.Path, expectedPath) + assert.Equal(t, expectedEnvKey, req.Header.Get("X-Environment-Key")) + + if expectedRequestBody != nil { + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + + assert.Equal(t, *expectedRequestBody, string(rawBody)) + } + + rw.Header().Set("Content-Type", "application/json") + + _, err := io.WriteString(rw, responseFixture) + + assert.NoError(t, err) + })) +} + func TestClientErrorsIfLocalEvaluationWithNonServerSideKey(t *testing.T) { // When, Then assert.Panics(t, func() { @@ -144,6 +165,135 @@ func TestClientUpdatesEnvironmentOnEachRefresh(t *testing.T) { assert.Equal(t, expectedEnvironmentRefreshCount, actualEnvironmentRefreshCounter.count) } +func TestGetFlags(t *testing.T) { + // Given + ctx := context.Background() + server := getTestHttpServer(t, "/api/v1/flags/", fixtures.EnvironmentAPIKey, nil, fixtures.FlagsJson) + defer server.Close() + + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithBaseURL(server.URL+"/api/v1/")) + + flags, err := client.GetFlags(ctx, nil) + + // Then + assert.NoError(t, err) + + allFlags := flags.AllFlags() + + assert.Equal(t, 1, len(allFlags)) + + assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName) + assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID) + assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) +} + +func TestGetFlagsTransientIdentity(t *testing.T) { + // Given + ctx := context.Background() + expectedRequestBody := `{"identifier":"transient","transient":true}` + server := getTestHttpServer(t, "/api/v1/identities/", fixtures.EnvironmentAPIKey, &expectedRequestBody, fixtures.IdentityResponseJson) + defer server.Close() + + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithBaseURL(server.URL+"/api/v1/")) + + flags, err := client.GetFlags(ctx, &flagsmith.EvaluationContext{Identity: &flagsmith.IdentityEvaluationContext{Identifier: "transient", Transient: true}}) + + // Then + assert.NoError(t, err) + + allFlags := flags.AllFlags() + + assert.Equal(t, 1, len(allFlags)) + + assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName) + assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID) + assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) +} + +func TestGetFlagsTransientTraits(t *testing.T) { + // Given + ctx := context.Background() + expectedRequestBody := `{"identifier":"test_identity","traits":` + + `[{"trait_key":"NullTrait","trait_value":null},` + + `{"trait_key":"StringTrait","trait_value":"value"},` + + `{"trait_key":"TransientTrait","trait_value":"value","transient":true}],"transient":false}` + server := getTestHttpServer(t, "/api/v1/identities/", fixtures.EnvironmentAPIKey, &expectedRequestBody, fixtures.IdentityResponseJson) + defer server.Close() + + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithBaseURL(server.URL+"/api/v1/")) + + flags, err := client.GetFlags( + ctx, + &flagsmith.EvaluationContext{ + Identity: &flagsmith.IdentityEvaluationContext{ + Identifier: "test_identity", + Traits: map[string]*flagsmith.TraitEvaluationContext{ + "NullTrait": nil, + "StringTrait": &flagsmith.TraitEvaluationContext{Value: "value"}, + "TransientTrait": &flagsmith.TraitEvaluationContext{ + Value: "value", + Transient: true, + }, + }, + }, + }) + + // Then + assert.NoError(t, err) + + allFlags := flags.AllFlags() + + assert.Equal(t, 1, len(allFlags)) + + assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName) + assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID) + assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) +} + +func TestGetFlagsEnvironmentEvaluationContextFlags(t *testing.T) { + // Given + ctx := context.Background() + expectedEnvKey := "different" + server := getTestHttpServer(t, "/api/v1/flags/", expectedEnvKey, nil, fixtures.FlagsJson) + defer server.Close() + + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithBaseURL(server.URL+"/api/v1/")) + + _, err := client.GetFlags( + ctx, + &flagsmith.EvaluationContext{ + Environment: &flagsmith.EnvironmentEvaluationContext{APIKey: expectedEnvKey}, + }) + + // Then + assert.NoError(t, err) +} + +func TestGetFlagsEnvironmentEvaluationContextIdentity(t *testing.T) { + // Given + ctx := context.Background() + expectedEnvKey := "different" + server := getTestHttpServer(t, "/api/v1/identities/", expectedEnvKey, nil, fixtures.IdentityResponseJson) + defer server.Close() + + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, flagsmith.WithBaseURL(server.URL+"/api/v1/")) + + _, err := client.GetFlags( + ctx, + &flagsmith.EvaluationContext{ + Environment: &flagsmith.EnvironmentEvaluationContext{APIKey: expectedEnvKey}, + Identity: &flagsmith.IdentityEvaluationContext{Identifier: "test_identity"}, + }) + + // Then + assert.NoError(t, err) +} + func TestGetEnvironmentFlagsUseslocalEnvironmentWhenAvailable(t *testing.T) { // Given ctx := context.Background() @@ -231,7 +381,7 @@ func TestGetIdentityFlagsUseslocalEnvironmentWhenAvailable(t *testing.T) { // Then assert.NoError(t, err) - flags, err := client.GetIdentityFlags(ctx, "test_identity", nil, nil) + flags, err := client.GetIdentityFlags(ctx, "test_identity", nil) assert.NoError(t, err) @@ -257,7 +407,7 @@ func TestGetIdentityFlagsUseslocalOverridesWhenAvailable(t *testing.T) { // Then assert.NoError(t, err) - flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil, nil) + flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil) assert.NoError(t, err) @@ -272,77 +422,55 @@ func TestGetIdentityFlagsUseslocalOverridesWhenAvailable(t *testing.T) { func TestGetIdentityFlagsCallsAPIWhenLocalEnvironmentNotAvailableWithTraits(t *testing.T) { // Given - stringTrait := flagsmith.Trait{TraitKey: "stringTrait", TraitValue: "trait_value"} - intTrait := flagsmith.Trait{TraitKey: "intTrait", TraitValue: 1} - floatTrait := flagsmith.Trait{TraitKey: "floatTrait", TraitValue: 1.11} - boolTrait := flagsmith.Trait{TraitKey: "boolTrait", TraitValue: true} - nillTrait := flagsmith.Trait{TraitKey: "NoneTrait", TraitValue: nil} - transientTrait := flagsmith.Trait{TraitKey: "TransientTrait", TraitValue: "not_persisted", Transient: true} - - testCases := []struct { - Identifier string - Traits []*flagsmith.Trait - Opts *flagsmith.GetIdentityFlagsOpts - ExpectedRequestBody string - }{ - { - "test_identity", - []*flagsmith.Trait{&stringTrait, &intTrait, &floatTrait, &boolTrait, &nillTrait, &transientTrait}, - nil, - `{"identifier":"test_identity","traits":[{"trait_key":"stringTrait","trait_value":"trait_value"},` + - `{"trait_key":"intTrait","trait_value":1},` + - `{"trait_key":"floatTrait","trait_value":1.11},` + - `{"trait_key":"boolTrait","trait_value":true},` + - `{"trait_key":"NoneTrait","trait_value":null},` + - `{"trait_key":"TransientTrait","trait_value":"not_persisted","transient":true}]}`, - }, - { - "test_transient_identity", - []*flagsmith.Trait{}, - &flagsmith.GetIdentityFlagsOpts{Transient: true}, - `{"identifier":"test_transient_identity","transient":true}`, - }, - } - ctx := context.Background() + expectedRequestBody := `{"identifier":"test_identity","traits":[{"trait_key":"stringTrait","trait_value":"trait_value"},` + + `{"trait_key":"intTrait","trait_value":1},` + + `{"trait_key":"floatTrait","trait_value":1.11},` + + `{"trait_key":"boolTrait","trait_value":true},` + + `{"trait_key":"NoneTrait","trait_value":null}]}` - for _, tc := range testCases { - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - assert.Equal(t, req.URL.Path, "/api/v1/identities/") - assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("X-Environment-Key")) + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.URL.Path, "/api/v1/identities/") + assert.Equal(t, fixtures.EnvironmentAPIKey, req.Header.Get("X-Environment-Key")) - // Test that we sent the correct body - rawBody, err := io.ReadAll(req.Body) - assert.NoError(t, err) - assert.Equal(t, tc.ExpectedRequestBody, string(rawBody)) + // Test that we sent the correct body + rawBody, err := io.ReadAll(req.Body) + assert.NoError(t, err) + assert.Equal(t, expectedRequestBody, string(rawBody)) - rw.Header().Set("Content-Type", "application/json") + rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(http.StatusOK) - _, err = io.WriteString(rw, fixtures.IdentityResponseJson) + rw.WriteHeader(http.StatusOK) + _, err = io.WriteString(rw, fixtures.IdentityResponseJson) - assert.NoError(t, err) - })) - defer server.Close() - // When - client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, - flagsmith.WithBaseURL(server.URL+"/api/v1/")) + assert.NoError(t, err) + })) + defer server.Close() + // When + client := flagsmith.NewClient(fixtures.EnvironmentAPIKey, + flagsmith.WithBaseURL(server.URL+"/api/v1/")) - // When + stringTrait := flagsmith.Trait{TraitKey: "stringTrait", TraitValue: "trait_value"} + intTrait := flagsmith.Trait{TraitKey: "intTrait", TraitValue: 1} + floatTrait := flagsmith.Trait{TraitKey: "floatTrait", TraitValue: 1.11} + boolTrait := flagsmith.Trait{TraitKey: "boolTrait", TraitValue: true} + nillTrait := flagsmith.Trait{TraitKey: "NoneTrait", TraitValue: nil} - flags, err := client.GetIdentityFlags(ctx, tc.Identifier, tc.Traits, tc.Opts) + traits := []*flagsmith.Trait{&stringTrait, &intTrait, &floatTrait, &boolTrait, &nillTrait} + // When - // Then - assert.NoError(t, err) + flags, err := client.GetIdentityFlags(ctx, "test_identity", traits) - allFlags := flags.AllFlags() + // Then + assert.NoError(t, err) - assert.Equal(t, 1, len(allFlags)) + allFlags := flags.AllFlags() - assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName) - assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID) - assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) - } + assert.Equal(t, 1, len(allFlags)) + + assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName) + assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID) + assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) } func TestDefaultHandlerIsUsedWhenNoMatchingEnvironmentFlagReturned(t *testing.T) { @@ -630,7 +758,7 @@ func TestOfflineMode(t *testing.T) { assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) // And GetIdentityFlags works as well - flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil) + flags, err = client.GetIdentityFlags(ctx, "test_identity", nil) assert.NoError(t, err) allFlags = flags.AllFlags() @@ -672,7 +800,7 @@ func TestOfflineHandlerIsUsedWhenRequestFails(t *testing.T) { assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value) // And GetIdentityFlags works as well - flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil) + flags, err = client.GetIdentityFlags(ctx, "test_identity", nil) assert.NoError(t, err) allFlags = flags.AllFlags() diff --git a/evaluationcontext.go b/evaluationcontext.go new file mode 100644 index 0000000..b166583 --- /dev/null +++ b/evaluationcontext.go @@ -0,0 +1,26 @@ +package flagsmith + +type EvaluationContext struct { + Environment *EnvironmentEvaluationContext `json:"environment,omitempty"` + Feature *FeatureEvaluationContext `json:"feature,omitempty"` + Identity *IdentityEvaluationContext `json:"identity,omitempty"` +} + +type EnvironmentEvaluationContext struct { + APIKey string `json:"api_key"` +} + +type FeatureEvaluationContext struct { + Name string `json:"name"` +} + +type IdentityEvaluationContext struct { + Identifier string `json:"identifier"` + Traits map[string]*TraitEvaluationContext `json:"traits"` + Transient bool `json:"transient,omitempty"` +} + +type TraitEvaluationContext struct { + Transient bool `json:"transient,omitempty"` + Value interface{} `json:"value"` +} diff --git a/evaluationcontext_static.go b/evaluationcontext_static.go new file mode 100644 index 0000000..ec1f356 --- /dev/null +++ b/evaluationcontext_static.go @@ -0,0 +1,33 @@ +package flagsmith + +func getTraitEvaluationContext(v interface{}) TraitEvaluationContext { + tCtx, ok := v.(TraitEvaluationContext) + if ok { + return tCtx + } + return TraitEvaluationContext{Value: v} +} + +func NewTraitEvaluationContext(value interface{}, transient bool) TraitEvaluationContext { + return TraitEvaluationContext{Value: value, Transient: transient} +} + +func NewEvaluationContext(identifier string, traits map[string]interface{}) EvaluationContext { + ec := EvaluationContext{} + traitsCtx := make(map[string]*TraitEvaluationContext, len(traits)) + for tKey, tValue := range traits { + tCtx := getTraitEvaluationContext(tValue) + traitsCtx[tKey] = &tCtx + } + ec.Identity = &IdentityEvaluationContext{ + Identifier: identifier, + Traits: traitsCtx, + } + return ec +} + +func NewTransientEvaluationContext(identifier string, traits map[string]interface{}) EvaluationContext { + ec := NewEvaluationContext(identifier, traits) + ec.Identity.Transient = true + return ec +} diff --git a/internal/flaghttp/client.go b/internal/flaghttp/client.go index ee30ef8..f823118 100644 --- a/internal/flaghttp/client.go +++ b/internal/flaghttp/client.go @@ -7,6 +7,8 @@ import ( "time" ) +const EnvironmentKeyHeader = "X-Environment-Key" + type Logger interface { Errorf(format string, v ...any) Warnf(format string, v ...any) @@ -58,6 +60,7 @@ func (c *client) NewRequest() Request { func (c *client) R() Request { return &request{ client: c, + header: http.Header{}, } } diff --git a/internal/flaghttp/request.go b/internal/flaghttp/request.go index 16b1be4..96286e9 100644 --- a/internal/flaghttp/request.go +++ b/internal/flaghttp/request.go @@ -17,12 +17,14 @@ type Request interface { Post(url string) (Response, error) SetBody(body any) Request SetContext(ctx context.Context) Request + SetHeader(header, value string) Request SetResult(res any) Request SetError(err any) Request } type request struct { client *client + header http.Header body any ctx context.Context err any @@ -69,6 +71,12 @@ func (r *request) SetError(err any) Request { return r } +func (r *request) SetHeader(header, value string) Request { + r.header.Set(header, value) + + return r +} + func (r *request) do(method, url string) (Response, error) { var ( transport = r.client.transport.Clone() @@ -115,6 +123,10 @@ func (r *request) do(method, url string) (Response, error) { req.Header[k] = v } + for k, v := range r.header { + req.Header[k] = v + } + if r.body != nil { req.Header.Set("Content-Type", "application/json") } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..3408a56 --- /dev/null +++ b/utils.go @@ -0,0 +1,31 @@ +package flagsmith + +import ( + "sort" +) + +func mapIdentityEvaluationContextToTraits(ic IdentityEvaluationContext) []*Trait { + traits := make([]*Trait, len(ic.Traits)) + for i, tKey := range sortedKeys(ic.Traits) { + traits[i] = mapTraitEvaluationContextToTrait(tKey, ic.Traits[tKey]) + } + return traits +} + +func mapTraitEvaluationContextToTrait(tKey string, tCtx *TraitEvaluationContext) *Trait { + if tCtx == nil { + return &Trait{TraitKey: tKey, TraitValue: nil} + } + return &Trait{TraitKey: tKey, TraitValue: tCtx.Value, Transient: tCtx.Transient} +} + +func sortedKeys[Map ~map[string]V, V any](m Map) []string { + keys := make([]string, len(m)) + i := 0 + for tKey := range m { + keys[i] = tKey + i++ + } + sort.Strings(keys) + return keys +}