Skip to content

Commit

Permalink
feat: Support transient identities and traits
Browse files Browse the repository at this point in the history
  • Loading branch information
khvn26 committed Jul 24, 2024
1 parent ad6f7fe commit 2d4d2a1
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 43 deletions.
14 changes: 11 additions & 3 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ 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.
//
Expand All @@ -118,13 +122,13 @@ func (c *Client) GetEnvironmentFlags(ctx context.Context) (f Flags, err error) {
// 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) (f Flags, err error) {
func (c *Client) GetIdentityFlags(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (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); err == nil {
if f, err = c.GetIdentityFlagsFromAPI(ctx, identifier, traits, opts); err == nil {
return f, nil
}
}
Expand Down Expand Up @@ -191,11 +195,15 @@ 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) (Flags, error) {
func (c *Client) GetIdentityFlagsFromAPI(ctx context.Context, identifier string, traits []*Trait, opts *GetIdentityFlagsOpts) (Flags, error) {
body := struct {
Identifier string `json:"identifier"`
Traits []*Trait `json:"traits,omitempty"`
GetIdentityFlagsOpts
}{Identifier: identifier, Traits: traits}
if opts != nil {
body.Transient = opts.Transient
}
resp, err := c.client.NewRequest().
SetBody(&body).
SetContext(ctx).
Expand Down
103 changes: 63 additions & 40 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func TestGetIdentityFlagsUseslocalEnvironmentWhenAvailable(t *testing.T) {
// Then
assert.NoError(t, err)

flags, err := client.GetIdentityFlags(ctx, "test_identity", nil)
flags, err := client.GetIdentityFlags(ctx, "test_identity", nil, nil)

assert.NoError(t, err)

Expand All @@ -257,7 +257,7 @@ func TestGetIdentityFlagsUseslocalOverridesWhenAvailable(t *testing.T) {
// Then
assert.NoError(t, err)

flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil)
flags, err := client.GetIdentityFlags(ctx, "overridden-id", nil, nil)

assert.NoError(t, err)

Expand All @@ -272,55 +272,78 @@ 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}]}`

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"))
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"))

// Test that we sent the correct body
rawBody, err := io.ReadAll(req.Body)
assert.NoError(t, err)
assert.Equal(t, expectedRequestBody, string(rawBody))
// Test that we sent the correct body
rawBody, err := io.ReadAll(req.Body)
assert.NoError(t, err)
assert.Equal(t, tc.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/"))

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}
// When

traits := []*flagsmith.Trait{&stringTrait, &intTrait, &floatTrait, &boolTrait, &nillTrait}
// When
flags, err := client.GetIdentityFlags(ctx, tc.Identifier, tc.Traits, tc.Opts)

flags, err := client.GetIdentityFlags(ctx, "test_identity", traits)
// Then
assert.NoError(t, err)

// Then
assert.NoError(t, err)
allFlags := flags.AllFlags()

allFlags := flags.AllFlags()
assert.Equal(t, 1, len(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)

assert.Equal(t, fixtures.Feature1Name, allFlags[0].FeatureName)
assert.Equal(t, fixtures.Feature1ID, allFlags[0].FeatureID)
assert.Equal(t, fixtures.Feature1Value, allFlags[0].Value)
}

Check failure on line 346 in client_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

unnecessary trailing newline (whitespace)
}

func TestDefaultHandlerIsUsedWhenNoMatchingEnvironmentFlagReturned(t *testing.T) {
Expand Down Expand Up @@ -608,7 +631,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)
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil)
assert.NoError(t, err)

allFlags = flags.AllFlags()
Expand Down Expand Up @@ -650,7 +673,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)
flags, err = client.GetIdentityFlags(ctx, "test_identity", nil, nil)
assert.NoError(t, err)

allFlags = flags.AllFlags()
Expand Down
2 changes: 2 additions & 0 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ type Flag struct {
type Trait struct {
TraitKey string `json:"trait_key"`
TraitValue interface{} `json:"trait_value"`
Transient bool `json:"transient,omitempty"`
}

type IdentityTraits struct {
Identifier string `json:"identifier"`
Traits []*Trait `json:"traits"`
Transient bool `json:"transient,omitempty"`
}

func (t *Trait) ToTraitModel() *traits.TraitModel {
Expand Down

0 comments on commit 2d4d2a1

Please sign in to comment.