rollout 2.6.2 → 3.1.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.
data/spec/spec_helper.rb CHANGED
@@ -5,23 +5,82 @@ require 'simplecov'
5
5
  SimpleCov.start
6
6
 
7
7
  require 'bundler/setup'
8
- require 'redis'
9
8
  require 'rollout'
10
9
 
11
- $redis = Redis.new(
12
- host: ENV.fetch('REDIS_HOST', '127.0.0.1'),
13
- port: ENV.fetch('REDIS_PORT', '6379'),
14
- db: ENV.fetch('REDIS_DB', '7'),
15
- )
10
+ class RolloutMemoryBackend
11
+ def initialize
12
+ @features = {}
13
+ @events = Hash.new { |hash, key| hash[key] = [] }
14
+ end
15
+
16
+ def fetch_feature(name)
17
+ state = @features[name.to_s]
18
+ return Rollout::FeatureState.new(name: name, percentage: 0) unless state
19
+
20
+ state.deep_clone
21
+ end
22
+
23
+ def fetch_features(names)
24
+ names.map { |name| fetch_feature(name) }
25
+ end
26
+
27
+ def feature_names
28
+ @features.keys
29
+ end
30
+
31
+ def feature_exists?(name)
32
+ @features.key?(name.to_s)
33
+ end
34
+
35
+ def save_feature(state)
36
+ @features[state.name] = state.deep_clone
37
+ end
38
+
39
+ def delete_feature(name)
40
+ @features.delete(name.to_s)
41
+ end
42
+
43
+ def clear_features
44
+ end
45
+
46
+ def mutate_feature(name)
47
+ mutation = yield fetch_feature(name)
48
+ save_feature(mutation.fetch(:state))
49
+ event = mutation[:event]
50
+ @events[name.to_s] << event if event
51
+ mutation
52
+ end
53
+
54
+ def feature_events(name, limit: nil)
55
+ limited_events(@events[name.to_s], limit)
56
+ end
57
+
58
+ def global_events(limit: nil)
59
+ limited_events([], limit)
60
+ end
61
+
62
+ def feature_updated_at(_name)
63
+ end
64
+
65
+ def delete_feature_events(name)
66
+ @events.delete(name.to_s)
67
+ end
68
+
69
+ private
70
+
71
+ def limited_events(events, limit)
72
+ return events if limit.nil?
73
+ raise ArgumentError, "limit must be an Integer" unless limit.is_a?(Integer)
74
+ raise ArgumentError, "limit must be >= 0" if limit < 0
75
+
76
+ events.last(limit)
77
+ end
78
+ end
16
79
 
17
80
  RSpec.configure do |config|
18
81
  config.example_status_persistence_file_path = '.rspec_status'
19
82
 
20
- # config.disable_monkey_patching!
21
-
22
83
  config.expect_with :rspec do |c|
23
84
  c.syntax = :expect
24
85
  end
25
-
26
- config.before { $redis.flushdb }
27
86
  end
@@ -0,0 +1,88 @@
1
+ require "date"
2
+
3
+ RSpec.shared_examples "a rollout feature backend" do
4
+ def empty_state(name)
5
+ Rollout::FeatureState.new(name: name, percentage: 0)
6
+ end
7
+
8
+ def json_metadata
9
+ {
10
+ "description" => "New navigation",
11
+ "updated_at" => 1,
12
+ "enabled" => true,
13
+ "ratio" => 0.5,
14
+ "owner" => nil,
15
+ "label" => :beta,
16
+ "released_at" => Time.utc(2026, 1, 1),
17
+ "day" => Date.new(2026, 1, 1),
18
+ "labels" => ["a", { "nested" => :ok }],
19
+ }
20
+ end
21
+
22
+ it "returns an empty state for a missing feature" do
23
+ state = backend.fetch_feature(:chat)
24
+
25
+ expect(state).to eq empty_state(:chat)
26
+ end
27
+
28
+ it "preserves fetch_features order and duplicates" do
29
+ backend.save_feature(Rollout::FeatureState.new(name: :chat, percentage: 25))
30
+
31
+ states = backend.fetch_features([:chat, :missing, :chat])
32
+
33
+ expect(states.map(&:name)).to eq %w[chat missing chat]
34
+ expect(states.map(&:percentage)).to eq [25.0, 0.0, 25.0]
35
+ end
36
+
37
+ it "returns no features for an empty fetch_features request" do
38
+ expect(backend.fetch_features([])).to eq []
39
+ end
40
+
41
+ it "does not share nested data with a fetched state" do
42
+ backend.save_feature(
43
+ Rollout::FeatureState.new(name: :chat, percentage: 0, data: { "labels" => ["a"] }),
44
+ )
45
+
46
+ backend.fetch_feature(:chat).data["labels"] << "b"
47
+
48
+ expect(backend.fetch_feature(:chat).data).to eq("labels" => ["a"])
49
+ end
50
+
51
+ it "does not share nested data with a saved state" do
52
+ state = Rollout::FeatureState.new(name: :chat, percentage: 0, data: { "labels" => ["a"] })
53
+ backend.save_feature(state)
54
+
55
+ state.data["labels"] << "b"
56
+
57
+ expect(backend.fetch_feature(:chat).data).to eq("labels" => ["a"])
58
+ end
59
+
60
+ it "returns the same canonical metadata after save_feature" do
61
+ state = Rollout::FeatureState.new(name: :chat, percentage: 0, data: json_metadata)
62
+ backend.save_feature(state)
63
+
64
+ expect(backend.fetch_feature(:chat)).to eq state
65
+ expect(state.data["label"]).to eq "beta"
66
+ expect(state.data["labels"]).to eq ["a", { "nested" => "ok" }]
67
+ end
68
+
69
+ it "tracks existence and names after save and delete" do
70
+ backend.save_feature(Rollout::FeatureState.new(name: :chat, percentage: 25))
71
+
72
+ expect(backend.feature_exists?(:chat)).to be_truthy
73
+ expect(backend.feature_names).to eq ["chat"]
74
+
75
+ backend.delete_feature(:chat)
76
+
77
+ expect(backend.feature_exists?(:chat)).to be_falsey
78
+ expect(backend.fetch_feature(:chat)).to eq empty_state(:chat)
79
+ end
80
+
81
+ it "does not persist when mutate_feature's block raises" do
82
+ expect {
83
+ backend.mutate_feature(:chat) { raise "nope" }
84
+ }.to raise_error("nope")
85
+
86
+ expect(backend.feature_exists?(:chat)).to be_falsey
87
+ end
88
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollout
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.2
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Golick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-06 00:00:00.000000000 Z
11
+ date: 2026-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: observer
@@ -24,26 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: redis
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '4.0'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '6'
37
- type: :runtime
38
- prerelease: false
39
- version_requirements: !ruby/object:Gem::Requirement
40
- requirements:
41
- - - ">="
42
- - !ruby/object:Gem::Version
43
- version: '4.0'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '6'
47
27
  - !ruby/object:Gem::Dependency
48
28
  name: rake
49
29
  requirement: !ruby/object:Gem::Requirement
@@ -64,14 +44,14 @@ dependencies:
64
44
  requirements:
65
45
  - - ">="
66
46
  - !ruby/object:Gem::Version
67
- version: '1.17'
47
+ version: '2.1'
68
48
  type: :development
69
49
  prerelease: false
70
50
  version_requirements: !ruby/object:Gem::Requirement
71
51
  requirements:
72
52
  - - ">="
73
53
  - !ruby/object:Gem::Version
74
- version: '1.17'
54
+ version: '2.1'
75
55
  - !ruby/object:Gem::Dependency
76
56
  name: pry
77
57
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +122,13 @@ dependencies:
142
122
  - - '='
143
123
  - !ruby/object:Gem::Version
144
124
  version: '0.17'
145
- description: Feature flippers with redis.
125
+ description: Feature flippers.
146
126
  email:
147
127
  - jamesgolick@gmail.com
148
128
  executables: []
149
129
  extensions: []
150
130
  extra_rdoc_files: []
151
131
  files:
152
- - ".circleci/config.yml"
153
132
  - ".github/workflows/release.yml"
154
133
  - ".github/workflows/test.yml"
155
134
  - ".gitignore"
@@ -161,15 +140,20 @@ files:
161
140
  - LICENSE
162
141
  - README.md
163
142
  - Rakefile
143
+ - docs/upgrading-to-v3.md
164
144
  - lib/rollout.rb
165
145
  - lib/rollout/feature.rb
146
+ - lib/rollout/feature_state.rb
166
147
  - lib/rollout/logging.rb
167
148
  - lib/rollout/version.rb
168
149
  - rollout.gemspec
169
150
  - spec/rollout/feature_spec.rb
151
+ - spec/rollout/feature_state_spec.rb
170
152
  - spec/rollout/logging_spec.rb
153
+ - spec/rollout/memory_backend_contract_spec.rb
171
154
  - spec/rollout_spec.rb
172
155
  - spec/spec_helper.rb
156
+ - spec/support/backend_contract.rb
173
157
  homepage: https://github.com/FetLife/rollout
174
158
  licenses:
175
159
  - MIT
@@ -189,9 +173,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
173
  - !ruby/object:Gem::Version
190
174
  version: '0'
191
175
  requirements: []
192
- rubyforge_project:
193
- rubygems_version: 2.5.1
176
+ rubygems_version: 3.5.22
194
177
  signing_key:
195
178
  specification_version: 4
196
- summary: Feature flippers with redis.
179
+ summary: Feature flippers.
197
180
  test_files: []
data/.circleci/config.yml DELETED
@@ -1,119 +0,0 @@
1
- version: 2.1
2
-
3
- workflows:
4
- main:
5
- jobs:
6
- - ruby33
7
- - ruby31
8
- - ruby32
9
- - ruby30
10
- - ruby27
11
- - ruby26
12
- - ruby25
13
- - ruby24
14
-
15
- executors:
16
- ruby33:
17
- docker:
18
- - image: cimg/ruby:3.3
19
- - image: cimg/redis:7.2
20
- ruby32:
21
- docker:
22
- - image: cimg/ruby:3.2
23
- - image: cimg/redis:7.2
24
- ruby31:
25
- docker:
26
- - image: cimg/ruby:3.1
27
- - image: cimg/redis:7.2
28
- ruby30:
29
- docker:
30
- - image: cimg/ruby:3.0
31
- - image: cimg/redis:7.2
32
- ruby27:
33
- docker:
34
- - image: cimg/ruby:2.7
35
- - image: cimg/redis:7.2
36
- ruby26:
37
- docker:
38
- - image: cimg/ruby:2.7
39
- - image: cimg/redis:7.2
40
- ruby25:
41
- docker:
42
- - image: cimg/ruby:2.7
43
- - image: cimg/redis:7.2
44
- ruby24:
45
- docker:
46
- - image: cimg/ruby:2.4
47
- - image: cimg/redis:7.2
48
-
49
- commands:
50
- test:
51
- steps:
52
- - run:
53
- name: Bundle Install
54
- command: bundle check --path vendor/bundle || bundle install
55
-
56
- - run:
57
- name: Run rspec
58
- command: |
59
- bundle exec rspec --format documentation --format RspecJunitFormatter --out test_results/rspec.xml
60
-
61
- jobs:
62
- ruby33:
63
- executor: ruby33
64
- steps:
65
- - checkout
66
- - test
67
-
68
- - run:
69
- name: Report Test Coverage
70
- command: |
71
- wget https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 -O cc-test-reporter
72
- chmod +x cc-test-reporter
73
- ./cc-test-reporter format-coverage -t simplecov -o coverage/codeclimate.json coverage/.resultset.json
74
- ./cc-test-reporter upload-coverage -i coverage/codeclimate.json
75
-
76
- - store_test_results:
77
- path: test_results
78
-
79
- ruby32:
80
- executor: ruby30
81
- steps:
82
- - checkout
83
- - test
84
-
85
- ruby31:
86
- executor: ruby30
87
- steps:
88
- - checkout
89
- - test
90
-
91
- ruby30:
92
- executor: ruby30
93
- steps:
94
- - checkout
95
- - test
96
-
97
- ruby27:
98
- executor: ruby27
99
- steps:
100
- - checkout
101
- - test
102
-
103
- ruby26:
104
- executor: ruby27
105
- steps:
106
- - checkout
107
- - test
108
-
109
- ruby25:
110
- executor: ruby27
111
- steps:
112
- - checkout
113
- - test
114
-
115
- ruby24:
116
- executor: ruby24
117
- steps:
118
- - checkout
119
- - test