flipper 0.26.0 → 1.3.6

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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
data/lib/flipper/dsl.rb CHANGED
@@ -10,7 +10,7 @@ module Flipper
10
10
  # Private: What is being used to instrument all the things.
11
11
  attr_reader :instrumenter
12
12
 
13
- def_delegators :@adapter, :memoize=, :memoizing?
13
+ def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
14
14
 
15
15
  # Public: Returns a new instance of the DSL.
16
16
  #
@@ -46,6 +46,25 @@ module Flipper
46
46
  feature(name).enable(*args)
47
47
  end
48
48
 
49
+ # Public: Enable a feature for an expression.
50
+ #
51
+ # name - The String or Symbol name of the feature.
52
+ # expression - a Flipper::Expression instance or a Hash.
53
+ #
54
+ # Returns result of Feature#enable.
55
+ def enable_expression(name, expression)
56
+ feature(name).enable_expression(expression)
57
+ end
58
+
59
+ # Public: Add an expression to a feature.
60
+ #
61
+ # expression - an expression or Hash that can be converted to an expression.
62
+ #
63
+ # Returns result of enable.
64
+ def add_expression(name, expression)
65
+ feature(name).add_expression(expression)
66
+ end
67
+
49
68
  # Public: Enable a feature for an actor.
50
69
  #
51
70
  # name - The String or Symbol name of the feature.
@@ -100,6 +119,24 @@ module Flipper
100
119
  feature(name).disable(*args)
101
120
  end
102
121
 
122
+ # Public: Disable expression for feature.
123
+ #
124
+ # name - The String or Symbol name of the feature.
125
+ #
126
+ # Returns result of Feature#disable.
127
+ def disable_expression(name)
128
+ feature(name).disable_expression
129
+ end
130
+
131
+ # Public: Remove an expression from a feature.
132
+ #
133
+ # expression - an Expression or Hash that can be converted to an expression.
134
+ #
135
+ # Returns result of enable.
136
+ def remove_expression(name, expression)
137
+ feature(name).remove_expression(expression)
138
+ end
139
+
103
140
  # Public: Disable a feature for an actor.
104
141
  #
105
142
  # name - The String or Symbol name of the feature.
@@ -210,22 +247,6 @@ module Flipper
210
247
  # Returns an instance of Flipper::Feature.
211
248
  alias_method :[], :feature
212
249
 
213
- # Public: Shortcut for getting a boolean type instance.
214
- #
215
- # value - The true or false value for the boolean.
216
- #
217
- # Returns a Flipper::Types::Boolean instance.
218
- def boolean(value = true)
219
- Types::Boolean.new(value)
220
- end
221
-
222
- # Public: Even shorter shortcut for getting a boolean type instance.
223
- #
224
- # value - The true or false value for the boolean.
225
- #
226
- # Returns a Flipper::Types::Boolean instance.
227
- alias_method :bool, :boolean
228
-
229
250
  # Public: Access a flipper group by name.
230
251
  #
231
252
  # name - The String or Symbol name of the feature.
@@ -235,35 +256,14 @@ module Flipper
235
256
  Flipper.group(name)
236
257
  end
237
258
 
238
- # Public: Wraps an object as a flipper actor.
239
- #
240
- # thing - The object that you would like to wrap.
259
+ # Public: Gets the expression for the feature.
241
260
  #
242
- # Returns an instance of Flipper::Types::Actor.
243
- # Raises ArgumentError if thing does not respond to `flipper_id`.
244
- def actor(thing)
245
- Types::Actor.new(thing)
246
- end
247
-
248
- # Public: Shortcut for getting a percentage of time instance.
249
- #
250
- # number - The percentage of time that should be enabled.
251
- #
252
- # Returns Flipper::Types::PercentageOfTime.
253
- def time(number)
254
- Types::PercentageOfTime.new(number)
255
- end
256
- alias_method :percentage_of_time, :time
257
-
258
- # Public: Shortcut for getting a percentage of actors instance.
259
- #
260
- # number - The percentage of actors that should be enabled.
261
+ # name - The String or Symbol name of the feature.
261
262
  #
262
- # Returns Flipper::Types::PercentageOfActors.
263
- def actors(number)
264
- Types::PercentageOfActors.new(number)
263
+ # Returns an instance of Flipper::Expression.
264
+ def expression(name)
265
+ feature(name).expression
265
266
  end
266
- alias_method :percentage_of_actors, :actors
267
267
 
268
268
  # Public: Returns a Set of the known features for this adapter.
269
269
  #
@@ -272,8 +272,9 @@ module Flipper
272
272
  adapter.features.map { |name| feature(name) }.to_set
273
273
  end
274
274
 
275
- def import(flipper)
276
- adapter.import(flipper.adapter)
275
+ # Public: Does this adapter support writes or not.
276
+ def read_only?
277
+ adapter.read_only?
277
278
  end
278
279
 
279
280
  # Cloud DSL method that does nothing for open source version.
@@ -0,0 +1,102 @@
1
+ module Flipper
2
+ class Engine < Rails::Engine
3
+ def self.default_strict_value
4
+ value = ENV["FLIPPER_STRICT"]
5
+ if value.in?(["warn", "raise", "noop"])
6
+ value.to_sym
7
+ elsif value
8
+ Typecast.to_boolean(value) ? :raise : false
9
+ elsif Rails.env.production?
10
+ false
11
+ else
12
+ # Warn in development for now. Future versions may default to :raise in development and test
13
+ Rails.env.development? && :warn
14
+ end
15
+ end
16
+
17
+ paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
18
+
19
+ config.before_configuration do
20
+ config.flipper = ActiveSupport::OrderedOptions.new.update(
21
+ env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
22
+ memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
23
+ preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
24
+ instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
25
+ log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
26
+ cloud_path: "_flipper",
27
+ strict: default_strict_value,
28
+ actor_limit: ENV["FLIPPER_ACTOR_LIMIT"]&.to_i || 100,
29
+ test_help: Flipper::Typecast.to_boolean(ENV["FLIPPER_TEST_HELP"] || Rails.env.test?),
30
+ )
31
+ end
32
+
33
+ initializer "flipper.properties" do
34
+ ActiveSupport.on_load(:active_record) do
35
+ require "flipper/model/active_record"
36
+ ActiveRecord::Base.include Flipper::Model::ActiveRecord
37
+ end
38
+ end
39
+
40
+ initializer "flipper.default", before: :load_config_initializers do |app|
41
+ # Load cloud secrets from Rails credentials
42
+ ENV["FLIPPER_CLOUD_TOKEN"] ||= app.credentials.dig(:flipper, :cloud_token)
43
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] ||= app.credentials.dig(:flipper, :cloud_sync_secret)
44
+
45
+ require 'flipper/cloud' if cloud?
46
+
47
+ Flipper.configure do |config|
48
+ config.default do
49
+ if cloud?
50
+ Flipper::Cloud.new(
51
+ local_adapter: config.adapter,
52
+ instrumenter: app.config.flipper.instrumenter
53
+ )
54
+ else
55
+ Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ initializer "flipper.log", after: :load_config_initializers do |app|
62
+ flipper = app.config.flipper
63
+
64
+ if flipper.log && flipper.instrumenter == ActiveSupport::Notifications
65
+ require "flipper/instrumentation/log_subscriber"
66
+ end
67
+ end
68
+
69
+ initializer "flipper.adapters", after: :load_config_initializers do |app|
70
+ flipper = app.config.flipper
71
+
72
+ Flipper.configure do |config|
73
+ config.use Flipper::Adapters::Strict, flipper.strict if flipper.strict
74
+ config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
75
+ end
76
+ end
77
+
78
+ initializer "flipper.memoizer", after: :load_config_initializers do |app|
79
+ flipper = app.config.flipper
80
+
81
+ if flipper.memoize
82
+ app.middleware.use Flipper::Middleware::Memoizer, {
83
+ env_key: flipper.env_key,
84
+ preload: flipper.preload,
85
+ if: flipper.memoize.respond_to?(:call) ? flipper.memoize : nil
86
+ }
87
+ end
88
+ end
89
+
90
+ initializer "flipper.test" do |app|
91
+ require "flipper/test_help" if app.config.flipper.test_help
92
+ end
93
+
94
+ def cloud?
95
+ !!ENV["FLIPPER_CLOUD_TOKEN"]
96
+ end
97
+
98
+ def self.deprecated_rails_version?
99
+ Gem::Version.new(Rails.version) < Gem::Version.new(Flipper::NEXT_REQUIRED_RAILS_VERSION)
100
+ end
101
+ end
102
+ end
@@ -2,10 +2,10 @@ module Flipper
2
2
  # Top level error that all other errors inherit from.
3
3
  class Error < StandardError; end
4
4
 
5
- # Raised when gate can not be found for a thing.
5
+ # Raised when gate can not be found for an actor.
6
6
  class GateNotFound < Error
7
- def initialize(thing)
8
- super "Could not find gate for #{thing.inspect}"
7
+ def initialize(actor)
8
+ super "Could not find gate for #{actor.inspect}"
9
9
  end
10
10
  end
11
11
 
@@ -0,0 +1,24 @@
1
+ module Flipper
2
+ class Export
3
+ attr_reader :contents, :format, :version
4
+
5
+ def initialize(contents:, format: :json, version: 1)
6
+ @contents = contents
7
+ @format = format
8
+ @version = version
9
+ end
10
+
11
+ def features
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def adapter
16
+ @adapter ||= Flipper::Adapters::Memory.new(features)
17
+ end
18
+
19
+ def eql?(other)
20
+ self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
21
+ end
22
+ alias_method :==, :eql?
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ require "flipper/exporters/json/v1"
2
+
3
+ module Flipper
4
+ module Exporter
5
+ extend self
6
+
7
+ FORMATTERS = {
8
+ json: {
9
+ 1 => Flipper::Exporters::Json::V1,
10
+ }
11
+ }.freeze
12
+
13
+ def build(format: :json, version: 1)
14
+ FORMATTERS.fetch(format).fetch(version).new
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ require "flipper/export"
2
+ require "flipper/typecast"
3
+
4
+ module Flipper
5
+ module Exporters
6
+ module Json
7
+ # Raised when the contents of the export are not valid.
8
+ class InvalidError < StandardError; end
9
+ class JsonError < InvalidError; end
10
+
11
+ # Internal: JSON export class that knows how to build features hash
12
+ # from data.
13
+ class Export < ::Flipper::Export
14
+ def initialize(contents:, version: 1)
15
+ super contents: contents, version: version, format: :json
16
+ end
17
+
18
+ # Public: The features hash identical to calling get_all on adapter.
19
+ def features
20
+ @features ||= begin
21
+ features = Typecast.from_json(contents).fetch("features")
22
+ Typecast.features_hash(features)
23
+ rescue JSON::ParserError
24
+ raise JsonError
25
+ rescue
26
+ raise InvalidError
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require "json"
2
+ require "flipper/exporters/json/export"
3
+
4
+ module Flipper
5
+ module Exporters
6
+ module Json
7
+ class V1
8
+ VERSION = 1
9
+
10
+ def call(adapter)
11
+ features = adapter.get_all
12
+
13
+ # Convert sets to arrays for json
14
+ features.each do |feature_key, gates|
15
+ gates.each do |key, value|
16
+ case value
17
+ when Set
18
+ features[feature_key][key] = value.to_a
19
+ end
20
+ end
21
+ end
22
+
23
+ json = Typecast.to_json({
24
+ version: VERSION,
25
+ features: features,
26
+ })
27
+
28
+ Json::Export.new(contents: json, version: VERSION)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ module Flipper
2
+ class Expression
3
+ module Builder
4
+ def build(object)
5
+ Expression.build(object)
6
+ end
7
+
8
+ def add(*expressions)
9
+ group? ? build(name => args + expressions.flatten) : any.add(*expressions)
10
+ end
11
+
12
+ def remove(*expressions)
13
+ group? ? build(name => args - expressions.flatten) : any.remove(*expressions)
14
+ end
15
+
16
+ def any
17
+ any? ? self : build({ Any: [self] })
18
+ end
19
+
20
+ def all
21
+ all? ? self : build({ All: [self] })
22
+ end
23
+
24
+ def equal(object)
25
+ build({ Equal: [self, object] })
26
+ end
27
+ alias eq equal
28
+
29
+ def not_equal(object)
30
+ build({ NotEqual: [self, object] })
31
+ end
32
+ alias neq not_equal
33
+
34
+ def greater_than(object)
35
+ build({ GreaterThan: [self, object] })
36
+ end
37
+ alias gt greater_than
38
+
39
+ def greater_than_or_equal_to(object)
40
+ build({ GreaterThanOrEqualTo: [self, object] })
41
+ end
42
+ alias gte greater_than_or_equal_to
43
+ alias greater_than_or_equal greater_than_or_equal_to
44
+
45
+ def less_than(object)
46
+ build({ LessThan: [self, object] })
47
+ end
48
+ alias lt less_than
49
+
50
+ def less_than_or_equal_to(object)
51
+ build({ LessThanOrEqualTo: [self, object] })
52
+ end
53
+ alias lte less_than_or_equal_to
54
+ alias less_than_or_equal less_than_or_equal_to
55
+
56
+ def percentage_of_actors(object)
57
+ build({ PercentageOfActors: [self, build(object)] })
58
+ end
59
+
60
+ def any?
61
+ is_a?(Expression) && function == Expressions::Any
62
+ end
63
+
64
+ def all?
65
+ is_a?(Expression) && function == Expressions::All
66
+ end
67
+
68
+ def group?
69
+ any? || all?
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ module Flipper
2
+ class Expression
3
+ # Public: A constant value like a "string", Number (1, 3.5), Boolean (true, false).
4
+ #
5
+ # Implements the same interface as Expression
6
+ class Constant
7
+ include Expression::Builder
8
+
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def evaluate(_ = nil)
16
+ value
17
+ end
18
+
19
+ def eql?(other)
20
+ other.is_a?(self.class) && other.value == value
21
+ end
22
+ alias_method :==, :eql?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,71 @@
1
+ require "flipper/expression/builder"
2
+ require "flipper/expression/constant"
3
+
4
+ module Flipper
5
+ class Expression
6
+ include Builder
7
+
8
+ def self.build(object)
9
+ return object if object.is_a?(self) || object.is_a?(Constant)
10
+
11
+ case object
12
+ when Hash
13
+ name = object.keys.first
14
+ args = object.values.first
15
+ unless name
16
+ raise ArgumentError, "#{object.inspect} cannot be converted into an expression"
17
+ end
18
+
19
+ new(name, Array(args).map { |o| build(o) })
20
+ when String, Numeric, FalseClass, TrueClass
21
+ Expression::Constant.new(object)
22
+ when Symbol
23
+ Expression::Constant.new(object.to_s)
24
+ else
25
+ raise ArgumentError, "#{object.inspect} cannot be converted into an expression"
26
+ end
27
+ end
28
+
29
+ # Use #build
30
+ private_class_method :new
31
+
32
+ attr_reader :name, :function, :args
33
+
34
+ def initialize(name, args = [])
35
+ @name = name.to_s
36
+ @function = Expressions.const_get(name)
37
+ @args = args
38
+ end
39
+
40
+ def evaluate(context = {})
41
+ if call_with_context?
42
+ function.call(*args.map {|arg| arg.evaluate(context) }, context: context)
43
+ else
44
+ function.call(*args.map {|arg| arg.evaluate(context) })
45
+ end
46
+ end
47
+
48
+ def eql?(other)
49
+ other.is_a?(self.class) && @function == other.function && @args == other.args
50
+ end
51
+ alias_method :==, :eql?
52
+
53
+ def value
54
+ {
55
+ name => args.map(&:value)
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def call_with_context?
62
+ function.method(:call).parameters.any? do |type, name|
63
+ name == :context && [:key, :keyreq].include?(type)
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ Dir[File.join(File.dirname(__FILE__), 'expressions', '*.rb')].sort.each do |file|
70
+ require "flipper/expressions/#{File.basename(file, '.rb')}"
71
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class All
4
+ def self.call(*args)
5
+ args.all?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Any
4
+ def self.call(*args)
5
+ args.any?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Boolean
4
+ def self.call(value)
5
+ Flipper::Typecast.to_boolean(value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Comparable
4
+ def self.operator
5
+ raise NotImplementedError
6
+ end
7
+
8
+ def self.call(left, right)
9
+ left.respond_to?(operator) && right.respond_to?(operator) && left.public_send(operator, right)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Duration
4
+ SECONDS_PER = {
5
+ "second" => 1,
6
+ "minute" => 60,
7
+ "hour" => 3600,
8
+ "day" => 86400,
9
+ "week" => 604_800,
10
+ "month" => 2_629_746, # 1/12 of a gregorian year
11
+ "year" => 31_556_952 # length of a gregorian year (365.2425 days)
12
+ }.freeze
13
+
14
+ def self.call(scalar, unit = 'second')
15
+ unit = unit.to_s.downcase.chomp("s")
16
+
17
+ unless scalar.is_a?(Numeric)
18
+ raise ArgumentError.new("Duration value must be a number but was #{scalar.inspect}")
19
+ end
20
+ unless SECONDS_PER[unit]
21
+ raise ArgumentError.new("Duration unit #{unit.inspect} must be one of: #{SECONDS_PER.keys.join(', ')}")
22
+ end
23
+
24
+ scalar * SECONDS_PER[unit]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Equal < Comparable
4
+ def self.operator
5
+ :==
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class GreaterThan < Comparable
4
+ def self.operator
5
+ :>
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class GreaterThanOrEqualTo < Comparable
4
+ def self.operator
5
+ :>=
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class LessThan < Comparable
4
+ def self.operator
5
+ :<
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class LessThanOrEqualTo < Comparable
4
+ def self.operator
5
+ :<=
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class NotEqual < Comparable
4
+ def self.operator
5
+ :!=
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Now
4
+ def self.call
5
+ ::Time.now.utc
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Number
4
+ def self.call(value)
5
+ Flipper::Typecast.to_number(value)
6
+ end
7
+ end
8
+ end
9
+ end