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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +15 -0
- data/LICENSE.txt +1 -1
- data/README.md +74 -43
- data/VERSION +1 -1
- data/abstract_feature_branch.gemspec +31 -56
- data/lib/abstract_feature_branch/configuration.rb +22 -7
- data/lib/abstract_feature_branch/redis/connection_pool_to_redis_adapter.rb +34 -0
- data/lib/abstract_feature_branch.rb +75 -7
- data/lib/ext/feature_branch.rb +4 -3
- data/lib/generators/templates/config/initializers/abstract_feature_branch.rb +7 -5
- metadata +76 -55
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -40
- data/RELEASE_NOTES.md +0 -55
- data/config/features/admin.local.yml +0 -15
- data/config/features/admin.yml +0 -17
- data/config/features/internal/wiki.local.yml +0 -15
- data/config/features/internal/wiki.yml +0 -17
- data/config/features/public.local.yml +0 -15
- data/config/features/public.yml +0 -17
- data/ruby187.Gemfile +0 -12
- data/ruby187.Gemfile.lock +0 -63
- data/spec/abstract_feature_branch/file_beautifier_spec.rb +0 -384
- data/spec/ext/feature_branch__feature_branch_per_user_spec.rb +0 -113
- data/spec/ext/feature_branch__feature_branch_spec.rb +0 -125
- data/spec/ext/feature_branch__feature_enabled_spec.rb +0 -260
- data/spec/fixtures/application_development_config/config/features.reference.yml +0 -21
- data/spec/fixtures/application_no_config/no_config +0 -1
- data/spec/fixtures/application_rails_config/config/features.local.yml +0 -16
- data/spec/fixtures/application_rails_config/config/features.yml +0 -20
- data/spec/fixtures/application_ugly_config_reference/config/another_application_configuration.yml +0 -31
- data/spec/fixtures/application_ugly_config_reference/config/database.yml +0 -17
- data/spec/fixtures/application_ugly_config_reference/config/features/admin.local.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features/admin.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features/empty.local.yml +0 -0
- data/spec/fixtures/application_ugly_config_reference/config/features/feature_empty_config.local.yml +0 -13
- data/spec/fixtures/application_ugly_config_reference/config/features/including_comments.local.yml +0 -52
- data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.local.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features/internal/wiki.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features/public.local.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features/public.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features.local.yml +0 -44
- data/spec/fixtures/application_ugly_config_reference/config/features.yml +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 52ff8ed098aa83bdd70eda839fa74b291c95993fb9859e2cc5ed8c0752bfa213
|
4
|
+
data.tar.gz: 9b5e6915905904be0b87690a73b1dfe14ec8893b2659383545bac23b0fe76978
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
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
|
38
|
-
-
|
39
|
-
-
|
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
|
-
-
|
48
|
-
-
|
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.
|
51
|
-
4.
|
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.
|
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.
|
59
|
-
5.
|
60
|
-
6.
|
61
|
-
7.
|
62
|
-
8.
|
63
|
-
9.
|
64
|
-
10.
|
65
|
-
11.
|
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
|
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
|
-
>
|
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.
|
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.
|
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
|
-
* [
|
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)
|
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.
|
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.
|
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.
|
12
|
-
s.
|
13
|
-
s.
|
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
|
-
".
|
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.
|
70
|
-
s.rubygems_version = "
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
87
|
-
s.add_dependency(%q<
|
88
|
-
s.add_dependency(%q<
|
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
|
-
|
45
|
-
|
46
|
+
|
47
|
+
def feature_store
|
48
|
+
@feature_store ||= initialize_feature_store
|
46
49
|
end
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
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(
|
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].
|
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
|
-
|
151
|
+
feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
|
84
152
|
else
|
85
|
-
|
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)
|
data/lib/ext/feature_branch.rb
CHANGED
@@ -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
|
-
|
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
|