flipper 0.26.0.rc2 → 0.26.1

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.
@@ -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