abstract_feature_branch 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +8 -0
- data/README.md +9 -2
- data/VERSION +1 -1
- data/abstract_feature_branch.gemspec +2 -2
- data/lib/abstract_feature_branch.rb +7 -1
- data/lib/ext/feature_branch.rb +6 -2
- data/ruby187.Gemfile.lock +5 -5
- data/spec/ext/feature_branch__feature_branch_per_user_spec.rb +25 -29
- data/spec/ext/feature_branch__feature_enabled_spec.rb +24 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 56c9f13ee57d4f800bba4ab8028e76ff0b5fecdb
|
4
|
+
data.tar.gz: 091ac1a493c92555c32402ff470ed523198777e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6a9fb212dd6c931e5cb5b74ac7376a98671cebe88091807cd09c3465dd83acb2d6344200a09b9ab707de113fdc979e7ffb5ac3ae87923e230994c6559baa539
|
7
|
+
data.tar.gz: ff9599ee6671b064457f4a1dc5f7e4df27bd6adab3fbd3b5d7db308c01779911829c039f4905a44f1d945c62319e1cb9449d8076c024431dcd372b729be28025
|
data/.travis.yml
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
language: ruby
|
2
|
+
services:
|
3
|
+
- redis-server
|
2
4
|
rvm:
|
3
5
|
- 2.0.0
|
4
6
|
- 1.9.3
|
@@ -22,11 +24,17 @@ matrix:
|
|
22
24
|
gemfile: ruby187.Gemfile
|
23
25
|
- rvm: ree
|
24
26
|
gemfile: Gemfile
|
27
|
+
- rvm: ree
|
28
|
+
gemfile: ruby187.Gemfile
|
25
29
|
- rvm: jruby-18mode
|
26
30
|
gemfile: Gemfile
|
27
31
|
- rvm: jruby-18mode
|
28
32
|
gemfile: ruby187.Gemfile
|
29
33
|
- rvm: jruby-19mode
|
30
34
|
gemfile: ruby187.Gemfile
|
35
|
+
- rvm: jruby-19mode
|
36
|
+
gemfile: Gemfile
|
31
37
|
- rvm: rbx-2.1.1
|
32
38
|
gemfile: Gemfile
|
39
|
+
- rvm: rbx-2.1.1
|
40
|
+
gemfile: ruby187.Gemfile
|
data/README.md
CHANGED
@@ -79,6 +79,7 @@ enabled (true) or disabled (false) per environment (e.g. production).
|
|
79
79
|
> feature1: true
|
80
80
|
> feature2: true
|
81
81
|
> feature3: false
|
82
|
+
> feature4: per_user
|
82
83
|
>
|
83
84
|
> development:
|
84
85
|
> <<: *defaults
|
@@ -122,8 +123,7 @@ Note that <code>feature_enabled?</code> returns false if the feature is disabled
|
|
122
123
|
|
123
124
|
### Per-User Feature Enablement
|
124
125
|
|
125
|
-
It is possible to restrict enablement of features per specific users
|
126
|
-
in features.yml (or one of the overrides like features.local.yml or environment variable overrides)
|
126
|
+
It is possible to restrict enablement of features per specific users by setting a feature value to <code>per_user</code>.
|
127
127
|
|
128
128
|
1. Use <code>toggle_features_for_user</code> in Ruby code to enable features per user ID (e.g. email address or database ID). This loads Redis client gem into memory and stores per-user feature configuration in Redis.
|
129
129
|
In the example below, current_user is a method that provides the current signed in user (e.g. using Rails [Devise] (https://github.com/plataformatec/devise) library).
|
@@ -155,6 +155,13 @@ Examples:
|
|
155
155
|
> # THIS ONE WILL NOT EXECUTE
|
156
156
|
> end
|
157
157
|
|
158
|
+
Note:
|
159
|
+
|
160
|
+
If a feature is enabled as <code>true</code> or disabled as <code>false</code> in features.yml (or one of the overrides
|
161
|
+
like features.local.yml or environment variable overrides), then it overrides toggled per-user restrictions, becoming
|
162
|
+
enabled or disabled globally.
|
163
|
+
|
164
|
+
|
158
165
|
Recommendations
|
159
166
|
---------------
|
160
167
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "abstract_feature_branch"
|
8
|
-
s.version = "1.
|
8
|
+
s.version = "1.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Annas \"Andy\" Maleh"]
|
12
|
-
s.date = "2014-01-
|
12
|
+
s.date = "2014-01-19"
|
13
13
|
s.description = "abstract_feature_branch is a Rails gem that enables developers to easily branch by abstraction as per this pattern:\nhttp://paulhammant.com/blog/branch_by_abstraction.html\n\nIt is a productivity and fault tolerance enhancing team practice that has been utilized by professional software development\nteams at large corporations, such as Sears and Groupon.\n\nIt provides the ability to wrap blocks of code with an abstract feature branch name, and then\nspecify in a configuration file which features to be switched on or off.\n\nThe goal is to build out upcoming features in the same source code repository branch, regardless of whether all are\ncompleted by the next release date or not, thus increasing team productivity by preventing integration delays.\nDevelopers then disable in-progress features until they are ready to be switched on in production, yet enable them\nlocally and in staging environments for in-progress testing.\n\nThis gives developers the added benefit of being able to switch a feature off after release should big problems arise\nfor a high risk feature.\n\nabstract_feature_branch additionally supports DDD's pattern of\nBounded Contexts by allowing developers to configure\ncontext-specific feature files if needed.\n"
|
14
14
|
s.extra_rdoc_files = [
|
15
15
|
"LICENSE.txt",
|
@@ -150,7 +150,13 @@ module AbstractFeatureBranch
|
|
150
150
|
end
|
151
151
|
|
152
152
|
def booleanize_values(hash)
|
153
|
-
|
153
|
+
hash_values = hash.map do |k, v|
|
154
|
+
normalized_value = v.to_s.downcase
|
155
|
+
boolean_value = normalized_value == 'true'
|
156
|
+
new_value = normalized_value == 'per_user' ? 'per_user' : boolean_value
|
157
|
+
[k, new_value]
|
158
|
+
end
|
159
|
+
Hash[hash_values]
|
154
160
|
end
|
155
161
|
|
156
162
|
def downcase_keys(hash)
|
data/lib/ext/feature_branch.rb
CHANGED
@@ -9,8 +9,12 @@ class Object
|
|
9
9
|
raise 'Abstract feature branch conflicts with another Ruby library' if respond_to?(:feature_enabled?)
|
10
10
|
def self.feature_enabled?(feature_name, user_id = nil)
|
11
11
|
normalized_feature_name = feature_name.to_s.downcase
|
12
|
-
|
13
|
-
|
12
|
+
|
13
|
+
value = AbstractFeatureBranch.application_features[normalized_feature_name]
|
14
|
+
if value == 'per_user'
|
15
|
+
value = AbstractFeatureBranch.user_features_storage.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", user_id)
|
16
|
+
end
|
17
|
+
value
|
14
18
|
end
|
15
19
|
|
16
20
|
raise 'Abstract feature branch conflicts with another Ruby library' if Object.new.respond_to?(:feature_branch)
|
data/ruby187.Gemfile.lock
CHANGED
@@ -5,7 +5,7 @@ GEM
|
|
5
5
|
builder (3.2.2)
|
6
6
|
deep_merge (1.0.0)
|
7
7
|
diff-lcs (1.2.5)
|
8
|
-
faraday (0.8.
|
8
|
+
faraday (0.8.9)
|
9
9
|
multipart-post (~> 1.2.0)
|
10
10
|
git (1.2.6)
|
11
11
|
github_api (0.10.1)
|
@@ -28,9 +28,9 @@ GEM
|
|
28
28
|
rake
|
29
29
|
rdoc
|
30
30
|
json (1.8.1)
|
31
|
-
jwt (0.1.
|
31
|
+
jwt (0.1.10)
|
32
32
|
multi_json (>= 1.5)
|
33
|
-
multi_json (1.8.
|
33
|
+
multi_json (1.8.4)
|
34
34
|
multi_xml (0.5.5)
|
35
35
|
multipart-post (1.2.0)
|
36
36
|
nokogiri (1.5.10)
|
@@ -42,8 +42,8 @@ GEM
|
|
42
42
|
multi_xml (~> 0.5)
|
43
43
|
rack (~> 1.2)
|
44
44
|
rack (1.5.2)
|
45
|
-
rake (10.1.
|
46
|
-
rdoc (4.
|
45
|
+
rake (10.1.1)
|
46
|
+
rdoc (4.1.1)
|
47
47
|
json (~> 1.4)
|
48
48
|
rspec (2.14.1)
|
49
49
|
rspec-core (~> 2.14.0)
|
@@ -32,56 +32,52 @@ describe 'feature_branch object extensions' do
|
|
32
32
|
user_id = 'email1@example.com'
|
33
33
|
Process.fork do
|
34
34
|
AbstractFeatureBranch.initialize_user_features_storage
|
35
|
-
AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 =>
|
35
|
+
AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => false, :feature3 => true, :feature6 => true, :feature7 => false)
|
36
36
|
end
|
37
37
|
Process.wait
|
38
38
|
features_enabled = []
|
39
39
|
feature_branch :feature1, user_id do
|
40
40
|
features_enabled << :feature1
|
41
41
|
end
|
42
|
-
feature_branch :feature2, user_id do
|
43
|
-
features_enabled << :feature2
|
44
|
-
end
|
45
42
|
feature_branch :feature3, user_id do
|
46
43
|
features_enabled << :feature3
|
47
44
|
end
|
48
|
-
feature_branch :
|
49
|
-
features_enabled << :
|
45
|
+
feature_branch :feature6, user_id do
|
46
|
+
features_enabled << :feature6
|
47
|
+
end
|
48
|
+
feature_branch :feature6, 'otheruser@example.com' do
|
49
|
+
features_enabled << :feature6_otheruser
|
50
|
+
end
|
51
|
+
feature_branch :feature6 do
|
52
|
+
features_enabled << :feature6_nouserspecified
|
50
53
|
end
|
51
|
-
feature_branch :
|
52
|
-
features_enabled << :
|
54
|
+
feature_branch :feature7, user_id do
|
55
|
+
features_enabled << :feature7
|
53
56
|
end
|
54
|
-
features_enabled.should include(:feature1)
|
55
|
-
features_enabled.should_not include(:
|
56
|
-
features_enabled.
|
57
|
-
features_enabled.
|
58
|
-
features_enabled.should_not include(:
|
57
|
+
features_enabled.should include(:feature1) #remains like features.yml
|
58
|
+
features_enabled.should_not include(:feature3) #remains like features.yml
|
59
|
+
features_enabled.should include(:feature6) #per user honored as true
|
60
|
+
features_enabled.should_not include(:feature6_otheruser) #per user honored as false
|
61
|
+
features_enabled.should_not include(:feature6_nouserspecified) #per user requires user id or it returns false
|
62
|
+
features_enabled.should_not include(:feature7) #per user honored as false
|
59
63
|
end
|
60
64
|
it 'update feature branching (disabling some features) after having stored feature configuration per user in a separate process (ensuring persistence)' do
|
61
65
|
user_id = 'email1@example.com'
|
62
66
|
Process.fork do
|
63
67
|
AbstractFeatureBranch.initialize_user_features_storage
|
64
|
-
AbstractFeatureBranch.toggle_features_for_user(user_id, :
|
65
|
-
AbstractFeatureBranch.toggle_features_for_user(user_id, :
|
68
|
+
AbstractFeatureBranch.toggle_features_for_user(user_id, :feature6 => true, :feature7 => false)
|
69
|
+
AbstractFeatureBranch.toggle_features_for_user(user_id, :feature6 => false, :feature7 => true)
|
66
70
|
end
|
67
71
|
Process.wait
|
68
72
|
features_enabled = []
|
69
|
-
feature_branch :
|
70
|
-
features_enabled << :
|
71
|
-
end
|
72
|
-
feature_branch :feature2, user_id do
|
73
|
-
features_enabled << :feature2
|
74
|
-
end
|
75
|
-
feature_branch :feature3, user_id do
|
76
|
-
features_enabled << :feature3
|
73
|
+
feature_branch :feature6, user_id do
|
74
|
+
features_enabled << :feature6
|
77
75
|
end
|
78
|
-
feature_branch :
|
79
|
-
features_enabled << :
|
76
|
+
feature_branch :feature7, user_id do
|
77
|
+
features_enabled << :feature7
|
80
78
|
end
|
81
|
-
features_enabled.should_not include(:
|
82
|
-
features_enabled.should include(:
|
83
|
-
features_enabled.should_not include(:feature3)
|
84
|
-
features_enabled.should_not include(:feature5)
|
79
|
+
features_enabled.should_not include(:feature6)
|
80
|
+
features_enabled.should include(:feature7)
|
85
81
|
end
|
86
82
|
|
87
83
|
end
|
@@ -65,7 +65,30 @@ describe 'feature_branch object extensions' do
|
|
65
65
|
feature_enabled?(:feature4a).should be_nil
|
66
66
|
feature_enabled?(:feature5).should be_nil
|
67
67
|
end
|
68
|
-
|
68
|
+
context 'per user' do
|
69
|
+
let(:user_id) { 'email1@example.com' }
|
70
|
+
let(:another_user_id) { 'another_email@example.com' }
|
71
|
+
before do
|
72
|
+
AbstractFeatureBranch.user_features_storage.del("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}feature6")
|
73
|
+
end
|
74
|
+
after do
|
75
|
+
AbstractFeatureBranch.user_features_storage.del("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}feature6")
|
76
|
+
end
|
77
|
+
it 'behaves as expected if member list is empty, regardless of the user provided' do
|
78
|
+
feature_enabled?('feature6').should == false
|
79
|
+
feature_enabled?('feature6', nil).should == false
|
80
|
+
feature_enabled?('feature6', user_id).should == false
|
81
|
+
end
|
82
|
+
it 'behaves as expected if member list is not empty, and user provided is in member list' do
|
83
|
+
AbstractFeatureBranch.toggle_features_for_user(user_id, :feature6 => true)
|
84
|
+
feature_enabled?('feature6').should == false
|
85
|
+
feature_enabled?('feature6', user_id).should == true
|
86
|
+
end
|
87
|
+
it 'behaves as expected if member list is not empty, and user provided is not in member list' do
|
88
|
+
AbstractFeatureBranch.toggle_features_for_user(another_user_id, :feature6 => true)
|
89
|
+
feature_enabled?('feature6', user_id).should == false
|
90
|
+
end
|
91
|
+
end
|
69
92
|
context 'cacheability' do
|
70
93
|
before do
|
71
94
|
@development_application_root = File.expand_path(File.join(__FILE__, '..', '..', 'fixtures', 'application_development_config'))
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: abstract_feature_branch
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Annas "Andy" Maleh
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-01-
|
11
|
+
date: 2014-01-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|