abstract_feature_branch 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a94572977e6efa72fc039c72a7674229b158a774
4
- data.tar.gz: 4afc8329e8a7122248ba1be4fd4af84d8f58f386
3
+ metadata.gz: 10b3189e83e418a64b851efe68587d4277cc370d
4
+ data.tar.gz: 4089ac0e1c27b0ae0df91e17fa20312ec65b941b
5
5
  SHA512:
6
- metadata.gz: b35df2e5e4fbdc3430c88ef4642895296063a2479584fceeed1c89d43dd95675905198d28edae0dbcdd1d5ce5952c73a7837046827feec60d253a46b6a8bea13
7
- data.tar.gz: 9ecc727ef80cae0d170e123fd310db95154ad7da8d12ddc7732e72e523bcf20a50781a0b88c256c3dcce273e6d22be2b1a68f53e2d67b860d1d78f6dfcb99161
6
+ metadata.gz: f1238ecd211de61432380e60b0f85fbcd8a50c03cb9f54588ec0257ca0d6f9a8227087442f1749bfe3027de9108fe38c73eb857c4d681dc564b74492c03838eb
7
+ data.tar.gz: 6569fd5e1a1b298e8a9c7791bd0f0fdbdde1b5c7076d47373a4cd27e49ec994c2c25bd1a61b69387ccb00f7ce94de7f97883e7cbc8199a91f888f3ebddd9fab4
@@ -18,8 +18,12 @@ matrix:
18
18
  gemfile: ruby187.Gemfile
19
19
  - rvm: 1.8.7
20
20
  gemfile: Gemfile
21
+ - rvm: 1.8.7
22
+ gemfile: ruby187.Gemfile
21
23
  - rvm: ree
22
24
  gemfile: Gemfile
25
+ - rvm: jruby-18mode
26
+ gemfile: Gemfile
23
27
  - rvm: jruby-18mode
24
28
  gemfile: ruby187.Gemfile
25
29
  - rvm: jruby-19mode
data/README.md CHANGED
@@ -29,6 +29,7 @@ Requirements
29
29
  ------------
30
30
  - Ruby ~> 2.0.0, ~> 1.9 or ~> 1.8.7
31
31
  - (Optional) Rails ~> 4.0.0, ~> 3.0 or ~> 2.0
32
+ - (Optional) Redis server
32
33
 
33
34
  Setup
34
35
  -----
@@ -109,18 +110,6 @@ single-line logic:
109
110
 
110
111
  Note that <code>feature_branch</code> returns nil and does not execute the block if the feature is disabled or non-existent.
111
112
 
112
- - Declaratively feature branch two paths of logic, one that runs when feature1 is enabled and one that runs when it is disabled:
113
-
114
- > feature_branch :feature1,
115
- > :true => lambda {
116
- > # perform logic
117
- > },
118
- > :false => lambda {
119
- > # perform alternate logic
120
- > }
121
-
122
- Note that <code>feature_branch</code> executes the false branch if the feature is non-existent.
123
-
124
113
  - Imperatively check if a feature is enabled or not:
125
114
 
126
115
  > if feature_enabled?(:feature1)
@@ -131,6 +120,41 @@ Note that <code>feature_branch</code> executes the false branch if the feature i
131
120
 
132
121
  Note that <code>feature_enabled?</code> returns false if the feature is disabled and nil if the feature is non-existent (practically the same effect, but nil can sometimes be useful to detect if a feature is referenced).
133
122
 
123
+ ### Per-User Feature Enablement
124
+
125
+ It is possible to restrict enablement of features per specific users. This works in concert with having a feature enabled
126
+ in features.yml (or one of the overrides like features.local.yml or environment variable overrides)
127
+
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
+ 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).
130
+
131
+ > user_id = current_user.email
132
+ > AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
133
+
134
+ Use alternate version of <code>feature_branch</code> and <code>feature_enabled?</code> passing extra <code>user_id</code> argument
135
+
136
+ Examples:
137
+
138
+ > feature_branch :feature1, current_user.email do
139
+ > # THIS WILL EXECUTE
140
+ > end
141
+
142
+ > if feature_enabled?(:feature2, current_user.email)
143
+ > # THIS ONE WILL NOT EXECUTE
144
+ > else
145
+ > # THIS ONE WILL EXECUTE
146
+ > end
147
+
148
+ > feature_branch :feature1, another_user.email do
149
+ > # THIS WILL NOT EXECUTE
150
+ > end
151
+
152
+ > if feature_enabled?(:feature2, another_user.email)
153
+ > # THIS ONE WILL EXECUTE (assuming feature2 is enabled in features.yml)
154
+ > else
155
+ > # THIS ONE WILL NOT EXECUTE
156
+ > end
157
+
134
158
  Recommendations
135
159
  ---------------
136
160
 
@@ -167,7 +191,7 @@ simply switching off the URL route to them. Example:
167
191
  > :website
168
192
  > )
169
193
 
170
- - In Rails 4 and 3.1+ with the asset pipeline, wrap newly added CSS or JavaScript using .erb format. Example (renamed projects.css.scss to projects.css.scss.erb and wrapped CSS with an abstract feature branch block):
194
+ - In Rails 4 and 3.1+ with the asset pipeline, wrap newly added CSS or JavaScript using .erb format ([gotcha and alternative solution](#gotcha-with-abstract-feature-branching-in-css-and-js-files)). Example (renamed projects.css.scss to projects.css.scss.erb and wrapped CSS with an abstract feature branch block):
171
195
 
172
196
  > <% feature_branch :project_gallery do %>
173
197
  > .exclude_display {
@@ -191,11 +215,11 @@ simply switching off the URL route to them. Example:
191
215
  it is **strongly recommended** that its feature branching code is plucked out of the codebase to simplify and improve
192
216
  future maintainability given that it is no longer needed at that point.
193
217
 
194
- - Once <code>config/features.yml</code> grows too big (e.g. 20+ features), it is **strongly recommended* to split it into
218
+ - Once <code>config/features.yml</code> grows too big (e.g. 20+ features), it is **strongly recommended** to split it into
195
219
  multiple context-specific feature files by utilizing the context generator mentioned above: <pre>rails g abstract_feature_branch:context context_path</pre>
196
220
 
197
221
  - When working on a new feature locally that the developer does not want others on the team to see yet, the feature
198
- can be enabled in <code>config/features.local.yml</code> only as it is git ignored, and disabled in <code>config/features.yml</code>
222
+ can be enabled in <code>config/features.local.yml</code> only as it is git ignored while the feature is disabled in <code>config/features.yml</code>
199
223
 
200
224
  - When troubleshooting a deployed feature by simulating a non-development environment (e.g. staging or production) locally,
201
225
  the developer can disable it temporarily in <code>config/features.local.yml</code> (git ignored) under the non-development environment,
@@ -231,7 +255,7 @@ Heroku
231
255
  Environment variable overrides can be extremely helpful on Heroku as they allow developers to enable/disable features
232
256
  at runtime without a redeploy.
233
257
 
234
- Examples:
258
+ ### Examples
235
259
 
236
260
  Enabling a new feature without a redeploy:
237
261
  <pre>heroku config:add ABSTRACT_FEATURE_BRANCH_FEATURE3=true -a heroku_application_name</pre>
@@ -242,12 +266,12 @@ Disabling a buggy recently deployed feature without a redeploy:
242
266
  Removing an environment variable override:
243
267
  <pre>heroku config:remove ABSTRACT_FEATURE_BRANCH_FEATURE2 -a heroku_application_name</pre>
244
268
 
245
- Recommendation:
269
+ ### Recommendation
246
270
 
247
271
  It is recommended that you use environment variable overrides on Heroku only as an emergency or temporary measure.
248
272
  Afterward, make the change officially in config/features.yml, deploy, and remove the environment variable override for the long term.
249
273
 
250
- Gotcha with abstract feature branching in CSS and JS files:
274
+ ### Gotcha with abstract feature branching in CSS and JS files
251
275
 
252
276
  If you've used abstract feature branching in CSS or JS files via ERB, setting environment variable overrides won't
253
277
  affect them as you need asset recompilation in addition to it, which can only be triggered by changing a CSS or JS
@@ -256,7 +280,7 @@ overrides have been recommended above as an emergency or temporary measure. If t
256
280
  variable overrides to alter the style or JavaScript behavior of a page back and forth without a redeploy, **one solution**
257
281
  is to do additional abstract feature branching in HTML templates (e.g. ERB or [HAML](http://haml.info) to
258
282
  link to different stylesheets and JS files, use different CSS classes, or invoke different JavaScript methods per branch
259
- of HTML for example.
283
+ of HTML for example.)
260
284
 
261
285
  Feature Configuration Load Order
262
286
  --------------------------------
@@ -275,6 +299,11 @@ Rails Initializer
275
299
 
276
300
  Here is the content of the generated initializer (<code>config/initializers/abstract_feature_branch.rb</code>), which contains instructions on how to customize via [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection):
277
301
 
302
+ > require 'redis'
303
+ >
304
+ > # Storage for user features, customizable over here (right now, only a Redis client is supported)
305
+ > AbstractFeatureBranch.user_features_storage = Redis.new
306
+ >
278
307
  > # Application root where config/features.yml or config/features/ is found
279
308
  > AbstractFeatureBranch.application_root = Rails.root
280
309
  >
@@ -388,6 +417,16 @@ Note that the beautifier ignores comments at the top, but deletes entire line co
388
417
  after invoking the rake task, **verify** that your feature file contents are to your satisfaction before committing the
389
418
  task changes.
390
419
 
420
+ Feature Branches vs Branch by Abstraction
421
+ ---------
422
+
423
+ Although feature branches and branching by abstraction are similar, there are different situations that recommend each approach.
424
+
425
+ Feature branching leverages your version control software (VCS) to create a branch that is independent of your main branch. Once you write your feature, you integrate it with the rest of your code base. Featuring branching is ideal for developing features that can be completed within the one or two iterations. But it can become cumbersome with larger features due to the fact your code is isolated and quickly falls out of sync with your main branch. You will have to regularly rebase with your main branch or devote substantial time to resolving merge conflicts.
426
+
427
+ Branching by abstraction, on the other hand, is ideal for substantial features, i.e. ones which take many iterations to complete. This approach to branching takes place outside of your VCS. Instead, you build your feature, but wrap the code inside configurable flags. These configuration flags will allow for different behavior, depending on the runtime environment. For example, a feature would be set to "on" when your app runs in development mode, but "off" when running in "production" mode. This approach avoids the pain of constantly rebasing or resolving a myriad of merge conflict when you do attempt to integrate your feature into the larger app.
428
+
429
+
391
430
  Contributing to abstract_feature_branch
392
431
  ---------------------------------------
393
432
 
@@ -401,15 +440,12 @@ Contributing to abstract_feature_branch
401
440
 
402
441
  Committers
403
442
  ---------------------------------------
404
- [Annas "Andy" Maleh (Author)](https://github.com/AndyObtiva)
443
+ * [Annas "Andy" Maleh (Author)](https://github.com/AndyObtiva)
405
444
 
406
445
  Contributors
407
446
  ---------------------------------------
408
- [Christian Nennemann](https://github.com/XORwell)
409
-
410
- Feedback and Idea Providers
411
- ---------------------------------------
412
- [Ben Downey](https://github.com/bnd5k)
447
+ * [Christian Nennemann](https://github.com/XORwell)
448
+ * [Ben Downey](https://github.com/bnd5k)
413
449
 
414
450
  Copyright
415
451
  ---------------------------------------
@@ -1,8 +1,8 @@
1
1
  Release Notes
2
2
  -------------
3
3
 
4
- Version 0.9.0:
5
- - Added configuration support for feature cacheability
4
+ Version 1.0.0:
5
+ - Added configuration support for feature cacheability. Completed documentation, adding more details.
6
6
 
7
7
  Version 0.9.0:
8
8
  - Added support for runtime read of feature files in development to ease local testing (trading off performance)
@@ -52,4 +52,4 @@ Version 0.3.0:
52
52
 
53
53
  Version 0.2.0:
54
54
  - Support an "else" block to execute when a feature is off (via <code>:true</code> and <code>:false</code> lambda arguments)
55
- - Support ability to check if a feature is enabled or not (via <code>feature_enabled?</code>)
55
+ - Support ability to check if a feature is enabled or not (via <code>feature_enabled?</code>)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.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.0.0"
8
+ s.version = "1.1.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 = "2013-11-26"
12
+ s.date = "2014-01-14"
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",
@@ -42,6 +42,7 @@ Gem::Specification.new do |s|
42
42
  "ruby187.Gemfile",
43
43
  "ruby187.Gemfile.lock",
44
44
  "spec/abstract_feature_branch/file_beautifier_spec.rb",
45
+ "spec/ext/feature_branch__feature_branch_per_user_spec.rb",
45
46
  "spec/ext/feature_branch__feature_branch_spec.rb",
46
47
  "spec/ext/feature_branch__feature_enabled_spec.rb",
47
48
  "spec/fixtures/application_development_config/config/features.reference.yml",
@@ -73,13 +74,16 @@ Gem::Specification.new do |s|
73
74
 
74
75
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
75
76
  s.add_runtime_dependency(%q<deep_merge>, ["= 1.0.0"])
77
+ s.add_runtime_dependency(%q<redis>, ["= 3.0.6"])
76
78
  s.add_development_dependency(%q<jeweler>, ["= 1.8.8"])
77
79
  else
78
80
  s.add_dependency(%q<deep_merge>, ["= 1.0.0"])
81
+ s.add_dependency(%q<redis>, ["= 3.0.6"])
79
82
  s.add_dependency(%q<jeweler>, ["= 1.8.8"])
80
83
  end
81
84
  else
82
85
  s.add_dependency(%q<deep_merge>, ["= 1.0.0"])
86
+ s.add_dependency(%q<redis>, ["= 3.0.6"])
83
87
  s.add_dependency(%q<jeweler>, ["= 1.8.8"])
84
88
  end
85
89
  end
@@ -13,132 +13,153 @@ require 'logger' unless defined?(Rails) && Rails.logger
13
13
  require 'deep_merge' unless {}.respond_to?(:deep_merge!)
14
14
 
15
15
  module AbstractFeatureBranch
16
- def self.application_root
17
- @application_root ||= initialize_application_root
18
- end
19
- def self.application_root=(path)
20
- @application_root = path
21
- end
22
- def self.initialize_application_root
23
- self.application_root = defined?(Rails) ? Rails.root : '.'
24
- end
25
- def self.application_environment
26
- @application_environment ||= initialize_application_environment
27
- end
28
- def self.application_environment=(environment)
29
- @application_environment = environment
30
- end
31
- def self.initialize_application_environment
32
- self.application_environment = defined?(Rails) ? Rails.env.to_s : ENV['APP_ENV'] || 'development'
33
- end
34
- def self.logger
35
- @logger ||= initialize_logger
36
- end
37
- def self.logger=(logger)
38
- @logger = logger
39
- end
40
- def self.initialize_logger
41
- self.logger = defined?(Rails) && Rails.logger ? Rails.logger : Logger.new(STDOUT)
42
- end
43
- def self.cacheable
44
- @cacheable ||= initialize_cacheable
45
- end
46
- def self.cacheable=(cacheable)
47
- @cacheable = cacheable
48
- end
49
- def self.initialize_cacheable
50
- self.cacheable = {
51
- :development => false,
52
- :test => true,
53
- :staging => true,
54
- :production => true
55
- }
56
- end
57
- def self.environment_variable_overrides
58
- @environment_variable_overrides ||= load_environment_variable_overrides
59
- end
60
- def self.load_environment_variable_overrides
61
- @environment_variable_overrides = featureize_keys(select_feature_keys(booleanize_values(downcase_keys(ENV))))
62
- end
63
- def self.local_features
64
- @local_features ||= load_local_features
65
- end
66
- def self.load_local_features
67
- @local_features = {}
68
- Dir.glob(File.join(application_root, 'config', 'features', '**', '*.local.yml')).each do |feature_configuration_file|
69
- @local_features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(feature_configuration_file)))
70
- end
71
- main_local_features_file = File.join(application_root, 'config', 'features.local.yml')
72
- @local_features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_local_features_file))) if File.exists?(main_local_features_file)
73
- @local_features
74
- end
75
- def self.features
76
- @features ||= load_features
77
- end
78
- def self.load_features
79
- @features = {}
80
- Dir.glob(File.join(application_root, 'config', 'features', '**', '*.yml')).each do |feature_configuration_file|
81
- @features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(feature_configuration_file)))
82
- end
83
- main_features_file = File.join(application_root, 'config', 'features.yml')
84
- @features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_features_file))) if File.exists?(main_features_file)
85
- @features
86
- end
87
- # performance optimization via caching of feature values resolved through environment variable overrides and local features
88
- def self.environment_features(environment)
89
- @environment_features ||= {}
90
- @environment_features[environment] ||= load_environment_features(environment)
91
- end
92
- def self.load_environment_features(environment)
93
- @environment_features ||= {}
94
- features[environment] ||= {}
95
- local_features[environment] ||= {}
96
- @environment_features[environment] = features[environment].merge(local_features[environment]).merge(environment_variable_overrides)
97
- end
98
- def self.application_features
99
- unload_application_features unless cacheable?
100
- environment_features(application_environment)
101
- end
102
- def self.load_application_features
103
- AbstractFeatureBranch.load_environment_variable_overrides
104
- AbstractFeatureBranch.load_features
105
- AbstractFeatureBranch.load_local_features
106
- AbstractFeatureBranch.load_environment_features(application_environment)
107
- end
108
- def self.unload_application_features
109
- @environment_variable_overrides = nil
110
- @features = nil
111
- @local_features = nil
112
- @environment_features = nil
113
- end
114
- def self.cacheable?
115
- value = downcase_keys(cacheable)[application_environment]
116
- value = (application_environment != 'development') if value.nil?
117
- value
118
- end
16
+ ENV_FEATURE_PREFIX = "abstract_feature_branch_"
119
17
 
120
- private
18
+ class << self
19
+ def application_root
20
+ @application_root ||= initialize_application_root
21
+ end
22
+ def application_root=(path)
23
+ @application_root = path
24
+ end
25
+ def initialize_application_root
26
+ self.application_root = defined?(Rails) ? Rails.root : '.'
27
+ end
28
+ def application_environment
29
+ @application_environment ||= initialize_application_environment
30
+ end
31
+ def application_environment=(environment)
32
+ @application_environment = environment
33
+ end
34
+ def initialize_application_environment
35
+ self.application_environment = defined?(Rails) ? Rails.env.to_s : ENV['APP_ENV'] || 'development'
36
+ end
37
+ def logger
38
+ @logger ||= initialize_logger
39
+ end
40
+ def logger=(logger)
41
+ @logger = logger
42
+ end
43
+ def initialize_logger
44
+ self.logger = defined?(Rails) && Rails.logger ? Rails.logger : Logger.new(STDOUT)
45
+ end
46
+ def cacheable
47
+ @cacheable ||= initialize_cacheable
48
+ end
49
+ def cacheable=(cacheable)
50
+ @cacheable = cacheable
51
+ end
52
+ def initialize_cacheable
53
+ self.cacheable = {
54
+ :development => false,
55
+ :test => true,
56
+ :staging => true,
57
+ :production => true
58
+ }
59
+ end
60
+ def user_features_storage
61
+ @user_features_storage ||= initialize_user_features_storage
62
+ end
63
+ def user_features_storage=(user_features_storage)
64
+ @user_features_storage = user_features_storage
65
+ end
66
+ def initialize_user_features_storage
67
+ require 'redis'
68
+ self.user_features_storage = Redis.new
69
+ end
70
+ def environment_variable_overrides
71
+ @environment_variable_overrides ||= load_environment_variable_overrides
72
+ end
73
+ def load_environment_variable_overrides
74
+ @environment_variable_overrides = featureize_keys(select_feature_keys(booleanize_values(downcase_keys(ENV))))
75
+ end
76
+ def local_features
77
+ @local_features ||= load_local_features
78
+ end
79
+ def load_local_features
80
+ @local_features = {}
81
+ Dir.glob(File.join(application_root, 'config', 'features', '**', '*.local.yml')).each do |feature_configuration_file|
82
+ @local_features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(feature_configuration_file)))
83
+ end
84
+ main_local_features_file = File.join(application_root, 'config', 'features.local.yml')
85
+ @local_features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_local_features_file))) if File.exists?(main_local_features_file)
86
+ @local_features
87
+ end
88
+ def features
89
+ @features ||= load_features
90
+ end
91
+ def load_features
92
+ @features = {}
93
+ Dir.glob(File.join(application_root, 'config', 'features', '**', '*.yml')).each do |feature_configuration_file|
94
+ @features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(feature_configuration_file)))
95
+ end
96
+ main_features_file = File.join(application_root, 'config', 'features.yml')
97
+ @features.deep_merge!(downcase_feature_hash_keys(YAML.load_file(main_features_file))) if File.exists?(main_features_file)
98
+ @features
99
+ end
100
+ # performance optimization via caching of feature values resolved through environment variable overrides and local features
101
+ def environment_features(environment)
102
+ @environment_features ||= {}
103
+ @environment_features[environment] ||= load_environment_features(environment)
104
+ end
105
+ def load_environment_features(environment)
106
+ @environment_features ||= {}
107
+ features[environment] ||= {}
108
+ local_features[environment] ||= {}
109
+ @environment_features[environment] = features[environment].merge(local_features[environment]).merge(environment_variable_overrides)
110
+ end
111
+ def application_features
112
+ unload_application_features unless cacheable?
113
+ environment_features(application_environment)
114
+ end
115
+ def load_application_features
116
+ AbstractFeatureBranch.load_environment_variable_overrides
117
+ AbstractFeatureBranch.load_features
118
+ AbstractFeatureBranch.load_local_features
119
+ AbstractFeatureBranch.load_environment_features(application_environment)
120
+ end
121
+ def unload_application_features
122
+ @environment_variable_overrides = nil
123
+ @features = nil
124
+ @local_features = nil
125
+ @environment_features = nil
126
+ end
127
+ def cacheable?
128
+ value = downcase_keys(cacheable)[application_environment]
129
+ value = (application_environment != 'development') if value.nil?
130
+ value
131
+ end
132
+ def toggle_features_for_user(user_id, features)
133
+ features.each do |name, value|
134
+ if value
135
+ user_features_storage.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
136
+ else
137
+ user_features_storage.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
138
+ end
139
+ end
140
+ end
121
141
 
122
- ENV_FEATURE_PREFIX = "abstract_feature_branch_"
142
+ private
123
143
 
124
- def self.featureize_keys(hash)
125
- Hash[hash.map {|k, v| [k.sub(ENV_FEATURE_PREFIX, ''), v]}]
126
- end
144
+ def featureize_keys(hash)
145
+ Hash[hash.map {|k, v| [k.sub(ENV_FEATURE_PREFIX, ''), v]}]
146
+ end
127
147
 
128
- def self.select_feature_keys(hash)
129
- hash.reject {|k, v| !k.start_with?(ENV_FEATURE_PREFIX)} # using reject for Ruby 1.8 compatibility as select returns an array in it
130
- end
148
+ def select_feature_keys(hash)
149
+ hash.reject {|k, v| !k.start_with?(ENV_FEATURE_PREFIX)} # using reject for Ruby 1.8 compatibility as select returns an array in it
150
+ end
131
151
 
132
- def self.booleanize_values(hash)
133
- Hash[hash.map {|k, v| [k, v.to_s.downcase == 'true']}]
134
- end
152
+ def booleanize_values(hash)
153
+ Hash[hash.map {|k, v| [k, v.to_s.downcase == 'true']}]
154
+ end
135
155
 
136
- def self.downcase_keys(hash)
137
- Hash[hash.map {|k, v| [k.to_s.downcase, v]}]
138
- end
156
+ def downcase_keys(hash)
157
+ Hash[hash.map {|k, v| [k.to_s.downcase, v]}]
158
+ end
139
159
 
140
- def self.downcase_feature_hash_keys(hash)
141
- Hash[(hash || {}).map {|k, v| [k, v && downcase_keys(v)]}]
160
+ def downcase_feature_hash_keys(hash)
161
+ Hash[(hash || {}).map {|k, v| [k, v && downcase_keys(v)]}]
162
+ end
142
163
  end
143
164
  end
144
165
 
@@ -1,24 +1,25 @@
1
1
  class Object
2
2
  raise 'Abstract feature branch conflicts with another Ruby library' if respond_to?(:feature_branch)
3
- def self.feature_branch(feature_name, branches = {}, &feature_work)
4
- branches[:true] ||= feature_work
5
- branches[:false] ||= lambda {}
6
- feature_branch_symbol_value = (!!feature_enabled?(feature_name)).to_s.to_sym
7
- branches[feature_branch_symbol_value].call
3
+ def self.feature_branch(feature_name, user_id = nil, &feature_work)
4
+ if feature_enabled?(feature_name, user_id)
5
+ feature_work.call
6
+ end
8
7
  end
9
8
 
10
9
  raise 'Abstract feature branch conflicts with another Ruby library' if respond_to?(:feature_enabled?)
11
- def self.feature_enabled?(feature_name)
12
- AbstractFeatureBranch.application_features[feature_name.to_s.downcase]
10
+ def self.feature_enabled?(feature_name, user_id = nil)
11
+ normalized_feature_name = feature_name.to_s.downcase
12
+ AbstractFeatureBranch.application_features[normalized_feature_name] &&
13
+ (user_id.nil? || AbstractFeatureBranch.user_features_storage.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", user_id))
13
14
  end
14
15
 
15
16
  raise 'Abstract feature branch conflicts with another Ruby library' if Object.new.respond_to?(:feature_branch)
16
- def feature_branch(feature_name, branches = {}, &feature_work)
17
- Object.feature_branch(feature_name.to_s, branches, &feature_work)
17
+ def feature_branch(feature_name, user_id = nil, &feature_work)
18
+ Object.feature_branch(feature_name.to_s, user_id, &feature_work)
18
19
  end
19
20
 
20
21
  raise 'Abstract feature branch conflicts with another Ruby library' if Object.new.respond_to?(:feature_enabled?)
21
- def feature_enabled?(feature_name)
22
- Object.feature_enabled?(feature_name.to_s)
22
+ def feature_enabled?(feature_name, user_id = nil)
23
+ Object.feature_enabled?(feature_name.to_s, user_id)
23
24
  end
24
25
  end
@@ -1,3 +1,8 @@
1
+ require 'redis'
2
+
3
+ # Storage for user features, customizable over here (right now, only a Redis client is supported)
4
+ AbstractFeatureBranch.user_features_storage = Redis.new
5
+
1
6
  # Application root where config/features.yml or config/features/ is found
2
7
  AbstractFeatureBranch.application_root = Rails.root
3
8
 
@@ -17,4 +22,4 @@ AbstractFeatureBranch.cacheable = {
17
22
  }
18
23
 
19
24
  # Pre-load application features to improve performance of first web-page hit
20
- AbstractFeatureBranch.load_application_features unless Rails.env.development?
25
+ AbstractFeatureBranch.load_application_features unless Rails.env.development?
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'feature_branch object extensions' do
4
+ before do
5
+ @app_env_backup = AbstractFeatureBranch.application_environment
6
+ @app_root_backup = AbstractFeatureBranch.application_root
7
+ AbstractFeatureBranch.logger.warn 'Environment variable ABSTRACT_FEATURE_BRANCH_FEATURE1 already set, potentially conflicting with another test' if ENV.keys.include?('ABSTRACT_FEATURE_BRANCH_FEATURE1')
8
+ AbstractFeatureBranch.logger.warn 'Environment variable Abstract_Feature_Branch_Feature2 already set, potentially conflicting with another test' if ENV.keys.include?('Abstract_Feature_Branch_Feature2')
9
+ AbstractFeatureBranch.logger.warn 'Environment variable abstract_feature_branch_feature3 already set, potentially conflicting with another test' if ENV.keys.include?('abstract_feature_branch_feature3')
10
+ AbstractFeatureBranch.user_features_storage.flushall
11
+ end
12
+ after do
13
+ ENV.delete('ABSTRACT_FEATURE_BRANCH_FEATURE1')
14
+ ENV.delete('Abstract_Feature_Branch_Feature2')
15
+ ENV.delete('abstract_feature_branch_feature3')
16
+ AbstractFeatureBranch.application_root = @app_root_backup
17
+ AbstractFeatureBranch.application_environment = @app_env_backup
18
+ AbstractFeatureBranch.unload_application_features
19
+ AbstractFeatureBranch.user_features_storage.flushall
20
+ end
21
+ describe '#feature_branch' do
22
+ context 'per user' do
23
+ it 'feature branches correctly after storing feature configuration per user in a separate process (ensuring persistence)' do
24
+ user_id = 'email1@example.com'
25
+ Process.fork do
26
+ AbstractFeatureBranch.initialize_user_features_storage
27
+ AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
28
+ end
29
+ Process.wait
30
+ features_enabled = []
31
+ feature_branch :feature1, user_id do
32
+ features_enabled << :feature1
33
+ end
34
+ feature_branch :feature2, user_id do
35
+ features_enabled << :feature2
36
+ end
37
+ feature_branch :feature3, user_id do
38
+ features_enabled << :feature3
39
+ end
40
+ feature_branch :feature5, user_id do
41
+ features_enabled << :feature5
42
+ end
43
+ feature_branch :feature5, 'otheruser@example.com' do
44
+ features_enabled << :feature5_otheruser
45
+ end
46
+ features_enabled.should include(:feature1)
47
+ features_enabled.should_not include(:feature2)
48
+ features_enabled.should_not include(:feature3)
49
+ features_enabled.should include(:feature5)
50
+ features_enabled.should_not include(:feature5_otheruser)
51
+ end
52
+ it 'update feature branching (disabling some features) after having stored feature configuration per user in a separate process (ensuring persistence)' do
53
+ user_id = 'email1@example.com'
54
+ Process.fork do
55
+ AbstractFeatureBranch.initialize_user_features_storage
56
+ AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
57
+ AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => false, :feature2 => true, :feature3 => false, :feature5 => false)
58
+ end
59
+ Process.wait
60
+ features_enabled = []
61
+ feature_branch :feature1, user_id do
62
+ features_enabled << :feature1
63
+ end
64
+ feature_branch :feature2, user_id do
65
+ features_enabled << :feature2
66
+ end
67
+ feature_branch :feature3, user_id do
68
+ features_enabled << :feature3
69
+ end
70
+ feature_branch :feature5, user_id do
71
+ features_enabled << :feature5
72
+ end
73
+ features_enabled.should_not include(:feature1)
74
+ features_enabled.should include(:feature2)
75
+ features_enabled.should_not include(:feature3)
76
+ features_enabled.should_not include(:feature5)
77
+ end
78
+
79
+ end
80
+ end
81
+ describe 'self#feature_branch' do
82
+ after do
83
+ Object.send(:remove_const, :TestObject)
84
+ end
85
+ # No need to retest all instance test cases, just a spot check due to implementation reuse
86
+ it 'feature branches instance level behavior (case-insensitive feature names)' do
87
+ class TestObject
88
+ def self.features_enabled
89
+ @features_enabled ||= []
90
+ end
91
+ def self.hit_me
92
+ feature_branch :feature1 do
93
+ self.features_enabled << :feature1
94
+ end
95
+ feature_branch :feature2 do
96
+ self.features_enabled << :feature2
97
+ end
98
+ feature_branch :feature3 do
99
+ self.features_enabled << :feature3
100
+ end
101
+ end
102
+ end
103
+ TestObject.hit_me
104
+ TestObject.features_enabled.should include(:feature1)
105
+ TestObject.features_enabled.should include(:feature2)
106
+ TestObject.features_enabled.should_not include(:feature3)
107
+ end
108
+ end
109
+ end
@@ -44,27 +44,6 @@ describe 'feature_branch object extensions' do
44
44
  end
45
45
  return_value.should be_nil
46
46
  end
47
- it 'supports an alternate branch of behavior for turned off features' do
48
- feature_behaviors = []
49
- feature_branch :feature1,
50
- :true => lambda {feature_behaviors << :feature1_true},
51
- :false => lambda {feature_behaviors << :feature1_false}
52
- feature_branch :feature3,
53
- :true => lambda {feature_behaviors << :feature3_true},
54
- :false => lambda {feature_behaviors << :feature3_false}
55
- feature_behaviors.should include(:feature1_true)
56
- feature_behaviors.should_not include(:feature1_false)
57
- feature_behaviors.should_not include(:feature3_true)
58
- feature_behaviors.should include(:feature3_false)
59
- end
60
- it 'executes alternate branch for an invalid feature name' do
61
- feature_behaviors = []
62
- feature_branch :invalid_feature_that_does_not_exist,
63
- :true => lambda {feature_behaviors << :main_branch},
64
- :false => lambda {feature_behaviors << :alternate_branch}
65
- feature_behaviors.should_not include(:main_branch)
66
- feature_behaviors.should include(:alternate_branch)
67
- end
68
47
  it 'allows environment variables (case-insensitive booleans) to override configuration file' do
69
48
  ENV['ABSTRACT_FEATURE_BRANCH_FEATURE1'] = 'FALSE'
70
49
  ENV['Abstract_Feature_Branch_Feature2'] = 'False'
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.0.0
4
+ version: 1.1.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: 2013-11-26 00:00:00.000000000 Z
11
+ date: 2014-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
26
  version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.0.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.6
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: jeweler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -92,6 +106,7 @@ files:
92
106
  - ruby187.Gemfile
93
107
  - ruby187.Gemfile.lock
94
108
  - spec/abstract_feature_branch/file_beautifier_spec.rb
109
+ - spec/ext/feature_branch__feature_branch_per_user_spec.rb
95
110
  - spec/ext/feature_branch__feature_branch_spec.rb
96
111
  - spec/ext/feature_branch__feature_enabled_spec.rb
97
112
  - spec/fixtures/application_development_config/config/features.reference.yml