feature_guard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: