feature_guard 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.3"
4
+ - "2.0.0"
5
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in feature_guard.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 t ddddddd
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ [![Build Status](https://api.travis-ci.org/tdumitrescu/feature_guard.png)](https://travis-ci.org/tdumitrescu/feature_guard)
2
+
3
+ # FeatureGuard
4
+
5
+ Lightweight Redis-based feature-flagging for Ruby apps. Provides a simple syntax for enabling and disabling features, or gradually ramping up and down by enabling features for a percentage of total traffic.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'feature_guard'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install feature_guard
20
+
21
+ ## Usage
22
+
23
+ Check whether a feature is enabled globally:
24
+
25
+ ```ruby
26
+ FeatureGuard.enabled? :my_feature
27
+ ```
28
+
29
+ Globally enable or disable a feature:
30
+
31
+ ```ruby
32
+ FeatureGuard.enable :my_feature
33
+ FeatureGuard.disable :my_feature
34
+ FeatureGuard.toggle :my_feature
35
+ ```
36
+
37
+ Feature names can be strings or symbols. No data setup is necessary; any check for a feature which has never been enabled simply returns false.
38
+
39
+ For more fine-grained control, set a ramp-up value to decide which percentage of traffic should see the feature:
40
+
41
+ ```ruby
42
+ FeatureGuard.set_ramp :my_feature, 30.5 # 30.5%
43
+ FeatureGuard.bump_ramp :my_feature, 12 # 30.5 + 12 = 42.5%
44
+ FeatureGuard.bump_ramp :my_feature # 42.5 + 10 = 52.5%
45
+
46
+ FeatureGuard.ramp_val :my_feature # 52.5
47
+ ```
48
+
49
+ `.set_ramp` sets the ramp-up value; `.bump_ramp` increments or decrements it by a given value (defaults to 10.0). Check the current ramp-up value with `.ramp_val`.
50
+
51
+ Check whether to show the feature at the current ramp-up value:
52
+
53
+ ```ruby
54
+ FeatureGuard.allow? :my_feature, user_id
55
+ # true for 52.5% of user_id values
56
+
57
+ FeatureGuard.allow? :my_feature
58
+ # true for 52.5% of checks (random)
59
+ ```
60
+
61
+ The optional second argument to`.allow?` can be of any type (e.g., user ID or name or even an object). It is hashed with the feature name to create a reproducible numeric value for checking whether to return true or false based on the current ramp-up value. With no second argument, `.allow?` uses a new random value on every call.
62
+
63
+ ## Configuration
64
+
65
+ Optionally change the Redis client with:
66
+
67
+ ```ruby
68
+ FeatureGuard.redis = my_redis_client
69
+ ```
70
+
71
+ Setting `FeatureGuard.redis` to `nil` will revert it to a new default instance (`Redis.new`).
72
+
73
+ ## Running tests
74
+
75
+ $ bundle exec rspec
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/feature_guard/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ted Dumitrescu"]
6
+ gem.email = ["miscmisc@cmme.org"]
7
+ gem.summary = %q{Simple Redis-based feature-flagging}
8
+ gem.description = <<end
9
+ Turn code on or off with Redis controls, allowing simple enabled/disabled states
10
+ as well as finer-grained percentage-based control.
11
+ end
12
+ gem.homepage = "https://github.com/tdumitrescu/feature_guard"
13
+
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "feature_guard"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = FeatureGuard::VERSION
20
+
21
+ gem.add_dependency "redis", "~> 3.0"
22
+
23
+ gem.add_development_dependency "fakeredis", "~> 0.4"
24
+ gem.add_development_dependency "rspec", "~> 2.13"
25
+ end
@@ -0,0 +1,20 @@
1
+ require "redis" unless defined? Redis
2
+
3
+ require "feature_guard/version"
4
+ require "feature_guard/guard"
5
+
6
+ module FeatureGuard
7
+ class << self
8
+ attr_writer :redis
9
+
10
+ def redis
11
+ @redis ||= Redis.new
12
+ end
13
+
14
+ [:allow?, :bump_ramp, :disable, :enable, :toggle, :enabled?, :ramp_val, :set_ramp].each do |mname|
15
+ define_method(mname) do |key, *args|
16
+ Guard.new(key).send(mname, *args)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,75 @@
1
+ module FeatureGuard; class Guard
2
+ attr_reader :feature_name
3
+
4
+ def initialize(_feature_name)
5
+ @feature_name = _feature_name
6
+ end
7
+
8
+ # binary flag methods (enabled/disabled)
9
+ def disable
10
+ redis.set(flag_key, 0)
11
+ end
12
+
13
+ def enable
14
+ redis.set(flag_key, 1)
15
+ end
16
+
17
+ def enabled?
18
+ redis.get(flag_key).tap { |v| return (!v.nil? && v.to_i > 0) }
19
+ rescue
20
+ false
21
+ end
22
+
23
+ def toggle
24
+ enabled? ? disable : enable
25
+ end
26
+
27
+ # ramp methods (0.0 .. 100.0)
28
+ def allow?(val = nil)
29
+ val = val.nil? ? random_val : hashed_val(val)
30
+ val < ramp_val
31
+ end
32
+
33
+ def bump_ramp(amount = 10.0)
34
+ set_ramp(ramp_val + amount)
35
+ end
36
+
37
+ def ramp_val
38
+ redis.get(ramp_key).to_f
39
+ end
40
+
41
+ def set_ramp(new_val)
42
+ new_val = new_val.to_f
43
+ new_val = 100.0 if new_val > 100.0
44
+ new_val = 0.0 if new_val < 0.0
45
+
46
+ redis.set(ramp_key, new_val)
47
+ new_val
48
+ end
49
+
50
+ private
51
+
52
+ def feature_key
53
+ @feature_key ||= feature_name.to_s.split.join('_')
54
+ end
55
+
56
+ def flag_key
57
+ @flag_key ||= "fgf_#{feature_key}"
58
+ end
59
+
60
+ def ramp_key
61
+ @ramp_key ||= "fgr_#{feature_key}"
62
+ end
63
+
64
+ def hashed_val(s)
65
+ (Digest::MD5.hexdigest("#{ramp_key}_#{s}").to_i(16) % 10000).to_f / 100.0
66
+ end
67
+
68
+ def random_val
69
+ rand * 100.0
70
+ end
71
+
72
+ def redis
73
+ FeatureGuard.redis
74
+ end
75
+ end; end
@@ -0,0 +1,3 @@
1
+ module FeatureGuard
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,83 @@
1
+ require "spec_helper"
2
+
3
+ describe FeatureGuard::Guard do
4
+ let(:feature) { 'exciting new code' }
5
+ let(:guard) { FeatureGuard::Guard.new(feature) }
6
+
7
+ describe '#allow?' do
8
+ before { guard.set_ramp 30.0 }
9
+
10
+ context 'when no value is provided' do
11
+ subject { guard.allow? }
12
+
13
+ it 'uses a random value' do
14
+ expect(guard).to receive(:random_val).and_return(29.9)
15
+ expect(subject).to be_true
16
+ end
17
+ end
18
+
19
+ context 'when a value is provided' do
20
+ subject { guard.allow? 'username_or_id' }
21
+
22
+ it 'hashes the value together with the feature name' do
23
+ expect(guard).to receive(:hashed_val).and_return(30.1)
24
+ expect(subject).to be_false
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#bump_ramp' do
30
+ before { guard.set_ramp(5.0) }
31
+
32
+ it 'changes the ramp value by the given amount' do
33
+ expect { guard.bump_ramp 14.5 }.to change { guard.ramp_val }.from(5.0).to(19.5)
34
+ end
35
+
36
+ it 'defaults to bumping by 10.0' do
37
+ expect { guard.bump_ramp }.to change { guard.ramp_val }.from(5.0).to(15.0)
38
+ end
39
+ end
40
+
41
+ describe '#enabled?' do
42
+ subject { guard.enabled? }
43
+
44
+ context 'for a non-existent flag' do
45
+ it { should be_false }
46
+ end
47
+
48
+ context 'for an enabled flag' do
49
+ before { guard.enable }
50
+
51
+ it { should be_true }
52
+ end
53
+ end
54
+
55
+ describe '#set_ramp' do
56
+ let(:new_val) { 51.7 }
57
+
58
+ subject { guard.set_ramp new_val }
59
+
60
+ it 'sets the ramp value' do
61
+ expect { subject }.to change { guard.ramp_val }.to new_val
62
+ end
63
+
64
+ it 'returns the new ramp value' do
65
+ expect(subject).to eq new_val
66
+ end
67
+
68
+ it 'does not set the value above 100.0' do
69
+ expect(guard.set_ramp 190.0).to eq 100.0
70
+ end
71
+
72
+ it 'does not set the value below 0.0' do
73
+ expect(guard.set_ramp -190.0).to eq 0.0
74
+ end
75
+ end
76
+
77
+ describe '#toggle' do
78
+ it 'toggles the feature on or off' do
79
+ expect { guard.toggle }.to change { guard.enabled? }.from(false).to(true)
80
+ expect { guard.toggle }.to change { guard.enabled? }.from(true).to(false)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,53 @@
1
+ require "spec_helper"
2
+
3
+ describe FeatureGuard do
4
+ let(:feature) { :some_feature_name }
5
+
6
+ describe 'enabling and disabling a feature' do
7
+ it 'turns the feature on and off in succession' do
8
+ expect {
9
+ FeatureGuard.enable feature
10
+ }.to change {
11
+ FeatureGuard.enabled? feature
12
+ }.from(false).to(true)
13
+
14
+ expect {
15
+ FeatureGuard.disable feature
16
+ }.to change {
17
+ FeatureGuard.enabled? feature
18
+ }.from(true).to(false)
19
+ end
20
+ end
21
+
22
+ describe 'ramping a feature up and down' do
23
+ let(:user_id) { 5435 }
24
+
25
+ it 'allows a percentage of calls to use the feature' do
26
+ expect {
27
+ FeatureGuard.set_ramp feature, 100.0
28
+ }.to change {
29
+ FeatureGuard.allow? feature, user_id
30
+ }.from(false).to(true)
31
+
32
+ expect {
33
+ FeatureGuard.set_ramp feature, 0.0
34
+ }.to change {
35
+ FeatureGuard.allow? feature, user_id
36
+ }.from(true).to(false)
37
+ end
38
+ end
39
+
40
+ describe '.enabled?' do
41
+ subject { FeatureGuard.enabled? feature }
42
+
43
+ context 'for a non-existent flag' do
44
+ it { should be_false }
45
+ end
46
+
47
+ context 'when the Redis client blows up or is non-existent' do
48
+ before { FeatureGuard.stub(redis: nil) }
49
+
50
+ it { should be_false }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,14 @@
1
+ require "fakeredis/rspec"
2
+ require "feature_guard"
3
+
4
+ RSpec.configure do |config|
5
+ config.treat_symbols_as_metadata_keys_with_true_values = true
6
+ config.run_all_when_everything_filtered = true
7
+ config.filter_run :focus
8
+
9
+ config.order = 'random'
10
+
11
+ config.expect_with(:rspec) do |c|
12
+ c.syntax = :expect
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feature_guard
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ted Dumitrescu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: &2164594960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2164594960
25
+ - !ruby/object:Gem::Dependency
26
+ name: fakeredis
27
+ requirement: &2164594440 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '0.4'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2164594440
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2164593760 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '2.13'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2164593760
47
+ description: ! "Turn code on or off with Redis controls, allowing simple enabled/disabled
48
+ states \nas well as finer-grained percentage-based control.\n"
49
+ email:
50
+ - miscmisc@cmme.org
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - .rspec
57
+ - .travis.yml
58
+ - Gemfile
59
+ - LICENSE
60
+ - README.md
61
+ - feature_guard.gemspec
62
+ - lib/feature_guard.rb
63
+ - lib/feature_guard/guard.rb
64
+ - lib/feature_guard/version.rb
65
+ - spec/feature_guard/guard_spec.rb
66
+ - spec/feature_guard_spec.rb
67
+ - spec/spec_helper.rb
68
+ homepage: https://github.com/tdumitrescu/feature_guard
69
+ licenses: []
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ requirements: []
87
+ rubyforge_project:
88
+ rubygems_version: 1.8.10
89
+ signing_key:
90
+ specification_version: 3
91
+ summary: Simple Redis-based feature-flagging
92
+ test_files:
93
+ - spec/feature_guard/guard_spec.rb
94
+ - spec/feature_guard_spec.rb
95
+ - spec/spec_helper.rb
96
+ has_rdoc: