feature 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile +7 -3
- data/README.md +58 -7
- data/Rakefile +9 -1
- data/lib/feature.rb +20 -21
- data/lib/feature/generators/install_generator.rb +18 -27
- data/lib/feature/repository.rb +1 -0
- data/lib/feature/repository/active_record_repository.rb +4 -8
- data/lib/feature/repository/redis_repository.rb +59 -0
- data/lib/feature/repository/simple_repository.rb +5 -8
- data/lib/feature/repository/yaml_repository.rb +39 -21
- data/lib/feature/testing.rb +0 -1
- data/spec/feature/active_record_repository_spec.rb +19 -13
- data/spec/feature/feature_spec.rb +71 -39
- data/spec/feature/redis_repository_spec.rb +41 -0
- data/spec/feature/simple_repository_spec.rb +12 -6
- data/spec/feature/testing_spec.rb +3 -3
- data/spec/feature/yaml_repository_spec.rb +39 -25
- data/spec/integration/rails/test-against-several-rails-versions.sh +1 -1
- data/spec/integration/rails/test-against-specific-rails-version.sh +4 -1
- data/spec/spec_helper.rb +3 -0
- metadata +4 -7
- data/lib/feature/generators/templates/create_feature_toggles.rb +0 -10
- data/lib/feature/generators/templates/feature_toggle.rb +0 -6
- data/lib/feature/generators/templates/feature_toggle_without_attr_accessible.rb +0 -4
- data/spec/integration/rails/gemfiles/rails3.gemfile +0 -4
- data/spec/integration/rails/gemfiles/rails3.gemfile.lock +0 -86
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83131f6c0bc8a7fe056a4c621130a968fcb1ec5a
|
4
|
+
data.tar.gz: 9772c00ec3dd58a84a259162591fe3f3983019f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcfa2c4877b32898c8f08dff2531eb22089b6bd38d63caa95bc6752ff396034d6aef6fba1c038f6a0b509471da8126a45a5b884ac2d267fa49a23668c2753317
|
7
|
+
data.tar.gz: 7c5cc34fbbe4007030e5708d2db9c50105a6cda84445492f5a881bb567f15db2cabef41b971fdabcc57a4fd45c0a917adc2b719935b1aa81647058ab4641475e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## 1.2.0 (2014-10-28)
|
2
|
+
|
3
|
+
* add Support for auto-refresh of feature data (javidjamae)
|
4
|
+
* read list of features at first usage instead of initialization (lazy-load)
|
5
|
+
* add RedisRepository (javidjamae)
|
6
|
+
* remove explicit Rails 3 Support of ActiveRecordRepository
|
7
|
+
|
1
8
|
## 1.1.0 (2014-09-30)
|
2
9
|
|
3
10
|
* generator compatible with Rails 3/4 (mumoshu)
|
data/Gemfile
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
2
|
|
3
|
-
gem
|
3
|
+
gem 'rake'
|
4
4
|
|
5
5
|
group :test do
|
6
|
-
gem
|
7
|
-
gem
|
6
|
+
gem 'rspec'
|
7
|
+
gem 'rspec-mocks'
|
8
8
|
gem 'coveralls', require: false
|
9
|
+
gem 'rubocop', require: false
|
10
|
+
gem 'fakeredis'
|
11
|
+
gem 'mutant'
|
12
|
+
gem 'mutant-rspec'
|
9
13
|
end
|
data/README.md
CHANGED
@@ -1,18 +1,21 @@
|
|
1
|
+
[](https://rubygems.org/gems/feature)
|
2
|
+
[](https://travis-ci.org/mgsnova/feature)
|
3
|
+
[](https://coveralls.io/r/mgsnova/feature)
|
4
|
+
[](https://codeclimate.com/github/mgsnova/feature)
|
5
|
+
[](http://inch-ci.org/github/mgsnova/feature)
|
6
|
+
[](https://gemnasium.com/mgsnova/feature)
|
7
|
+
|
1
8
|
# Feature
|
2
9
|
|
3
10
|
Feature is a battle-tested [feature toggle](http://martinfowler.com/bliki/FeatureToggle.html) library for ruby.
|
4
11
|
|
5
12
|
The feature toggle functionality has to be configured by feature repositories. A feature repository simply provides lists of active features (symbols!). Unknown features are assumed inactive.
|
13
|
+
|
6
14
|
With this approach Feature is higly configurable and not bound to a specific kind of configuration.
|
7
15
|
|
8
16
|
**NOTE:** Ruby 1.8 is only supported until version 0.7.0. Later Versions require at least Ruby 1.9.
|
9
17
|
|
10
|
-
|
11
|
-
|
12
|
-
[](https://rubygems.org/gems/feature)
|
13
|
-
[](https://travis-ci.org/mgsnova/feature)
|
14
|
-
[](https://codeclimate.com/github/mgsnova/feature)
|
15
|
-
[](https://coveralls.io/r/mgsnova/feature)
|
18
|
+
**NOTE:** Using feature with ActiveRecord and Rails 3 MAY work. Use version 1.1.0 if you need support for Rails 3.
|
16
19
|
|
17
20
|
## Installation
|
18
21
|
|
@@ -22,7 +25,7 @@ With this approach Feature is higly configurable and not bound to a specific kin
|
|
22
25
|
|
23
26
|
* Setup Feature
|
24
27
|
* Create a repository (see examples below)
|
25
|
-
*
|
28
|
+
* Set repository to Feature
|
26
29
|
|
27
30
|
Feature.set_repository(your_repository)
|
28
31
|
|
@@ -57,6 +60,25 @@ With this approach Feature is higly configurable and not bound to a specific kin
|
|
57
60
|
# your test code
|
58
61
|
end
|
59
62
|
|
63
|
+
* Feature-toggle caching
|
64
|
+
|
65
|
+
* By default, Feature will lazy-load the active features from the
|
66
|
+
underlying repository the first time you try to check whether a
|
67
|
+
feature is set or not.
|
68
|
+
|
69
|
+
* Subsequent toggle-status checks will access the cached, in-memory
|
70
|
+
representation of the toggle status, so changes to toggles in the
|
71
|
+
underlying repository would not be reflected in the application
|
72
|
+
until you restart the application or manally call
|
73
|
+
|
74
|
+
Feature.refresh!
|
75
|
+
|
76
|
+
* You can optionally pass in true as a second argument on
|
77
|
+
set_repository, to force Feature to auto-refresh the feature list
|
78
|
+
on every feature-toggle check you make.
|
79
|
+
|
80
|
+
Feature.set_repository(your_repository, true)
|
81
|
+
|
60
82
|
## Examples
|
61
83
|
|
62
84
|
### Vanilla Ruby using SimpleRepository
|
@@ -77,6 +99,35 @@ With this approach Feature is higly configurable and not bound to a specific kin
|
|
77
99
|
puts "you can read this"
|
78
100
|
end
|
79
101
|
|
102
|
+
### Ruby or Rails using RedisRepository
|
103
|
+
|
104
|
+
# See here to learn how to configure redis: https://github.com/redis/redis-rb
|
105
|
+
|
106
|
+
# File: Gemfile
|
107
|
+
gem 'feature'
|
108
|
+
gem 'redis'
|
109
|
+
|
110
|
+
# setup code (or Rails initializer: config/initializers/feature.rb)
|
111
|
+
require 'feature'
|
112
|
+
|
113
|
+
repo = Feature::Repository::RedisRepository.new("feature_toggles")
|
114
|
+
Feature.set_repository repo
|
115
|
+
|
116
|
+
# production code
|
117
|
+
Feature.active?(:be_nice)
|
118
|
+
# => true
|
119
|
+
|
120
|
+
Feature.with(:be_nice) do
|
121
|
+
puts "you can read this"
|
122
|
+
end
|
123
|
+
|
124
|
+
# see all features in Redis
|
125
|
+
Redis.current.hgetall("feature_toggles")
|
126
|
+
|
127
|
+
# add/toggle features in Redis
|
128
|
+
Redis.current.hset("feature_toggles", "ActiveFeature", true)
|
129
|
+
Redis.current.hset("feature_toggles", "InActiveFeature", false)
|
130
|
+
|
80
131
|
### Rails using YamlRepository
|
81
132
|
|
82
133
|
# File: Gemfile
|
data/Rakefile
CHANGED
@@ -1,10 +1,18 @@
|
|
1
1
|
require 'rake/testtask'
|
2
2
|
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
require 'mutant'
|
5
|
+
|
6
|
+
RuboCop::RakeTask.new
|
3
7
|
|
4
8
|
RSpec::Core::RakeTask.new(:spec) do |spec|
|
5
9
|
spec.pattern = 'spec/**/*_spec.rb'
|
6
10
|
spec.rspec_opts = ['--colour', '-f documentation', '--backtrace']
|
7
11
|
end
|
8
12
|
|
9
|
-
task
|
13
|
+
task :mutant do
|
14
|
+
result = Mutant::CLI.run(%w(--include lib --require feature --use rspec Feature*))
|
15
|
+
fail unless result == Mutant::CLI::EXIT_SUCCESS
|
10
16
|
end
|
17
|
+
|
18
|
+
task default: [:spec, :rubocop, :mutant]
|
data/lib/feature.rb
CHANGED
@@ -22,8 +22,7 @@
|
|
22
22
|
#
|
23
23
|
module Feature
|
24
24
|
require 'feature/repository'
|
25
|
-
|
26
|
-
require 'feature/generators/install_generator' if defined?(Rails) and Rails::VERSION::STRING > "3"
|
25
|
+
require 'feature/generators/install_generator'
|
27
26
|
|
28
27
|
@repository = nil
|
29
28
|
@active_features = nil
|
@@ -32,20 +31,24 @@ module Feature
|
|
32
31
|
# The given repository has to respond to method 'active_features' with an array of symbols
|
33
32
|
#
|
34
33
|
# @param [Object] repository the repository to get the features from
|
34
|
+
# @param [Boolean] auto_refresh optional (default: false) - refresh feature toggles on every check if set true
|
35
35
|
#
|
36
|
-
def self.set_repository(repository)
|
37
|
-
|
38
|
-
|
36
|
+
def self.set_repository(repository, auto_refresh = false)
|
37
|
+
unless repository.respond_to?(:active_features)
|
38
|
+
fail ArgumentError, 'given repository does not respond to active_features'
|
39
39
|
end
|
40
40
|
|
41
|
+
@auto_refresh = auto_refresh
|
42
|
+
@perform_initial_refresh = true
|
41
43
|
@repository = repository
|
42
|
-
refresh!
|
43
44
|
end
|
44
45
|
|
45
|
-
#
|
46
|
+
# Refreshes list of active features from repository.
|
47
|
+
# Useful when using an repository with external source.
|
46
48
|
#
|
47
49
|
def self.refresh!
|
48
50
|
@active_features = @repository.active_features
|
51
|
+
@perform_initial_refresh = false
|
49
52
|
end
|
50
53
|
|
51
54
|
##
|
@@ -55,9 +58,9 @@ module Feature
|
|
55
58
|
# @return [Boolean]
|
56
59
|
#
|
57
60
|
def self.active?(feature)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
+
fail 'missing Repository for obtaining feature lists' unless @repository
|
62
|
+
|
63
|
+
refresh! if @auto_refresh || @perform_initial_refresh
|
61
64
|
|
62
65
|
@active_features.include?(feature)
|
63
66
|
end
|
@@ -68,7 +71,7 @@ module Feature
|
|
68
71
|
# @return [Boolean]
|
69
72
|
#
|
70
73
|
def self.inactive?(feature)
|
71
|
-
!
|
74
|
+
!active?(feature)
|
72
75
|
end
|
73
76
|
|
74
77
|
# Execute the given block if feature is active
|
@@ -76,9 +79,7 @@ module Feature
|
|
76
79
|
# @param [Symbol] feature
|
77
80
|
#
|
78
81
|
def self.with(feature)
|
79
|
-
|
80
|
-
raise ArgumentError, "no block given to #{__method__}"
|
81
|
-
end
|
82
|
+
fail ArgumentError, "no block given to #{__method__}" unless block_given?
|
82
83
|
|
83
84
|
yield if active?(feature)
|
84
85
|
end
|
@@ -88,9 +89,7 @@ module Feature
|
|
88
89
|
# @param [Symbol] feature
|
89
90
|
#
|
90
91
|
def self.without(feature)
|
91
|
-
|
92
|
-
raise ArgumentError, "no block given to #{__method__}"
|
93
|
-
end
|
92
|
+
fail ArgumentError, "no block given to #{__method__}" unless block_given?
|
94
93
|
|
95
94
|
yield if inactive?(feature)
|
96
95
|
end
|
@@ -98,14 +97,14 @@ module Feature
|
|
98
97
|
# Return value or execute Proc/lambda depending on Feature status.
|
99
98
|
#
|
100
99
|
# @param [Symbol] feature
|
101
|
-
# @param [Object] value
|
102
|
-
# @param [Object] value
|
100
|
+
# @param [Object] value / lambda to use if feature is active
|
101
|
+
# @param [Object] value / lambda to use if feature is inactive
|
103
102
|
#
|
104
103
|
def self.switch(feature, l1, l2)
|
105
104
|
if active?(feature)
|
106
|
-
l1.
|
105
|
+
l1.instance_of?(Proc) ? l1.call : l1
|
107
106
|
else
|
108
|
-
l2.
|
107
|
+
l2.instance_of?(Proc) ? l2.call : l2
|
109
108
|
end
|
110
109
|
end
|
111
110
|
end
|
@@ -1,33 +1,24 @@
|
|
1
|
-
|
2
|
-
require 'rails/generators
|
3
|
-
require 'rails/generators/active_record'
|
1
|
+
if defined?(Rails) && Rails::VERSION::STRING > '3'
|
2
|
+
require 'rails/generators'
|
4
3
|
|
5
|
-
module Feature
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
module Feature
|
5
|
+
# Rails generator for generating feature ActiveRecord model
|
6
|
+
# and migration step for creating the table
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
desc 'This generator creates a migration and a model for FeatureToggles.'
|
9
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def self.next_migration_number(path)
|
20
|
-
ActiveRecord::Generators::Base.next_migration_number(path)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
11
|
+
def generate_model
|
12
|
+
generate :model, 'feature_toggle name:string active:boolean'
|
13
|
+
inject_into_class 'app/models/feature_toggle.rb', 'FeatureToggle' do
|
14
|
+
" attr_accessible :name, :active if ActiveRecord::Base.respond_to? :attr_accessible\n"\
|
15
|
+
" # Feature name should be present and unique\n"\
|
16
|
+
" validates :name, presence: true, uniqueness: true\n"
|
17
|
+
end
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
if ActiveRecord::Base.respond_to? :attr_accessible
|
28
|
-
'feature_toggle.rb'
|
29
|
-
else
|
30
|
-
'feature_toggle_without_attr_accessible.rb'
|
20
|
+
def generate_initializer
|
21
|
+
template 'feature.rb', 'config/initializers/feature.rb'
|
31
22
|
end
|
32
23
|
end
|
33
24
|
end
|
data/lib/feature/repository.rb
CHANGED
@@ -3,7 +3,7 @@ module Feature
|
|
3
3
|
# AcitveRecordRepository for active feature list
|
4
4
|
# Right now we assume you have at least name:string and active:boolean
|
5
5
|
# defined in your table.
|
6
|
-
#
|
6
|
+
#
|
7
7
|
# Example usage:
|
8
8
|
# repository = ActiveRecordRepository.new(FeatureToggle)
|
9
9
|
# repository.add_active_feature(:feature_name)
|
@@ -31,7 +31,7 @@ module Feature
|
|
31
31
|
def add_active_feature(feature)
|
32
32
|
check_feature_is_not_symbol(feature)
|
33
33
|
check_feature_already_in_list(feature)
|
34
|
-
@model.
|
34
|
+
@model.create!(name: feature.to_s, active: true)
|
35
35
|
end
|
36
36
|
|
37
37
|
# Checks if the given feature is a not symbol and raises an exception if so
|
@@ -39,9 +39,7 @@ module Feature
|
|
39
39
|
# @param [Sybmol] feature the feature to be checked
|
40
40
|
#
|
41
41
|
def check_feature_is_not_symbol(feature)
|
42
|
-
|
43
|
-
raise ArgumentError, "given feature #{feature} is not a symbol"
|
44
|
-
end
|
42
|
+
fail ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol)
|
45
43
|
end
|
46
44
|
private :check_feature_is_not_symbol
|
47
45
|
|
@@ -51,9 +49,7 @@ module Feature
|
|
51
49
|
# @param [Symbol] feature the feature to be checked
|
52
50
|
#
|
53
51
|
def check_feature_already_in_list(feature)
|
54
|
-
if @model.exists?(feature.to_s)
|
55
|
-
raise ArgumentError, "feature :#{feature} already added to list of active features"
|
56
|
-
end
|
52
|
+
fail ArgumentError, "feature :#{feature} already added" if @model.exists?(feature.to_s)
|
57
53
|
end
|
58
54
|
private :check_feature_already_in_list
|
59
55
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Feature
|
2
|
+
module Repository
|
3
|
+
# RedisRepository for active feature list
|
4
|
+
#
|
5
|
+
# Example usage:
|
6
|
+
# repository = RedisRepository.new("feature_toggles")
|
7
|
+
# repository.add_active_feature(:feature_name)
|
8
|
+
#
|
9
|
+
# 'feature_toggles' can be whatever name you want to use for
|
10
|
+
# the Redis hash that will store all of your feature toggles.
|
11
|
+
#
|
12
|
+
class RedisRepository
|
13
|
+
# Constructor
|
14
|
+
#
|
15
|
+
# @param redis_key the key of the redis hash where all the toggles will be stored
|
16
|
+
#
|
17
|
+
def initialize(redis_key)
|
18
|
+
@redis_key = redis_key
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns list of active features
|
22
|
+
#
|
23
|
+
# @return [Array<Symbol>] list of active features
|
24
|
+
#
|
25
|
+
def active_features
|
26
|
+
Redis.current.hgetall(@redis_key).select { |_k, v| v.to_s == 'true' }.map { |k, _v| k.to_sym }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add an active feature to repository
|
30
|
+
#
|
31
|
+
# @param [Symbol] feature the feature to be added
|
32
|
+
#
|
33
|
+
def add_active_feature(feature)
|
34
|
+
check_feature_is_not_symbol(feature)
|
35
|
+
check_feature_already_in_list(feature)
|
36
|
+
Redis.current.hset(@redis_key, feature, true)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Checks that given feature is a symbol, raises exception otherwise
|
40
|
+
#
|
41
|
+
# @param [Sybmol] feature the feature to be checked
|
42
|
+
#
|
43
|
+
def check_feature_is_not_symbol(feature)
|
44
|
+
fail ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol)
|
45
|
+
end
|
46
|
+
private :check_feature_is_not_symbol
|
47
|
+
|
48
|
+
# Checks if given feature is already added to list of active features
|
49
|
+
# and raises an exception if so
|
50
|
+
#
|
51
|
+
# @param [Symbol] feature the feature to be checked
|
52
|
+
#
|
53
|
+
def check_feature_already_in_list(feature)
|
54
|
+
fail ArgumentError, "feature :#{feature} already added" if Redis.current.hexists(@redis_key, feature)
|
55
|
+
end
|
56
|
+
private :check_feature_already_in_list
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module Feature
|
2
2
|
module Repository
|
3
3
|
# SimpleRepository for active feature list
|
4
|
-
# Simply add features to that should be active,
|
4
|
+
# Simply add features to that should be active,
|
5
|
+
# no config or data sources required.
|
5
6
|
#
|
6
7
|
# Example usage:
|
7
8
|
# repository = SimpleRepository.new
|
@@ -33,14 +34,12 @@ module Feature
|
|
33
34
|
@active_features << feature
|
34
35
|
end
|
35
36
|
|
36
|
-
# Checks
|
37
|
+
# Checks that given feature is a symbol, raises exception otherwise
|
37
38
|
#
|
38
39
|
# @param [Sybmol] feature the feature to be checked
|
39
40
|
#
|
40
41
|
def check_feature_is_not_symbol(feature)
|
41
|
-
|
42
|
-
raise ArgumentError, "given feature #{feature} is not a symbol"
|
43
|
-
end
|
42
|
+
fail ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol)
|
44
43
|
end
|
45
44
|
private :check_feature_is_not_symbol
|
46
45
|
|
@@ -50,9 +49,7 @@ module Feature
|
|
50
49
|
# @param [Symbol] feature the feature to be checked
|
51
50
|
#
|
52
51
|
def check_feature_already_in_list(feature)
|
53
|
-
if @active_features.include?(feature)
|
54
|
-
raise ArgumentError, "feature :#{feature} already added to list of active features"
|
55
|
-
end
|
52
|
+
fail ArgumentError, "feature :#{feature} already added" if @active_features.include?(feature)
|
56
53
|
end
|
57
54
|
private :check_feature_already_in_list
|
58
55
|
end
|