flipper 0.26.0.rc2 → 0.26.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -23,18 +23,8 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- if protects?(context.thing)
31
- actor = wrap(context.thing)
32
- enabled_actor_ids = value
33
- enabled_actor_ids.include?(actor.value)
34
- else
35
- false
36
- end
37
- end
26
+ return false if context.thing.nil?
27
+ context.values.actors.include?(context.thing.value)
38
28
  end
39
29
 
40
30
  def wrap(thing)
@@ -24,7 +24,7 @@ module Flipper
24
24
  # Returns true if explicitly set to true, false if explicitly set to false
25
25
  # or nil if not explicitly set.
26
26
  def open?(context)
27
- context.values[key]
27
+ context.values.boolean
28
28
  end
29
29
 
30
30
  def wrap(thing)
@@ -23,14 +23,10 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- if context.thing.nil?
28
- false
29
- else
30
- value.any? do |name|
31
- group = Flipper.group(name)
32
- group.match?(context.thing, context)
33
- end
26
+ return false if context.thing.nil?
27
+
28
+ context.values.groups.any? do |name|
29
+ Flipper.group(name).match?(context.thing, context)
34
30
  end
35
31
  end
36
32
 
@@ -21,21 +21,19 @@ module Flipper
21
21
  value > 0
22
22
  end
23
23
 
24
+ # Private: this constant is used to support up to 3 decimal places
25
+ # in percentages.
26
+ SCALING_FACTOR = 1_000
27
+ private_constant :SCALING_FACTOR
28
+
24
29
  # Internal: Checks if the gate is open for a thing.
25
30
  #
26
31
  # Returns true if gate open for thing, false if not.
27
32
  def open?(context)
28
- percentage = context.values[key]
29
-
30
- if Types::Actor.wrappable?(context.thing)
31
- actor = Types::Actor.wrap(context.thing)
32
- id = "#{context.feature_name}#{actor.value}"
33
- # this is to support up to 3 decimal places in percentages
34
- scaling_factor = 1_000
35
- Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
36
- else
37
- false
38
- end
33
+ return false if context.thing.nil?
34
+
35
+ id = "#{context.feature_name}#{context.thing.value}"
36
+ Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
39
37
  end
40
38
 
41
39
  def protects?(thing)
@@ -23,8 +23,7 @@ module Flipper
23
23
  #
24
24
  # Returns true if gate open for thing, false if not.
25
25
  def open?(context)
26
- value = context.values[key]
27
- rand < (value / 100.0)
26
+ rand < (context.values.percentage_of_time / 100.0)
28
27
  end
29
28
 
30
29
  def protects?(thing)
@@ -1,5 +1,5 @@
1
1
  module Flipper
2
2
  METADATA = {
3
- 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/master/Changelog.md',
3
+ 'changelog_uri' => 'https://github.com/jnunemaker/flipper/blob/main/Changelog.md',
4
4
  }.freeze
5
5
  end
@@ -0,0 +1,117 @@
1
+ require 'logger'
2
+ require 'concurrent/utility/monotonic_time'
3
+ require 'concurrent/map'
4
+ require 'concurrent/atomic/atomic_fixnum'
5
+
6
+ module Flipper
7
+ class Poller
8
+ attr_reader :adapter, :thread, :pid, :mutex, :interval, :last_synced_at
9
+
10
+ def self.instances
11
+ @instances ||= Concurrent::Map.new
12
+ end
13
+ private_class_method :instances
14
+
15
+ def self.get(key, options = {})
16
+ instances.compute_if_absent(key) { new(options) }
17
+ end
18
+
19
+ def self.reset
20
+ instances.each {|_,poller| poller.stop }.clear
21
+ end
22
+
23
+ def initialize(options = {})
24
+ @thread = nil
25
+ @pid = Process.pid
26
+ @mutex = Mutex.new
27
+ @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
28
+ @remote_adapter = options.fetch(:remote_adapter)
29
+ @interval = options.fetch(:interval, 10).to_f
30
+ @last_synced_at = Concurrent::AtomicFixnum.new(0)
31
+ @adapter = Adapters::Memory.new
32
+
33
+ if @interval < 1
34
+ warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
35
+ @interval = 1
36
+ end
37
+
38
+ @start_automatically = options.fetch(:start_automatically, true)
39
+
40
+ if options.fetch(:shutdown_automatically, true)
41
+ at_exit { stop }
42
+ end
43
+ end
44
+
45
+ def start
46
+ reset if forked?
47
+ ensure_worker_running
48
+ end
49
+
50
+ def stop
51
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
52
+ operation: :stop,
53
+ })
54
+ @thread&.kill
55
+ end
56
+
57
+ def run
58
+ loop do
59
+ sleep jitter
60
+ start = Concurrent.monotonic_time
61
+ begin
62
+ sync
63
+ rescue => exception
64
+ # you can instrument these using poller.flipper
65
+ end
66
+
67
+ sleep_interval = interval - (Concurrent.monotonic_time - start)
68
+ sleep sleep_interval if sleep_interval.positive?
69
+ end
70
+ end
71
+
72
+ def sync
73
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", operation: :poll) do
74
+ @adapter.import @remote_adapter
75
+ @last_synced_at.update { |time| Concurrent.monotonic_time }
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def jitter
82
+ rand
83
+ end
84
+
85
+ def forked?
86
+ pid != Process.pid
87
+ end
88
+
89
+ def ensure_worker_running
90
+ # Return early if thread is alive and avoid the mutex lock and unlock.
91
+ return if thread_alive?
92
+
93
+ # If another thread is starting worker thread, then return early so this
94
+ # thread can enqueue and move on with life.
95
+ return unless mutex.try_lock
96
+
97
+ begin
98
+ return if thread_alive?
99
+ @thread = Thread.new { run }
100
+ @instrumenter.instrument("poller.#{InstrumentationNamespace}", {
101
+ operation: :thread_start,
102
+ })
103
+ ensure
104
+ mutex.unlock
105
+ end
106
+ end
107
+
108
+ def thread_alive?
109
+ @thread && @thread.alive?
110
+ end
111
+
112
+ def reset
113
+ @pid = Process.pid
114
+ mutex.unlock if mutex.locked?
115
+ end
116
+ end
117
+ end
@@ -2,11 +2,11 @@ module Flipper
2
2
  class Railtie < Rails::Railtie
3
3
  config.before_configuration do
4
4
  config.flipper = ActiveSupport::OrderedOptions.new.update(
5
- env_key: "flipper",
6
- memoize: true,
7
- preload: true,
8
- instrumenter: ActiveSupport::Notifications,
9
- log: true
5
+ env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
6
+ memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
7
+ preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
8
+ instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
9
+ log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?
10
10
  )
11
11
  end
12
12
 
@@ -21,11 +21,9 @@ module Flipper
21
21
  # Returns an Integer representation of the value.
22
22
  # Raises ArgumentError if conversion is not possible.
23
23
  def self.to_integer(value)
24
- if value.respond_to?(:to_i)
25
- value.to_i
26
- else
27
- raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
28
- end
24
+ value.to_i
25
+ rescue NoMethodError
26
+ raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
29
27
  end
30
28
 
31
29
  # Internal: Convert value to a float.
@@ -33,11 +31,9 @@ module Flipper
33
31
  # Returns a Float representation of the value.
34
32
  # Raises ArgumentError if conversion is not possible.
35
33
  def self.to_float(value)
36
- if value.respond_to?(:to_f)
37
- value.to_f
38
- else
39
- raise ArgumentError, "#{value.inspect} cannot be converted to a float"
40
- end
34
+ value.to_f
35
+ rescue NoMethodError
36
+ raise ArgumentError, "#{value.inspect} cannot be converted to a float"
41
37
  end
42
38
 
43
39
  # Internal: Convert value to a percentage.
@@ -45,11 +41,11 @@ module Flipper
45
41
  # Returns a Integer or Float representation of the value.
46
42
  # Raises ArgumentError if conversion is not possible.
47
43
  def self.to_percentage(value)
48
- if value.to_s.include?('.'.freeze)
49
- to_float(value)
50
- else
51
- to_integer(value)
52
- end
44
+ result_to_f = value.to_f
45
+ result_to_i = result_to_f.to_i
46
+ result_to_f == result_to_i ? result_to_i : result_to_f
47
+ rescue NoMethodError
48
+ raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
53
49
  end
54
50
 
55
51
  # Internal: Convert value to a set.
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.26.0.rc2'.freeze
2
+ VERSION = '0.26.1'.freeze
3
3
  end
data/lib/flipper.rb CHANGED
@@ -155,6 +155,7 @@ require 'flipper/instrumenters/noop'
155
155
  require 'flipper/identifier'
156
156
  require 'flipper/middleware/memoizer'
157
157
  require 'flipper/middleware/setup_env'
158
+ require 'flipper/poller'
158
159
  require 'flipper/registry'
159
160
  require 'flipper/type'
160
161
  require 'flipper/types/actor'
@@ -14,7 +14,9 @@ RSpec.describe Flipper::Adapters::Memory do
14
14
  flipper.enable_actor :following, Flipper::Actor.new('3')
15
15
  flipper.enable_group :following, Flipper::Types::Group.new(:staff)
16
16
 
17
- expect(source).to eq({
17
+ dup = described_class.new(subject.get_all)
18
+
19
+ expect(dup.get_all).to eq({
18
20
  "subscriptions" => subject.default_config.merge(boolean: "true"),
19
21
  "search" => subject.default_config,
20
22
  "logging" => subject.default_config.merge(:percentage_of_time => "30"),
@@ -11,7 +11,7 @@ RSpec.describe Flipper::FeatureCheckContext do
11
11
  end
12
12
 
13
13
  it 'initializes just fine' do
14
- instance = described_class.new(options)
14
+ instance = described_class.new(**options)
15
15
  expect(instance.feature_name).to eq(feature_name)
16
16
  expect(instance.values).to eq(values)
17
17
  expect(instance.thing).to eq(thing)
@@ -20,46 +20,46 @@ RSpec.describe Flipper::FeatureCheckContext do
20
20
  it 'requires feature_name' do
21
21
  options.delete(:feature_name)
22
22
  expect do
23
- described_class.new(options)
24
- end.to raise_error(KeyError)
23
+ described_class.new(**options)
24
+ end.to raise_error(ArgumentError)
25
25
  end
26
26
 
27
27
  it 'requires values' do
28
28
  options.delete(:values)
29
29
  expect do
30
- described_class.new(options)
31
- end.to raise_error(KeyError)
30
+ described_class.new(**options)
31
+ end.to raise_error(ArgumentError)
32
32
  end
33
33
 
34
34
  it 'requires thing' do
35
35
  options.delete(:thing)
36
36
  expect do
37
- described_class.new(options)
38
- end.to raise_error(KeyError)
37
+ described_class.new(**options)
38
+ end.to raise_error(ArgumentError)
39
39
  end
40
40
 
41
41
  it 'knows actors_value' do
42
42
  args = options.merge(values: Flipper::GateValues.new(actors: Set['User;1']))
43
- expect(described_class.new(args).actors_value).to eq(Set['User;1'])
43
+ expect(described_class.new(**args).actors_value).to eq(Set['User;1'])
44
44
  end
45
45
 
46
46
  it 'knows groups_value' do
47
47
  args = options.merge(values: Flipper::GateValues.new(groups: Set['admins']))
48
- expect(described_class.new(args).groups_value).to eq(Set['admins'])
48
+ expect(described_class.new(**args).groups_value).to eq(Set['admins'])
49
49
  end
50
50
 
51
51
  it 'knows boolean_value' do
52
- instance = described_class.new(options.merge(values: Flipper::GateValues.new(boolean: true)))
52
+ instance = described_class.new(**options.merge(values: Flipper::GateValues.new(boolean: true)))
53
53
  expect(instance.boolean_value).to eq(true)
54
54
  end
55
55
 
56
56
  it 'knows percentage_of_actors_value' do
57
57
  args = options.merge(values: Flipper::GateValues.new(percentage_of_actors: 14))
58
- expect(described_class.new(args).percentage_of_actors_value).to eq(14)
58
+ expect(described_class.new(**args).percentage_of_actors_value).to eq(14)
59
59
  end
60
60
 
61
61
  it 'knows percentage_of_time_value' do
62
62
  args = options.merge(values: Flipper::GateValues.new(percentage_of_time: 41))
63
- expect(described_class.new(args).percentage_of_time_value).to eq(41)
63
+ expect(described_class.new(**args).percentage_of_time_value).to eq(41)
64
64
  end
65
65
  end
@@ -80,13 +80,13 @@ RSpec.describe Flipper::GateValues do
80
80
  it 'raises argument error for percentage of time value that cannot be converted to an integer' do
81
81
  expect do
82
82
  described_class.new(percentage_of_time: ['asdf'])
83
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
83
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
84
84
  end
85
85
 
86
86
  it 'raises argument error for percentage of actors value that cannot be converted to an int' do
87
87
  expect do
88
88
  described_class.new(percentage_of_actors: ['asdf'])
89
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
89
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
90
90
  end
91
91
 
92
92
  it 'raises argument error for actors value that cannot be converted to a set' do
@@ -100,35 +100,4 @@ RSpec.describe Flipper::GateValues do
100
100
  described_class.new(groups: 'asdf')
101
101
  end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
102
102
  end
103
-
104
- describe '#[]' do
105
- it 'can read the boolean value' do
106
- expect(described_class.new(boolean: true)[:boolean]).to be(true)
107
- expect(described_class.new(boolean: true)['boolean']).to be(true)
108
- end
109
-
110
- it 'can read the actors value' do
111
- expect(described_class.new(actors: Set[1, 2])[:actors]).to eq(Set[1, 2])
112
- expect(described_class.new(actors: Set[1, 2])['actors']).to eq(Set[1, 2])
113
- end
114
-
115
- it 'can read the groups value' do
116
- expect(described_class.new(groups: Set[:admins])[:groups]).to eq(Set[:admins])
117
- expect(described_class.new(groups: Set[:admins])['groups']).to eq(Set[:admins])
118
- end
119
-
120
- it 'can read the percentage of time value' do
121
- expect(described_class.new(percentage_of_time: 15)[:percentage_of_time]).to eq(15)
122
- expect(described_class.new(percentage_of_time: 15)['percentage_of_time']).to eq(15)
123
- end
124
-
125
- it 'can read the percentage of actors value' do
126
- expect(described_class.new(percentage_of_actors: 15)[:percentage_of_actors]).to eq(15)
127
- expect(described_class.new(percentage_of_actors: 15)['percentage_of_actors']).to eq(15)
128
- end
129
-
130
- it 'returns nil for value that is not present' do
131
- expect(described_class.new({})['not legit']).to be(nil)
132
- end
133
- end
134
103
  end
@@ -9,7 +9,7 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
9
9
  Flipper::FeatureCheckContext.new(
10
10
  feature_name: feature,
11
11
  values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
12
- thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
12
+ thing: Flipper::Types::Actor.new(thing || Flipper::Actor.new(1))
13
13
  )
14
14
  end
15
15
 
@@ -0,0 +1,47 @@
1
+ require "flipper/poller"
2
+
3
+ RSpec.describe Flipper::Poller do
4
+ let(:remote_adapter) { Flipper::Adapters::Memory.new }
5
+ let(:remote) { Flipper.new(remote_adapter) }
6
+ let(:local) { Flipper.new(subject.adapter) }
7
+
8
+ subject do
9
+ described_class.new(
10
+ remote_adapter: remote_adapter,
11
+ start_automatically: false,
12
+ interval: Float::INFINITY
13
+ )
14
+ end
15
+
16
+ before do
17
+ allow(subject).to receive(:loop).and_yield # Make loop just call once
18
+ allow(subject).to receive(:sleep) # Disable sleep
19
+ allow(Thread).to receive(:new).and_yield # Disable separate thread
20
+ end
21
+
22
+ describe "#adapter" do
23
+ it "always returns same memory adapter instance" do
24
+ expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
25
+ expect(subject.adapter.object_id).to eq(subject.adapter.object_id)
26
+ end
27
+ end
28
+
29
+ describe "#sync" do
30
+ it "syncs remote adapter to local adapter" do
31
+ remote.enable :polling
32
+
33
+ expect(local.enabled?(:polling)).to be(false)
34
+ subject.sync
35
+ expect(local.enabled?(:polling)).to be(true)
36
+ end
37
+ end
38
+
39
+ describe "#start" do
40
+ it "starts the poller thread" do
41
+ expect(Thread).to receive(:new).and_yield
42
+ expect(subject).to receive(:loop).and_yield
43
+ expect(subject).to receive(:sync)
44
+ subject.start
45
+ end
46
+ end
47
+ end
@@ -18,6 +18,42 @@ RSpec.describe Flipper::Railtie do
18
18
  subject { application.initialize! }
19
19
 
20
20
  describe 'initializers' do
21
+ it 'can set env_key from ENV' do
22
+ ENV['FLIPPER_ENV_KEY'] = 'flopper'
23
+
24
+ subject
25
+ expect(config.env_key).to eq('flopper')
26
+ end
27
+
28
+ it 'can set memoize from ENV' do
29
+ ENV['FLIPPER_MEMOIZE'] = 'false'
30
+
31
+ subject
32
+ expect(config.memoize).to eq(false)
33
+ end
34
+
35
+ it 'can set preload from ENV' do
36
+ ENV['FLIPPER_PRELOAD'] = 'false'
37
+
38
+ subject
39
+ expect(config.preload).to eq(false)
40
+ end
41
+
42
+ it 'can set instrumenter from ENV' do
43
+ stub_const('My::Cool::Instrumenter', Class.new)
44
+ ENV['FLIPPER_INSTRUMENTER'] = 'My::Cool::Instrumenter'
45
+
46
+ subject
47
+ expect(config.instrumenter).to eq(My::Cool::Instrumenter)
48
+ end
49
+
50
+ it 'can set log from ENV' do
51
+ ENV['FLIPPER_LOG'] = 'false'
52
+
53
+ subject
54
+ expect(config.log).to eq(false)
55
+ end
56
+
21
57
  it 'sets defaults' do
22
58
  subject # initialize
23
59
  expect(config.env_key).to eq("flipper")
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
56
56
  nil => 0,
57
57
  '' => 0,
58
58
  0 => 0,
59
- 0.0 => 0.0,
59
+ 0.0 => 0,
60
60
  1 => 1,
61
61
  1.1 => 1.1,
62
62
  '0.01' => 0.01,
@@ -100,13 +100,13 @@ RSpec.describe Flipper::Typecast do
100
100
  it 'raises argument error for bad integer percentage' do
101
101
  expect do
102
102
  described_class.to_percentage(['asdf'])
103
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
103
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
104
104
  end
105
105
 
106
106
  it 'raises argument error for bad float percentage' do
107
107
  expect do
108
108
  described_class.to_percentage(['asdf.0'])
109
- end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a float))
109
+ end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a percentage))
110
110
  end
111
111
 
112
112
  it 'raises argument error for set value that cannot be converted to a set' do
data/spec/spec_helper.rb CHANGED
@@ -22,7 +22,7 @@ Dir[FlipperRoot.join('spec/support/**/*.rb')].sort.each { |f| require f }
22
22
 
23
23
  RSpec.configure do |config|
24
24
  config.before(:example) do
25
- Flipper::Adapters::Poll::Poller.reset if defined?(Flipper::Adapters::Poll::Poller)
25
+ Flipper::Poller.reset if defined?(Flipper::Poller)
26
26
  Flipper.unregister_groups
27
27
  Flipper.configuration = nil
28
28
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0.rc2
4
+ version: 0.26.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-16 00:00:00.000000000 Z
11
+ date: 2023-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -35,7 +35,6 @@ files:
35
35
  - ".github/dependabot.yml"
36
36
  - ".github/workflows/ci.yml"
37
37
  - ".github/workflows/examples.yml"
38
- - ".github/workflows/release.yml"
39
38
  - ".rspec"
40
39
  - ".tool-versions"
41
40
  - CODE_OF_CONDUCT.md
@@ -45,6 +44,10 @@ files:
45
44
  - LICENSE
46
45
  - README.md
47
46
  - Rakefile
47
+ - benchmark/enabled_ips.rb
48
+ - benchmark/enabled_profile.rb
49
+ - benchmark/instrumentation_ips.rb
50
+ - benchmark/typecast_ips.rb
48
51
  - docker-compose.yml
49
52
  - docs/DockerCompose.md
50
53
  - docs/README.md
@@ -112,6 +115,7 @@ files:
112
115
  - lib/flipper/metadata.rb
113
116
  - lib/flipper/middleware/memoizer.rb
114
117
  - lib/flipper/middleware/setup_env.rb
118
+ - lib/flipper/poller.rb
115
119
  - lib/flipper/railtie.rb
116
120
  - lib/flipper/registry.rb
117
121
  - lib/flipper/spec/shared_adapter_specs.rb
@@ -160,6 +164,7 @@ files:
160
164
  - spec/flipper/instrumenters/noop_spec.rb
161
165
  - spec/flipper/middleware/memoizer_spec.rb
162
166
  - spec/flipper/middleware/setup_env_spec.rb
167
+ - spec/flipper/poller_spec.rb
163
168
  - spec/flipper/railtie_spec.rb
164
169
  - spec/flipper/registry_spec.rb
165
170
  - spec/flipper/typecast_spec.rb
@@ -183,7 +188,7 @@ homepage: https://github.com/jnunemaker/flipper
183
188
  licenses:
184
189
  - MIT
185
190
  metadata:
186
- changelog_uri: https://github.com/jnunemaker/flipper/blob/master/Changelog.md
191
+ changelog_uri: https://github.com/jnunemaker/flipper/blob/main/Changelog.md
187
192
  post_install_message:
188
193
  rdoc_options: []
189
194
  require_paths:
@@ -195,9 +200,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
195
200
  version: '0'
196
201
  required_rubygems_version: !ruby/object:Gem::Requirement
197
202
  requirements:
198
- - - ">"
203
+ - - ">="
199
204
  - !ruby/object:Gem::Version
200
- version: 1.3.1
205
+ version: '0'
201
206
  requirements: []
202
207
  rubygems_version: 3.3.7
203
208
  signing_key:
@@ -239,6 +244,7 @@ test_files:
239
244
  - spec/flipper/instrumenters/noop_spec.rb
240
245
  - spec/flipper/middleware/memoizer_spec.rb
241
246
  - spec/flipper/middleware/setup_env_spec.rb
247
+ - spec/flipper/poller_spec.rb
242
248
  - spec/flipper/railtie_spec.rb
243
249
  - spec/flipper/registry_spec.rb
244
250
  - spec/flipper/typecast_spec.rb