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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3f9b9580a7aa39fe081df357dce3bcfaf4611ba9
4
- data.tar.gz: 5ec1b69eb13c4c20689fd227194796e26969b4ef
3
+ metadata.gz: 83131f6c0bc8a7fe056a4c621130a968fcb1ec5a
4
+ data.tar.gz: 9772c00ec3dd58a84a259162591fe3f3983019f6
5
5
  SHA512:
6
- metadata.gz: 5d12f49a521091b2d2ad0787f33bfa4352f5db57ea3801a86088e03ea2c61ddf116e5c9cae9f6f2fb83cb560b196f47a1916455e78e293cba4f1b3b4705bd794
7
- data.tar.gz: d84ff821a0bd30c0534ce68e971b671640881c829a46384d9f2165ea5945f1594e1db0d1264d38b1ad80caa766e8b31a775879c36faecd22f14157992ce9984d
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 "rake"
3
+ gem 'rake'
4
4
 
5
5
  group :test do
6
- gem "rspec"
7
- gem "rspec-mocks"
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
+ [![Gem Version](https://badge.fury.io/rb/feature.svg)](https://rubygems.org/gems/feature)
2
+ [![Travis-CI Build Status](https://travis-ci.org/mgsnova/feature.svg)](https://travis-ci.org/mgsnova/feature)
3
+ [![Coverage Status](http://img.shields.io/coveralls/mgsnova/feature/master.svg)](https://coveralls.io/r/mgsnova/feature)
4
+ [![Code Climate](https://codeclimate.com/github/mgsnova/feature.svg)](https://codeclimate.com/github/mgsnova/feature)
5
+ [![Inline docs](http://inch-ci.org/github/mgsnova/feature.svg)](http://inch-ci.org/github/mgsnova/feature)
6
+ [![Dependency Status](https://gemnasium.com/mgsnova/feature.svg)](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
- ## CI status
11
-
12
- [![Gem Version](https://badge.fury.io/rb/feature.png)](https://rubygems.org/gems/feature)
13
- [![Travis-CI Build Status](https://travis-ci.org/mgsnova/feature.png)](https://travis-ci.org/mgsnova/feature)
14
- [![Code Climate](https://codeclimate.com/github/mgsnova/feature.png)](https://codeclimate.com/github/mgsnova/feature)
15
- [![Coverage Status](https://coveralls.io/repos/mgsnova/feature/badge.png?branch=master)](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
- * set repository to Feature
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 default: [:spec] do
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
- # Only load the generator if Rails is defined and Version is greater than 3
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
- if !repository.respond_to?(:active_features)
38
- raise ArgumentError, "given repository does not respond to active_features"
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
- # Obtains list of active features from repository (for the case they change e.g. when using db-backed repository)
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
- if !@repository
59
- raise "Feature is missing Repository for obtaining feature lists"
60
- end
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
- !self.active?(feature)
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
- if !block_given?
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
- if !block_given?
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 to be returned / lambda to be evaluated if feature is active
102
- # @param [Object] value to be returned / lambda to be evaluated if feature is inactive
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.is_a?(Proc) ? l1.call : l1
105
+ l1.instance_of?(Proc) ? l1.call : l1
107
106
  else
108
- l2.is_a?(Proc) ? l2.call : l2
107
+ l2.instance_of?(Proc) ? l2.call : l2
109
108
  end
110
109
  end
111
110
  end
@@ -1,33 +1,24 @@
1
- require 'rails/generators'
2
- require 'rails/generators/migration'
3
- require 'rails/generators/active_record'
1
+ if defined?(Rails) && Rails::VERSION::STRING > '3'
2
+ require 'rails/generators'
4
3
 
5
- module Feature
6
- class InstallGenerator < Rails::Generators::Base
7
- include Rails::Generators::Migration
8
- extend Rails::Generators::Migration
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
- desc 'This generator creates a migration and a model for FeatureToggles.'
11
- source_root File.expand_path('../templates', __FILE__)
12
-
13
- def create_model_file
14
- template 'feature.rb', 'config/initializers/feature.rb'
15
- template appropriate_feature_toggle_rb, 'app/models/feature_toggle.rb'
16
- migration_template 'create_feature_toggles.rb', 'db/migrate/create_feature_toggles.rb'
17
- end
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
- def appropriate_feature_toggle_rb
26
- # For Rails 4 + protected_attributes or Rails 3, we prefer the model with `attr_accessible` pre-populated
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
@@ -4,5 +4,6 @@ module Feature
4
4
  require 'feature/repository/simple_repository'
5
5
  require 'feature/repository/yaml_repository'
6
6
  require 'feature/repository/active_record_repository'
7
+ require 'feature/repository/redis_repository'
7
8
  end
8
9
  end
@@ -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.new(name: feature.to_s, active: true).save
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
- if !feature.is_a?(Symbol)
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, no config or data sources required
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 if the given feature is a not symbol and raises an exception if so
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
- if !feature.is_a?(Symbol)
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