Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Each of these components are comprised of lower level libraries that you can use

### About Feature Flags

Feature flags have many use cases and there are many implementations. With Decider, the three supported types of flags are `boolean`, `percentile`, and `scalar`. For our purposes at [VSCO](http://vsco.co), these have been enough to handle our needs.
Feature flags have many use cases and there are many implementations. With Decider, the supported types of flags are `boolean`, `percentile`, `scalar`, and `string`. For our purposes at [VSCO](http://vsco.co), these have been enough to handle our needs.

#### Boolean Flags
An example use case for a `boolean` flag would be an API kill switch that could alleviate load for a backing database.
Expand Down Expand Up @@ -75,6 +75,18 @@ waitMS := dcdr.ScaleValue("daemon-db-insert-wait-ms", 0, 1000)
time.Sleep(waitMS * time.Millisecond)
```

#### String Flags
A `string` flag holds a free-form text value. Any `-value` that is not a boolean or a number is stored as a string. A common use case is a runtime-tunable setting such as a minimum log level.

```
min-log-level => "debug"
```

```Go
// Returns the configured value, or "" if the flag is absent or not a string.
level := dcdr.GetString("min-log-level")
```

[Read more](#using-the-go-client) on how to use the `Client`.

### Caveat
Expand Down Expand Up @@ -349,7 +361,7 @@ if err != nil {

### Checking feature flags

The client has three main methods for interacting with flags `IsAvailable(feature string)`. `IsAvailableForID(feature string, id uint64)`, and `ScaleValue(feature string, min float64, max float64)`.
The client has four main methods for interacting with flags `IsAvailable(feature string)`. `IsAvailableForID(feature string, id uint64)`, `ScaleValue(feature string, min float64, max float64)`, and `GetString(feature string)`.

#### IsAvailable

Expand Down Expand Up @@ -472,6 +484,24 @@ for {
}
```

### GetString

`GetString` returns the value of a `string` feature. It returns `""` when the feature is absent or is not a string-typed flag, so callers should fall back to a sane default for unknown values.

```
# set a string feature
dcdr set -n min-log-level -v debug
```

```Go
// min-log-level would be "debug"
level := dcdr.GetString("min-log-level")

if level == "" {
level = "info" // fall back to a default
}
```

## Building a custom Server

Exposing your feature flags to the open internet would be a terrible idea in most cases. The default server will work fine as long as access is restricted to internal network clients but what if we want to allow access to mobile devices? Since there are entirely too many auth strategies to cover and we are kind of lazy, Decider `Server` allows you to add middleware to customize its behavior to suit your authentication needs.
Expand Down
4 changes: 2 additions & 2 deletions cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (c *CLI) Commands() []climax.Command {
{
Name: "set",
Brief: "create or update a feature flag",
Usage: `set -name flag_name -value [0.0-1.0|true/false] -comment "flag description"`,
Usage: `set -name flag_name -value [0.0-1.0|true/false|string] -comment "flag description"`,
Comment thread
a-karev-vsc marked this conversation as resolved.
Outdated
Help: `


Expand Down Expand Up @@ -117,7 +117,7 @@ func (c *CLI) Commands() []climax.Command {
{
Name: "value",
Short: "v",
Usage: `--value=0.0-1.0 or true|false`,
Usage: `--value=0.0-1.0, true|false, or a string`,
Help: `the value of the flag`,
Variable: true,
},
Expand Down
2 changes: 1 addition & 1 deletion cli/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
const filePerms = 0775

var (
errInvalidFeatureType = errors.New("invalid -value format. use -value=[0.0-1.0] or [true|false]")
errInvalidFeatureType = errors.New("invalid -value format. use -value=[0.0-1.0], [true|false], or a string")
errInvalidRange = errors.New("invalid -value for percentile. use -value=[0.0-1.0]")
errNameRequired = errors.New("-name is required")
)
Expand Down
11 changes: 11 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type IFace interface {
IsAvailable(feature string) bool
IsAvailableForID(feature string, id uint64) bool
GetString(feature string) string
ScaleValue(feature string, min float64, max float64) float64
UpdateFeatures(bts []byte)
FeatureExists(feature string) bool
Expand Down Expand Up @@ -168,6 +169,16 @@ func (c *Client) IsAvailable(feature string) bool {
}
}

// GetString returns the string value of `feature`, or "" if it is absent or
// not a string-typed feature.
func (c *Client) GetString(feature string) string {
if val, ok := c.Features()[feature].(string); ok {
return val
Comment thread
a-karev-vsc marked this conversation as resolved.
}

return ""
}

// IsAvailableForID used to check features with float values between 0.0-1.0.
// Returns false if a non-percentile type `feature` is passed.
func (c *Client) IsAvailableForID(feature string, id uint64) bool {
Expand Down
12 changes: 11 additions & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ var JSONBytes = []byte(`{
"float": 0,
"bool_false": false,
"bool": true,
"default_float": 0.5
"default_float": 0.5,
"str": "debug"
}
},
"info": {
Expand Down Expand Up @@ -151,6 +152,15 @@ func TestIsAvailableForID(t *testing.T) {
assert.True(t, c.IsAvailableForID("default_float", 5))
}

func TestGetString(t *testing.T) {
m := MockFeatureMap()
c := NewTestClient().SetFeatureMap(m)

assert.Equal(t, "debug", c.GetString("str"))
assert.Equal(t, "", c.GetString("nope"))
assert.Equal(t, "", c.GetString("bool"))
}

func TestScaleValue(t *testing.T) {
m := MockFeatureMap()
c := NewTestClient().SetFeatureMap(m)
Expand Down
14 changes: 13 additions & 1 deletion models/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const (
Percentile FeatureType = "percentile"
// Boolean boolean `FeatureType`
Boolean FeatureType = "boolean"
// String string `FeatureType`
String FeatureType = "string"
// Invalid invalid `FeatureType`
Invalid FeatureType = "invalid"
// FeatureScope scoping for feature keys
Expand All @@ -47,7 +49,10 @@ func ParseValueAndFeatureType(v string) (interface{}, FeatureType) {
return i, Percentile
}

return nil, Invalid
// Any value that is not a bool or a number is treated as a free-form
// string feature, so this function never returns Invalid. The Invalid
// constant is retained for callers that compare against it explicitly.
return v, String
}

// Feature KV model for feature flags
Expand Down Expand Up @@ -89,6 +94,8 @@ func NewFeature(name string, value interface{}, comment string, user string, sco
ft = Percentile
case bool:
ft = Boolean
case string:
ft = String
}

f = &Feature{
Expand All @@ -114,6 +121,11 @@ func (f *Feature) BoolValue() bool {
return f.Value.(bool)
}

// StringValue cast Value to string
func (f *Feature) StringValue() string {
return f.Value.(string)
}

// ToJSON marshal feature to json
func (f *Feature) ToJSON() ([]byte, error) {
return json.Marshal(f)
Expand Down
19 changes: 19 additions & 0 deletions models/feature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ func TestGetFeatureTypeFromValue(t *testing.T) {
_, ft := ParseValueAndFeatureType(v)
assert.Equal(t, Percentile, ft, v)
}

booleans := []string{"true", "false"}

for _, v := range booleans {
_, ft := ParseValueAndFeatureType(v)
assert.Equal(t, Boolean, ft, v)
}

strings := []string{"debug", "info", "some-string"}

for _, v := range strings {
val, ft := ParseValueAndFeatureType(v)
assert.Equal(t, String, ft, v)
assert.Equal(t, v, val, v)
}
}

func TestMarshaling(t *testing.T) {
Expand All @@ -40,4 +55,8 @@ func TestTypes(t *testing.T) {
pf = NewFeature("key", true, "comment", "user", "scope", "n")
assert.Equal(t, Boolean, pf.FeatureType)
assert.Equal(t, true, pf.BoolValue())

pf = NewFeature("key", "debug", "comment", "user", "scope", "n")
assert.Equal(t, String, pf.FeatureType)
assert.Equal(t, "debug", pf.StringValue())
}
Loading