abstract_feature_branch 1.3.3 → 1.5.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
  SHA256:
3
- metadata.gz: 5a8c7180bcc59c08383c35f7aaaa993c8d64684d8d5f52174f5cd27c6fbca1da
4
- data.tar.gz: f22086f5d839b540864caa62ab5fc3f31baf8f99d194c5516135ec0ddc85f032
3
+ metadata.gz: 0240cf2db5b90b0e267e2655cdc6492dc601983959d5438f6e8489ab2eb2c93b
4
+ data.tar.gz: 17a136533fdc8dc4332428d302581aa154463272a0a3dff94ca43d3a076a85b7
5
5
  SHA512:
6
- metadata.gz: 44908f37b872e59a8160d87b7a598949635489553b82a8cb56fcaa28ca5eac43679f9334ef273908aacaa90d6e2b7fe255913d1805eaff05cca2a9792c3d5f6e
7
- data.tar.gz: 8723b3f9ed9ce9131cbdc4c259e3700ad708925f50226c736115ee2f65da5a897f0f08537e1bac7b24d39741a0ef710ae732f8196e6ee7388b22058075318479
6
+ metadata.gz: cf380b5f6c9fd78ea192887bad5628a8632e26ade45636ee4ee10028efa69e3413c92d19748349e21421bed4666ac72c7b88a3ab8979be97621075a4abc36f0e
7
+ data.tar.gz: 441c92f87e112691be34ee9b703e039a7a368b6a7e4d0e082242892dc14907472e3892d3b81a95378eee8a70197e12b31daff631e017772a00d60b0ffce28e24
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.5.0
4
+
5
+ - Generalize "Per-User Feature Enablement" as "Scoped Feature Enablement" (using `scoped` value instead of `per_user` value) to scope features by any scope IDs (e.g. entity IDs or value objects)
6
+ - Rename `toggle_features_for_user` to `toggle_features_for_scope`
7
+
8
+ ## 1.4.0
9
+
10
+ - Avoid live loading of per-user features when `AbstractFeatureBranch.feature_store_live_fetching` is `false` to ensure better performance through caching.
11
+
3
12
  ## 1.3.3
4
13
 
5
14
  - Redis network failure error handling for per-user feature enablement to default to `nil` value instead of crashing
data/README.md CHANGED
@@ -1,17 +1,15 @@
1
- # Abstract Feature Branch 1.3.3
1
+ # Abstract Feature Branch 1.5.0
2
2
  [![Gem Version](https://badge.fury.io/rb/abstract_feature_branch.png)](http://badge.fury.io/rb/abstract_feature_branch)
3
3
  [![Build Status](https://api.travis-ci.org/AndyObtiva/abstract_feature_branch.png?branch=master)](https://travis-ci.org/AndyObtiva/abstract_feature_branch)
4
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)
5
5
  [![Code Climate](https://codeclimate.com/github/AndyObtiva/abstract_feature_branch.png)](https://codeclimate.com/github/AndyObtiva/abstract_feature_branch)
6
6
 
7
- [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) is a Ruby gem that provides a variation on the [Branch by Abstraction Pattern](http://paulhammant.com/blog/branch_by_abstraction.html) by [Paul Hammant](https://paulhammant.com/) and the [Feature Toggles Pattern](https://martinfowler.com/bliki/FeatureToggle.html) by [Martin Fowler](https://martinfowler.com/).
8
-
9
- It is a productivity and fault tolerance enhancing team practice.
7
+ [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) is a Ruby gem that provides a unique variation on the [Branch by Abstraction Pattern](http://paulhammant.com/blog/branch_by_abstraction.html) by [Paul Hammant](https://paulhammant.com/) and the [Feature Toggles Pattern](https://martinfowler.com/bliki/FeatureToggle.html) by [Martin Fowler](https://martinfowler.com/) to enhance team productivity and improve software fault tolerance.
10
8
 
11
9
  It provides the ability to wrap blocks of code with an abstract feature branch name, and then
12
10
  [specify in a configuration file](#instructions) which features to be switched on or off.
13
11
 
14
- The goal is to build out upcoming features in the same source code repository branch (i.e. continuous integration), regardless of whether all are
12
+ The goal is to build out upcoming features in the same source code repository branch (i.e. [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration) and [Trunk-Based Development](https://trunkbaseddevelopment.com)), regardless of whether all are
15
13
  completed by the next release date or not, thus increasing team productivity by preventing integration delays.
16
14
  Developers then disable in-progress features until they are ready to be switched on in production, yet enable them
17
15
  locally and in staging environments for in-progress testing.
@@ -23,7 +21,7 @@ for a high risk feature.
23
21
  [Bounded Contexts](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) by allowing developers to configure
24
22
  context-specific feature files if needed.
25
23
 
26
- [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) is one of the simplest and most minimalistic "Feature Flags" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use Redis as a very fast in-memory data store).
24
+ [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) is one of the simplest and most minimalistic "Feature Flags" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use [Redis](https://redis.com) as a very fast in-memory data store).
27
25
 
28
26
 
29
27
 
@@ -40,17 +38,17 @@ Setup
40
38
  ### Rails Application Use
41
39
 
42
40
  1. Configure Rubygem
43
- - With `rails` between `~> 7.0` and `~> 2.0`: Add the following to Gemfile <pre>gem 'abstract_feature_branch', '~> 1.3.3'</pre>
44
- - With `rails` `~> 2.0` only: Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.3.3'</pre>
41
+ - With `rails` between `~> 7.0` and `~> 2.0`: Add the following to Gemfile <pre>gem 'abstract_feature_branch', '~> 1.5.0'</pre>
42
+ - With `rails` `~> 2.0` only: Add the following to config/environment.rb <pre>config.gem 'abstract_feature_branch', :version => '1.5.0'</pre>
45
43
  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>
46
44
  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))
47
- 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](#per-user-feature-enablement), or troubleshooting a specific Rails environment feature configuration)
45
+ 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 [scoped feature enablement](#scoped-feature-enablement) (e.g. per-user), or troubleshooting a specific Rails environment feature configuration)
48
46
  5. [Optional] Redis Server (between `~> 7.0` and `~> 3.0`): On the Mac, you can install simply via [Homebrew](https://brew.sh/) with `brew install redis`
49
47
  6. [Optional] `redis` client gem (between `~> 5.0` and `~> 3.0`): Add the following to Gemfile above [`abstract_feature_branch`](https://rubygems.org/gems/abstract_feature_branch) <pre>gem 'redis', '~> 5.0.5'</pre>
50
48
 
51
49
  ### Ruby Application General Use
52
50
 
53
- 1. <pre>gem install abstract_feature_branch -v 1.3.3</pre>
51
+ 1. <pre>gem install abstract_feature_branch -v 1.5.0</pre>
54
52
  2. Add code <code>require 'abstract_feature_branch'</code>
55
53
  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).
56
54
  4. [Optional] Create <code>config/features.local.yml</code> under <code>AbstractFeatureBranch.application_root</code> (more details under [**instructions**](#instructions))
@@ -60,8 +58,8 @@ Setup
60
58
  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).
61
59
  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).
62
60
  10. [Optional] Add code <code>AbstractFeatureBranch.load_application_features</code> to pre-load application features for improved first-use performance
63
- 11. [Optional] Add code <code>AbstractFeatureBranch.feature_store = Redis.new(options)</code> to configure Redis for overrides and/or [per-user feature enablement](#per-user-feature-enablement)
64
- 12. [Optional] Set <code>AbstractFeatureBranch.feature_store_live_fetching = true</code> to enable live fetching of features from store (e.g. Redis) to avoid need for app/server restart upon feature changes, with the trade-off of slightly more latency due to making calls to feature store over the network
61
+ 11. [Optional] Add code <code>AbstractFeatureBranch.feature_store = Redis.new(options)</code> to configure Redis for overrides and/or [scoped feature enablement](#scoped-feature-enablement) (e.g. per-user)
62
+ 12. [Optional] Set <code>AbstractFeatureBranch.feature_store_live_fetching = true</code> to enable live fetching of features from store (e.g. Redis) to avoid need for app/server restart upon feature changes, with the trade-off of slightly more latency due to making calls to feature store over the network (this affects scoped feature enablement too, pre-caching all user IDs for features on app/server restart when disabled)
65
63
 
66
64
  Instructions
67
65
  ------------
@@ -86,7 +84,7 @@ enabled (true) or disabled (false) per environment (e.g. production).
86
84
  > feature1: true
87
85
  > feature2: true
88
86
  > feature3: false
89
- > feature4: per_user
87
+ > feature4: scoped
90
88
  >
91
89
  > development:
92
90
  > <<: *defaults
@@ -133,17 +131,17 @@ Note that <code>feature_enabled?</code> returns false if the feature is disabled
133
131
  > AbstractFeatureBranch.environment_features('development')
134
132
  > # => {"feature1"=>true, "feature2"=>false, "feature3"=>false, "feature4"=>true}
135
133
 
136
- ### Per-User Feature Enablement
134
+ ### Scoped Feature Enablement
137
135
 
138
- It is possible to restrict enablement of features per specific users (or per entities of any kind) by setting a feature value to <code>per_user</code>, and then toggling features for specific users (or other entities).
136
+ It is possible to restrict enablement of features per specific users (or per entities of any kind) by setting a feature value to <code>scoped</code>, and then toggling features for specific users (or other entities).
139
137
 
140
- 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.
138
+ 1. Use <code>toggle_features_for_scope</code> in Ruby code to enable features per scope ID (e.g. entity ID, comma-separated compound ID, JSON string, or value object), which must be a String or a value that is safely-convertable to a String like Integer (e.g. email address or database ID). This loads Redis client gem into memory and stores scoped feature configuration in Redis.
141
139
  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).
142
140
 
143
- > user_id = current_user.email
144
- > AbstractFeatureBranch.toggle_features_for_user(user_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
141
+ > scope_id = current_user.email
142
+ > AbstractFeatureBranch.toggle_features_for_scope(scope_id, :feature1 => true, :feature2 => false, :feature3 => true, :feature5 => true)
145
143
 
146
- Use alternate version of <code>feature_branch</code> and <code>feature_enabled?</code> passing extra <code>user_id</code> argument
144
+ Use alternate version of <code>feature_branch</code> and <code>feature_enabled?</code> passing extra <code>scope_id</code> argument
147
145
 
148
146
  Examples:
149
147
 
@@ -170,7 +168,7 @@ Examples:
170
168
  Note:
171
169
 
172
170
  If a feature is enabled as <code>true</code> or disabled as <code>false</code> in features.yml (or one of the overrides
173
- like features.local.yml or environment variable overrides), then it overrides toggled per-user restrictions, becoming
171
+ like features.local.yml or environment variable overrides), then it overrides toggled scoped feature restrictions, becoming
174
172
  enabled or disabled globally.
175
173
 
176
174
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.3
1
+ 1.5.0
@@ -2,17 +2,17 @@
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.3 ruby lib
5
+ # stub: abstract_feature_branch 1.5.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "abstract_feature_branch".freeze
9
- s.version = "1.3.3"
9
+ s.version = "1.5.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Andy Maleh".freeze]
14
- s.date = "2023-01-15"
15
- s.description = "abstract_feature_branch is a Ruby gem that provides a variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler.\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 specify 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 (i.e. continuous integration), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally 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 for a high risk feature.\n\nabstract_feature_branch additionally supports Domain Driven Design's pattern of Bounded Contexts by allowing developers to configure context-specific feature files if needed.\n\nabstract_feature_branch is one of the simplest and most minimalistic \"Feature Flags\" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use Redis as a very fast in-memory data store).\n".freeze
14
+ s.date = "2023-02-12"
15
+ s.description = "abstract_feature_branch is a Ruby gem that provides a unique variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler to enhance team productivity and improve software fault tolerance.\n\nIt provides the ability to wrap blocks of code with an abstract feature branch name, and then specify 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 (i.e. Continuous Integration and Trunk-Based Development), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally 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 for a high risk feature.\n\nabstract_feature_branch additionally supports Domain Driven Design's pattern of Bounded Contexts by allowing developers to configure context-specific feature files if needed.\n\nabstract_feature_branch is one of the simplest and most minimalistic \"Feature Flags\" Ruby gems out there as it enables you to get started very quickly by simply leveraging YAML files without having to set up a data store if you do not need it (albeit, you also have the option to use Redis as a very fast in-memory data store).\n".freeze
16
16
  s.extra_rdoc_files = [
17
17
  "CHANGELOG.md",
18
18
  "LICENSE.txt",
@@ -39,28 +39,30 @@ Gem::Specification.new do |s|
39
39
  ]
40
40
  s.homepage = "http://github.com/AndyObtiva/abstract_feature_branch".freeze
41
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.1.4".freeze
44
- s.summary = "abstract_feature_branch is a Ruby gem that provides a variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler (aka Feature Flags).".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 scoped feature enablement (e.g. per-user),\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 Ruby gem that provides a variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler (aka Feature Flags) to enable Continuous Integration and Trunk-Based Development.".freeze
45
45
 
46
46
  if s.respond_to? :specification_version then
47
47
  s.specification_version = 4
48
48
  end
49
49
 
50
50
  if s.respond_to? :add_runtime_dependency then
51
- s.add_runtime_dependency(%q<deep_merge>.freeze, ["~> 1.0.0"])
51
+ s.add_runtime_dependency(%q<deep_merge>.freeze, [">= 1.0.0", "< 2.0.0"])
52
52
  s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
53
53
  s.add_development_dependency(%q<bundler>.freeze, [">= 2.1.4"])
54
54
  s.add_development_dependency(%q<rspec>.freeze, ["= 2.14.1"])
55
55
  s.add_development_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
56
56
  s.add_development_dependency(%q<psych>.freeze, ["= 3.3.4"])
57
+ s.add_development_dependency(%q<rake-tui>.freeze, ["~> 0.2"])
57
58
  else
58
- s.add_dependency(%q<deep_merge>.freeze, ["~> 1.0.0"])
59
+ s.add_dependency(%q<deep_merge>.freeze, [">= 1.0.0", "< 2.0.0"])
59
60
  s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
60
61
  s.add_dependency(%q<bundler>.freeze, [">= 2.1.4"])
61
62
  s.add_dependency(%q<rspec>.freeze, ["= 2.14.1"])
62
63
  s.add_dependency(%q<rdoc>.freeze, ["= 5.1.0"])
63
64
  s.add_dependency(%q<psych>.freeze, ["= 3.3.4"])
65
+ s.add_dependency(%q<rake-tui>.freeze, ["~> 0.2"])
64
66
  end
65
67
  end
66
68
 
@@ -20,13 +20,16 @@ require 'abstract_feature_branch/configuration'
20
20
  module AbstractFeatureBranch
21
21
  ENV_FEATURE_PREFIX = "abstract_feature_branch_"
22
22
  REDIS_HKEY = "abstract_feature_branch"
23
+ VALUE_SCOPED = 'scoped'
24
+ SCOPED_SPECIAL_VALUES = [VALUE_SCOPED, 'per_user', 'per-user', 'per user']
23
25
 
24
26
  class << self
25
27
  extend Forwardable
26
- def_delegators :configuration, :application_root, :application_root=, :initialize_application_root, :application_environment, :application_environment=, :initialize_application_environment,
28
+ def_delegators :configuration, # delegating the following methods to configuration
29
+ :application_root, :application_root=, :initialize_application_root, :application_environment, :application_environment=, :initialize_application_environment,
27
30
  :logger, :logger=, :initialize_logger, :cacheable, :cacheable=, :initialize_cacheable, :feature_store, :feature_store=, :user_features_storage, :user_features_storage=,
28
31
  :feature_store_live_fetching, :feature_store_live_fetching=
29
-
32
+
30
33
  def configuration
31
34
  @configuration ||= Configuration.new
32
35
  end
@@ -84,6 +87,35 @@ module AbstractFeatureBranch
84
87
  merge(environment_variable_overrides).
85
88
  merge(redis_overrides)
86
89
  end
90
+
91
+ def redis_scoped_features
92
+ @redis_scoped_features ||= load_redis_scoped_features
93
+ end
94
+ def load_redis_scoped_features
95
+ @redis_scoped_features = {}
96
+ return @redis_scoped_features if AbstractFeatureBranch.configuration.feature_store_live_fetching?
97
+
98
+ @environment_features.each do |environment, features|
99
+ features.each do |feature, value|
100
+ if SCOPED_SPECIAL_VALUES.include?(value.to_s.downcase)
101
+ normalized_feature_name = feature.to_s.downcase
102
+ @redis_scoped_features[normalized_feature_name] ||= []
103
+ begin
104
+ scoped_feature_scope_ids = AbstractFeatureBranch.
105
+ feature_store.
106
+ smembers("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}")
107
+ @redis_scoped_features[normalized_feature_name] += scoped_feature_scope_ids
108
+ rescue Exception => error
109
+ AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User values for feature \"#{normalized_feature_name}\"! Defaulting to no values...\n\nError: #{error.full_message}\n\n"
110
+ nil
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ @redis_scoped_features
117
+ end
118
+
87
119
  def application_features
88
120
  unload_application_features unless cacheable?
89
121
  environment_features(application_environment)
@@ -94,6 +126,7 @@ module AbstractFeatureBranch
94
126
  AbstractFeatureBranch.load_features
95
127
  AbstractFeatureBranch.load_local_features
96
128
  AbstractFeatureBranch.load_environment_features(application_environment)
129
+ AbstractFeatureBranch.load_redis_scoped_features
97
130
  end
98
131
  def unload_application_features
99
132
  @redis_overrides = nil
@@ -101,13 +134,19 @@ module AbstractFeatureBranch
101
134
  @features = nil
102
135
  @local_features = nil
103
136
  @environment_features = nil
137
+ @redis_scoped_features = nil
104
138
  end
139
+
105
140
  def cacheable?
106
141
  value = downcase_keys(cacheable)[application_environment]
107
142
  value = (application_environment != 'development') if value.nil?
108
143
  value
109
144
  end
110
145
 
146
+ def scoped_value?(value)
147
+ SCOPED_SPECIAL_VALUES.include?(value.to_s.downcase)
148
+ end
149
+
111
150
  # Sets feature value (true or false) in storage (e.g. Redis client)
112
151
  def set_store_feature(feature, value)
113
152
  raise 'Feature storage (e.g. Redis) is not setup!' if feature_store.nil?
@@ -128,7 +167,7 @@ module AbstractFeatureBranch
128
167
  value = feature_store.hget(REDIS_HKEY, matching_feature) if matching_feature
129
168
  end
130
169
  return nil if value.nil?
131
- return 'per_user' if value.to_s.downcase == 'per_user'
170
+ return VALUE_SCOPED if scoped_value?(value)
132
171
  value.to_s.downcase == 'true'
133
172
  end
134
173
 
@@ -153,15 +192,16 @@ module AbstractFeatureBranch
153
192
  end
154
193
  end
155
194
 
156
- def toggle_features_for_user(user_id, features)
195
+ def toggle_features_for_scope(scope_id, features)
157
196
  features.each do |name, value|
158
197
  if value
159
- feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
198
+ feature_store.sadd("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope_id)
160
199
  else
161
- feature_store.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", user_id)
200
+ feature_store.srem("#{ENV_FEATURE_PREFIX}#{name.to_s.downcase}", scope_id)
162
201
  end
163
202
  end
164
203
  end
204
+ alias toggle_features_for_user toggle_features_for_scope
165
205
 
166
206
  private
167
207
 
@@ -186,7 +226,7 @@ module AbstractFeatureBranch
186
226
  hash_values = hash.map do |k, v|
187
227
  normalized_value = v.to_s.downcase
188
228
  boolean_value = normalized_value == 'true'
189
- new_value = normalized_value == 'per_user' ? 'per_user' : boolean_value
229
+ new_value = scoped_value?(normalized_value) ? VALUE_SCOPED : boolean_value
190
230
  [k, new_value]
191
231
  end
192
232
  Hash[hash_values]
@@ -1,20 +1,20 @@
1
1
  class Object
2
2
  class << self
3
3
  raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_branch' if respond_to?(:feature_branch)
4
- def feature_branch(feature_name, user_id = nil, &feature_work)
5
- if feature_enabled?(feature_name, user_id)
4
+ def feature_branch(feature_name, scope_id = nil, &feature_work)
5
+ if feature_enabled?(feature_name, scope_id)
6
6
  feature_work.call
7
7
  end
8
8
  end
9
9
 
10
10
  raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_enabled?' if respond_to?(:feature_enabled?)
11
- def feature_enabled?(feature_name, user_id = nil)
11
+ def feature_enabled?(feature_name, scope_id = nil)
12
12
  normalized_feature_name = feature_name.to_s.downcase
13
13
 
14
14
  redis_override_value = feature_enabled_reddis_override_value(normalized_feature_name)
15
15
  value = !redis_override_value.nil? ? redis_override_value : AbstractFeatureBranch.application_features[normalized_feature_name]
16
- if value == 'per_user'
17
- value = !user_id.nil? && feature_enabled_per_user_value(feature_name, user_id)
16
+ if AbstractFeatureBranch.scoped_value?(value)
17
+ value = !scope_id.nil? && feature_enabled_scoped_value(feature_name, scope_id)
18
18
  end
19
19
  value
20
20
  end
@@ -33,24 +33,28 @@ class Object
33
33
  end
34
34
  end
35
35
 
36
- raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_enabled_per_user_value' if Object.new.respond_to?(:feature_enabled_per_user_value, true)
37
- def feature_enabled_per_user_value(normalized_feature_name, user_id)
38
- begin
39
- AbstractFeatureBranch.user_features_storage.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", user_id)
40
- rescue Exception => error
41
- AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User value for feature \"#{normalized_feature_name}\" and user_id #{user_id}! Defaulting to nil value...\n\nError: #{error.full_message}\n\n"
42
- nil
36
+ raise 'Abstract feature branch conflicts with another Ruby library having Object::feature_enabled_scoped_value' if Object.new.respond_to?(:feature_enabled_scoped_value, true)
37
+ def feature_enabled_scoped_value(normalized_feature_name, scope_id)
38
+ if AbstractFeatureBranch.configuration.feature_store_live_fetching?
39
+ begin
40
+ AbstractFeatureBranch.feature_store.sismember("#{AbstractFeatureBranch::ENV_FEATURE_PREFIX}#{normalized_feature_name}", scope_id)
41
+ rescue Exception => error
42
+ AbstractFeatureBranch.logger.error "AbstractFeatureBranch encountered an error in retrieving Per-User value for feature \"#{normalized_feature_name}\" and scope_id #{scope_id}! Defaulting to nil value...\n\nError: #{error.full_message}\n\n"
43
+ nil
44
+ end
45
+ else
46
+ AbstractFeatureBranch.redis_scoped_features[normalized_feature_name]&.include?(scope_id.to_s)
43
47
  end
44
48
  end
45
49
  end
46
50
 
47
51
  raise 'Abstract feature branch conflicts with another Ruby library having Object#feature_branch' if Object.new.respond_to?(:feature_branch)
48
- def feature_branch(feature_name, user_id = nil, &feature_work)
49
- Object.feature_branch(feature_name.to_s, user_id, &feature_work)
52
+ def feature_branch(feature_name, scope_id = nil, &feature_work)
53
+ Object.feature_branch(feature_name.to_s, scope_id, &feature_work)
50
54
  end
51
55
 
52
56
  raise 'Abstract feature branch conflicts with another Ruby library having Object#feature_enabled?' if Object.new.respond_to?(:feature_enabled?)
53
- def feature_enabled?(feature_name, user_id = nil)
54
- Object.feature_enabled?(feature_name.to_s, user_id)
57
+ def feature_enabled?(feature_name, scope_id = nil)
58
+ Object.feature_enabled?(feature_name.to_s, scope_id)
55
59
  end
56
60
  end
@@ -7,8 +7,10 @@
7
7
  # The following example line works with Heroku Redis To Go while still operating on local Redis for local development
8
8
  # AbstractFeatureBranch.feature_store = Redis.new(:url => ENV['REDISTOGO_URL'])
9
9
 
10
- # Enable live fetching of feature configuration from storage system, to update features without app/server restart.
10
+ # Enable live fetching of feature configuration from storage system (Redis), to update features without app/server restart.
11
11
  # false by default to only load features on app/server start for faster performance (requires restart on change)
12
+ # This also affects scoped feature loading from storage system (Redis), pre-caching scope IDs for features on app/server
13
+ # restart when false, for better performance.
12
14
  AbstractFeatureBranch.feature_store_live_fetching = false
13
15
 
14
16
  # Application root where config/features.yml or config/features/ is found
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: abstract_feature_branch
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.3
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Maleh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-15 00:00:00.000000000 Z
11
+ date: 2023-02-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 1.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: jeweler
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -94,14 +100,26 @@ dependencies:
94
100
  - - '='
95
101
  - !ruby/object:Gem::Version
96
102
  version: 3.3.4
103
+ - !ruby/object:Gem::Dependency
104
+ name: rake-tui
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.2'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.2'
97
117
  description: |
98
- abstract_feature_branch is a Ruby gem that provides a variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler.
99
-
100
- It is a productivity and fault tolerance enhancing team practice.
118
+ abstract_feature_branch is a Ruby gem that provides a unique variation on the Branch by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin Fowler to enhance team productivity and improve software fault tolerance.
101
119
 
102
120
  It provides the ability to wrap blocks of code with an abstract feature branch name, and then specify in a configuration file which features to be switched on or off.
103
121
 
104
- The goal is to build out upcoming features in the same source code repository branch (i.e. continuous integration), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally and in staging environments for in-progress testing.
122
+ The goal is to build out upcoming features in the same source code repository branch (i.e. Continuous Integration and Trunk-Based Development), regardless of whether all are completed by the next release date or not, thus increasing team productivity by preventing integration delays. Developers then disable in-progress features until they are ready to be switched on in production, yet enable them locally and in staging environments for in-progress testing.
105
123
 
106
124
  This gives developers the added benefit of being able to switch a feature off after release should big problems arise for a high risk feature.
107
125
 
@@ -146,8 +164,9 @@ post_install_message: "\nRails-only post-install instructions:\n\n1) Run the fol
146
164
  following line to Gemfile above abstract_feature_branch:\n\ngem 'redis', '~> 5.0.5'\n\nAfterwards,
147
165
  run:\n\nbundle\n\n5) Optionally, customize configuration in config/initializers/abstract_feature_branch.rb\n\n(can
148
166
  be useful for changing location of feature files in Rails application,\nconfiguring
149
- Redis with a Redis or ConnectionPool instance to use for overrides and per-user
150
- feature enablement,\nand/or troubleshooting specific Rails environment feature configurations)\n\n"
167
+ Redis with a Redis or ConnectionPool instance to use for overrides and scoped feature
168
+ enablement (e.g. per-user),\nand/or troubleshooting specific Rails environment feature
169
+ configurations)\n\n"
151
170
  rdoc_options: []
152
171
  require_paths:
153
172
  - lib
@@ -162,10 +181,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
181
  - !ruby/object:Gem::Version
163
182
  version: '0'
164
183
  requirements: []
165
- rubygems_version: 3.1.4
184
+ rubygems_version: 3.3.6
166
185
  signing_key:
167
186
  specification_version: 4
168
187
  summary: abstract_feature_branch is a Ruby gem that provides a variation on the Branch
169
188
  by Abstraction Pattern by Paul Hammant and the Feature Toggles Pattern by Martin
170
- Fowler (aka Feature Flags).
189
+ Fowler (aka Feature Flags) to enable Continuous Integration and Trunk-Based Development.
171
190
  test_files: []