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
@@ -11,6 +11,17 @@ module Feature
|
|
11
11
|
# repository = YamlRepository.new('/path/to/yaml/file')
|
12
12
|
# # use repository with Feature
|
13
13
|
#
|
14
|
+
# A yaml config also can have this format:
|
15
|
+
# features:
|
16
|
+
# development:
|
17
|
+
# a_feature: true
|
18
|
+
# production:
|
19
|
+
# a_feature: false
|
20
|
+
#
|
21
|
+
# This way you have to use:
|
22
|
+
# repository = YamlRepository.new('/path/to/yaml/file', '_your_environment_')
|
23
|
+
# # use repository with Feature
|
24
|
+
#
|
14
25
|
class YamlRepository
|
15
26
|
require 'erb'
|
16
27
|
require 'yaml'
|
@@ -20,7 +31,7 @@ module Feature
|
|
20
31
|
# @param [String] yaml_file_name the yaml config filename
|
21
32
|
# @param [String] environment optional environment to use from config
|
22
33
|
#
|
23
|
-
def initialize(yaml_file_name, environment='')
|
34
|
+
def initialize(yaml_file_name, environment = '')
|
24
35
|
@yaml_file_name = yaml_file_name
|
25
36
|
@environment = environment
|
26
37
|
end
|
@@ -30,37 +41,44 @@ module Feature
|
|
30
41
|
# @return [Array<Symbol>] list of active features
|
31
42
|
#
|
32
43
|
def active_features
|
33
|
-
|
34
|
-
|
35
|
-
features.sort.map(&:to_sym)
|
44
|
+
data = read_file(@yaml_file_name)
|
45
|
+
get_active_features(data, @environment)
|
36
46
|
end
|
37
47
|
|
38
|
-
# Read
|
48
|
+
# Read given file, perform erb evaluation and yaml parsing
|
39
49
|
#
|
40
|
-
# @
|
50
|
+
# @param file_name [String] the file name fo the yaml config
|
51
|
+
# @return [Hash]
|
41
52
|
#
|
42
|
-
def
|
43
|
-
raw_data = File.read(
|
53
|
+
def read_file(file_name)
|
54
|
+
raw_data = File.read(file_name)
|
44
55
|
evaluated_data = ERB.new(raw_data).result
|
45
|
-
|
46
|
-
|
56
|
+
YAML.load(evaluated_data)
|
57
|
+
end
|
58
|
+
private :read_file
|
47
59
|
|
48
|
-
|
49
|
-
|
60
|
+
# Extracts active features from given hash
|
61
|
+
#
|
62
|
+
# @param data [Hash] hash parsed from yaml file
|
63
|
+
# @param selector [String] uses the value for this key as source of feature data
|
64
|
+
#
|
65
|
+
def get_active_features(data, selector)
|
66
|
+
data = data[selector] unless selector.empty?
|
67
|
+
|
68
|
+
if !data.is_a?(Hash) || !data.key?('features')
|
69
|
+
fail ArgumentError, 'yaml config does not contain proper config'
|
50
70
|
end
|
51
71
|
|
52
|
-
|
53
|
-
invalid_value = data['features'].values.detect { |value| ![true, false].include?(value) }
|
54
|
-
if invalid_value
|
55
|
-
raise ArgumentError, "#{invalid_value} is not allowed value in config, use true/false"
|
56
|
-
end
|
72
|
+
return [] unless data['features']
|
57
73
|
|
58
|
-
|
59
|
-
|
60
|
-
{}
|
74
|
+
invalid_value = data['features'].values.select { |value| ![true, false].include?(value) }
|
75
|
+
unless invalid_value.empty?
|
76
|
+
fail ArgumentError, "#{invalid_value.first} is not allowed value in config, use true/false"
|
61
77
|
end
|
78
|
+
|
79
|
+
data['features'].keys.select { |key| data['features'][key] }.map(&:to_sym)
|
62
80
|
end
|
63
|
-
private :
|
81
|
+
private :get_active_features
|
64
82
|
end
|
65
83
|
end
|
66
84
|
end
|
data/lib/feature/testing.rb
CHANGED
@@ -5,36 +5,42 @@ include Feature::Repository
|
|
5
5
|
describe Feature::Repository::ActiveRecordRepository do
|
6
6
|
before(:each) do
|
7
7
|
# Mock the model
|
8
|
-
@features = double(
|
8
|
+
@features = double('FeatureToggle')
|
9
9
|
@repository = ActiveRecordRepository.new(@features)
|
10
10
|
end
|
11
11
|
|
12
|
-
it
|
13
|
-
allow(@features).to receive(:where) {
|
12
|
+
it 'should have no active features after initialization' do
|
13
|
+
allow(@features).to receive(:where) { [] }
|
14
14
|
|
15
15
|
expect(@repository.active_features).to eq([])
|
16
16
|
end
|
17
17
|
|
18
|
-
it
|
19
|
-
|
20
|
-
|
18
|
+
it 'should have active features' do
|
19
|
+
allow(@features).to receive(:where).with(active: true) { [double(name: 'active')] }
|
20
|
+
|
21
|
+
expect(@repository.active_features).to eq([:active])
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should add an active feature' do
|
25
|
+
expect(@features).to receive(:exists?).with('feature_a').and_return(false)
|
26
|
+
expect(@features).to receive(:create!).with(name: 'feature_a', active: true)
|
21
27
|
|
22
28
|
@repository.add_active_feature :feature_a
|
23
29
|
end
|
24
30
|
|
25
|
-
it
|
26
|
-
expect
|
31
|
+
it 'should raise an exception when adding not a symbol as active feature' do
|
32
|
+
expect do
|
27
33
|
@repository.add_active_feature 'feature_a'
|
28
|
-
|
34
|
+
end.to raise_error(ArgumentError, 'feature_a is not a symbol')
|
29
35
|
end
|
30
36
|
|
31
|
-
it
|
32
|
-
expect(@features).to receive(:
|
37
|
+
it 'should raise an exception when adding a active feature already added as active' do
|
38
|
+
expect(@features).to receive(:create!).with(name: 'feature_a', active: true)
|
33
39
|
allow(@features).to receive(:exists?).and_return(false, true)
|
34
40
|
|
35
41
|
@repository.add_active_feature :feature_a
|
36
|
-
expect
|
42
|
+
expect do
|
37
43
|
@repository.add_active_feature :feature_a
|
38
|
-
|
44
|
+
end.to raise_error(ArgumentError, 'feature :feature_a already added')
|
39
45
|
end
|
40
46
|
end
|
@@ -1,89 +1,121 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Feature do
|
4
|
-
context
|
5
|
-
it
|
4
|
+
context 'without FeatureRepository' do
|
5
|
+
it 'should raise an exception when calling active?' do
|
6
6
|
expect do
|
7
7
|
Feature.active?(:feature_a)
|
8
|
-
end.to raise_error(
|
8
|
+
end.to raise_error('missing Repository for obtaining feature lists')
|
9
9
|
end
|
10
10
|
|
11
|
-
it
|
11
|
+
it 'should raise an exception when calling inactive?' do
|
12
12
|
expect do
|
13
13
|
Feature.inactive?(:feature_a)
|
14
|
-
end.to raise_error(
|
14
|
+
end.to raise_error('missing Repository for obtaining feature lists')
|
15
15
|
end
|
16
16
|
|
17
|
-
it
|
17
|
+
it 'should raise an exception when calling with' do
|
18
18
|
expect do
|
19
19
|
Feature.with(:feature_a) do
|
20
20
|
end
|
21
|
-
end.to raise_error(
|
21
|
+
end.to raise_error('missing Repository for obtaining feature lists')
|
22
22
|
end
|
23
23
|
|
24
|
-
it
|
24
|
+
it 'should raise an exception when calling without' do
|
25
25
|
expect do
|
26
26
|
Feature.without(:feature_a) do
|
27
27
|
end
|
28
|
-
end.to raise_error(
|
28
|
+
end.to raise_error('missing Repository for obtaining feature lists')
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
context
|
32
|
+
context 'setting Repository' do
|
33
33
|
before(:each) do
|
34
34
|
@repository = Feature::Repository::SimpleRepository.new
|
35
|
-
Feature.set_repository @repository
|
36
35
|
end
|
37
36
|
|
38
|
-
|
39
|
-
|
40
|
-
Feature.set_repository
|
41
|
-
end
|
37
|
+
context 'with auto_refresh set to false' do
|
38
|
+
before(:each) do
|
39
|
+
Feature.set_repository @repository
|
40
|
+
end
|
41
|
+
it 'should raise an exception when add repository with wrong class' do
|
42
|
+
expect do
|
43
|
+
Feature.set_repository('not a repository')
|
44
|
+
end.to raise_error(ArgumentError, 'given repository does not respond to active_features')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should get active features lazy on first usage' do
|
48
|
+
@repository.add_active_feature(:feature_a)
|
49
|
+
# the line below will be the first usage of feature in this case
|
50
|
+
expect(Feature.active?(:feature_a)).to be_truthy
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should get active features from repository once' do
|
54
|
+
Feature.active?(:does_not_matter)
|
55
|
+
@repository.add_active_feature(:feature_a)
|
56
|
+
expect(Feature.active?(:feature_a)).to be_falsey
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should reload active features on first call only' do
|
60
|
+
@repository.add_active_feature(:feature_a)
|
61
|
+
expect(@repository).to receive(:active_features).exactly(1).times
|
62
|
+
.and_return(@repository.active_features)
|
63
|
+
Feature.active?(:feature_a)
|
64
|
+
Feature.active?(:feature_a)
|
65
|
+
end
|
42
66
|
end
|
43
67
|
|
44
|
-
|
45
|
-
|
46
|
-
|
68
|
+
context 'with auto_refresh set to true' do
|
69
|
+
before(:each) do
|
70
|
+
Feature.set_repository @repository, true
|
71
|
+
end
|
72
|
+
it 'should reload active features on every call' do
|
73
|
+
@repository.add_active_feature(:feature_a)
|
74
|
+
expect(@repository).to receive(:active_features).exactly(2).times
|
75
|
+
.and_return(@repository.active_features)
|
76
|
+
Feature.active?(:feature_a)
|
77
|
+
Feature.active?(:feature_a)
|
78
|
+
end
|
47
79
|
end
|
48
80
|
end
|
49
81
|
|
50
|
-
context
|
82
|
+
context 'refresh features' do
|
51
83
|
before(:each) do
|
52
84
|
@repository = Feature::Repository::SimpleRepository.new
|
53
85
|
Feature.set_repository @repository
|
54
86
|
end
|
55
87
|
|
56
|
-
it
|
88
|
+
it 'should refresh active feature lists from repository' do
|
57
89
|
@repository.add_active_feature(:feature_a)
|
58
90
|
Feature.refresh!
|
59
91
|
expect(Feature.active?(:feature_a)).to be_truthy
|
60
92
|
end
|
61
93
|
end
|
62
94
|
|
63
|
-
context
|
95
|
+
context 'request features' do
|
64
96
|
before(:each) do
|
65
97
|
repository = Feature::Repository::SimpleRepository.new
|
66
98
|
repository.add_active_feature :feature_active
|
67
99
|
Feature.set_repository repository
|
68
100
|
end
|
69
101
|
|
70
|
-
it
|
102
|
+
it 'should affirm active feature is active' do
|
71
103
|
expect(Feature.active?(:feature_active)).to be_truthy
|
72
104
|
end
|
73
105
|
|
74
|
-
it
|
106
|
+
it 'should not affirm active feature is inactive' do
|
75
107
|
expect(Feature.inactive?(:feature_active)).to be_falsey
|
76
108
|
end
|
77
109
|
|
78
|
-
it
|
110
|
+
it 'should affirm inactive feature is inactive' do
|
79
111
|
expect(Feature.inactive?(:feature_inactive)).to be_truthy
|
80
112
|
end
|
81
113
|
|
82
|
-
it
|
114
|
+
it 'should not affirm inactive feature is active' do
|
83
115
|
expect(Feature.active?(:feature_inactive)).to be_falsey
|
84
116
|
end
|
85
117
|
|
86
|
-
it
|
118
|
+
it 'should call block with active feature in active list' do
|
87
119
|
reached = false
|
88
120
|
|
89
121
|
Feature.with(:feature_active) do
|
@@ -93,7 +125,7 @@ describe Feature do
|
|
93
125
|
expect(reached).to be_truthy
|
94
126
|
end
|
95
127
|
|
96
|
-
it
|
128
|
+
it 'should not call block with active feature not in active list' do
|
97
129
|
reached = false
|
98
130
|
|
99
131
|
Feature.with(:feature_inactive) do
|
@@ -103,13 +135,13 @@ describe Feature do
|
|
103
135
|
expect(reached).to be_falsey
|
104
136
|
end
|
105
137
|
|
106
|
-
it
|
138
|
+
it 'should raise exception when no block given to with' do
|
107
139
|
expect do
|
108
140
|
Feature.with(:feature_active)
|
109
|
-
end.to raise_error(ArgumentError,
|
141
|
+
end.to raise_error(ArgumentError, 'no block given to with')
|
110
142
|
end
|
111
143
|
|
112
|
-
it
|
144
|
+
it 'should call block without inactive feature in inactive list' do
|
113
145
|
reached = false
|
114
146
|
|
115
147
|
Feature.without(:feature_inactive) do
|
@@ -119,7 +151,7 @@ describe Feature do
|
|
119
151
|
expect(reached).to be_truthy
|
120
152
|
end
|
121
153
|
|
122
|
-
it
|
154
|
+
it 'should not call block without inactive feature in inactive list' do
|
123
155
|
reached = false
|
124
156
|
|
125
157
|
Feature.without(:feature_active) do
|
@@ -129,33 +161,33 @@ describe Feature do
|
|
129
161
|
expect(reached).to be_falsey
|
130
162
|
end
|
131
163
|
|
132
|
-
it
|
164
|
+
it 'should raise exception when no block given to without' do
|
133
165
|
expect do
|
134
166
|
Feature.without(:feature_inactive)
|
135
|
-
end.to raise_error(ArgumentError,
|
167
|
+
end.to raise_error(ArgumentError, 'no block given to without')
|
136
168
|
end
|
137
169
|
|
138
170
|
describe 'switch()' do
|
139
171
|
context 'given a value' do
|
140
|
-
it
|
172
|
+
it 'should return the first value if the feature is active' do
|
141
173
|
retval = Feature.switch(:feature_active, 1, 2)
|
142
174
|
expect(retval).to eq(1)
|
143
175
|
end
|
144
176
|
|
145
|
-
it
|
177
|
+
it 'should return the second value if the feature is inactive' do
|
146
178
|
retval = Feature.switch(:feature_inactive, 1, 2)
|
147
179
|
expect(retval).to eq(2)
|
148
180
|
end
|
149
181
|
end
|
150
182
|
|
151
183
|
context 'given a proc/lambda' do
|
152
|
-
it
|
153
|
-
retval = Feature.switch(:feature_active,
|
184
|
+
it 'should call the first proc/lambda if the feature is active' do
|
185
|
+
retval = Feature.switch(:feature_active, -> { 1 }, -> { 2 })
|
154
186
|
expect(retval).to eq(1)
|
155
187
|
end
|
156
188
|
|
157
|
-
it
|
158
|
-
retval = Feature.switch(:feature_inactive,
|
189
|
+
it 'should call the second proc/lambda if the feature is active' do
|
190
|
+
retval = Feature.switch(:feature_inactive, -> { 1 }, -> { 2 })
|
159
191
|
expect(retval).to eq(2)
|
160
192
|
end
|
161
193
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
include Feature::Repository
|
4
|
+
|
5
|
+
describe Feature::Repository::RedisRepository do
|
6
|
+
before(:each) do
|
7
|
+
@repository = RedisRepository.new('application_features')
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should have no active features after initialization' do
|
11
|
+
expect(@repository.active_features).to eq([])
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should add an active feature' do
|
15
|
+
@repository.add_active_feature :feature_a
|
16
|
+
expect(@repository.active_features).to eq([:feature_a])
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should only show active feature' do
|
20
|
+
Redis.current.hset('application_features', 'inactive_a', false)
|
21
|
+
Redis.current.hset('application_features', 'inactive_b', false)
|
22
|
+
Redis.current.hset('application_features', 'feature_a', true)
|
23
|
+
Redis.current.hset('application_features', 'feature_b', true)
|
24
|
+
|
25
|
+
expect(@repository.active_features).to eq([:feature_a, :feature_b])
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should raise an exception when adding not a symbol as active feature' do
|
29
|
+
expect do
|
30
|
+
@repository.add_active_feature 'feature_a'
|
31
|
+
end.to raise_error(ArgumentError, 'feature_a is not a symbol')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should raise an exception when adding a active feature already added as active' do
|
35
|
+
@repository.add_active_feature :feature_a
|
36
|
+
expect do
|
37
|
+
@repository.add_active_feature :feature_a
|
38
|
+
end.to raise_error(ArgumentError, 'feature :feature_a already added')
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -7,25 +7,31 @@ describe Feature::Repository::SimpleRepository do
|
|
7
7
|
@repository = SimpleRepository.new
|
8
8
|
end
|
9
9
|
|
10
|
-
it
|
10
|
+
it 'should have no active features after initialization' do
|
11
11
|
expect(@repository.active_features).to eq([])
|
12
12
|
end
|
13
13
|
|
14
|
-
it
|
14
|
+
it 'should add an active feature' do
|
15
15
|
@repository.add_active_feature :feature_a
|
16
16
|
expect(@repository.active_features).to eq([:feature_a])
|
17
17
|
end
|
18
18
|
|
19
|
-
it
|
19
|
+
it 'should add an feature without having impact on internal structure' do
|
20
|
+
list = @repository.active_features
|
21
|
+
@repository.add_active_feature :feature_a
|
22
|
+
expect(list).to eq([])
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should raise an exception when adding not a symbol as active feature' do
|
20
26
|
expect do
|
21
27
|
@repository.add_active_feature 'feature_a'
|
22
|
-
end.to raise_error(ArgumentError,
|
28
|
+
end.to raise_error(ArgumentError, 'feature_a is not a symbol')
|
23
29
|
end
|
24
30
|
|
25
|
-
it
|
31
|
+
it 'should raise an exception when adding a active feature already added as active' do
|
26
32
|
@repository.add_active_feature :feature_a
|
27
33
|
expect do
|
28
34
|
@repository.add_active_feature :feature_a
|
29
|
-
end.to raise_error(ArgumentError,
|
35
|
+
end.to raise_error(ArgumentError, 'feature :feature_a already added')
|
30
36
|
end
|
31
37
|
end
|