Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement part of the Go client for unleash.
This is a work in progress for a version of the Go client for Unleash. It is largely based on the Node.js implementation, but with an attempt at keeping as close to idiomatic Go as possible. There is still quite a bit left, but this is a start. The syncing of the repository seems to work now. Things left include metrics, docs and autotests.
- Loading branch information
Showing
25 changed files
with
1,136 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
package unleash_client_go | ||
|
||
import ( | ||
"fmt" | ||
"github.com/unleash/unleash-client-go/context" | ||
s "github.com/unleash/unleash-client-go/internal/strategies" | ||
"github.com/unleash/unleash-client-go/strategy" | ||
"net/url" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const deprecatedSuffix = "/features" | ||
|
||
var defaultStrategies = []strategy.Strategy{ | ||
*s.NewDefaultStrategy(), | ||
*s.NewApplicationHostnameStrategy(), | ||
*s.NewGradualRolloutRandomStrategy(), | ||
*s.NewGradualRolloutSessionId(), | ||
*s.NewGradualRolloutUserId(), | ||
*s.NewRemoteAddressStrategy(), | ||
*s.NewUserWithIdStrategy(), | ||
} | ||
|
||
type featureOption struct { | ||
fallback *bool | ||
ctx *context.Context | ||
} | ||
|
||
type FeatureOption func(*featureOption) | ||
|
||
func WithFallback(fallback bool) FeatureOption { | ||
return func(opts *featureOption) { | ||
opts.fallback = &fallback | ||
} | ||
} | ||
|
||
func WithContext(ctx context.Context) FeatureOption { | ||
return func(opts *featureOption) { | ||
opts.ctx = &ctx | ||
} | ||
} | ||
|
||
type Client struct { | ||
errorEmitterImpl | ||
options configOption | ||
repository *repository | ||
strategies []strategy.Strategy | ||
ready chan bool | ||
} | ||
|
||
func NewClient(options ...ConfigOption) (*Client, error) { | ||
uc := &Client{ | ||
errorEmitterImpl: *newErrorEmitter(), | ||
options: configOption{ | ||
refreshInterval: 15 * time.Second, | ||
metricsInterval: 60 * time.Second, | ||
disableMetrics: false, | ||
backupPath: getTmpDirPath(), | ||
strategies: []strategy.Strategy{}, | ||
}, | ||
} | ||
|
||
for _, opt := range options { | ||
opt(&uc.options) | ||
} | ||
|
||
if uc.options.url == "" { | ||
return nil, fmt.Errorf("Unleash server URL missing") | ||
} | ||
|
||
if strings.HasSuffix(uc.options.url, deprecatedSuffix) { | ||
uc.warn(fmt.Errorf("Unleash server URL %s should no longer link directly to /features", uc.options.url)) | ||
uc.options.url = strings.TrimSuffix(uc.options.url, deprecatedSuffix) | ||
} | ||
|
||
if !strings.HasSuffix(uc.options.url, "/") { | ||
uc.options.url += "/" | ||
} | ||
|
||
parsedUrl, err := url.Parse(uc.options.url) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if uc.options.appName == "" { | ||
return nil, fmt.Errorf("Unleash client appName missing") | ||
} | ||
|
||
if uc.options.instanceId == "" { | ||
uc.options.instanceId = generateInstanceId() | ||
} | ||
|
||
uc.repository = NewRepository(RepositoryOptions{ | ||
BackupPath: uc.options.backupPath, | ||
Url: *parsedUrl, | ||
AppName: uc.options.appName, | ||
InstanceId: uc.options.instanceId, | ||
RefreshInterval: uc.options.refreshInterval, | ||
}) | ||
|
||
uc.repository.Forward(uc) | ||
|
||
uc.strategies = append(defaultStrategies, uc.options.strategies...) | ||
|
||
return uc, nil | ||
|
||
} | ||
|
||
func (uc Client) IsEnabled(feature string, options ...FeatureOption) bool { | ||
f := uc.repository.GetToggle(feature) | ||
|
||
var opts featureOption | ||
for _, o := range options { | ||
o(&opts) | ||
} | ||
|
||
if f == nil { | ||
if opts.fallback != nil { | ||
return *opts.fallback | ||
} | ||
return false | ||
} | ||
|
||
if !f.Enabled { | ||
return false | ||
} | ||
|
||
for _, s := range f.Strategies { | ||
foundStrategy := uc.getStrategy(s.Name) | ||
if foundStrategy == nil { | ||
// TODO: warnOnce missingStrategy | ||
continue | ||
} | ||
return foundStrategy.IsEnabled(f.Parameters, opts.ctx) | ||
} | ||
return false | ||
} | ||
|
||
func (uc *Client) Close() error { | ||
uc.repository.Close() | ||
return nil | ||
} | ||
|
||
func (uc Client) Ready() <-chan bool { | ||
return uc.ready | ||
} | ||
|
||
func (uc Client) getStrategy(name string) strategy.Strategy { | ||
for _, strategy := range uc.strategies { | ||
if strategy.Name() == name { | ||
return strategy | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package unleash_client_go | ||
|
||
import ( | ||
"github.com/unleash/unleash-client-go/strategy" | ||
"time" | ||
) | ||
|
||
type configOption struct { | ||
appName string | ||
instanceId string | ||
url string | ||
refreshInterval time.Duration | ||
metricsInterval time.Duration | ||
disableMetrics bool | ||
backupPath string | ||
strategies []strategy.Strategy | ||
} | ||
|
||
type ConfigOption func(*configOption) | ||
|
||
func WithAppName(appName string) ConfigOption { | ||
return func(o *configOption) { | ||
o.appName = appName | ||
} | ||
} | ||
|
||
func WithInstanceId(instanceId string) ConfigOption { | ||
return func(o *configOption) { | ||
o.instanceId = instanceId | ||
} | ||
} | ||
|
||
func WithUrl(url string) ConfigOption { | ||
return func(o *configOption) { | ||
o.url = url | ||
} | ||
} | ||
|
||
func WithRefreshInterval(refreshInterval time.Duration) ConfigOption { | ||
return func(o *configOption) { | ||
o.refreshInterval = refreshInterval | ||
} | ||
} | ||
|
||
func WithMetricsInterval(metricsInterval time.Duration) ConfigOption { | ||
return func(o *configOption) { | ||
o.metricsInterval = metricsInterval | ||
} | ||
} | ||
|
||
func WithDisableMetrics(disableMetrics bool) ConfigOption { | ||
return func(o *configOption) { | ||
o.disableMetrics = disableMetrics | ||
} | ||
} | ||
|
||
func WithBackupPath(backupPath string) ConfigOption { | ||
return func(o *configOption) { | ||
o.backupPath = backupPath | ||
} | ||
} | ||
|
||
func WithStrategies(strategies ...strategy.Strategy) ConfigOption { | ||
return func(o *configOption) { | ||
o.strategies = strategies | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package context | ||
|
||
type Context struct { | ||
UserId string | ||
SessionId string | ||
RemoteAddress string | ||
Properties map[string]string | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package unleash_client_go | ||
|
||
import "io" | ||
|
||
type errorEmitter interface { | ||
io.Closer | ||
warn(error) | ||
err(error) | ||
Warnings() <-chan error | ||
Errors() <-chan error | ||
Forward(errorEmitter) | ||
} | ||
|
||
type errorEmitterImpl struct { | ||
warnings chan error | ||
errors chan error | ||
close chan bool | ||
} | ||
|
||
func newErrorEmitter() *errorEmitterImpl { | ||
return &errorEmitterImpl{ | ||
warnings: make(chan error, 5), | ||
errors: make(chan error, 5), | ||
close: make(chan bool), | ||
} | ||
} | ||
|
||
func (e errorEmitterImpl) warn(warning error) { | ||
e.warnings <- warning | ||
} | ||
|
||
func (e errorEmitterImpl) err(err error) { | ||
e.errors <- err | ||
} | ||
|
||
func (e errorEmitterImpl) Close() error { | ||
e.close <- true | ||
return nil | ||
} | ||
|
||
func (e errorEmitterImpl) Warnings() <-chan error { | ||
return e.warnings | ||
} | ||
|
||
func (e errorEmitterImpl) Errors() <-chan error { | ||
return e.errors | ||
} | ||
|
||
func (e errorEmitterImpl) Forward(to errorEmitter) { | ||
go func() { | ||
for { | ||
select { | ||
case w := <-e.Warnings(): | ||
to.warn(w) | ||
case err := <-e.Errors(): | ||
to.err(err) | ||
case <-e.close: | ||
break | ||
} | ||
} | ||
}() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
unleash "github.com/unleash/unleash-client-go" | ||
"github.com/unleash/unleash-client-go/context" | ||
"strings" | ||
"time" | ||
) | ||
|
||
func init() { | ||
unleash.Initialize( | ||
unleash.WithAppName("my-application"), | ||
unleash.WithUrl("https://unleash.herokuapp.com/api/"), | ||
unleash.WithRefreshInterval(5*time.Second), | ||
unleash.WithMetricsInterval(5*time.Second), | ||
unleash.WithStrategies(&ActiveForUserWithEmailStrategy{}), | ||
) | ||
} | ||
|
||
type ActiveForUserWithEmailStrategy struct{} | ||
|
||
func (s ActiveForUserWithEmailStrategy) Name() string { | ||
return "ActiveForUserWithEmail" | ||
} | ||
|
||
func (s ActiveForUserWithEmailStrategy) IsEnabled(params map[string]interface{}, ctx *context.Context) bool { | ||
|
||
if ctx == nil { | ||
return false | ||
} | ||
value, found := params["emails"] | ||
if !found { | ||
return false | ||
} | ||
|
||
emails, ok := value.(string) | ||
if !ok { | ||
return false | ||
} | ||
|
||
for _, e := range strings.Split(emails, ",") { | ||
if e == ctx.Properties["emails"] { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
func main() { | ||
|
||
ctx := context.Context{ | ||
Properties: map[string]string{ | ||
"emails": "example@example.com", | ||
}, | ||
} | ||
|
||
timer := time.NewTimer(1 * time.Second) | ||
|
||
for { | ||
select { | ||
case warning := <-unleash.Warnings(): | ||
fmt.Printf("WARNING: %s", warning.Error()) | ||
case err := <-unleash.Errors(): | ||
fmt.Printf("ERROR: %s", err.Error()) | ||
case <-timer.C: | ||
enabled := unleash.IsEnabled("unleash.me", unleash.WithContext(ctx)) | ||
fmt.Printf("feature is enabled? %v\n", enabled) | ||
timer.Reset(1 * time.Second) | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.