offs 1.4.0 → 2.0.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: 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