flipper 1.0.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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -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
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Percentage
4
+ def self.call(value)
5
+ value.to_f.clamp(0, 100)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Flipper
2
+ module Expressions
3
+ class PercentageOfActors
4
+ SCALING_FACTOR = 1_000
5
+
6
+ def self.call(text, percentage, context: {})
7
+ prefix = context[:feature_name] || ""
8
+ Zlib.crc32("#{prefix}#{text}") % (100 * SCALING_FACTOR) < percentage * SCALING_FACTOR
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Property
4
+ def self.call(key, context:)
5
+ context.dig(:properties, key.to_s)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Random
4
+ def self.call(max = 0)
5
+ rand max
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class String
4
+ def self.call(value)
5
+ value.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Time
4
+ def self.call(value)
5
+ ::Time.parse(value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -100,7 +100,14 @@ module Flipper
100
100
  #
101
101
  # Returns true if enabled, false if not.
102
102
  def enabled?(*actors)
103
- actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
103
+ actors = Array(actors).
104
+ # Avoids to_ary warning that happens when passing DelegateClass of an
105
+ # ActiveRecord object and using flatten here. This is tested in
106
+ # spec/flipper/model/active_record_spec.rb.
107
+ flat_map { |actor| actor.is_a?(Array) ? actor : [actor] }.
108
+ # Allows null object pattern. See PR for more. https://github.com/flippercloud/flipper/pull/887
109
+ reject(&:nil?).
110
+ map { |actor| Types::Actor.wrap(actor) }
104
111
  actors = nil if actors.empty?
105
112
 
106
113
  # thing is left for backwards compatibility
@@ -120,6 +127,28 @@ module Flipper
120
127
  end
121
128
  end
122
129
 
130
+ # Public: Enables an expression_to_add for a feature.
131
+ #
132
+ # expression - an Expression or Hash that can be converted to an expression.
133
+ #
134
+ # Returns result of enable.
135
+ def enable_expression(expression)
136
+ enable Expression.build(expression)
137
+ end
138
+
139
+ # Public: Add an expression for a feature.
140
+ #
141
+ # expression_to_add - an expression or Hash that can be converted to an expression.
142
+ #
143
+ # Returns result of enable.
144
+ def add_expression(expression_to_add)
145
+ if (current_expression = expression)
146
+ enable current_expression.add(expression_to_add)
147
+ else
148
+ enable expression_to_add
149
+ end
150
+ end
151
+
123
152
  # Public: Enables a feature for an actor.
124
153
  #
125
154
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -160,6 +189,27 @@ module Flipper
160
189
  enable Types::PercentageOfActors.wrap(percentage)
161
190
  end
162
191
 
192
+ # Public: Disables an expression for a feature.
193
+ #
194
+ # expression - an expression or Hash that can be converted to an expression.
195
+ #
196
+ # Returns result of disable.
197
+ def disable_expression
198
+ disable Flipper.all # just need an expression to clear
199
+ end
200
+
201
+ # Public: Remove an expression from a feature. Does nothing if no expression is
202
+ # currently enabled.
203
+ #
204
+ # expression - an Expression or Hash that can be converted to an expression.
205
+ #
206
+ # Returns result of enable or nil (if no expression enabled).
207
+ def remove_expression(expression_to_remove)
208
+ if (current_expression = expression)
209
+ enable current_expression.remove(expression_to_remove)
210
+ end
211
+ end
212
+
163
213
  # Public: Disables a feature for an actor.
164
214
  #
165
215
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -252,6 +302,10 @@ module Flipper
252
302
  Flipper.groups - enabled_groups
253
303
  end
254
304
 
305
+ def expression
306
+ Flipper::Expression.build(expression_value) if expression_value
307
+ end
308
+
255
309
  # Public: Get the adapter value for the groups gate.
256
310
  #
257
311
  # Returns Set of String group names.
@@ -259,6 +313,13 @@ module Flipper
259
313
  gate_values.groups
260
314
  end
261
315
 
316
+ # Public: Get the adapter value for the expression gate.
317
+ #
318
+ # Returns expression.
319
+ def expression_value
320
+ gate_values.expression
321
+ end
322
+
262
323
  # Public: Get the adapter value for the actors gate.
263
324
  #
264
325
  # Returns Set of String flipper_id's.
@@ -347,6 +408,7 @@ module Flipper
347
408
  def gates_hash
348
409
  @gates_hash ||= {
349
410
  boolean: Gates::Boolean.new,
411
+ expression: Gates::Expression.new,
350
412
  actor: Gates::Actor.new,
351
413
  percentage_of_actors: Gates::PercentageOfActors.new,
352
414
  percentage_of_time: Gates::PercentageOfTime.new,
data/lib/flipper/gate.rb CHANGED
@@ -26,7 +26,7 @@ module Flipper
26
26
  # in subclass.
27
27
  #
28
28
  # Returns true if gate open for any actor, false if not.
29
- def open?(actors, value, options = {})
29
+ def open?(context)
30
30
  false
31
31
  end
32
32
 
@@ -60,3 +60,4 @@ require 'flipper/gates/boolean'
60
60
  require 'flipper/gates/group'
61
61
  require 'flipper/gates/percentage_of_actors'
62
62
  require 'flipper/gates/percentage_of_time'
63
+ require 'flipper/gates/expression'
@@ -6,6 +6,7 @@ module Flipper
6
6
  attr_reader :boolean
7
7
  attr_reader :actors
8
8
  attr_reader :groups
9
+ attr_reader :expression
9
10
  attr_reader :percentage_of_actors
10
11
  attr_reader :percentage_of_time
11
12
 
@@ -13,8 +14,9 @@ module Flipper
13
14
  @boolean = Typecast.to_boolean(adapter_values[:boolean])
14
15
  @actors = Typecast.to_set(adapter_values[:actors])
15
16
  @groups = Typecast.to_set(adapter_values[:groups])
16
- @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors])
17
- @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
17
+ @expression = adapter_values[:expression]
18
+ @percentage_of_actors = Typecast.to_number(adapter_values[:percentage_of_actors])
19
+ @percentage_of_time = Typecast.to_number(adapter_values[:percentage_of_time])
18
20
  end
19
21
 
20
22
  def eql?(other)
@@ -22,6 +24,7 @@ module Flipper
22
24
  boolean == other.boolean &&
23
25
  actors == other.actors &&
24
26
  groups == other.groups &&
27
+ expression == other.expression &&
25
28
  percentage_of_actors == other.percentage_of_actors &&
26
29
  percentage_of_time == other.percentage_of_time
27
30
  end
@@ -0,0 +1,75 @@
1
+ require "flipper/expression"
2
+
3
+ module Flipper
4
+ module Gates
5
+ class Expression < Gate
6
+ # Internal: The name of the gate. Used for instrumentation, etc.
7
+ def name
8
+ :expression
9
+ end
10
+
11
+ # Internal: Name converted to value safe for adapter.
12
+ def key
13
+ :expression
14
+ end
15
+
16
+ def data_type
17
+ :json
18
+ end
19
+
20
+ def enabled?(value)
21
+ !value.nil? && !value.empty?
22
+ end
23
+
24
+ # Internal: Checks if the gate is open for a thing.
25
+ #
26
+ # Returns true if gate open for thing, false if not.
27
+ def open?(context)
28
+ data = context.values.expression
29
+ return false if data.nil? || data.empty?
30
+ expression = Flipper::Expression.build(data)
31
+
32
+ if context.actors.nil? || context.actors.empty?
33
+ !!expression.evaluate(feature_name: context.feature_name, properties: DEFAULT_PROPERTIES)
34
+ else
35
+ context.actors.any? do |actor|
36
+ !!expression.evaluate(feature_name: context.feature_name, properties: properties(actor))
37
+ end
38
+ end
39
+ end
40
+
41
+ def protects?(thing)
42
+ thing.is_a?(Flipper::Expression) || thing.is_a?(Hash)
43
+ end
44
+
45
+ def wrap(thing)
46
+ Flipper::Expression.build(thing)
47
+ end
48
+
49
+ private
50
+
51
+ # Internal
52
+ DEFAULT_PROPERTIES = {}.freeze
53
+
54
+ def properties(actor)
55
+ return DEFAULT_PROPERTIES if actor.nil?
56
+
57
+ properties = {}
58
+
59
+ if actor.respond_to?(:flipper_properties)
60
+ properties.update(actor.flipper_properties)
61
+ else
62
+ warn "#{actor.inspect} does not respond to `flipper_properties` but should."
63
+ end
64
+
65
+ properties.transform_keys!(&:to_s)
66
+
67
+ if actor.respond_to?(:flipper_id)
68
+ properties["flipper_id".freeze] = actor.flipper_id
69
+ end
70
+
71
+ properties
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,4 +1,5 @@
1
1
  require 'securerandom'
2
+ require 'active_support/gem_version'
2
3
  require 'active_support/notifications'
3
4
  require 'active_support/log_subscriber'
4
5
 
@@ -52,11 +53,10 @@ module Flipper
52
53
 
53
54
  feature_name = event.payload[:feature_name]
54
55
  adapter_name = event.payload[:adapter_name]
55
- gate_name = event.payload[:gate_name]
56
56
  operation = event.payload[:operation]
57
57
  result = event.payload[:result]
58
58
 
59
- description = 'Flipper '
59
+ description = String.new('Flipper ')
60
60
  description << "feature(#{feature_name}) " unless feature_name.nil?
61
61
  description << "adapter(#{adapter_name}) "
62
62
  description << "#{operation} "
@@ -71,11 +71,19 @@ module Flipper
71
71
  self.class.logger
72
72
  end
73
73
 
74
+ def self.attach
75
+ attach_to InstrumentationNamespace
76
+ end
77
+
78
+ def self.detach
79
+ # Rails 5.2 doesn't support this, that's fine
80
+ detach_from InstrumentationNamespace if respond_to?(:detach_from)
81
+ end
82
+
74
83
  private
75
84
 
76
85
  # Rails 7.1 changed the signature of this function.
77
- # Checking if > 7.0.99 rather than >= 7.1 so that 7.1 pre-release versions are included.
78
- COLOR_OPTIONS = if Rails.gem_version > Gem::Version.new('7.0.99')
86
+ COLOR_OPTIONS = if Gem::Requirement.new(">=7.1").satisfied_by?(ActiveSupport.gem_version)
79
87
  { bold: true }.freeze
80
88
  else
81
89
  true
@@ -88,5 +96,5 @@ module Flipper
88
96
  end
89
97
  end
90
98
 
91
- Instrumentation::LogSubscriber.attach_to InstrumentationNamespace
99
+ Instrumentation::LogSubscriber.attach
92
100
  end
@@ -2,5 +2,7 @@ require 'securerandom'
2
2
  require 'active_support/notifications'
3
3
  require 'flipper/instrumentation/statsd_subscriber'
4
4
 
5
- ActiveSupport::Notifications.subscribe /\.flipper$/,
6
- Flipper::Instrumentation::StatsdSubscriber
5
+ ActiveSupport::Notifications.subscribe(
6
+ /\.flipper$/,
7
+ Flipper::Instrumentation::StatsdSubscriber
8
+ )
@@ -12,13 +12,11 @@ module Flipper
12
12
  end
13
13
 
14
14
  def update_timer(metric)
15
- if self.class.client
16
- self.class.client.timing metric, (@duration * 1_000).round
17
- end
15
+ self.class.client&.timing metric, (@duration * 1_000).round
18
16
  end
19
17
 
20
18
  def update_counter(metric)
21
- self.class.client.increment metric if self.class.client
19
+ self.class.client&.increment metric
22
20
  end
23
21
  end
24
22
  end
@@ -42,7 +42,6 @@ module Flipper
42
42
  # Private
43
43
  def update_feature_operation_metrics
44
44
  feature_name = @payload[:feature_name]
45
- gate_name = @payload[:gate_name]
46
45
  operation = strip_trailing_question_mark(@payload[:operation])
47
46
  result = @payload[:result]
48
47
 
@@ -64,9 +63,6 @@ module Flipper
64
63
  def update_adapter_operation_metrics
65
64
  adapter_name = @payload[:adapter_name]
66
65
  operation = @payload[:operation]
67
- result = @payload[:result]
68
- value = @payload[:value]
69
- key = @payload[:key]
70
66
 
71
67
  update_timer "flipper.adapter.#{adapter_name}.#{operation}"
72
68
  end
@@ -1,9 +1,12 @@
1
+ require_relative './version'
2
+
1
3
  module Flipper
2
4
  METADATA = {
3
5
  "documentation_uri" => "https://www.flippercloud.io/docs",
4
6
  "homepage_uri" => "https://www.flippercloud.io",
5
7
  "source_code_uri" => "https://github.com/flippercloud/flipper",
6
8
  "bug_tracker_uri" => "https://github.com/flippercloud/flipper/issues",
7
- "changelog_uri" => "https://github.com/flippercloud/flipper/blob/main/Changelog.md",
9
+ "changelog_uri" => "https://github.com/flippercloud/flipper/releases/tag/v#{Flipper::VERSION}",
10
+ "funding_uri" => "https://github.com/sponsors/flippercloud",
8
11
  }.freeze
9
12
  end