offs 1.4.0 → 2.0.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: 205ff73781d83a027350f1de8da287e0a8fec834
4
- data.tar.gz: 4f6e7f8bdd804461590bdbbd0b81b54f36748b08
3
+ metadata.gz: ef7d1cafac11c0b1e750c87fea4efc1a6ada9476
4
+ data.tar.gz: 3876047faae8388987ac0d1ed5ecbe62372a5c1a
5
5
  SHA512:
6
- metadata.gz: 72aaffded2072d1316c5bb33b6c235d113e1737a7db79d52d643f6e6879d0f538b70f2b5f6ef65d5e431f2766f6b8f7a12f9fab75b0f28206cb0f4dd181d9305
7
- data.tar.gz: a73eff40479677cc647c291c166b9ada2e79465da6b582673478abfa9957d60cc4f527504a538d14420474a3b200024833123242b68de906815bf0128a2ca548
6
+ metadata.gz: 895c580a906b1ed220d8a16a1f6244ba4d11a54f840d3b8f22b8e92c91b6d4cd3a5db2f9ac17517f58599a83e74beee0d45e0c1d2d0ff0bea1f7b5b07a72f941
7
+ data.tar.gz: 6ab68811b1910a87fc79fdd47f76daca0dac8104e76e0f01936b7cea736852bc55983ce744459a9c73f9729a51a4deec28a1b6f780f11eebfd885e0f6f968e77
@@ -0,0 +1,5 @@
1
+ class OFFS
2
+ class FeatureDisabled < RuntimeError; end
3
+ class UndefinedFlagError < RuntimeError; end
4
+ class AlreadyInitializedError < StandardError; end
5
+ end
data/lib/offs/flags.rb CHANGED
@@ -1,54 +1,60 @@
1
- class OFFS
2
- class Flags
3
- UndefinedFlagError = Class.new(StandardError)
1
+ require 'delegate'
2
+ require 'offs/exceptions'
4
3
 
4
+ class OFFS
5
+ class Flags < DelegateClass(Array)
5
6
  class << self
6
- def instance
7
- @instance ||= new
7
+ private :new
8
+
9
+ def instance(*args)
10
+ unless @instance.nil? || args.empty?
11
+ raise AlreadyInitializedError
12
+ end
13
+ @instance ||= new(*args)
8
14
  end
9
15
 
10
- def set(&block)
11
- block.call(instance)
12
- instance
16
+ def reset_instance!
17
+ @instance = nil
13
18
  end
14
19
  end
15
20
 
16
- def flag(name, default)
17
- env_var_name = name.to_s.upcase
18
- feature_flags[name] = if ENV.has_key?(env_var_name)
19
- ENV[env_var_name].strip == '1'
20
- else
21
- default
22
- end
21
+ def initialize(*flags, value_sources: {})
22
+ self.value_sources = array_wrap(value_sources)
23
+ __setobj__(flags)
23
24
  end
24
25
 
25
- def feature_flags
26
- @feature_flags ||= {}
26
+ def validate!(flag)
27
+ return true if include?(flag)
28
+ raise UndefinedFlagError, "The #{flag} flag has not been defined."
27
29
  end
28
30
 
29
31
  def enabled?(flag)
30
- status = feature_flags[flag]
31
- if status.respond_to?(:call)
32
- status.call
33
- else
34
- !!status
35
- end
32
+ validate!(flag)
33
+ !!final_values[flag]
36
34
  end
37
35
 
38
- def valid?(flag)
39
- feature_flags.has_key?(flag)
36
+ private
37
+
38
+ attr_accessor :value_sources
39
+
40
+ def array_wrap(obj)
41
+ return [obj] unless obj.kind_of?(Array)
42
+ obj
40
43
  end
41
44
 
42
- def validate!(flag)
43
- if valid?(flag)
44
- flag
45
- else
46
- raise UndefinedFlagError, "The #{flag} flag has not been defined."
47
- end
45
+ def final_values
46
+ value_sources.reverse.reduce({}) { |final, source|
47
+ final.merge(sanitize(source))
48
+ }
48
49
  end
49
50
 
50
- def to_a
51
- feature_flags.keys
51
+ def sanitize(data_hash)
52
+ data_hash.reduce({}) { |result, k_v_pair|
53
+ key = k_v_pair.first.to_s.downcase.to_sym
54
+ value = [true, 'true', 1, '1', 'on'].include?(k_v_pair.last)
55
+ result[key] = value
56
+ result
57
+ }
52
58
  end
53
59
  end
54
60
  end
@@ -1,23 +1,19 @@
1
1
  require 'delegate'
2
- require 'injectable_dependencies'
3
2
 
4
3
  class OFFS
5
4
  class Permutations < DelegateClass(Enumerator)
6
- include InjectableDependencies
7
-
8
- dependency(:flags) { Flags.instance }
9
-
10
- def initialize(options={})
11
- initialize_dependencies(options[:dependencies])
5
+ def initialize(flags: Flags.instance)
6
+ self.flags = flags
12
7
  __setobj__ create_permutations
13
8
  end
14
9
 
15
10
  private
16
11
 
12
+ attr_accessor :flags
13
+
17
14
  def create_permutations
18
- keys = flags.to_a
19
- permutations = [true,false].repeated_permutation(keys.size).map { |values|
20
- keys.zip(values).inject({}) { |m, pair|
15
+ permutations = [true,false].repeated_permutation(flags.size).map { |values|
16
+ flags.zip(values).inject({}) { |m, pair|
21
17
  m[pair[0]] = pair[1]
22
18
  m
23
19
  }
data/lib/offs/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class OFFS
2
- VERSION = "1.4.0"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/offs.rb CHANGED
@@ -1,42 +1,33 @@
1
1
  require "offs/version"
2
- require 'offs/flags'
3
- require 'injectable_dependencies'
2
+ require "offs/flags"
3
+ require 'offs/exceptions'
4
4
 
5
5
  class OFFS
6
- include InjectableDependencies
7
-
8
- class FeatureDisabled < RuntimeError; end
9
-
10
6
  class << self
11
- def so_you_want_to(flag, &block)
12
- new(flag).so_you_want_to(&block)
7
+ def so_you_want_to(flag, flag_status_checker: nil, &block)
8
+ new(flag, flag_status_checker: flag_status_checker).so_you_want_to(&block)
13
9
  end
14
10
 
15
- def if_you_would_like_to(flag, &block)
16
- so_you_want_to(flag) do |you|
11
+ def if_you_would_like_to(flag, flag_status_checker: nil, &block)
12
+ so_you_want_to(flag, flag_status_checker: flag_status_checker) do |you|
17
13
  you.would_like_to(&block)
18
14
  end
19
15
  end
20
16
 
21
- def if_you_do_not_want_to(flag, &block)
22
- so_you_want_to(flag) do |you|
17
+ def if_you_do_not_want_to(flag, flag_status_checker: nil, &block)
18
+ so_you_want_to(flag, flag_status_checker: flag_status_checker) do |you|
23
19
  you.may_still_need_to(&block)
24
20
  end
25
21
  end
26
22
 
27
- def raise_error_unless_we(flag)
28
- new(flag).raise_error_unless_we
29
- end
30
-
31
- def feature_flags
32
- Flags.instance.to_a
23
+ def raise_error_unless_we(flag, flag_status_checker:)
24
+ new(flag, flag_status_checker: flag_status_checker) \
25
+ .raise_error_unless_we
33
26
  end
34
27
  end
35
28
 
36
- dependency(:feature_flags) { Flags.instance }
37
-
38
- def initialize(flag, options={})
39
- initialize_dependencies options[:dependencies]
29
+ def initialize(flag, flag_status_checker: nil)
30
+ self.flag_status_checker = flag_status_checker || Flags.instance
40
31
  self.flag = flag
41
32
  end
42
33
 
@@ -64,18 +55,18 @@ class OFFS
64
55
  private
65
56
 
66
57
  attr_reader :flag
67
- attr_accessor :result
58
+ attr_accessor :result, :flag_status_checker
68
59
 
69
- def when_flag(bool, &block)
70
- self.result = block.call if flag_status == bool
60
+ def flag=(new_flag)
61
+ flag_status_checker.validate!(new_flag)
62
+ @flag = new_flag
71
63
  end
72
64
 
73
- def flag_status
74
- feature_flags.enabled?(flag)
65
+ def when_flag(bool, &block)
66
+ self.result = block.call if flag_enabled? == bool
75
67
  end
76
- alias_method :flag_enabled?, :flag_status
77
68
 
78
- def flag=(new_flag)
79
- @flag = feature_flags.validate!(new_flag)
69
+ def flag_enabled?
70
+ flag_status_checker.enabled?(flag)
80
71
  end
81
72
  end
data/offs.gemspec CHANGED
@@ -21,8 +21,6 @@ Gem::Specification.new do |spec|
21
21
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.add_dependency "injectable_dependencies"
25
-
26
24
  spec.add_development_dependency "bundler", "~> 1.7"
27
25
  spec.add_development_dependency "rake", "~> 10.0"
28
26
  spec.add_development_dependency "rspec"
@@ -0,0 +1,156 @@
1
+ require 'offs/flags'
2
+
3
+ describe OFFS::Flags do
4
+ subject {
5
+ # Must use #send, because it is usually a Singleton, but we want seperate
6
+ # objects for testing.
7
+ described_class.send(:new, :use_feature_a, :use_feature_b, :use_feature_c,
8
+ value_sources: value_sources)
9
+ }
10
+
11
+ let(:value_sources) {{}}
12
+
13
+ it 'acts like an Array of all defined flag names' do
14
+ expect(subject).to eq [:use_feature_a, :use_feature_b, :use_feature_c]
15
+ end
16
+
17
+ it 'provides singleton behavior' do
18
+ expect(described_class.instance).to be_kind_of(described_class)
19
+ expect(described_class.instance).to be described_class.instance
20
+ end
21
+
22
+ it 'allows flags and value_sources to be defined when the instance is ' \
23
+ + 'first created' do
24
+ described_class.reset_instance!
25
+ instance = described_class.instance(:foo, :bar, :baz)
26
+ expect(instance).to eq [:foo, :bar, :baz]
27
+ end
28
+
29
+ it 'raises an AlreadyInitializedError when #instance is called with ' \
30
+ + 'arguments on subsequent calls' do
31
+ described_class.reset_instance!
32
+ described_class.instance(:foo)
33
+ expect {
34
+ described_class.instance(:foo)
35
+ }.to raise_error(OFFS::AlreadyInitializedError)
36
+ end
37
+
38
+ context 'when a flag has not been defined' do
39
+ it 'raises an exception when asked to validate the flag' do
40
+ expect{
41
+ subject.validate!(:flag_that_is_not_defined)
42
+ }.to raise_error(OFFS::UndefinedFlagError,
43
+ /flag_that_is_not_defined/)
44
+ end
45
+
46
+ it 'raises an exception when asked for the status of the flag' do
47
+ expect{
48
+ subject.enabled?(:flag_that_is_not_defined)
49
+ }.to raise_error(OFFS::UndefinedFlagError,
50
+ /flag_that_is_not_defined/)
51
+ end
52
+ end
53
+
54
+ context 'when a flag has been defined' do
55
+ it 'returns true when asked to validate the flag' do
56
+ expect(subject.validate!(:use_feature_a)).to eq true
57
+ end
58
+
59
+ context 'when no value sources are defined' do
60
+ it 'assumes the flag is disabled' do
61
+ expect(subject.enabled?(:use_feature_a)).to eq false
62
+ end
63
+ end
64
+
65
+ context 'when one value source is defined' do
66
+ let(:value_sources) { [source_a] }
67
+
68
+ let(:source_a) {Hash.new}
69
+
70
+ context 'and the flag is set in the value source' do
71
+ shared_examples_for 'it determines flag status based on value source when' do |source_key|
72
+ let(:source_a) {{
73
+ source_key => true,
74
+ }}
75
+
76
+ it "has a value source with a key of #{source_key.inspect}" do
77
+ expect(subject.enabled?(:use_feature_a)).to eq true
78
+ end
79
+ end
80
+
81
+ it_behaves_like 'it determines flag status based on value source when',
82
+ :use_feature_a
83
+
84
+ it_behaves_like 'it determines flag status based on value source when',
85
+ 'use_feature_a'
86
+
87
+ it_behaves_like 'it determines flag status based on value source when',
88
+ 'USE_FEATURE_A'
89
+
90
+ shared_examples_for "it has a true value when" do |value|
91
+ it "the value source is set to #{value.inspect}" do
92
+ source_a[:use_feature_a] = value
93
+ expect(subject.enabled?(:use_feature_a)).to eq true
94
+ end
95
+ end
96
+
97
+ it_behaves_like 'it has a true value when', 'true'
98
+ it_behaves_like 'it has a true value when', '1'
99
+ it_behaves_like 'it has a true value when', 'on'
100
+ it_behaves_like 'it has a true value when', 1
101
+
102
+ shared_examples_for "it has a false value when" do |value|
103
+ it "the value source is set to #{value.inspect}" do
104
+ source_a[:use_feature_a] = value
105
+ expect(subject.enabled?(:use_feature_a)).to eq false
106
+ end
107
+ end
108
+
109
+ it_behaves_like 'it has a false value when', 'false'
110
+ it_behaves_like 'it has a false value when', '0'
111
+ it_behaves_like 'it has a false value when', 'off'
112
+ it_behaves_like 'it has a false value when', 0
113
+ end
114
+
115
+ context 'and the flag is not set in the value source' do
116
+ let(:source_a) {{
117
+ :use_feature_b => true,
118
+ }}
119
+
120
+ it 'assumes the flag is disabled' do
121
+ expect(subject.enabled?(:use_feature_a)).to eq false
122
+ end
123
+ end
124
+ end
125
+
126
+ context 'when multiple value sources are defined' do
127
+ let(:value_sources) { [source_c, source_b, source_a] }
128
+ let(:source_a) {{
129
+ :use_feature_a => true,
130
+ :use_feature_b => false,
131
+ :use_feature_c => true
132
+ }}
133
+
134
+ let(:source_b) {{
135
+ 'USE_FEATURE_B' => true
136
+ }}
137
+
138
+ let(:source_c) {{
139
+ 'use_feature_c' => false
140
+ }}
141
+
142
+ it 'determines whether the flag is enabled based on the first value ' \
143
+ + 'source in which the flag value is set' do
144
+ expect(subject.enabled?(:use_feature_a)).to eq true
145
+ expect(subject.enabled?(:use_feature_b)).to eq true
146
+ expect(subject.enabled?(:use_feature_c)).to eq false
147
+ end
148
+
149
+ it 'recalculates status if the value source changes state' do
150
+ expect(subject.enabled?(:use_feature_c)).to eq false
151
+ source_c[:use_feature_c] = true
152
+ expect(subject.enabled?(:use_feature_c)).to eq true
153
+ end
154
+ end
155
+ end
156
+ end
@@ -2,13 +2,9 @@ require 'spec_helper'
2
2
  require 'offs/permutations'
3
3
 
4
4
  describe OFFS::Permutations do
5
- subject { described_class.new(dependencies: dependencies) }
5
+ subject { described_class.new(flags: flags) }
6
6
 
7
- let(:dependencies) {{
8
- flags: flags
9
- }}
10
-
11
- let(:flags) { double(:flags, to_a: [:feature_a, :feature_b, :feature_c]) }
7
+ let(:flags) { [:feature_a, :feature_b, :feature_c] }
12
8
 
13
9
  let(:possible_permutations) {[
14
10
  { feature_a: true, feature_b: true, feature_c: true },
data/spec/offs_spec.rb CHANGED
@@ -5,28 +5,19 @@ describe OFFS do
5
5
  expect(described_class::VERSION).not_to be nil
6
6
  end
7
7
 
8
- subject { described_class.new(flag, dependencies: dependencies) }
8
+ subject { described_class.new(flag, flag_status_checker: flag_status_checker) }
9
9
 
10
- let(:flag) { :my_cool_new_feature }
10
+ let(:flag) { :use_my_new_feature }
11
11
 
12
- let(:dependencies) {{
13
- feature_flags: feature_flags
14
- }}
15
-
16
- let(:feature_flags) {
17
- OFFS::Flags.set do |f|
18
- f.flag :my_cool_new_feature, feature_status
19
- f.flag :my_runtime_feature, ->{ feature_status }
20
- end
21
- }
12
+ let(:flag_status_checker) { double(:flag_status_checker, validate!: nil) }
22
13
 
23
14
  context 'when the specified feature flag is not defined' do
24
- let(:feature_flags) { OFFS::Flags.new }
25
-
26
15
  it 'raises an error' do
27
- expect{ subject.so_you_want_to {} }.to \
28
- raise_error(OFFS::Flags::UndefinedFlagError,
29
- "The #{flag} flag has not been defined.")
16
+ allow(flag_status_checker).to receive(:validate!).with(flag) do
17
+ raise described_class::UndefinedFlagError, "Some message"
18
+ end
19
+ expect{ subject.would_like_to {} }.to \
20
+ raise_error(described_class::UndefinedFlagError, "Some message")
30
21
  end
31
22
  end
32
23
 
@@ -48,7 +39,12 @@ describe OFFS do
48
39
  end
49
40
  end
50
41
 
51
- shared_examples_for 'the feature is enabled' do
42
+ context "and the feature is turned on by default" do
43
+ before(:each) do
44
+ allow(flag_status_checker).to receive(:enabled?).with(flag) \
45
+ .and_return(true)
46
+ end
47
+
52
48
  it 'executes the would_like_to block' do
53
49
  expect(would_like_to_blk).to receive(:call)
54
50
  do_it
@@ -66,7 +62,8 @@ describe OFFS do
66
62
 
67
63
  it 'will execute the block for if_you_would_like_to' do
68
64
  x = nil
69
- OFFS.if_you_would_like_to(:my_cool_new_feature) do
65
+ OFFS.if_you_would_like_to(:use_my_new_feature,
66
+ flag_status_checker: flag_status_checker) do
70
67
  x = 1
71
68
  end
72
69
  expect(x).to eq 1
@@ -74,30 +71,26 @@ describe OFFS do
74
71
 
75
72
  it 'will not execute the block for if_you_do_not_want_to' do
76
73
  x = nil
77
- OFFS.if_you_do_not_want_to(:my_cool_new_feature) do
74
+ OFFS.if_you_do_not_want_to(:use_my_new_feature,
75
+ flag_status_checker: flag_status_checker) do
78
76
  x = 1
79
77
  end
80
78
  expect(x).to be_nil
81
79
  end
82
80
 
83
81
  it 'noops for raise_error_unless_we' do
84
- OFFS.raise_error_unless_we(:my_cool_new_feature)
82
+ OFFS.raise_error_unless_we(:use_my_new_feature,
83
+ flag_status_checker: flag_status_checker)
85
84
  end
86
85
  end
87
86
 
88
- context "and the feature is turned on by default" do
89
- let(:feature_status) { true }
90
-
91
- it_behaves_like 'the feature is enabled'
92
-
93
- context "and the feature status is a Proc" do
94
- let(:flag) { :my_runtime_feature }
95
-
96
- it_behaves_like 'the feature is enabled'
87
+ context "and the feature is turned off by default" do
88
+ before(:each) do
89
+ allow(flag_status_checker).to receive(:enabled?).with(flag) \
90
+ .and_return(false)
97
91
  end
98
- end
99
92
 
100
- shared_examples_for 'the feature is disabled' do
93
+
101
94
  it "executes the may_still_need_to block" do
102
95
  expect(may_still_need_to_blk).to receive(:call)
103
96
  do_it
@@ -115,7 +108,8 @@ describe OFFS do
115
108
 
116
109
  it 'will not execute the block for if_you_would_like_to' do
117
110
  x = nil
118
- OFFS.if_you_would_like_to(:my_cool_new_feature) do
111
+ OFFS.if_you_would_like_to(:use_my_new_feature,
112
+ flag_status_checker: flag_status_checker) do
119
113
  x = 1
120
114
  end
121
115
  expect(x).to be_nil
@@ -123,27 +117,18 @@ describe OFFS do
123
117
 
124
118
  it 'will execute the block for if_you_do_not_want_to' do
125
119
  x = nil
126
- OFFS.if_you_do_not_want_to(:my_cool_new_feature) do
120
+ OFFS.if_you_do_not_want_to(:use_my_new_feature,
121
+ flag_status_checker: flag_status_checker) do
127
122
  x = 1
128
123
  end
129
124
  expect(x).to eq 1
130
125
  end
131
126
 
132
127
  it 'raises an OFFS::FeatureDisabled error for raise_error_unless_we' do
133
- expect { OFFS.raise_error_unless_we(:my_cool_new_feature) }.to \
134
- raise_error(OFFS::FeatureDisabled, /my_cool_new_feature/)
135
- end
136
- end
137
-
138
- context "and the feature is turned off by default" do
139
- let(:feature_status) { false }
140
-
141
- it_behaves_like 'the feature is disabled'
142
-
143
- context "and the feature status is a Proc" do
144
- let(:flag) { :my_runtime_feature }
145
-
146
- it_behaves_like 'the feature is disabled'
128
+ expect {
129
+ OFFS.raise_error_unless_we(:use_my_new_feature,
130
+ flag_status_checker: flag_status_checker)
131
+ }.to raise_error(OFFS::FeatureDisabled, /use_my_new_feature/)
147
132
  end
148
133
  end
149
134
  end
data/spec/spec_helper.rb CHANGED
@@ -1,2 +1,10 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'offs'
3
+
4
+ RSpec.configure do |config|
5
+ config.order = "random"
6
+ config.run_all_when_everything_filtered = true
7
+ config.filter_run :focus
8
+ end
9
+
10
+
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: offs
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Wilger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-19 00:00:00.000000000 Z
11
+ date: 2015-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: injectable_dependencies
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -85,10 +71,12 @@ files:
85
71
  - README.md
86
72
  - Rakefile
87
73
  - lib/offs.rb
74
+ - lib/offs/exceptions.rb
88
75
  - lib/offs/flags.rb
89
76
  - lib/offs/permutations.rb
90
77
  - lib/offs/version.rb
91
78
  - offs.gemspec
79
+ - spec/offs/flags_spec.rb
92
80
  - spec/offs/permutations_spec.rb
93
81
  - spec/offs_spec.rb
94
82
  - spec/spec_helper.rb
@@ -117,6 +105,7 @@ signing_key:
117
105
  specification_version: 4
118
106
  summary: OFFS Feature Flagging System
119
107
  test_files:
108
+ - spec/offs/flags_spec.rb
120
109
  - spec/offs/permutations_spec.rb
121
110
  - spec/offs_spec.rb
122
111
  - spec/spec_helper.rb