Skip to content

Commit

Permalink
Implement part of the Go client for unleash.
Browse files Browse the repository at this point in the history
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
jrbarron committed Mar 2, 2017
1 parent f0375bf commit 86565ae
Show file tree
Hide file tree
Showing 25 changed files with 1,136 additions and 0 deletions.
156 changes: 156 additions & 0 deletions client.go
@@ -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
}
67 changes: 67 additions & 0 deletions config.go
@@ -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
}
}
8 changes: 8 additions & 0 deletions context/context.go
@@ -0,0 +1,8 @@
package context

type Context struct {
UserId string
SessionId string
RemoteAddress string
Properties map[string]string
}
62 changes: 62 additions & 0 deletions emitter.go
@@ -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
}
}
}()
}
74 changes: 74 additions & 0 deletions examples/custom_strategy.go
@@ -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)
}
}

}

0 comments on commit 86565ae

Please sign in to comment.