featurer 0.0.4 → 0.1.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/.gitignore +2 -0
- data/.hound.yml +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -1
- data/.travis.yml +4 -6
- data/Gemfile +8 -5
- data/Guardfile +1 -0
- data/Rakefile +2 -1
- data/app/helpers/featurer_helper.rb +7 -0
- data/app/views/featurer/_head_tag.html.erb +8 -0
- data/featurer.gemspec +3 -2
- data/lib/featurer/adapter.rb +44 -8
- data/lib/featurer/adapter_manager.rb +4 -3
- data/lib/featurer/adapter_proxy.rb +1 -0
- data/lib/featurer/adapters/redis.rb +58 -14
- data/lib/featurer/facade.rb +5 -1
- data/lib/featurer/version.rb +3 -1
- data/lib/featurer.rb +9 -0
- data/spec/lib/featurer/adapter_manager_spec.rb +1 -1
- data/spec/lib/featurer/adapter_proxy_spec.rb +4 -4
- data/spec/lib/featurer/adapter_spec.rb +27 -0
- data/spec/lib/featurer/adapters/redis_spec.rb +69 -5
- data/spec/lib/featurer/facade_spec.rb +51 -13
- data/spec/lib/featurer_spec.rb +142 -72
- data/spec/spec_helper.rb +4 -0
- data/spec/support/fast_feedback_formatter.rb +8 -3
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b841a0cb7f4bdda8b8484436e69c086ed3c356e8
|
4
|
+
data.tar.gz: b3cef4ae265b6de20e91b174923e676afaaa2dbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 77548e6ba64d74da3a3f94a309084469e9fe6fffdd9f1b4bdae38aa632bbc589f872de9bd66ccffe1be7e1d74dbdbc0c7a326b5305dba4da6a8ca713393db89f
|
7
|
+
data.tar.gz: 8ea8674ab337fbb44b1eccdd95da7d5cde70d22a251c871569143ad21834da0ea77b657d930d1d5dc3b27668ed7b83aca74bdb98dd54237d044ac4c9c6fd90d1
|
data/.gitignore
CHANGED
data/.hound.yml
ADDED
data/.rspec
CHANGED
data/.rubocop.yml
ADDED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.3.1
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,15 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
source 'https://rubygems.org'
|
2
3
|
|
3
4
|
# Specify your gem's dependencies in featurer.gemspec
|
4
5
|
gemspec
|
5
6
|
|
6
7
|
group :development do
|
7
|
-
gem 'rubocop', '~> 0.
|
8
|
-
|
9
|
-
|
8
|
+
gem 'rubocop', '~> 0.47.1'
|
9
|
+
end
|
10
|
+
|
11
|
+
group :test do
|
12
|
+
gem 'fakeredis', '~> 0.5.0'
|
10
13
|
end
|
11
14
|
|
12
15
|
group :debug do
|
13
|
-
gem 'pry', '~> 0.10'
|
14
|
-
gem 'pry-
|
16
|
+
gem 'pry', '~> 0.10.4'
|
17
|
+
gem 'pry-byebug', '~> 3.4.2'
|
15
18
|
end
|
data/Guardfile
CHANGED
data/Rakefile
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'bundler/gem_tasks'
|
2
3
|
|
3
4
|
require 'rspec/core/rake_task'
|
@@ -6,4 +7,4 @@ RSpec::Core::RakeTask.new(:spec) do |task|
|
|
6
7
|
task.rspec_opts = ['--color', '--format', 'documentation']
|
7
8
|
end
|
8
9
|
|
9
|
-
task :
|
10
|
+
task default: :spec
|
data/featurer.gemspec
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
lib = File.expand_path('../lib', __FILE__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'featurer/version'
|
@@ -8,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
8
9
|
spec.version = Featurer::VERSION
|
9
10
|
spec.authors = ['Alejandro El Informatico']
|
10
11
|
spec.email = ['aeinformatico@gmail.com']
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
12
|
+
spec.summary = 'Easy feature flag for your project'
|
13
|
+
spec.description = 'Easy feature flag for your project shipped with Redis support'
|
13
14
|
spec.homepage = 'https://github.com/ainformatico/featurer'
|
14
15
|
spec.license = 'MIT'
|
15
16
|
|
data/lib/featurer/adapter.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Featurer
|
2
3
|
class Adapter
|
3
4
|
attr_reader :config
|
@@ -10,20 +11,55 @@ module Featurer
|
|
10
11
|
@config = config
|
11
12
|
end
|
12
13
|
|
13
|
-
|
14
|
-
|
14
|
+
# Attaches a new matching value to the given feature.
|
15
|
+
# If the feature doesn't exist, the adapter should create it automatically.
|
16
|
+
# Feature matching_values must be matched in the order they were added.
|
17
|
+
#
|
18
|
+
# @param _feature the name of the feature as will be used when questioning if the feature is #on?
|
19
|
+
# @param _matching_value the value that will be used to match when calling #on?. Valid values are:
|
20
|
+
# => - true: it will match any value (ie: the feature enabled globally)
|
21
|
+
# => - false: the feature is disabled globally
|
22
|
+
# => - regular expression: matched when calling #on?, if regular expression is passed
|
23
|
+
# => - other types: matched literally when calling #on?, based on the #to_s representation
|
24
|
+
def add(_feature, _matching_value)
|
25
|
+
raise NotImplementedError
|
15
26
|
end
|
16
27
|
|
17
|
-
|
18
|
-
|
28
|
+
# Completely removes a feature from the system.
|
29
|
+
# If the feature doesn't exist, the adapter should not fail.
|
30
|
+
def delete(_feature)
|
31
|
+
raise NotImplementedError
|
19
32
|
end
|
20
33
|
|
21
|
-
|
22
|
-
|
34
|
+
# Returns true if the feature has a matching value attached wich matches the given value.
|
35
|
+
#
|
36
|
+
# @param _feature the feature name, as provided when calling #add or #register
|
37
|
+
# @param _value the value that will be used when matching against the stored matching_value for the given feature
|
38
|
+
# @return a Boolean that depends on the _value provided
|
39
|
+
# => - true if _value is nil and the _feature has a matching_value of true
|
40
|
+
# => - true if _value is an integer and the _feature has that exact integer attached as a matching_value
|
41
|
+
# => - true if _value is a string and the _feature has a matching_value which is a regexp that matches _value
|
42
|
+
# => - true if _value is any other type and the _feature has a matching_value which has the same exact #to_s
|
43
|
+
# => representation
|
44
|
+
# => - false in any other case
|
45
|
+
def on?(_feature, _value = true)
|
46
|
+
raise NotImplementedError
|
23
47
|
end
|
24
48
|
|
25
|
-
|
26
|
-
|
49
|
+
# Returns an array with all the enabled features that match the given value (or just the global ones if no value is
|
50
|
+
# provided)
|
51
|
+
#
|
52
|
+
# @param _value the value that will be used when matching against the stored matching_values
|
53
|
+
def enabled_features(_value = true)
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
|
57
|
+
# First deletes the given _feature and then creates it again with only the provided _matching_value attached.
|
58
|
+
#
|
59
|
+
# @param _feature the feature to register
|
60
|
+
# @param _matching_value the new matching_value for the feature that replaces any existing matching_value on it
|
61
|
+
def register(_feature, _matching_value = true)
|
62
|
+
raise NotImplementedError
|
27
63
|
end
|
28
64
|
end
|
29
65
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module Featurer
|
2
3
|
class AdapterManager
|
3
4
|
class << self
|
@@ -20,9 +21,9 @@ module Featurer
|
|
20
21
|
|
21
22
|
def extract_name(klass)
|
22
23
|
klass.name
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
.downcase[/(?:\w+$)/] # get only class name
|
25
|
+
.sub('adapter', '') # remove adapter prefix
|
26
|
+
.to_sym
|
26
27
|
end
|
27
28
|
end
|
28
29
|
end
|
@@ -1,35 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'redis'
|
2
3
|
|
3
4
|
module Featurer
|
4
5
|
class RedisAdapter < Adapter
|
5
6
|
def prepare
|
6
|
-
@redis = ::Redis.new(
|
7
|
-
|
8
|
-
|
9
|
-
db: @config[:db]
|
10
|
-
})
|
11
|
-
end
|
12
|
-
|
13
|
-
def delete(feature)
|
14
|
-
delete_key(feature)
|
7
|
+
@redis = @config[:client] || ::Redis.new(host: @config[:host],
|
8
|
+
port: @config[:port],
|
9
|
+
db: @config[:db])
|
15
10
|
end
|
16
11
|
|
17
12
|
def add(feature, value)
|
18
13
|
save_set(feature, value)
|
19
14
|
end
|
20
15
|
|
16
|
+
def delete(feature)
|
17
|
+
delete_key(feature)
|
18
|
+
end
|
19
|
+
|
21
20
|
def on?(feature, value = true)
|
22
21
|
fetch_from_set(feature, value)
|
22
|
+
rescue => e
|
23
|
+
@config[:logger].warn e
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def enabled_features(value = true)
|
28
|
+
all_features.select { |feature| on?(feature, value) }
|
29
|
+
rescue => e
|
30
|
+
@config[:logger].warn e
|
31
|
+
[]
|
23
32
|
end
|
24
33
|
|
25
34
|
def off(feature, value)
|
26
35
|
remove_from_set(feature, value)
|
27
36
|
end
|
28
37
|
|
29
|
-
def register(
|
38
|
+
def register(feature, value = true)
|
30
39
|
# ensure old data is wiped
|
31
|
-
delete(
|
32
|
-
save_set(
|
40
|
+
delete(feature)
|
41
|
+
save_set(feature, value)
|
42
|
+
end
|
43
|
+
|
44
|
+
def feature_list
|
45
|
+
full_feature_names.map do |full_feature_name|
|
46
|
+
{
|
47
|
+
name: short_feature_name(full_feature_name),
|
48
|
+
matching_values: @redis.smembers(full_feature_name)
|
49
|
+
}
|
50
|
+
end
|
33
51
|
end
|
34
52
|
|
35
53
|
private
|
@@ -46,12 +64,38 @@ module Featurer
|
|
46
64
|
@redis.sadd key(name), value
|
47
65
|
end
|
48
66
|
|
49
|
-
def fetch_from_set(name,
|
50
|
-
@redis.
|
67
|
+
def fetch_from_set(name, value)
|
68
|
+
matching_values = @redis.smembers(key(name))
|
69
|
+
|
70
|
+
matching_values.each do |matching_value|
|
71
|
+
return true if matching_value == 'true' # Globally enabled feature
|
72
|
+
|
73
|
+
if value.is_a?(String) && matching_value =~ /\(\?-mix:.+\)/ # Regexp matching
|
74
|
+
return true if Regexp.new(matching_value).match(value)
|
75
|
+
elsif matching_value == value.to_s # By exact value
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
51
81
|
end
|
52
82
|
|
53
83
|
def remove_from_set(name, id)
|
54
84
|
@redis.srem(key(name), id)
|
55
85
|
end
|
86
|
+
|
87
|
+
def all_features
|
88
|
+
full_feature_names.map do |key|
|
89
|
+
short_feature_name(key)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def full_feature_names
|
94
|
+
@redis.keys("#{@config[:prefix]}:feature:*")
|
95
|
+
end
|
96
|
+
|
97
|
+
def short_feature_name(full_feature_name)
|
98
|
+
full_feature_name.split("#{@config[:prefix]}:feature:").last.to_sym
|
99
|
+
end
|
56
100
|
end
|
57
101
|
end
|
data/lib/featurer/facade.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'logger'
|
3
|
+
|
1
4
|
module Featurer
|
2
5
|
module Facade
|
3
|
-
attr_accessor :adapter
|
6
|
+
attr_accessor :adapter, :logger
|
4
7
|
|
5
8
|
def configure(config)
|
9
|
+
@logger = config[:logger] ||= Logger.new(STDOUT)
|
6
10
|
@adapter = AdapterProxy.new(config).adapter
|
7
11
|
end
|
8
12
|
|
data/lib/featurer/version.rb
CHANGED
data/lib/featurer.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'featurer/adapter_proxy'
|
2
3
|
require 'featurer/adapter_manager'
|
3
4
|
require 'featurer/adapter'
|
@@ -7,4 +8,12 @@ require 'featurer/version'
|
|
7
8
|
|
8
9
|
module Featurer
|
9
10
|
extend Facade
|
11
|
+
|
12
|
+
if const_defined?(:Rails)
|
13
|
+
class Engine < ::Rails::Engine
|
14
|
+
initializer 'featurer:init_adapter' do
|
15
|
+
Featurer.init! unless Featurer.adapter
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
10
19
|
end
|
@@ -1,7 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe Featurer::AdapterProxy do
|
4
|
-
|
5
5
|
describe 'default adapter' do
|
6
6
|
it 'creates the default adapter' do
|
7
7
|
proxy = Featurer::AdapterProxy.new
|
@@ -9,11 +9,11 @@ describe Featurer::AdapterProxy do
|
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'creates a new adapter' do
|
12
|
-
class
|
12
|
+
class ProxyAdapter < Featurer::Adapter
|
13
13
|
end
|
14
14
|
|
15
|
-
proxy = Featurer::AdapterProxy.new(adapter: :
|
16
|
-
expect(proxy.adapter).to be_an(
|
15
|
+
proxy = Featurer::AdapterProxy.new(adapter: :proxy)
|
16
|
+
expect(proxy.adapter).to be_an(ProxyAdapter)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Featurer::Adapter do
|
4
|
+
describe '#add' do
|
5
|
+
it 'raises an error' do
|
6
|
+
expect { subject.add(:feature, :matching_value) }.to raise_error(NotImplementedError)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '#delete' do
|
11
|
+
it 'raises an error' do
|
12
|
+
expect { subject.delete(:feature) }.to raise_error(NotImplementedError)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#on?' do
|
17
|
+
it 'raises an error' do
|
18
|
+
expect { subject.on?(:feature, :value) }.to raise_error(NotImplementedError)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '#register' do
|
23
|
+
it 'raises an error' do
|
24
|
+
expect { subject.register(:feature, :matching_value) }.to raise_error(NotImplementedError)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,14 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe Featurer::RedisAdapter do
|
4
|
-
|
5
|
-
expect_any_instance_of(Featurer::RedisAdapter).to receive(:prepare)
|
6
|
-
.and_call_original
|
5
|
+
let(:redis_handler) { Featurer.adapter.instance_variable_get(:@redis) }
|
7
6
|
|
8
|
-
|
9
|
-
redis_handler = Featurer.adapter.instance_variable_get(:@redis)
|
7
|
+
before { Featurer.init! }
|
10
8
|
|
9
|
+
it 'creates the connection' do
|
11
10
|
expect(redis_handler).to be_an(Redis)
|
12
11
|
expect(redis_handler.ping).to eq('PONG')
|
13
12
|
end
|
13
|
+
|
14
|
+
describe '#on?' do
|
15
|
+
context 'when there is an exception' do
|
16
|
+
let(:exception) { StandardError.new }
|
17
|
+
let(:logger) { double(Logger) }
|
18
|
+
|
19
|
+
before do
|
20
|
+
subject.instance_variable_set :@config, logger: logger
|
21
|
+
|
22
|
+
expect(logger).to receive(:warn)
|
23
|
+
expect(subject).to receive(:fetch_from_set).and_raise(exception)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "doesn't propagate the exception, just logs it" do
|
27
|
+
expect(subject.on?(:feature)).to be(false)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#enabled_features' do
|
33
|
+
context 'when there is an exception' do
|
34
|
+
let(:exception) { StandardError.new }
|
35
|
+
let(:logger) { double(Logger) }
|
36
|
+
|
37
|
+
before do
|
38
|
+
subject.instance_variable_set :@config, logger: logger
|
39
|
+
|
40
|
+
expect(logger).to receive(:warn)
|
41
|
+
expect(subject).to receive(:all_features).and_raise(exception)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "doesn't propagate the exception, just logs it" do
|
45
|
+
expect(subject.enabled_features(:feature)).to eq([])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#prepare' do
|
51
|
+
subject { described_class.new(config).prepare }
|
52
|
+
|
53
|
+
context 'a preconfigured client is provided' do
|
54
|
+
let(:client) { double(Redis) }
|
55
|
+
let(:config) { { client: client } }
|
56
|
+
|
57
|
+
it 'sets the given client as the adapter client' do
|
58
|
+
expect(subject).to eq(client)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'the connection details are provided' do
|
63
|
+
let(:config) do
|
64
|
+
{
|
65
|
+
host: 'the_host',
|
66
|
+
port: 3425,
|
67
|
+
db: 89
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'creates a new redis client with the given settings' do
|
72
|
+
expect(subject.client.host).to eq(config[:host])
|
73
|
+
expect(subject.client.port).to eq(config[:port])
|
74
|
+
expect(subject.client.db).to eq(config[:db])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
14
78
|
end
|
@@ -1,20 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe Featurer::Facade do
|
4
5
|
describe 'configuration' do
|
5
|
-
it '
|
6
|
+
it 'uses default options' do
|
6
7
|
Featurer.init!
|
8
|
+
|
7
9
|
expect(Featurer.adapter.config).to include(adapter: :redis)
|
8
10
|
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
context 'when providing a configuration' do
|
13
|
+
let(:new_config) do
|
14
|
+
{
|
15
|
+
adapter: :redis,
|
16
|
+
prefix: :custom,
|
17
|
+
host: '192.168.1.1'
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'configures the adapter' do
|
22
|
+
Featurer.configure(new_config)
|
23
|
+
|
24
|
+
expect(Featurer.adapter.config).to include(new_config)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'supports passing a custom logger in the config' do
|
29
|
+
let(:logger) { double(Logger) }
|
30
|
+
|
31
|
+
context 'no logger is passed in the config' do
|
32
|
+
before do
|
33
|
+
expect(Logger).to receive(:new).with(STDOUT).and_return(logger)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets a default logger' do
|
37
|
+
Featurer.configure({})
|
38
|
+
|
39
|
+
expect(Featurer.logger).to eq(logger)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'a logger is passed in the config' do
|
44
|
+
it 'sets a default logger' do
|
45
|
+
Featurer.configure(logger: logger)
|
46
|
+
|
47
|
+
expect(Featurer.logger).to eq(logger)
|
48
|
+
end
|
49
|
+
end
|
18
50
|
end
|
19
51
|
end
|
20
52
|
|
@@ -25,23 +57,23 @@ describe Featurer::Facade do
|
|
25
57
|
end
|
26
58
|
|
27
59
|
describe '#init' do
|
28
|
-
it '
|
60
|
+
it 'resets the adapter' do
|
29
61
|
Featurer.configure(adapter: :nil)
|
30
62
|
Featurer.init!
|
31
63
|
|
32
64
|
expect(Featurer.config).to include(adapter: :redis)
|
33
65
|
end
|
34
66
|
|
35
|
-
it '
|
67
|
+
it "doesn't reset the adapter" do
|
36
68
|
Featurer.configure(adapter: :nil, prefix: :custom)
|
37
69
|
Featurer.init
|
38
70
|
|
39
|
-
expect(Featurer.config).to
|
71
|
+
expect(Featurer.config).to include(adapter: :nil, prefix: :custom)
|
40
72
|
end
|
41
73
|
end
|
42
74
|
|
43
75
|
describe '#reset' do
|
44
|
-
it '
|
76
|
+
it 'resets the adapter' do
|
45
77
|
Featurer.configure(adapter: :nil)
|
46
78
|
Featurer.reset
|
47
79
|
|
@@ -49,4 +81,10 @@ describe Featurer::Facade do
|
|
49
81
|
end
|
50
82
|
end
|
51
83
|
end
|
84
|
+
|
85
|
+
describe 'calling methods not defined in the adapter' do
|
86
|
+
it 'produces an error' do
|
87
|
+
expect { Featurer.this_is_clearly_not_a_valid_method_ok }.to raise_error(NoMethodError)
|
88
|
+
end
|
89
|
+
end
|
52
90
|
end
|
data/spec/lib/featurer_spec.rb
CHANGED
@@ -1,104 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe Featurer do
|
4
|
-
|
5
|
-
after(:each) { Featurer.delete(:feature) }
|
5
|
+
before { Featurer.init! }
|
6
6
|
|
7
|
+
let(:user_id) { 1 }
|
8
|
+
|
9
|
+
describe '#enabled_features' do
|
10
|
+
before do
|
11
|
+
Featurer.register(:feature_1)
|
12
|
+
Featurer.register(:feature_2, [user_id])
|
13
|
+
Featurer.register(:feature_3, [user_id + 1, /client_123/])
|
14
|
+
Featurer.register(:feature_4, /^admin_/)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns all enabled features' do
|
18
|
+
expect(Featurer.enabled_features.to_set).to eq([:feature_1].to_set)
|
19
|
+
expect(Featurer.enabled_features(user_id).to_set).to eq([:feature_1, :feature_2].to_set)
|
20
|
+
expect(Featurer.enabled_features('admin_client_123').to_set).to eq([:feature_1, :feature_3, :feature_4].to_set)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'globally enabled feature' do
|
7
25
|
before do
|
8
|
-
Featurer.
|
26
|
+
Featurer.register :feature, true
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'matches on an integer' do
|
30
|
+
expect(Featurer.on?(:feature, 1)).to be
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'matches on a boolean' do
|
34
|
+
expect(Featurer.on?(:feature, true)).to be
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'matches on a string' do
|
38
|
+
expect(Featurer.on?(:feature, 'admin_client_123')).to be
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'feature matching_value is a list of integers' do
|
43
|
+
it 'returns true because user is in the list' do
|
44
|
+
Featurer.register :feature, [user_id]
|
45
|
+
|
46
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'defines the user twice and returns true' do
|
50
|
+
Featurer.register :feature, [user_id]
|
51
|
+
Featurer.register :feature, [user_id]
|
52
|
+
|
53
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns false because user is not in the list' do
|
57
|
+
Featurer.register :feature, [user_id + 1]
|
58
|
+
|
59
|
+
expect(Featurer.on?(:feature, user_id)).to_not be
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'gives priority to global features' do
|
63
|
+
Featurer.register :feature, [user_id]
|
64
|
+
Featurer.register :feature, true
|
65
|
+
|
66
|
+
expect(Featurer.on?(:feature)).to be
|
67
|
+
expect(Featurer.on?(:feature, user_id + 1)).to be
|
9
68
|
end
|
10
69
|
|
11
|
-
|
70
|
+
it 'removes a single user from the feature' do
|
71
|
+
Featurer.register :feature, [user_id, user_id + 1]
|
72
|
+
Featurer.off :feature, user_id
|
12
73
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
expect(Featurer.on?(:feature, user_id)).to be
|
17
|
-
end
|
74
|
+
expect(Featurer.on?(:feature, user_id)).to_not be
|
75
|
+
expect(Featurer.on?(:feature, user_id + 1)).to be
|
76
|
+
end
|
18
77
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
expect(Featurer.on?(:feature, user_id)).to be
|
23
|
-
end
|
78
|
+
it 'removes multiple users from the feature' do
|
79
|
+
Featurer.register :feature, [user_id, user_id + 1]
|
80
|
+
Featurer.off :feature, [user_id, user_id + 1]
|
24
81
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
82
|
+
expect(Featurer.on?(:feature, user_id)).to_not be
|
83
|
+
expect(Featurer.on?(:feature, user_id + 1)).to_not be
|
84
|
+
end
|
29
85
|
|
30
|
-
|
31
|
-
|
32
|
-
Featurer.register :feature, true
|
86
|
+
it 'adds a user to a non-existing feature' do
|
87
|
+
Featurer.add :feature, user_id
|
33
88
|
|
34
|
-
|
35
|
-
|
36
|
-
end
|
89
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
90
|
+
end
|
37
91
|
|
38
|
-
|
39
|
-
|
40
|
-
|
92
|
+
it 'adds a single user to a existing feature' do
|
93
|
+
Featurer.register :feature, [user_id]
|
94
|
+
Featurer.add :feature, user_id + 1
|
41
95
|
|
42
|
-
|
43
|
-
|
44
|
-
|
96
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
97
|
+
expect(Featurer.on?(:feature, user_id + 1)).to be
|
98
|
+
end
|
45
99
|
|
46
|
-
|
47
|
-
|
100
|
+
it 'adda multiple users to a feature' do
|
101
|
+
Featurer.register :feature, [user_id]
|
102
|
+
Featurer.add :feature, [user_id + 1, user_id + 2]
|
48
103
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
104
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
105
|
+
expect(Featurer.on?(:feature, user_id + 1)).to be
|
106
|
+
expect(Featurer.on?(:feature, user_id + 2)).to be
|
107
|
+
end
|
53
108
|
|
54
|
-
|
55
|
-
|
109
|
+
it "doesn't match a string" do
|
110
|
+
Featurer.register :feature, [user_id]
|
56
111
|
|
57
|
-
|
58
|
-
|
112
|
+
expect(Featurer.on?(:feature, user_id.to_s))
|
113
|
+
end
|
114
|
+
end
|
59
115
|
|
60
|
-
|
61
|
-
|
62
|
-
|
116
|
+
context 'feature matching_value is a regular expressions' do
|
117
|
+
it 'matches a matching string' do
|
118
|
+
Featurer.register :feature, [/^admin_/]
|
63
119
|
|
64
|
-
|
65
|
-
|
66
|
-
end
|
120
|
+
expect(Featurer.on?(:feature, 'admin_1234')).to be(true)
|
121
|
+
end
|
67
122
|
|
68
|
-
|
69
|
-
|
70
|
-
Featurer.add :feature, [user_id + 1, user_id + 2]
|
123
|
+
it "doesn't match a non-matching string" do
|
124
|
+
Featurer.register :feature, [/^admin_/]
|
71
125
|
|
72
|
-
|
73
|
-
expect(Featurer.on?(:feature, user_id + 1)).to be
|
74
|
-
expect(Featurer.on?(:feature, user_id + 2)).to be
|
75
|
-
end
|
126
|
+
expect(Featurer.on?(:feature, '_admin_1234')).to be(false)
|
76
127
|
end
|
128
|
+
end
|
77
129
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
130
|
+
context 'feature matching_value is a Boolean' do
|
131
|
+
it 'returns true because the feature is enabled(default)' do
|
132
|
+
Featurer.register :feature
|
133
|
+
expect(Featurer.on?(:feature)).to be
|
134
|
+
end
|
83
135
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
136
|
+
it 'returns true because the feature is enabled(manually)' do
|
137
|
+
Featurer.register :feature, true
|
138
|
+
expect(Featurer.on?(:feature)).to be
|
139
|
+
end
|
88
140
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
141
|
+
it 'returns false because the feature is disabled' do
|
142
|
+
Featurer.register :feature, false
|
143
|
+
expect(Featurer.on?(:feature)).to_not be
|
144
|
+
end
|
93
145
|
|
94
|
-
|
95
|
-
|
96
|
-
|
146
|
+
it 'converts a global feature into a per user one' do
|
147
|
+
Featurer.register :feature, true
|
148
|
+
Featurer.register :feature, [user_id]
|
97
149
|
|
98
|
-
|
99
|
-
|
100
|
-
end
|
150
|
+
expect(Featurer.on?(:feature, user_id)).to be
|
151
|
+
expect(Featurer.on?(:feature)).to_not be
|
101
152
|
end
|
102
153
|
end
|
103
154
|
|
155
|
+
context 'feature matching_value is a String' do
|
156
|
+
let(:value) { 'this is the value' }
|
157
|
+
|
158
|
+
before do
|
159
|
+
Featurer.register :feature, value
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'matches on the exact value of the string' do
|
163
|
+
expect(Featurer.on?(:feature, value)).to be
|
164
|
+
end
|
165
|
+
|
166
|
+
it "doesn't match partially" do
|
167
|
+
expect(Featurer.on?(:feature, value[0..-3])).to_not be
|
168
|
+
end
|
169
|
+
|
170
|
+
it "doesn't match on an empty string" do
|
171
|
+
expect(Featurer.on?(:feature, '')).to_not be
|
172
|
+
end
|
173
|
+
end
|
104
174
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rspec/core/formatters/progress_formatter'
|
2
3
|
|
3
4
|
class FastFeedbackFormatter < RSpec::Core::Formatters::ProgressFormatter
|
4
5
|
def example_failed(example)
|
5
6
|
super(example)
|
6
7
|
|
7
|
-
|
8
|
+
print_formatted_example(example.metadata[:example_group])
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
8
12
|
|
9
|
-
|
13
|
+
def print_formatted_example(example_group)
|
14
|
+
output.puts ''
|
10
15
|
output.puts red("#{example_group[:file_path]}:#{example_group[:line_number]}")
|
11
16
|
output.puts red(example.execution_result[:exception].to_s.strip)
|
12
17
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: featurer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alejandro El Informatico
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
@@ -74,7 +74,9 @@ extensions: []
|
|
74
74
|
extra_rdoc_files: []
|
75
75
|
files:
|
76
76
|
- ".gitignore"
|
77
|
+
- ".hound.yml"
|
77
78
|
- ".rspec"
|
79
|
+
- ".rubocop.yml"
|
78
80
|
- ".ruby-version"
|
79
81
|
- ".travis.yml"
|
80
82
|
- Gemfile
|
@@ -82,6 +84,8 @@ files:
|
|
82
84
|
- LICENSE.txt
|
83
85
|
- README.md
|
84
86
|
- Rakefile
|
87
|
+
- app/helpers/featurer_helper.rb
|
88
|
+
- app/views/featurer/_head_tag.html.erb
|
85
89
|
- featurer.gemspec
|
86
90
|
- lib/featurer.rb
|
87
91
|
- lib/featurer/adapter.rb
|
@@ -92,6 +96,7 @@ files:
|
|
92
96
|
- lib/featurer/version.rb
|
93
97
|
- spec/lib/featurer/adapter_manager_spec.rb
|
94
98
|
- spec/lib/featurer/adapter_proxy_spec.rb
|
99
|
+
- spec/lib/featurer/adapter_spec.rb
|
95
100
|
- spec/lib/featurer/adapters/redis_spec.rb
|
96
101
|
- spec/lib/featurer/facade_spec.rb
|
97
102
|
- spec/lib/featurer_spec.rb
|
@@ -117,13 +122,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
122
|
version: '0'
|
118
123
|
requirements: []
|
119
124
|
rubyforge_project:
|
120
|
-
rubygems_version: 2.
|
125
|
+
rubygems_version: 2.5.1
|
121
126
|
signing_key:
|
122
127
|
specification_version: 4
|
123
128
|
summary: Easy feature flag for your project
|
124
129
|
test_files:
|
125
130
|
- spec/lib/featurer/adapter_manager_spec.rb
|
126
131
|
- spec/lib/featurer/adapter_proxy_spec.rb
|
132
|
+
- spec/lib/featurer/adapter_spec.rb
|
127
133
|
- spec/lib/featurer/adapters/redis_spec.rb
|
128
134
|
- spec/lib/featurer/facade_spec.rb
|
129
135
|
- spec/lib/featurer_spec.rb
|