abstract_feature_branch 1.2.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +15 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +74 -43
  5. data/VERSION +1 -1
  6. data/abstract_feature_branch.gemspec +31 -56
  7. data/lib/abstract_feature_branch/configuration.rb +22 -7
  8. data/lib/abstract_feature_branch/redis/connection_pool_to_redis_adapter.rb +34 -0
  9. data/lib/abstract_feature_branch.rb +75 -7
  10. data/lib/ext/feature_branch.rb +4 -3
  11. data/lib/generators/templates/config/initializers/abstract_feature_branch.rb +7 -5
  12. metadata +76 -55
  13. data/.coveralls.yml +0 -1
  14. data/.travis.yml +0 -40
  15. data/RELEASE_NOTES.md +0 -55
  16. data/config/features/admin.local.yml +0 -15
  17. data/config/features/admin.yml +0 -17
  18. data/config/features/internal/wiki.local.yml +0 -15
  19. data/config/features/internal/wiki.yml +0 -17
  20. data/config/features/public.local.yml +0 -15
  21. data/config/features/public.yml +0 -17
  22. data/ruby187.Gemfile +0 -12
  23. data/ruby187.Gemfile.lock +0 -63
  24. data/spec/abstract_feature_branch/file_beautifier_spec.rb +0 -384
  25. data/spec/ext/feature_branch__feature_branch_per_user_spec.rb +0 -113
  26. data/spec/ext/feature_branch__feature_branch_spec.rb +0 -125
  27. data/spec/ext/feature_branch__feature_enabled_spec.rb +0 -260
  28. data/spec/fixtures/application_development_config/config/features.reference.yml +0 -21
  29. data/spec/fixtures/application_no_config/no_config +0 -1
  30. data/spec/fixtures/application_rails_config/config/features.local.yml +0 -16
  31. data/spec/fixtures/application_rails_config/config/features.yml +0 -20
  32. data/spec/fixtures/application_ugly_config_reference/config/another_application_configuration.yml +0 -31
  33. data/spec/fixtures/application_ugly_config_reference/config/database.yml +0 -17
  34. data/spec/fixtures/application_ugly_config_reference/config/features/admin.local.yml +0 -44
  35. data/spec/fixtures/application_ugly_config_reference/config/features/admin.yml +0 -44
  36. data/spec/fixtures/application_ugly_config_reference/config/features/empty.local.yml +0 -0
  37. data/spec/fixtures/application_ugly_config_reference/config/features/feature_empty_config.local.yml +0 -13
  38. data/spec/fixtures/application_ugly_config_reference/config/features/including_comments.local.yml +0 -52
  39. data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.local.yml +0 -44
  40. data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.yml +0 -44
  41. data/spec/fixtures/application_ugly_config_reference/config/features/public.local.yml +0 -44
  42. data/spec/fixtures/application_ugly_config_reference/config/features/public.yml +0 -44
  43. data/spec/fixtures/application_ugly_config_reference/config/features.local.yml +0 -44
  44. data/spec/fixtures/application_ugly_config_reference/config/features.yml +0 -49
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 171dc3b05d9257bd90a08f4675cb4be9649e0c1e
4
- data.tar.gz: caa97d72c5866c47023ce82bba130b747e526936
2
+ SHA256:
3
+ metadata.gz: 52ff8ed098aa83bdd70eda839fa74b291c95993fb9859e2cc5ed8c0752bfa213
4
+ data.tar.gz: 9b5e6915905904be0b87690a73b1dfe14ec8893b2659383545bac23b0fe76978
5
5
  SHA512:
6
- metadata.gz: 92bc2cf8e35055a73a2e44a276b89cd5d3a27b2e499ed176243f836046392bb2f38109aa50b38a75b6c8ab254dc7695796ee0fb5b89d5e8ea652dc29ffc76edb
7
- data.tar.gz: b63edde1fb4b6beeece611e173dc002a56ff00a577d10003d769b1f21945a9b9e924663b87dfdefe673b3093c741e4ce6becc65e9f7d5af85b6b32efee1de76f
6
+ metadata.gz: 2f948fd18d057bed300d4cb9eb89e36b784ec2e8a445edaeaf2d742866884828795e8873de02ea9d1794604757dee8d6af2b63934a4f7fcd5b7c0d65aa1171ec
7
+ data.tar.gz: c4a1ddc94547f390e2960ddc97a20034cf7bd3dbaa378e17060ae38509b019897d0364913573856254269217c22731de15e7e1e8c8825b1d30c0615521ee6610
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Change Log
2
+
3
+ ## 1.3.1
4
+
5
+ - Support Redis `ConnectionPool` `AbstractFeatureBranch::Configuration#feature_store`
6
+
7
+ ## 1.3.0
8
+
9
+ - Officially support newer `redis` client gem version 5
10
+ - Support (general-user) Redis Overrides (similar to Environment Variable Overrides)
11
+ - Remove `redis` gem from required dependencies to allow using `abstract_feature_branch` without Redis
12
+ - Make configuration of Redis optional in generated Rails initializer
13
+ - Provide alias of `AbstractFeatureBranch::Configuration#feature_store` to `AbstractFeatureBranch::Configuration#user_features_storage` (plus corresponding aliases `feature_store=` and `initialize_feature_store`)
14
+ - Document support for Rails 7 and Redis Server 7
15
+ - Add gem post install instructions, including how to run the Rails generators and install/use Redis
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Annas "Andy" Maleh
1
+ Copyright (c) 2012-2022 Andy Maleh
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,21 +1,13 @@
1
- Abstract Feature Branch
2
- =======================
1
+ # Abstract Feature Branch 1.3.1
3
2
  [![Gem Version](https://badge.fury.io/rb/abstract_feature_branch.png)](http://badge.fury.io/rb/abstract_feature_branch)
4
3
  [![Build Status](https://api.travis-ci.org/AndyObtiva/abstract_feature_branch.png?branch=master)](https://travis-ci.org/AndyObtiva/abstract_feature_branch)
5
4
  [![Coverage Status](https://coveralls.io/repos/AndyObtiva/abstract_feature_branch/badge.png?branch=master)](https://coveralls.io/r/AndyObtiva/abstract_feature_branch?branch=master)
6
5
  [![Code Climate](https://codeclimate.com/github/AndyObtiva/abstract_feature_branch.png)](https://codeclimate.com/github/AndyObtiva/abstract_feature_branch)
7
6
 
8
- **As Featured In**
9
-
10
- [![Factor 75](https://dzd6ppgm28vds.cloudfront.net/assets/logo-b190de0b423855600e216d490fc160ad.png)](https://www.factor75.com)..[![Character Business Card](https://characterbusinesscard.s3.amazonaws.com/assets/Character-Business-Card-Logo-Desktop-44eb97b18b0a100488ba9c322343b91a.png)](https://www.characterbusinesscard.com)..[![Early Shares](http://early-shares-assets-production.s3.amazonaws.com/assets/logo2x-67fadfc8bb942ba92cca60c464010f1f.png)](https://www.earlyshares.com)
11
-
12
-
13
-
14
7
  abstract_feature_branch is a Rails gem that enables developers to easily branch by abstraction as per this pattern:
15
8
  http://paulhammant.com/blog/branch_by_abstraction.html
16
9
 
17
- It is a productivity and fault tolerance enhancing team practice that has been utilized by professional software development
18
- teams at large corporations, such as [Sears](http://www.sears.com) and [Groupon](http://www.groupon.com).
10
+ It is a productivity and fault tolerance enhancing team practice.
19
11
 
20
12
  It provides the ability to wrap blocks of code with an abstract feature branch name, and then
21
13
  specify in a configuration file which features to be switched on or off.
@@ -34,9 +26,10 @@ context-specific feature files if needed.
34
26
 
35
27
  Requirements
36
28
  ------------
37
- - Ruby ~> 2.0.0, ~> 1.9 or ~> 1.8.7
38
- - (Optional) Rails ~> 4.0.0, ~> 3.0 or ~> 2.0
39
- - (Optional) Redis server
29
+ - Ruby (between `~> 3.1.0` and `~> 1.8.7`) ([click for a list of tested Ruby versions](https://travis-ci.org/AndyObtiva/abstract_feature_branch))
30
+ - [Optional] Rails (between `~> 7.0` and `~> 2.0`)
31
+ - [Optional] Redis Server (between `~> 7.0` and `~> 2.0`)
32
+ - [Optional] Redis client gem (between `~> 5.0` and `~> 3.0`)
40
33
 
41
34
  Setup
42
35
  -----
@@ -44,25 +37,27 @@ Setup
44
37
  ### Rails Application Use
45
38
 
46
39
  1. Configure Rubygem
47
- - Rails (~> 4.0.0 or ~> 3.0): Add the following to Gemfile <pre>gem 'abstract_feature_branch', '1.2.2'</pre>
48
- - Rails (~> 2.0): Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.2.2'</pre>
40
+ - With `rails` between `~> 7.0` and `~> 2.0`: Add the following to Gemfile <pre>gem 'abstract_feature_branch', '~> 1.3.1'</pre>
41
+ - With `rails` `~> 2.0` only: Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.3.1'</pre>
49
42
  2. Generate <code>config/initializers/abstract_feature_branch.rb</code>, <code>lib/tasks/abstract_feature_branch.rake</code>, <code>config/features.yml</code> and <code>config/features.local.yml</code> in your Rails app directory by running <pre>rails g abstract_feature_branch:install</pre>
50
- 3. (Optional) Generate <code>config/features/[context_path].yml</code> in your Rails app directory by running <pre>rails g abstract_feature_branch:context context_path</pre> (more details under [**instructions**](#instructions))
51
- 4. (Optional) Customize configuration in <code>config/initializers/abstract_feature_branch.rb</code> (can be useful for changing location of feature files in Rails application, configuring Redis for per-user feature enablement, or troubleshooting a specific Rails environment feature configuration)
43
+ 3. [Optional] Generate <code>config/features/[context_path].yml</code> in your Rails app directory by running <pre>rails g abstract_feature_branch:context context_path</pre> (more details under [**instructions**](#instructions))
44
+ 4. [Optional] Customize configuration in <code>config/initializers/abstract_feature_branch.rb</code> (can be useful for changing location of feature files in Rails application, configuring Redis with a Redis or ConnectionPool instance to use for overrides, and per-user feature enablement, or troubleshooting a specific Rails environment feature configuration)
45
+ 5. [Optional] Redis Server (between `~> 7.0` and `~> 3.0`): Install view [Homebrew](https://brew.sh/) with `brew install redis`
46
+ 6. [Optional] `redis` client gem (between `~> 5.0` and `~> 3.0`): Add the following to Gemfile above `abstract_feature_branch` <pre>gem 'redis', '~> 5.0.5'</pre>
52
47
 
53
48
  ### Ruby Application General Use
54
49
 
55
- 1. <pre>gem install abstract_feature_branch -v 1.2.2</pre>
50
+ 1. <pre>gem install abstract_feature_branch -v 1.3.1</pre>
56
51
  2. Add code <code>require 'abstract_feature_branch'</code>
57
52
  3. Create <code>config/features.yml</code> under <code>AbstractFeatureBranch.application_root</code> and fill it with content similar to that of the sample <code>config/features.yml</code> mentioned under [**instructions**](#instructions).
58
- 4. (Optional) Create <code>config/features.local.yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
59
- 5. (Optional) Create <code>config/features/[context_path].yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
60
- 6. (Optional) Add code <code>AbstractFeatureBranch.application_root = "[your_application_path]"</code> to configure the location of feature files (it defaults to <code>'.'</code>)
61
- 7. (Optional) Add code <code>AbstractFeatureBranch.application_environment = "[your_application_environment]"</code> (it defaults to <code>'development'</code>). Alternatively, you can set <code>ENV['APP_ENV']</code> before the <code>require</code> statement or an an external environment variable.
62
- 8. (Optional) Add code <code>AbstractFeatureBranch.logger = "[your_application_logger]"</code> (it defaults to a new instance of Ruby <code>Logger</code>. Must use a logger with <code>info</code> and <code>warn</code> methods).
63
- 9. (Optional) Add code <code>AbstractFeatureBranch.cacheable = {[environment] => [true/false]}</code> to indicate cacheability of loaded feature files for enhanced performance (it defaults to true for every environment other than development).
64
- 10. (Optional) Add code <code>AbstractFeatureBranch.load_application_features</code> to pre-load application features for improved first-use performance
65
- 11. (Optional) Add code <code>AbstractFeatureBranch.user_features_storage = Redis.new(options)</code> to configure Redis for per-user feature enablement
53
+ 4. [Optional] Create <code>config/features.local.yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
54
+ 5. [Optional] Create <code>config/features/[context_path].yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
55
+ 6. [Optional] Add code <code>AbstractFeatureBranch.application_root = "[your_application_path]"</code> to configure the location of feature files (it defaults to <code>'.'</code>)
56
+ 7. [Optional] Add code <code>AbstractFeatureBranch.application_environment = "[your_application_environment]"</code> (it defaults to <code>'development'</code>). Alternatively, you can set <code>ENV['APP_ENV']</code> before the <code>require</code> statement or an an external environment variable.
57
+ 8. [Optional] Add code <code>AbstractFeatureBranch.logger = "[your_application_logger]"</code> (it defaults to a new instance of Ruby <code>Logger</code>. Must use a logger with <code>info</code> and <code>warn</code> methods).
58
+ 9. [Optional] Add code <code>AbstractFeatureBranch.cacheable = {[environment] => [true/false]}</code> to indicate cacheability of loaded feature files for enhanced performance (it defaults to true for every environment other than development).
59
+ 10. [Optional] Add code <code>AbstractFeatureBranch.load_application_features</code> to pre-load application features for improved first-use performance
60
+ 11. [Optional] Add code <code>AbstractFeatureBranch.feature_store = Redis.new(options)</code> to configure Redis for overrides or per-user feature enablement
66
61
 
67
62
  Instructions
68
63
  ------------
@@ -264,6 +259,45 @@ The benefits can be achieved more easily via <code>config/features.local.yml</co
264
259
  However, environment variable overrides are implemented to support overriding feature configuration for a Heroku deployed
265
260
  application more easily.
266
261
 
262
+ Redis Overrides
263
+ ---------------
264
+
265
+ Prerequisites: Redis server and client (`redis` gem) and optional Redis configuration of `AbstractFeatureBranch.feature_store` in `config/initializers/abstract_feature_branch.rb` (`Redis` `ConnectionPool` instance is recommended for Production environments)
266
+
267
+ To be able to override feature configuration in a production environment, you can utilize Redis Overrides.
268
+
269
+ Alternatively, you may use Redis Overrides as your main source of feature configuration if you prefer that instead of relying on YAML files.
270
+
271
+ You can override feature configuration with Redis hash values by calling `AbstractFeatureBranch#set_store_feature` in `rails console` (or `irb` after requiring `redis` and `abstract_feature_branch`):
272
+
273
+ ```ruby
274
+ AbstractFeatureBranch.set_store_feature('feature1', true)
275
+ ```
276
+
277
+ Behind the scenes, that is the equivalent of the following Redis client invocation, which stores a hash value in a `abstract_feature_branch` key:
278
+
279
+ ```ruby
280
+ AbstractFeatureBranch.configuration.feature_store.hset('abstract_feature_branch', 'feature1', 'true')
281
+ ```
282
+
283
+ To remove a Redis override, you can run the following in `rails console` (or `irb`):
284
+
285
+ ```ruby
286
+ AbstractFeatureBranch.delete_store_feature('feature1')
287
+ ```
288
+
289
+ To get a Redis override value, you can run the following in `rails console` (or `irb`):
290
+
291
+ ```ruby
292
+ AbstractFeatureBranch.get_store_feature('feature1')
293
+ ```
294
+
295
+ To get an array of all Redis Override features, you can run the following in `rails console` (or `irb`):
296
+
297
+ ```ruby
298
+ AbstractFeatureBranch.get_store_features
299
+ ```
300
+
267
301
  Heroku
268
302
  ------
269
303
 
@@ -284,7 +318,7 @@ Removing an environment variable override:
284
318
  ### Recommendation
285
319
 
286
320
  It is recommended that you use environment variable overrides on Heroku only as an emergency or temporary measure.
287
- Afterward, make the change officially in config/features.yml, deploy, and remove the environment variable override for the long term.
321
+ Afterward, make the change official in config/features.yml, deploy, and remove the environment variable override for the long term.
288
322
 
289
323
  ### Gotcha with abstract feature branching in CSS and JS files
290
324
 
@@ -308,18 +342,21 @@ the former if overlap in features occurs:
308
342
  3. Context-specific local feature file overrides: <code>config/features/**/*.local.yml</code>
309
343
  4. Main local feature file override: <code>config/features.local.yml</code>
310
344
  5. Environment variable overrides
345
+ 6. Redis overrides
311
346
 
312
347
  Rails Initializer
313
348
  -----------------
314
349
 
315
- 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):
350
+ Here is the content of the generated initializer [with `redis` client gem added] (<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):
316
351
 
317
- > require 'redis'
352
+ > # Storage system for features (other than YAML/Env-Vars). Right now, only Redis and ConnectionPool are supported.
353
+ > # AbstractFeatureBranch.feature_store = Redis.new
354
+ >
355
+ > # Storage can be a Redis ConnectionPool instance
356
+ > # AbstractFeatureBranch.feature_store = ConnectionPool.new { Redis.new }
318
357
  >
319
- > # Storage for user features, customizable over here. Right now, only a Redis client is supported.
320
358
  > # The following example line works with Heroku Redis To Go while still operating on local Redis for local development
321
- > # AbstractFeatureBranch.user_features_storage = Redis.new(:url => ENV['REDISTOGO_URL'])
322
- > AbstractFeatureBranch.user_features_storage = Redis.new
359
+ > # AbstractFeatureBranch.feature_store = Redis.new(:url => ENV['REDISTOGO_URL'])
323
360
  >
324
361
  > # Application root where config/features.yml or config/features/ is found
325
362
  > AbstractFeatureBranch.application_root = Rails.root
@@ -342,8 +379,6 @@ Here is the content of the generated initializer (<code>config/initializers/abst
342
379
  > # Pre-load application features to improve performance of first web-page hit
343
380
  > AbstractFeatureBranch.load_application_features unless Rails.env.development?
344
381
 
345
- TODO Document in Heroku (:url => ENV['REDISTOGO_URL'])
346
-
347
382
  Rake Task
348
383
  ---------
349
384
 
@@ -437,15 +472,14 @@ after invoking the rake task, **verify** that your feature file contents are to
437
472
  task changes.
438
473
 
439
474
  Feature Branches vs Branch by Abstraction
440
- ---------
475
+ -----------------------------------------
441
476
 
442
- Although feature branches and branching by abstraction are similar, there are different situations that recommend each approach.
477
+ Although feature branches and branching by abstraction are similar, there are different situations that recommend each approach.
443
478
 
444
- 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.
479
+ 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. Feature 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.
445
480
 
446
481
  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.
447
482
 
448
-
449
483
  Contributing to abstract_feature_branch
450
484
  ---------------------------------------
451
485
 
@@ -459,17 +493,14 @@ Contributing to abstract_feature_branch
459
493
 
460
494
  Committers
461
495
  ---------------------------------------
462
- * [Annas "Andy" Maleh (Author)](https://github.com/AndyObtiva)
496
+ * [Andy Maleh (Author)](https://github.com/AndyObtiva)
463
497
 
464
498
  Contributors
465
499
  ---------------------------------------
466
- * [Christian Nennemann](https://github.com/XORwell)
467
- * [Ben Downey](https://github.com/bnd5k)
468
500
  * [Mark Moschel](https://github.com/mmosche2)
469
501
 
470
502
  Copyright
471
503
  ---------------------------------------
472
504
 
473
- Copyright (c) 2013 Annas "Andy" Maleh. See LICENSE.txt for
505
+ Copyright (c) 2012-2022 Andy Maleh. See [LICENSE.txt](LICENSE.txt) for
474
506
  further details.
475
-
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.2
1
+ 1.3.1
@@ -2,36 +2,32 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
+ # stub: abstract_feature_branch 1.3.1 ruby lib
5
6
 
6
7
  Gem::Specification.new do |s|
7
- s.name = "abstract_feature_branch"
8
- s.version = "1.2.2"
8
+ s.name = "abstract_feature_branch".freeze
9
+ s.version = "1.3.1"
9
10
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Annas \"Andy\" Maleh"]
12
- s.date = "2014-02-23"
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"
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["Annas \"Andy\" Maleh".freeze]
14
+ s.date = "2022-12-12"
15
+ 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.\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".freeze
14
16
  s.extra_rdoc_files = [
17
+ "CHANGELOG.md",
15
18
  "LICENSE.txt",
16
19
  "README.md"
17
20
  ]
18
21
  s.files = [
19
- ".coveralls.yml",
20
- ".travis.yml",
22
+ "CHANGELOG.md",
21
23
  "LICENSE.txt",
22
24
  "README.md",
23
- "RELEASE_NOTES.md",
24
25
  "VERSION",
25
26
  "abstract_feature_branch.gemspec",
26
- "config/features/admin.local.yml",
27
- "config/features/admin.yml",
28
- "config/features/internal/wiki.local.yml",
29
- "config/features/internal/wiki.yml",
30
- "config/features/public.local.yml",
31
- "config/features/public.yml",
32
27
  "lib/abstract_feature_branch.rb",
33
28
  "lib/abstract_feature_branch/configuration.rb",
34
29
  "lib/abstract_feature_branch/file_beautifier.rb",
30
+ "lib/abstract_feature_branch/redis/connection_pool_to_redis_adapter.rb",
35
31
  "lib/ext/feature_branch.rb",
36
32
  "lib/generators/abstract_feature_branch/context_generator.rb",
37
33
  "lib/generators/abstract_feature_branch/install_generator.rb",
@@ -39,53 +35,32 @@ Gem::Specification.new do |s|
39
35
  "lib/generators/templates/config/features.local.yml",
40
36
  "lib/generators/templates/config/features.yml",
41
37
  "lib/generators/templates/config/initializers/abstract_feature_branch.rb",
42
- "lib/generators/templates/lib/tasks/abstract_feature_branch.rake",
43
- "ruby187.Gemfile",
44
- "ruby187.Gemfile.lock",
45
- "spec/abstract_feature_branch/file_beautifier_spec.rb",
46
- "spec/ext/feature_branch__feature_branch_per_user_spec.rb",
47
- "spec/ext/feature_branch__feature_branch_spec.rb",
48
- "spec/ext/feature_branch__feature_enabled_spec.rb",
49
- "spec/fixtures/application_development_config/config/features.reference.yml",
50
- "spec/fixtures/application_no_config/no_config",
51
- "spec/fixtures/application_rails_config/config/features.local.yml",
52
- "spec/fixtures/application_rails_config/config/features.yml",
53
- "spec/fixtures/application_ugly_config_reference/config/another_application_configuration.yml",
54
- "spec/fixtures/application_ugly_config_reference/config/database.yml",
55
- "spec/fixtures/application_ugly_config_reference/config/features.local.yml",
56
- "spec/fixtures/application_ugly_config_reference/config/features.yml",
57
- "spec/fixtures/application_ugly_config_reference/config/features/admin.local.yml",
58
- "spec/fixtures/application_ugly_config_reference/config/features/admin.yml",
59
- "spec/fixtures/application_ugly_config_reference/config/features/empty.local.yml",
60
- "spec/fixtures/application_ugly_config_reference/config/features/feature_empty_config.local.yml",
61
- "spec/fixtures/application_ugly_config_reference/config/features/including_comments.local.yml",
62
- "spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.local.yml",
63
- "spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.yml",
64
- "spec/fixtures/application_ugly_config_reference/config/features/public.local.yml",
65
- "spec/fixtures/application_ugly_config_reference/config/features/public.yml"
38
+ "lib/generators/templates/lib/tasks/abstract_feature_branch.rake"
66
39
  ]
67
- s.homepage = "http://github.com/AndyObtiva/abstract_feature_branch"
68
- s.licenses = ["MIT"]
69
- s.require_paths = ["lib"]
70
- s.rubygems_version = "2.0.6"
71
- s.summary = "abstract_feature_branch is a Rails gem that enables developers to easily branch by abstraction as per this pattern: http://paulhammant.com/blog/branch_by_abstraction.html"
40
+ s.homepage = "http://github.com/AndyObtiva/abstract_feature_branch".freeze
41
+ s.licenses = ["MIT".freeze]
42
+ s.post_install_message = "\nRails-only post-install instructions:\n\n1) Run the following command to generate the Rails initializer and basic feature files:\n\nrails g abstract_feature_branch:install\n\n2) Optionally, you may run this command to generate feature files per context:\n\nrails g abstract_feature_branch:context context_path\n \n3) Optionally, install Redis server with [Homebrew](https://brew.sh/) by running:\n\nbrew install redis\n\n4) Optionally, install redis client gem (required with Redis server) by adding the following line to Gemfile above abstract_feature_branch:\n\ngem 'redis', '~> 5.0.5'\n\nAfterwards, run:\n\nbundle\n\n5) Optionally, customize configuration in config/initializers/abstract_feature_branch.rb\n\n(can be useful for changing location of feature files in Rails application,\nconfiguring Redis with a Redis or ConnectionPool instance to use for overrides and per-user feature enablement,\nand/or troubleshooting specific Rails environment feature configurations)\n\n".freeze
43
+ s.rubygems_version = "3.3.6".freeze
44
+ s.summary = "abstract_feature_branch is a Rails gem that enables developers to easily branch by abstraction as per this pattern: http://paulhammant.com/blog/branch_by_abstraction.html".freeze
72
45
 
73
46
  if s.respond_to? :specification_version then
74
47
  s.specification_version = 4
48
+ end
75
49
 
76
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
77
- s.add_runtime_dependency(%q<deep_merge>, ["= 1.0.0"])
78
- s.add_runtime_dependency(%q<redis>, ["~> 3.0.0"])
79
- s.add_development_dependency(%q<jeweler>, ["= 1.8.8"])
80
- else
81
- s.add_dependency(%q<deep_merge>, ["= 1.0.0"])
82
- s.add_dependency(%q<redis>, ["~> 3.0.0"])
83
- s.add_dependency(%q<jeweler>, ["= 1.8.8"])
84
- end
50
+ if s.respond_to? :add_runtime_dependency then
51
+ s.add_runtime_dependency(%q<deep_merge>.freeze, ["~> 1.0.0"])
52
+ s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
53
+ s.add_development_dependency(%q<bundler>.freeze, [">= 2.1.4"])
54
+ s.add_development_dependency(%q<rspec>.freeze, ["= 2.14.1"])
55
+ s.add_development_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
56
+ s.add_development_dependency(%q<psych>.freeze, ["= 3.3.4"])
85
57
  else
86
- s.add_dependency(%q<deep_merge>, ["= 1.0.0"])
87
- s.add_dependency(%q<redis>, ["~> 3.0.0"])
88
- s.add_dependency(%q<jeweler>, ["= 1.8.8"])
58
+ s.add_dependency(%q<deep_merge>.freeze, ["~> 1.0.0"])
59
+ s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
60
+ s.add_dependency(%q<bundler>.freeze, [">= 2.1.4"])
61
+ s.add_dependency(%q<rspec>.freeze, ["= 2.14.1"])
62
+ s.add_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
63
+ s.add_dependency(%q<psych>.freeze, ["= 3.3.4"])
89
64
  end
90
65
  end
91
66
 
@@ -1,3 +1,5 @@
1
+ require 'abstract_feature_branch/redis/connection_pool_to_redis_adapter'
2
+
1
3
  module AbstractFeatureBranch
2
4
  class Configuration
3
5
  def application_root
@@ -41,15 +43,28 @@ module AbstractFeatureBranch
41
43
  :production => true
42
44
  }
43
45
  end
44
- def user_features_storage
45
- @user_features_storage ||= initialize_user_features_storage
46
+
47
+ def feature_store
48
+ @feature_store ||= initialize_feature_store
46
49
  end
47
- def user_features_storage=(user_features_storage)
48
- @user_features_storage = user_features_storage
50
+ alias user_features_storage feature_store
51
+
52
+ def feature_store=(feature_store)
53
+ if feature_store.nil?
54
+ @feature_store = nil
55
+ else
56
+ @feature_store = feature_store.is_a?(::ConnectionPool) ? AbstractFeatureBranch::Redis::ConnectionPoolToRedisAdapter.new(feature_store) : feature_store
57
+ end
49
58
  end
50
- def initialize_user_features_storage
51
- require 'redis'
52
- self.user_features_storage = Redis.new
59
+ alias user_features_storage= feature_store=
60
+
61
+ def initialize_feature_store
62
+ self.feature_store = ::Redis.new
63
+ rescue => e
64
+ logger.debug { "Redis is not enabled!" }
65
+ logger.debug { e.full_message }
66
+ nil
53
67
  end
68
+ alias initialize_user_features_storage initialize_feature_store
54
69
  end
55
70
  end
@@ -0,0 +1,34 @@
1
+ module AbstractFeatureBranch
2
+ module Redis
3
+ # Adapts a ConnectionPool instance to the Redis object interface
4
+ class ConnectionPoolToRedisAdapter
5
+ attr_reader :connection_pool
6
+
7
+ def initialize(connection_pool)
8
+ @connection_pool = connection_pool
9
+ end
10
+
11
+ def respond_to?(method_name, include_private = false, &block)
12
+ result = false
13
+ @connection_pool.with do |connection|
14
+ result ||= connection.respond_to?(method_name, include_private, &block)
15
+ end
16
+ result || super
17
+ end
18
+
19
+ def method_missing(method_name, *args, &block)
20
+ connection_can_respond_to = nil
21
+ result = nil
22
+ @connection_pool.with do |connection|
23
+ connection_can_respond_to = connection.respond_to?(method_name, true)
24
+ result = connection.send(method_name, *args, &block) if connection_can_respond_to
25
+ end
26
+ if connection_can_respond_to
27
+ result
28
+ else
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -11,26 +11,44 @@ rescue Bundler::BundlerError => e
11
11
  end
12
12
  require 'logger' unless defined?(Rails) && Rails.logger
13
13
  require 'deep_merge' unless {}.respond_to?(:deep_merge!)
14
+ require 'forwardable'
14
15
 
15
- require File.join(File.dirname(__FILE__), 'abstract_feature_branch', 'configuration')
16
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
17
+
18
+ require 'abstract_feature_branch/configuration'
16
19
 
17
20
  module AbstractFeatureBranch
18
21
  ENV_FEATURE_PREFIX = "abstract_feature_branch_"
22
+ REDIS_HKEY = "abstract_feature_branch"
19
23
 
20
24
  class << self
21
25
  extend Forwardable
22
- def_delegators :configuration, :application_root, :application_root=, :initialize_application_root, :application_environment, :application_environment=, :initialize_application_environment, :logger, :logger=, :initialize_logger, :cacheable, :cacheable=, :initialize_cacheable, :user_features_storage, :user_features_storage=, :initialize_user_features_storage
26
+ def_delegators :configuration, :application_root, :application_root=, :initialize_application_root, :application_environment, :application_environment=, :initialize_application_environment, :logger, :logger=, :initialize_logger, :cacheable, :cacheable=, :initialize_cacheable, :feature_store, :feature_store=, :initialize_feature_store, :user_features_storage, :user_features_storage=, :initialize_user_features_storage
23
27
 
24
28
  def configuration
25
29
  @configuration ||= Configuration.new
26
30
  end
27
31
 
32
+ def redis_overrides
33
+ load_redis_overrides
34
+ end
35
+ def load_redis_overrides
36
+ return {} if feature_store.nil?
37
+
38
+ redis_feature_hash = get_store_features.inject({}) do |output, feature|
39
+ output.merge(feature => get_store_feature(feature))
40
+ end
41
+
42
+ downcase_keys(redis_feature_hash)
43
+ end
44
+
28
45
  def environment_variable_overrides
29
46
  @environment_variable_overrides ||= load_environment_variable_overrides
30
47
  end
31
48
  def load_environment_variable_overrides
32
- @environment_variable_overrides = featureize_keys(select_feature_keys(booleanize_values(downcase_keys(ENV))))
49
+ @environment_variable_overrides = featureize_keys(downcase_keys(booleanize_values(select_feature_keys(ENV))))
33
50
  end
51
+
34
52
  def local_features
35
53
  @local_features ||= load_local_features
36
54
  end
@@ -38,6 +56,7 @@ module AbstractFeatureBranch
38
56
  @local_features = {}
39
57
  load_specific_features(@local_features, '.local.yml')
40
58
  end
59
+
41
60
  def features
42
61
  @features ||= load_features
43
62
  end
@@ -45,6 +64,7 @@ module AbstractFeatureBranch
45
64
  @features = {}
46
65
  load_specific_features(@features, '.yml')
47
66
  end
67
+
48
68
  # performance optimization via caching of feature values resolved through environment variable overrides and local features
49
69
  def environment_features(environment)
50
70
  @environment_features ||= {}
@@ -54,7 +74,9 @@ module AbstractFeatureBranch
54
74
  @environment_features ||= {}
55
75
  features[environment] ||= {}
56
76
  local_features[environment] ||= {}
57
- @environment_features[environment] = features[environment].merge(local_features[environment]).merge(environment_variable_overrides)
77
+ @environment_features[environment] = features[environment].
78
+ merge(local_features[environment]).
79
+ merge(environment_variable_overrides)
58
80
  end
59
81
  def application_features
60
82
  unload_application_features unless cacheable?
@@ -77,12 +99,58 @@ module AbstractFeatureBranch
77
99
  value = (application_environment != 'development') if value.nil?
78
100
  value
79
101
  end
102
+
103
+ # Sets feature value (true or false) in storage (e.g. Redis client)
104
+ def set_store_feature(feature, value)
105
+ raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
106
+ feature = feature.to_s
107
+ return delete_store_feature(feature) if value.nil?
108
+ value = 'true' if value == true
109
+ value = 'false' if value == false
110
+ feature_store.hset(REDIS_HKEY, feature, value)
111
+ end
112
+
113
+ # Gets feature value (true or false) from storage (e.g. Redis client)
114
+ def get_store_feature(feature)
115
+ raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
116
+ feature = feature.to_s
117
+ value = feature_store.hget(REDIS_HKEY, feature)
118
+ if value.nil?
119
+ matching_feature = get_store_features.find { |store_feature| store_feature.downcase == feature.downcase }
120
+ value = feature_store.hget(REDIS_HKEY, matching_feature) if matching_feature
121
+ end
122
+ return nil if value.nil?
123
+ return 'per_user' if value.to_s.downcase == 'per_user'
124
+ value.to_s.downcase == 'true'
125
+ end
126
+
127
+ # Gets feature value (true or false) from storage (e.g. Redis client)
128
+ def delete_store_feature(feature)
129
+ raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
130
+ feature = feature.to_s
131
+ feature_store.hdel(REDIS_HKEY, feature)
132
+ end
133
+
134
+ # Gets features array (all features) from storage (e.g. Redis client)
135
+ def get_store_features
136
+ raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
137
+ feature_store.hkeys(REDIS_HKEY)
138
+ end
139
+
140
+ # Gets features array (all features) from storage (e.g. Redis client)
141
+ def clear_store_features
142
+ raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
143
+ feature_store.hkeys(REDIS_HKEY).each do |feature|
144
+ feature_store.hdel(REDIS_HKEY, feature)
145
+ end
146
+ end
147
+
80
148
  def toggle_features_for_user(user_id, features)
81
149
  features.each do |name, value|
82
150
  if value
83
- user_features_storage.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
151
+ feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
84
152
  else
85
- user_features_storage.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
153
+ feature_store.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
86
154
  end
87
155
  end
88
156
  end
@@ -103,7 +171,7 @@ module AbstractFeatureBranch
103
171
  end
104
172
 
105
173
  def select_feature_keys(hash)
106
- hash.reject {|k, v| !k.start_with?(ENV_FEATURE_PREFIX)} # using reject for Ruby 1.8 compatibility as select returns an array in it
174
+ hash.reject {|k, v| !k.downcase.start_with?(ENV_FEATURE_PREFIX)} # using reject for Ruby 1.8 compatibility as select returns an array in it
107
175
  end
108
176
 
109
177
  def booleanize_values(hash)
@@ -10,9 +10,10 @@ class Object
10
10
  def self.feature_enabled?(feature_name, user_id = nil)
11
11
  normalized_feature_name = feature_name.to_s.downcase
12
12
 
13
- value = AbstractFeatureBranch.application_features[normalized_feature_name]
13
+ redis_override_value = AbstractFeatureBranch.get_store_feature(normalized_feature_name) rescue nil
14
+ value = !redis_override_value.nil? ? redis_override_value : AbstractFeatureBranch.application_features[normalized_feature_name]
14
15
  if value == 'per_user'
15
- value = AbstractFeatureBranch.user_features_storage.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", user_id)
16
+ value = !user_id.nil? && AbstractFeatureBranch.user_features_storage.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", user_id)
16
17
  end
17
18
  value
18
19
  end
@@ -26,4 +27,4 @@ class Object
26
27
  def feature_enabled?(feature_name, user_id = nil)
27
28
  Object.feature_enabled?(feature_name.to_s, user_id)
28
29
  end
29
- end
30
+ end