feature 1.1.0 → 1.2.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
  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