flipper 0.28.3 → 1.4.2

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.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +56 -10
  4. data/.github/workflows/examples.yml +54 -11
  5. data/.github/workflows/release.yml +54 -0
  6. data/.superset/config.json +4 -0
  7. data/CLAUDE.md +93 -0
  8. data/Changelog.md +1 -559
  9. data/Gemfile +17 -8
  10. data/README.md +34 -29
  11. data/Rakefile +2 -2
  12. data/benchmark/typecast_ips.rb +8 -0
  13. data/docs/images/banner.jpg +0 -0
  14. data/docs/images/flipper_cloud.png +0 -0
  15. data/examples/cloud/app.ru +12 -0
  16. data/examples/cloud/backoff_policy.rb +13 -0
  17. data/examples/cloud/basic.rb +22 -0
  18. data/examples/cloud/cloud_setup.rb +20 -0
  19. data/examples/cloud/forked.rb +36 -0
  20. data/examples/cloud/import.rb +17 -0
  21. data/examples/cloud/poll_interval/README.md +111 -0
  22. data/examples/cloud/poll_interval/client.rb +108 -0
  23. data/examples/cloud/poll_interval/server.rb +98 -0
  24. data/examples/cloud/threaded.rb +33 -0
  25. data/examples/dsl.rb +0 -14
  26. data/examples/expressions.rb +237 -0
  27. data/examples/strict.rb +18 -0
  28. data/exe/flipper +5 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +8 -4
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +27 -1
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/actor_limit.rb +54 -0
  35. data/lib/flipper/adapters/cache_base.rb +161 -0
  36. data/lib/flipper/adapters/dual_write.rb +7 -5
  37. data/lib/flipper/adapters/failover.rb +9 -7
  38. data/lib/flipper/adapters/failsafe.rb +2 -6
  39. data/lib/flipper/adapters/http/client.rb +53 -14
  40. data/lib/flipper/adapters/http/error.rb +2 -2
  41. data/lib/flipper/adapters/http.rb +56 -16
  42. data/lib/flipper/adapters/instrumented.rb +2 -6
  43. data/lib/flipper/adapters/memoizable.rb +17 -22
  44. data/lib/flipper/adapters/memory.rb +5 -7
  45. data/lib/flipper/adapters/operation_logger.rb +18 -92
  46. data/lib/flipper/adapters/poll.rb +16 -3
  47. data/lib/flipper/adapters/pstore.rb +18 -12
  48. data/lib/flipper/adapters/read_only.rb +8 -41
  49. data/lib/flipper/adapters/strict.rb +75 -0
  50. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  51. data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
  52. data/lib/flipper/adapters/sync.rb +7 -7
  53. data/lib/flipper/adapters/wrapper.rb +54 -0
  54. data/lib/flipper/cli.rb +314 -0
  55. data/lib/flipper/cloud/configuration.rb +267 -0
  56. data/lib/flipper/cloud/dsl.rb +27 -0
  57. data/lib/flipper/cloud/message_verifier.rb +95 -0
  58. data/lib/flipper/cloud/middleware.rb +63 -0
  59. data/lib/flipper/cloud/migrate.rb +71 -0
  60. data/lib/flipper/cloud/routes.rb +14 -0
  61. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  62. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  63. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  64. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  65. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  66. data/lib/flipper/cloud/telemetry.rb +191 -0
  67. data/lib/flipper/cloud.rb +54 -0
  68. data/lib/flipper/configuration.rb +25 -4
  69. data/lib/flipper/dsl.rb +48 -43
  70. data/lib/flipper/engine.rb +102 -0
  71. data/lib/flipper/export.rb +0 -2
  72. data/lib/flipper/exporters/json/export.rb +1 -1
  73. data/lib/flipper/exporters/json/v1.rb +1 -1
  74. data/lib/flipper/expression/builder.rb +73 -0
  75. data/lib/flipper/expression/constant.rb +25 -0
  76. data/lib/flipper/expression.rb +71 -0
  77. data/lib/flipper/expressions/all.rb +9 -0
  78. data/lib/flipper/expressions/any.rb +9 -0
  79. data/lib/flipper/expressions/boolean.rb +9 -0
  80. data/lib/flipper/expressions/comparable.rb +13 -0
  81. data/lib/flipper/expressions/equal.rb +9 -0
  82. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  83. data/lib/flipper/expressions/greater_than.rb +9 -0
  84. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  85. data/lib/flipper/expressions/less_than.rb +9 -0
  86. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  87. data/lib/flipper/expressions/not_equal.rb +9 -0
  88. data/lib/flipper/expressions/now.rb +9 -0
  89. data/lib/flipper/expressions/number.rb +9 -0
  90. data/lib/flipper/expressions/percentage.rb +9 -0
  91. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  92. data/lib/flipper/expressions/property.rb +9 -0
  93. data/lib/flipper/expressions/random.rb +9 -0
  94. data/lib/flipper/expressions/string.rb +9 -0
  95. data/lib/flipper/expressions/time.rb +16 -0
  96. data/lib/flipper/feature.rb +63 -1
  97. data/lib/flipper/gate.rb +2 -1
  98. data/lib/flipper/gate_values.rb +5 -2
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  101. data/lib/flipper/instrumentation/statsd.rb +4 -2
  102. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  103. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  104. data/lib/flipper/metadata.rb +8 -1
  105. data/lib/flipper/middleware/memoizer.rb +30 -14
  106. data/lib/flipper/model/active_record.rb +23 -0
  107. data/lib/flipper/poller.rb +54 -12
  108. data/lib/flipper/serializers/gzip.rb +22 -0
  109. data/lib/flipper/serializers/json.rb +17 -0
  110. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  111. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  112. data/lib/flipper/test_help.rb +43 -0
  113. data/lib/flipper/typecast.rb +37 -9
  114. data/lib/flipper/types/percentage.rb +1 -1
  115. data/lib/flipper/version.rb +11 -1
  116. data/lib/flipper.rb +61 -8
  117. data/lib/generators/flipper/setup_generator.rb +68 -0
  118. data/lib/generators/flipper/templates/initializer.rb +45 -0
  119. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  120. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  121. data/lib/generators/flipper/update_generator.rb +35 -0
  122. data/package-lock.json +41 -0
  123. data/package.json +10 -0
  124. data/spec/fixtures/environment.rb +1 -0
  125. data/spec/flipper/adapter_builder_spec.rb +72 -0
  126. data/spec/flipper/adapter_spec.rb +21 -0
  127. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  128. data/spec/flipper/adapters/dual_write_spec.rb +15 -2
  129. data/spec/flipper/adapters/failover_spec.rb +12 -0
  130. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  131. data/spec/flipper/adapters/http_spec.rb +375 -74
  132. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  133. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  134. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  135. data/spec/flipper/adapters/poll_spec.rb +41 -0
  136. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  137. data/spec/flipper/adapters/strict_spec.rb +122 -0
  138. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
  139. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  140. data/spec/flipper/adapters/sync_spec.rb +13 -0
  141. data/spec/flipper/cli_spec.rb +217 -0
  142. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  143. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  144. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  145. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  146. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  147. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  148. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  149. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  150. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  151. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  152. data/spec/flipper/cloud_spec.rb +186 -0
  153. data/spec/flipper/configuration_spec.rb +17 -0
  154. data/spec/flipper/dsl_spec.rb +34 -73
  155. data/spec/flipper/engine_spec.rb +374 -0
  156. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  157. data/spec/flipper/expression/builder_spec.rb +248 -0
  158. data/spec/flipper/expression_spec.rb +188 -0
  159. data/spec/flipper/expressions/all_spec.rb +15 -0
  160. data/spec/flipper/expressions/any_spec.rb +15 -0
  161. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  162. data/spec/flipper/expressions/equal_spec.rb +24 -0
  163. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  164. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  165. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  166. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  167. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  168. data/spec/flipper/expressions/now_spec.rb +11 -0
  169. data/spec/flipper/expressions/number_spec.rb +21 -0
  170. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  171. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  172. data/spec/flipper/expressions/property_spec.rb +13 -0
  173. data/spec/flipper/expressions/random_spec.rb +9 -0
  174. data/spec/flipper/expressions/string_spec.rb +11 -0
  175. data/spec/flipper/expressions/time_spec.rb +29 -0
  176. data/spec/flipper/feature_spec.rb +380 -10
  177. data/spec/flipper/gate_values_spec.rb +2 -2
  178. data/spec/flipper/gates/expression_spec.rb +190 -0
  179. data/spec/flipper/identifier_spec.rb +4 -5
  180. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  181. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  182. data/spec/flipper/middleware/memoizer_spec.rb +107 -7
  183. data/spec/flipper/model/active_record_spec.rb +72 -0
  184. data/spec/flipper/poller_spec.rb +347 -4
  185. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  186. data/spec/flipper/serializers/json_spec.rb +13 -0
  187. data/spec/flipper/typecast_spec.rb +43 -7
  188. data/spec/flipper/types/actor_spec.rb +18 -1
  189. data/spec/flipper_integration_spec.rb +247 -16
  190. data/spec/flipper_spec.rb +92 -29
  191. data/spec/spec_helper.rb +24 -17
  192. data/spec/support/actor_names.yml +1 -0
  193. data/spec/support/fail_on_output.rb +8 -0
  194. data/spec/support/fake_backoff_policy.rb +15 -0
  195. data/spec/support/spec_helpers.rb +34 -8
  196. data/test/adapters/actor_limit_test.rb +20 -0
  197. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  198. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  199. data/test_rails/helper.rb +22 -2
  200. data/test_rails/system/test_help_test.rb +52 -0
  201. metadata +139 -75
  202. data/.tool-versions +0 -1
  203. data/lib/flipper/railtie.rb +0 -47
  204. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,237 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+
4
+ def assert(value)
5
+ if value
6
+ p value
7
+ else
8
+ puts "Expected true but was #{value}. Please correct."
9
+ exit 1
10
+ end
11
+ end
12
+
13
+ def refute(value)
14
+ if value
15
+ puts "Expected false but was #{value}. Please correct."
16
+ exit 1
17
+ else
18
+ p value
19
+ end
20
+ end
21
+
22
+ def reset
23
+ Flipper.disable_expression :something
24
+ end
25
+
26
+ class User < Struct.new(:id, :flipper_properties)
27
+ include Flipper::Identifier
28
+ end
29
+
30
+ class Org < Struct.new(:id, :flipper_properties)
31
+ include Flipper::Identifier
32
+ end
33
+
34
+ org = Org.new(1, {
35
+ "type" => "Org",
36
+ "id" => 1,
37
+ })
38
+
39
+ user = User.new(1, {
40
+ "type" => "User",
41
+ "id" => 1,
42
+ "plan" => "basic",
43
+ "age" => 39,
44
+ "team_user" => true,
45
+ })
46
+
47
+ admin_user = User.new(2, {
48
+ "type" => "User",
49
+ "id" => 2,
50
+ "admin" => true,
51
+ "team_user" => true,
52
+ })
53
+
54
+ other_user = User.new(3, {
55
+ "type" => "User",
56
+ "id" => 3,
57
+ "plan" => "plus",
58
+ "age" => 18,
59
+ "org_admin" => true,
60
+ })
61
+
62
+ age_expression = Flipper.property(:age).gte(21)
63
+ plan_expression = Flipper.property(:plan).eq("basic")
64
+ admin_expression = Flipper.property(:admin).eq(true)
65
+
66
+ puts "Single Expression"
67
+ refute Flipper.enabled?(:something, user)
68
+
69
+ puts "Enabling single expression"
70
+ Flipper.enable :something, plan_expression
71
+ assert Flipper.enabled?(:something, user)
72
+ refute Flipper.enabled?(:something, admin_user)
73
+ refute Flipper.enabled?(:something, other_user)
74
+
75
+ puts "Disabling single expression"
76
+ reset
77
+ refute Flipper.enabled?(:something, user)
78
+
79
+ puts "\n\nAny Expression"
80
+ any_expression = Flipper.any(plan_expression, age_expression)
81
+ refute Flipper.enabled?(:something, user)
82
+
83
+ puts "Enabling any expression"
84
+ Flipper.enable :something, any_expression
85
+ assert Flipper.enabled?(:something, user)
86
+ refute Flipper.enabled?(:something, admin_user)
87
+ refute Flipper.enabled?(:something, other_user)
88
+
89
+ puts "Disabling any expression"
90
+ reset
91
+ refute Flipper.enabled?(:something, user)
92
+
93
+ puts "\n\nAll Expression"
94
+ all_expression = Flipper.all(plan_expression, age_expression)
95
+ refute Flipper.enabled?(:something, user)
96
+
97
+ puts "Enabling all expression"
98
+ Flipper.enable :something, all_expression
99
+ assert Flipper.enabled?(:something, user)
100
+ refute Flipper.enabled?(:something, admin_user)
101
+ refute Flipper.enabled?(:something, other_user)
102
+
103
+ puts "Disabling all expression"
104
+ reset
105
+ refute Flipper.enabled?(:something, user)
106
+
107
+ puts "\n\nNested Expression"
108
+ nested_expression = Flipper.any(admin_expression, all_expression)
109
+ refute Flipper.enabled?(:something, user)
110
+ refute Flipper.enabled?(:something, admin_user)
111
+ refute Flipper.enabled?(:something, other_user)
112
+
113
+ puts "Enabling nested expression"
114
+ Flipper.enable :something, nested_expression
115
+ assert Flipper.enabled?(:something, user)
116
+ assert Flipper.enabled?(:something, admin_user)
117
+ refute Flipper.enabled?(:something, other_user)
118
+
119
+ puts "Disabling nested expression"
120
+ reset
121
+ refute Flipper.enabled?(:something, user)
122
+ refute Flipper.enabled?(:something, admin_user)
123
+ refute Flipper.enabled?(:something, other_user)
124
+
125
+ puts "\n\nBoolean Expression"
126
+ boolean_expression = Flipper.boolean(true)
127
+ Flipper.enable :something, boolean_expression
128
+ assert Flipper.enabled?(:something)
129
+ assert Flipper.enabled?(:something, user)
130
+ reset
131
+
132
+ puts "\n\nSet of Actors Expression"
133
+ set_of_actors_expression = Flipper.any(
134
+ Flipper.property(:flipper_id).eq("User;1"),
135
+ Flipper.property(:flipper_id).eq("User;3"),
136
+ )
137
+ Flipper.enable :something, set_of_actors_expression
138
+ assert Flipper.enabled?(:something, user)
139
+ assert Flipper.enabled?(:something, other_user)
140
+ refute Flipper.enabled?(:something, admin_user)
141
+ reset
142
+
143
+ puts "\n\n% of Actors Expression"
144
+ percentage_of_actors = Flipper.property(:flipper_id).percentage_of_actors(30)
145
+ Flipper.enable :something, percentage_of_actors
146
+ refute Flipper.enabled?(:something, user)
147
+ refute Flipper.enabled?(:something, other_user)
148
+ assert Flipper.enabled?(:something, admin_user)
149
+ reset
150
+
151
+ puts "\n\n% of Actors Per Type Expression"
152
+ percentage_of_actors_per_type = Flipper.any(
153
+ Flipper.all(
154
+ Flipper.property(:type).eq("User"),
155
+ Flipper.property(:flipper_id).percentage_of_actors(40),
156
+ ),
157
+ Flipper.all(
158
+ Flipper.property(:type).eq("Org"),
159
+ Flipper.property(:flipper_id).percentage_of_actors(10),
160
+ )
161
+ )
162
+ Flipper.enable :something, percentage_of_actors_per_type
163
+ refute Flipper.enabled?(:something, user) # not in the 40% enabled for Users
164
+ assert Flipper.enabled?(:something, other_user)
165
+ assert Flipper.enabled?(:something, admin_user)
166
+ refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs
167
+ reset
168
+
169
+ puts "\n\nPercentage of Time Expression"
170
+ percentage_of_time_expression = Flipper.random(100).lt(50)
171
+ Flipper.enable :something, percentage_of_time_expression
172
+ results = (1..10000).map { |n| Flipper.enabled?(:something, user) }
173
+ enabled, disabled = results.partition { |r| r }
174
+ p enabled: enabled.size
175
+ p disabled: disabled.size
176
+ assert (4_700..5_200).include?(enabled.size)
177
+ assert (4_700..5_200).include?(disabled.size)
178
+ reset
179
+
180
+ puts "\n\nChanging single expression to all expression"
181
+ Flipper.enable :something, plan_expression
182
+ Flipper.enable :something, Flipper.expression(:something).all.add(age_expression)
183
+ assert Flipper.enabled?(:something, user)
184
+ refute Flipper.enabled?(:something, admin_user)
185
+ refute Flipper.enabled?(:something, other_user)
186
+
187
+ puts "\n\nChanging single expression to any expression"
188
+ Flipper.enable :something, plan_expression
189
+ Flipper.enable :something, Flipper.expression(:something).any.add(age_expression, admin_expression)
190
+ assert Flipper.enabled?(:something, user)
191
+ assert Flipper.enabled?(:something, admin_user)
192
+ refute Flipper.enabled?(:something, other_user)
193
+
194
+ puts "\n\nChanging single expression to any expression by adding to condition"
195
+ Flipper.enable :something, plan_expression
196
+ Flipper.enable :something, Flipper.expression(:something).add(admin_expression)
197
+ assert Flipper.enabled?(:something, user)
198
+ assert Flipper.enabled?(:something, admin_user)
199
+ refute Flipper.enabled?(:something, other_user)
200
+
201
+ puts "\n\nEnabling based on time (epoch)"
202
+ scheduled_epoch = Flipper.now.gte(Flipper.time(Time.now.to_i - 86_400))
203
+ Flipper.enable :something, scheduled_epoch
204
+ assert Flipper.enabled?(:something, user)
205
+ assert Flipper.enabled?(:something, admin_user)
206
+ assert Flipper.enabled?(:something, other_user)
207
+ reset
208
+
209
+ puts "\n\nEnabling based on time (datetime)"
210
+ past_time = (Time.now.utc - 86_400).iso8601
211
+ scheduled_datetime = Flipper.now.gte(Flipper.time(past_time))
212
+ Flipper.enable :something, scheduled_datetime
213
+ assert Flipper.enabled?(:something, user)
214
+ assert Flipper.enabled?(:something, admin_user)
215
+ assert Flipper.enabled?(:something, other_user)
216
+ reset
217
+
218
+ puts "\n\nDisabling after a time (expiring feature)"
219
+ future_time = (Time.now.utc + 86_400).iso8601
220
+ expiring_expression = Flipper.now.lt(Flipper.time(future_time))
221
+ Flipper.enable :something, expiring_expression
222
+ assert Flipper.enabled?(:something, user)
223
+ assert Flipper.enabled?(:something, admin_user)
224
+ assert Flipper.enabled?(:something, other_user)
225
+ reset
226
+
227
+ puts "\n\nEnabling within a time window"
228
+ start_time = (Time.now.utc - 86_400).iso8601
229
+ end_time = (Time.now.utc + 86_400).iso8601
230
+ time_window = Flipper.all(
231
+ Flipper.now.gte(Flipper.time(start_time)),
232
+ Flipper.now.lt(Flipper.time(end_time))
233
+ )
234
+ Flipper.enable :something, time_window
235
+ assert Flipper.enabled?(:something, user)
236
+ assert Flipper.enabled?(:something, admin_user)
237
+ assert Flipper.enabled?(:something, other_user)
@@ -0,0 +1,18 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+
4
+ adapter = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
5
+ flipper = Flipper.new(adapter)
6
+
7
+ begin
8
+ puts "Checking :unknown_feature, which should raise an error."
9
+ flipper.enabled?(:unknown_feature)
10
+ warn "An error was not raised, but should have been"
11
+ exit 1
12
+ rescue Flipper::Adapters::Strict::NotFound => exception
13
+ puts "Ok, the exepcted error was raised: #{exception.message}"
14
+ end
15
+
16
+ puts "Flipper.add(:new_feature)"
17
+ flipper.add(:new_feature)
18
+ puts "Flipper.enabled?(:new_feature) => #{flipper.enabled?(:new_feature)}"
data/exe/flipper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "flipper/cli"
4
+
5
+ Flipper::CLI.run(ARGV)
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/flipper/version', __FILE__)
3
+ require File.expand_path('../lib/flipper/metadata', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ['John Nunemaker']
7
+ gem.email = 'support@flippercloud.io'
8
+ gem.summary = '[DEPRECATED] This gem has been merged into the `flipper` gem'
9
+ gem.license = 'MIT'
10
+ gem.homepage = 'https://www.flippercloud.io'
11
+
12
+ gem.files = [ 'lib/flipper-cloud.rb', 'lib/flipper/version.rb' ]
13
+ gem.name = 'flipper-cloud'
14
+ gem.require_paths = ['lib']
15
+ gem.version = Flipper::VERSION
16
+ gem.metadata = Flipper::METADATA
17
+
18
+ gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
19
+ end
data/flipper.gemspec CHANGED
@@ -6,7 +6,7 @@ plugin_files = []
6
6
  plugin_test_files = []
7
7
 
8
8
  Dir['flipper-*.gemspec'].map do |gemspec|
9
- spec = eval(File.read(gemspec))
9
+ spec = Gem::Specification.load(gemspec)
10
10
  plugin_files << spec.files
11
11
  plugin_test_files << spec.files
12
12
  end
@@ -22,11 +22,13 @@ ignored_test_files.flatten!.uniq!
22
22
 
23
23
  Gem::Specification.new do |gem|
24
24
  gem.authors = ['John Nunemaker']
25
- gem.email = ['nunemaker@gmail.com']
26
- gem.summary = 'Feature flipper for ANYTHING'
27
- gem.homepage = 'https://github.com/jnunemaker/flipper'
25
+ gem.email = 'support@flippercloud.io'
26
+ gem.summary = 'Beautiful, performant feature flags for Ruby and Rails.'
27
+ gem.homepage = 'https://www.flippercloud.io/docs'
28
28
  gem.license = 'MIT'
29
29
 
30
+ gem.bindir = "exe"
31
+ gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
30
32
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
31
33
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
32
34
  gem.name = 'flipper'
@@ -35,4 +37,6 @@ Gem::Specification.new do |gem|
35
37
  gem.metadata = Flipper::METADATA
36
38
 
37
39
  gem.add_dependency 'concurrent-ruby', '< 2'
40
+
41
+ gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
38
42
  end
data/lib/flipper/actor.rb CHANGED
@@ -2,14 +2,17 @@
2
2
  # to Flipper::Feature#enabled?.
3
3
  module Flipper
4
4
  class Actor
5
- attr_reader :flipper_id
5
+ attr_reader :flipper_id, :flipper_properties
6
6
 
7
- def initialize(flipper_id)
7
+ def initialize(flipper_id, flipper_properties = {})
8
8
  @flipper_id = flipper_id
9
+ @flipper_properties = flipper_properties
9
10
  end
10
11
 
11
12
  def eql?(other)
12
- self.class.eql?(other.class) && @flipper_id == other.flipper_id
13
+ self.class.eql?(other.class) &&
14
+ @flipper_id == other.flipper_id &&
15
+ @flipper_properties == other.flipper_properties
13
16
  end
14
17
  alias_method :==, :eql?
15
18
 
@@ -12,6 +12,7 @@ module Flipper
12
12
  boolean: nil,
13
13
  groups: Set.new,
14
14
  actors: Set.new,
15
+ expression: nil,
15
16
  percentage_of_actors: nil,
16
17
  percentage_of_time: nil,
17
18
  }
@@ -23,10 +24,14 @@ module Flipper
23
24
  end
24
25
  end
25
26
 
27
+ def read_only?
28
+ false
29
+ end
30
+
26
31
  # Public: Get all features and gate values in one call. Defaults to one call
27
32
  # to features and another to get_multi. Feel free to override per adapter to
28
33
  # make this more efficient.
29
- def get_all
34
+ def get_all(**kwargs)
30
35
  instances = features.map { |key| Flipper::Feature.new(key, self) }
31
36
  get_multi(instances)
32
37
  end
@@ -63,6 +68,27 @@ module Flipper
63
68
  def default_config
64
69
  self.class.default_config
65
70
  end
71
+
72
+ # Public: default name of the adapter
73
+ def name
74
+ @name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
75
+ end
76
+
77
+ # Public: Returns a string representation of the adapter stack for debugging.
78
+ # Shows the full chain of wrapped adapters.
79
+ #
80
+ # Examples:
81
+ # "memoizable -> active_support_cache_store -> active_record"
82
+ # "memoizable -> failover(primary: redis, secondary: memory)"
83
+ #
84
+ # Returns a String.
85
+ def adapter_stack
86
+ if respond_to?(:adapter) && adapter
87
+ "#{name} -> #{adapter.adapter_stack}"
88
+ else
89
+ name.to_s
90
+ end
91
+ end
66
92
  end
67
93
  end
68
94
 
@@ -0,0 +1,44 @@
1
+ module Flipper
2
+ # Builds an adapter from a stack of adapters.
3
+ #
4
+ # adapter = Flipper::AdapterBuilder.new do
5
+ # use Flipper::Adapters::Strict
6
+ # use Flipper::Adapters::Memoizable
7
+ # store Flipper::Adapters::Memory
8
+ # end.to_adapter
9
+ #
10
+ class AdapterBuilder
11
+ def initialize(&block)
12
+ @stack = []
13
+
14
+ # Default to memory adapter
15
+ store Flipper::Adapters::Memory
16
+
17
+ block.arity == 0 ? instance_eval(&block) : block.call(self) if block
18
+ end
19
+
20
+ if RUBY_VERSION >= '3.0'
21
+ def use(klass, *args, **kwargs, &block)
22
+ @stack.push ->(adapter) { klass.new(adapter, *args, **kwargs, &block) }
23
+ end
24
+ else
25
+ def use(klass, *args, &block)
26
+ @stack.push ->(adapter) { klass.new(adapter, *args, &block) }
27
+ end
28
+ end
29
+
30
+ if RUBY_VERSION >= '3.0'
31
+ def store(adapter, *args, **kwargs, &block)
32
+ @store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, **kwargs, &block) }
33
+ end
34
+ else
35
+ def store(adapter, *args, &block)
36
+ @store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, &block) }
37
+ end
38
+ end
39
+
40
+ def to_adapter
41
+ @stack.reverse.inject(@store.call) { |adapter, wrapper| wrapper.call(adapter) }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,54 @@
1
+ require "flipper/adapters/wrapper"
2
+
3
+ module Flipper
4
+ module Adapters
5
+ class ActorLimit < Wrapper
6
+ LimitExceeded = Class.new(Flipper::Error)
7
+
8
+ attr_reader :limit
9
+
10
+ class << self
11
+ # Returns whether sync mode is enabled for the current thread.
12
+ # When sync mode is enabled, actor limits are not enforced,
13
+ # allowing sync operations to bring local state in line with
14
+ # remote state regardless of limits.
15
+ def sync_mode
16
+ Thread.current[:flipper_actor_limit_sync_mode]
17
+ end
18
+
19
+ def sync_mode=(value)
20
+ Thread.current[:flipper_actor_limit_sync_mode] = value
21
+ end
22
+
23
+ # Executes a block with sync mode enabled. Actor limits will
24
+ # not be enforced within the block.
25
+ def with_sync_mode
26
+ old_value = sync_mode
27
+ self.sync_mode = true
28
+ yield
29
+ ensure
30
+ self.sync_mode = old_value
31
+ end
32
+ end
33
+
34
+ def initialize(adapter, limit = 100)
35
+ super(adapter)
36
+ @limit = limit
37
+ end
38
+
39
+ def enable(feature, gate, resource)
40
+ if gate.is_a?(Flipper::Gates::Actor) && !self.class.sync_mode && over_limit?(feature)
41
+ raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def over_limit?(feature)
50
+ feature.actors_value.size >= @limit
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,161 @@
1
+ module Flipper
2
+ module Adapters
3
+ # Base class for caching adapters. Inherit from this and then override
4
+ # cache_fetch, cache_read_multi, cache_write, and cache_delete.
5
+ class CacheBase
6
+ include ::Flipper::Adapter
7
+
8
+ # Public: The adapter being cached.
9
+ attr_reader :adapter
10
+
11
+ # Public: The ActiveSupport::Cache::Store to cache with.
12
+ attr_reader :cache
13
+
14
+ # Public: The ttl for all cached data.
15
+ attr_reader :ttl
16
+
17
+ # Public: The cache key where the set of known features is cached.
18
+ attr_reader :features_cache_key
19
+
20
+ # Public: The cache key where the set of all features with gates is cached.
21
+ attr_reader :get_all_cache_key
22
+
23
+ # Public: Alias expires_in to ttl for compatibility.
24
+ alias_method :expires_in, :ttl
25
+
26
+ def initialize(adapter, cache, ttl = 300, prefix: nil)
27
+ @adapter = adapter
28
+ @cache = cache
29
+ @ttl = ttl
30
+
31
+ @cache_version = 'v1'.freeze
32
+ @namespace = "flipper/#{@cache_version}"
33
+ @namespace = @namespace.prepend(prefix) if prefix
34
+ @features_cache_key = "#{@namespace}/features"
35
+ @get_all_cache_key = "#{@namespace}/get_all"
36
+ end
37
+
38
+ # Public: Expire the cache for the set of all features with gates.
39
+ def expire_get_all_cache
40
+ cache_delete @get_all_cache_key
41
+ end
42
+
43
+ # Public: Expire the cache for the set of known feature names.
44
+ def expire_features_cache
45
+ cache_delete @features_cache_key
46
+ expire_get_all_cache
47
+ end
48
+
49
+ # Public: Expire the cache for a given feature.
50
+ def expire_feature_cache(key)
51
+ cache_delete feature_cache_key(key)
52
+ expire_get_all_cache
53
+ end
54
+
55
+ # Public
56
+ def features
57
+ read_feature_keys
58
+ end
59
+
60
+ # Public
61
+ def add(feature)
62
+ result = @adapter.add(feature)
63
+ expire_features_cache
64
+ result
65
+ end
66
+
67
+ # Public
68
+ def remove(feature)
69
+ result = @adapter.remove(feature)
70
+ expire_features_cache
71
+ expire_feature_cache(feature.key)
72
+ result
73
+ end
74
+
75
+ # Public
76
+ def clear(feature)
77
+ result = @adapter.clear(feature)
78
+ expire_feature_cache(feature.key)
79
+ result
80
+ end
81
+
82
+ # Public
83
+ def get(feature)
84
+ read_feature(feature)
85
+ end
86
+
87
+ # Public
88
+ def get_multi(features)
89
+ read_many_features(features)
90
+ end
91
+
92
+ # Public
93
+ def get_all(**kwargs)
94
+ cache_fetch(@get_all_cache_key) {
95
+ result = read_all_features(**kwargs)
96
+ cache_write @features_cache_key, result.keys.to_set
97
+ result
98
+ }
99
+ end
100
+
101
+ # Public
102
+ def enable(feature, gate, thing)
103
+ result = @adapter.enable(feature, gate, thing)
104
+ expire_feature_cache(feature.key)
105
+ result
106
+ end
107
+
108
+ # Public
109
+ def disable(feature, gate, thing)
110
+ result = @adapter.disable(feature, gate, thing)
111
+ expire_feature_cache(feature.key)
112
+ result
113
+ end
114
+
115
+ # Public: Generate the cache key for a given feature.
116
+ #
117
+ # key - The String or Symbol feature key.
118
+ def feature_cache_key(key)
119
+ "#{@namespace}/feature/#{key}"
120
+ end
121
+
122
+ private
123
+
124
+ def read_all_features(**kwargs)
125
+ @adapter.get_all(**kwargs)
126
+ end
127
+
128
+ # Private: Returns the Set of known feature keys.
129
+ def read_feature_keys
130
+ cache_fetch(@features_cache_key) { @adapter.features }
131
+ end
132
+
133
+ # Private: Read through caching for a single feature.
134
+ def read_feature(feature)
135
+ cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
136
+ end
137
+
138
+ # Private: Given an array of features, attempts to read through cache in
139
+ # as few network calls as possible.
140
+ def read_many_features(features)
141
+ keys = features.map { |feature| feature_cache_key(feature.key) }
142
+ cache_result = cache_read_multi(keys)
143
+ uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
144
+
145
+ if uncached_features.any?
146
+ response = @adapter.get_multi(uncached_features)
147
+ response.each do |key, value|
148
+ cache_write feature_cache_key(key), value
149
+ cache_result[feature_cache_key(key)] = value
150
+ end
151
+ end
152
+
153
+ result = {}
154
+ features.each do |feature|
155
+ result[feature.key] = cache_result[feature_cache_key(feature.key)]
156
+ end
157
+ result
158
+ end
159
+ end
160
+ end
161
+ end
@@ -3,8 +3,7 @@ module Flipper
3
3
  class DualWrite
4
4
  include ::Flipper::Adapter
5
5
 
6
- # Public: The name of the adapter.
7
- attr_reader :name, :local, :remote
6
+ attr_reader :local, :remote
8
7
 
9
8
  # Public: Build a new sync instance.
10
9
  #
@@ -12,11 +11,14 @@ module Flipper
12
11
  # remote - The remote flipper adapter that writes should go to first (in
13
12
  # addition to the local adapter).
14
13
  def initialize(local, remote, options = {})
15
- @name = :dual_write
16
14
  @local = local
17
15
  @remote = remote
18
16
  end
19
17
 
18
+ def adapter_stack
19
+ "#{name}(local: #{@local.adapter_stack}, remote: #{@remote.adapter_stack})"
20
+ end
21
+
20
22
  def features
21
23
  @local.features
22
24
  end
@@ -29,8 +31,8 @@ module Flipper
29
31
  @local.get_multi(features)
30
32
  end
31
33
 
32
- def get_all
33
- @local.get_all
34
+ def get_all(**kwargs)
35
+ @local.get_all(**kwargs)
34
36
  end
35
37
 
36
38
  def add(feature)