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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +83 -0
- data/feature_guard.gemspec +25 -0
- data/lib/feature_guard.rb +20 -0
- data/lib/feature_guard/guard.rb +75 -0
- data/lib/feature_guard/version.rb +3 -0
- data/spec/feature_guard/guard_spec.rb +83 -0
- data/spec/feature_guard_spec.rb +53 -0
- data/spec/spec_helper.rb +14 -0
- metadata +96 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
[](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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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:
|