Skip to content

Commit

Permalink
Implement variations
Browse files Browse the repository at this point in the history
    Added/Improved:
    - unit tests for variations
    - tests for metrics
    - documentation
    - some code clean up
  • Loading branch information
rarruda committed Jul 11, 2019
1 parent 708fd32 commit c88196a
Show file tree
Hide file tree
Showing 23 changed files with 777 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ Style/RedundantSelf:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/StringLiterals:
Enabled: false

Layout/SpaceBeforeBlockBraces:
Enabled: false
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ rvm:
- 2.6
- jruby
before_install:
- gem install bundler -v 2.0.1
- gem install bundler -v 2.0.2
# install client spec from official repository:
- git clone --depth 5 https://github.com/Unleash/client-specification.git client-specification
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@

[![Build Status](https://travis-ci.org/Unleash/unleash-client-ruby.svg?branch=master)](https://travis-ci.org/Unleash/unleash-client-ruby)
[![Coverage Status](https://coveralls.io/repos/github/Unleash/unleash-client-ruby/badge.svg?branch=master)](https://coveralls.io/github/Unleash/unleash-client-ruby?branch=master)
[![Gem Version](https://badge.fury.io/rb/unleash.svg)](https://badge.fury.io/rb/unleash)

Unleash client so you can roll out your features with confidence.

Leverage the [Unleash Server](https://github.com/Unleash/unleash) for powerful feature toggling in your ruby/rails applications.

## Supported Ruby Interpreters

* MRI 2.3
* MRI 2.4
* MRI 2.5
* MRI 2.6
* jruby

## Installation

Add this line to your application's Gemfile:
Expand Down Expand Up @@ -52,7 +61,7 @@ Argument | Description | Required? | Type | Default Value|
`instance_id` | Identifier for the running instance of program. Important so you can trace back to where metrics are being collected from. **Highly recommended be be set.** | N | String | random UUID |
`refresh_interval` | How often the unleash client should check with the server for configuration changes. | N | Integer | 15 |
`metrics_interval` | How often the unleash client should send metrics to server. | N | Integer | 10 |
`disable_client` | Disables all communication with the Unleash server. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
`disable_client` | Disables all communication with the Unleash server, effectively taking it *offline*. If set, `is_enabled?` will always answer with the `default_value` and configuration validation is skipped. Defeats the entire purpose of using unleash, but can be useful in when running tests. | N | Boolean | `false` |
`disable_metrics` | Disables sending metrics to Unleash server. | N | Boolean | `false` |
`custom_http_headers` | Custom headers to send to Unleash. | N | Hash | {} |
`timeout` | How long to wait for the connection to be established or wait in reading state (open_timeout/read_timeout) | N | Integer | 30 |
Expand Down Expand Up @@ -168,12 +177,34 @@ if UNLEASH.is_enabled? "AwesomeFeature", @unleash_context, true
end
```

Alternatively by using `if_enabled` you can send a code block to be executed as a parameter:

```ruby
UNLEASH.if_enabled "AwesomeFeature", @unleash_context, true do
puts "AwesomeFeature is enabled by default"
end
```

##### Variations

If no variant is found in the server, use the fallback variant.

```ruby
fallback_variant = Unleash::Variant.new(name: 'default', enabled: true, payload: {"color" => "blue"})
variant = UNLEASH.get_variant "ColorVariants", @unleash_context, fallback_variant

puts "variant color is: #{variant.payload.fetch('color')}"
```


#### Client methods

Method Name | Description | Return Type |
---------|-------------|-------------|
`is_enabled?` | Check if feature toggle is to be enabled or not. | Boolean |
`enabled?` | Alias to the `is_enabled?` method. But more ruby idiomatic. | Boolean |
`if_enabled` | Run a code block, if a feature is enabled. | `yield` |
`get_variant` | Get variant for a given feature | `Unleash::Variant` |
`shutdown` | Save metrics to disk, flush metrics to server, and then kill ToggleFetcher and MetricsReporter threads. A safe shutdown. Not really useful in long running applications, like web applications. | nil |
`shutdown!` | Kill ToggleFetcher and MetricsReporter threads immediately. | nil |

Expand Down
37 changes: 31 additions & 6 deletions bin/unleash-client
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,30 @@ require 'unleash/client'
require 'unleash/context'

options = {
variant: false,
verbose: false,
quiet: false,
url: 'http://localhost:4242',
demo: false,
disable_metrics: true,
sleep: 0.1,
}

OptionParser.new do |opts|
opts.banner = "Usage: #{__FILE__} [options] feature [key1=val1] [key2=val2]"

opts.on("-V", "--variant", "Fetch variant for feature") do |v|
options[:variant] = v
end

opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
options[:verbose] = v
end

opts.on("-q", "--quiet", "Quiet mode, minimum output only") do |v|
options[:quiet] = v
end

opts.on("-uURL", "--url=URL", "URL base for the Unleash feature toggle service") do |u|
options[:url] = u
end
Expand All @@ -27,6 +38,10 @@ OptionParser.new do |opts|
options[:demo] = d
end

opts.on("-m", "--[no-]metrics", "Enable metrics reporting") do |m|
options[:disable_metrics] = !m
end

opts.on("-sSLEEP", "--sleep=SLEEP", Float, "Sleep interval between checks (seconds) in demo") do |s|
options[:sleep] = s
end
Expand All @@ -40,17 +55,19 @@ end.parse!
feature_name = ARGV.shift
raise 'feature_name is required. see --help for usage.' unless feature_name

options[:verbose] = false if options[:quiet]

@unleash = Unleash::Client.new(
url: options[:url],
app_name: 'unleash-client-ruby-cli',
log_level: options[:verbose] ? Logger::DEBUG : Logger::WARN,
disable_metrics: options[:metrics],
log_level: log_level = options[:quiet] ? Logger::ERROR : (options[:verbose] ? Logger::DEBUG : Logger::WARN),
)

context_params = ARGV.map{ |e| e.split("=")}.map{|k,v| [k.to_sym, v]}.to_h
context_params = ARGV.map{ |e| e.split("=") }.map{ |k,v| [k.to_sym, v] }.to_h
context_properties = context_params.reject{ |k,v| [:user_id, :session_id, :remote_address].include? k }
context_params.select!{ |k,v| [:user_id, :session_id, :remote_address].include? k }
context_params.merge!({properties: context_properties}) unless context_properties.nil?
context_params.merge!( properties: context_properties ) unless context_properties.nil?
unleash_context = Unleash::Context.new(context_params)

if options[:verbose]
Expand All @@ -63,18 +80,26 @@ if options[:verbose]
puts ""
end



if options[:demo]
loop do
enabled = @unleash.is_enabled?(feature_name, unleash_context)
print enabled ? '.' : '|'
sleep options[:sleep]
end
else
if @unleash.is_enabled?(feature_name, unleash_context)
puts " \'#{feature_name}\' is enabled according to unleash"
if options[:variant]
variant = @unleash.get_variant(feature_name, unleash_context)
puts " For feature \'#{feature_name}\' got variant \'#{variant}\'"
else
puts " \'#{feature_name}\' is disabled according to unleash"
if @unleash.is_enabled?(feature_name, unleash_context)
puts " \'#{feature_name}\' is enabled according to unleash"
else
puts " \'#{feature_name}\' is disabled according to unleash"
end
end
end


@unleash.shutdown
33 changes: 33 additions & 0 deletions lib/unleash/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,39 @@ def is_enabled?(feature, context = nil, default_value = false)
# enabled? is a more ruby idiomatic method name than is_enabled?
alias_method :enabled?, :is_enabled?

# execute a code block (passed as a parameter), if is_enabled? is true.
def if_enabled(feature, context = nil, default_value = false, &blk)
yield if is_enabled?(feature, context, default_value)
end


def get_variant(feature, context = nil, fallback_variant = false)
Unleash.logger.debug "Unleash::Client.get_variant for feature: #{feature} with context #{context}"

if Unleash.configuration.disable_client
Unleash.logger.warn "unleash_client is disabled! Always returning #{default_variant} for feature #{feature}!"
return fallback_variant || Unleash::FeatureToggle.disabled_variant
end

toggle_as_hash = Unleash.toggles.select{ |toggle| toggle['name'] == feature }.first if Unleash.toggles

if toggle_as_hash.nil?
Unleash.logger.debug "Unleash::Client.is_enabled? feature: #{feature} not found"
return fallback_variant || Unleash::FeatureToggle.disabled_variant
end

toggle = Unleash::FeatureToggle.new(toggle_as_hash)
variant = toggle.get_variant(context, fallback_variant)

if variant.nil?
Unleash.logger.debug "Unleash::Client.get_variant variants for feature: #{feature} not found"
return fallback_variant || Unleash::FeatureToggle.disabled_variant
end

# TODO: Add to README: name, payload, enabled (bool)

return variant
end

# safe shutdown: also flush metrics to server and toggles to disk
def shutdown
Expand Down
9 changes: 2 additions & 7 deletions lib/unleash/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,8 @@ def metrics_interval_in_millis
def validate!
return if self.disable_client

if self.app_name.nil? or self.url.nil?
raise ArgumentError, "URL and app_name are required parameters."
end

if ! self.custom_http_headers.is_a?(Hash)
raise ArgumentError, "custom_http_headers must be a hash."
end
raise ArgumentError, "URL and app_name are required parameters." if self.app_name.nil? or self.url.nil?
raise ArgumentError, "custom_http_headers must be a hash." unless self.custom_http_headers.is_a?(Hash)
end

def refresh_backup_file!
Expand Down
34 changes: 8 additions & 26 deletions lib/unleash/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,18 @@ class Context
attr_accessor :user_id, :session_id, :remote_address, :properties

def initialize(params = {})
params_is_a_hash = params.is_a?(Hash)
self.user_id = fetch(params, 'userId')
self.session_id = fetch(params, 'sessionId')
self.remote_address = fetch(params, 'remoteAddress')
self.properties =
if params_is_a_hash && ( params.fetch(:properties, nil) || params.fetch('properties', nil) ).is_a?(Hash)
fetch(params, 'properties', {})
else
{}
end
end
raise ArgumentError, "Unleash::Context must be initialized with a hash." unless params.is_a?(Hash)

def to_s
"<Context: user_id=#{self.user_id},session_id=#{self.session_id},remote_address=#{self.remote_address},properties=#{self.properties}>"
end
self.user_id = params.values_at('userId', :user_id).compact.first || ''
self.session_id = params.values_at('sessionId', :session_id).compact.first || ''
self.remote_address = params.values_at('remoteAddress', :remote_address).compact.first || ''

private
# Fetch key from hash. Try first with using camelCase, and if not found, try with snake case.
# This way we are are idiomatically compliant with ruby, but still giving priority to the same
# key names as in the other clients.
def fetch(params, camelcase_key, default_ret = '')
return default_ret unless params.is_a?(Hash)
return default_ret unless camelcase_key.is_a?(String) or camelcase_key.is_a?(Symbol)

params.fetch(camelcase_key, nil) || params.fetch(snake_sym(camelcase_key), nil) || default_ret
properties = params.values_at('properties', :properties).compact.first
self.properties = properties.is_a?(Hash) ? properties : {}
end

# transform CamelCase to snake_case and make it a sym, if it is a string
def snake_sym(key)
key.is_a?(String) ? key.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym : key
def to_s
"<Context: user_id=#{self.user_id},session_id=#{self.session_id},remote_address=#{self.remote_address},properties=#{self.properties}>"
end
end
end
Loading

0 comments on commit c88196a

Please sign in to comment.