flipper 0.24.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +45 -14
  5. data/.github/workflows/examples.yml +39 -16
  6. data/Changelog.md +2 -443
  7. data/Gemfile +19 -11
  8. data/README.md +31 -27
  9. data/Rakefile +6 -4
  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/instrumentation.rb +1 -0
  31. data/examples/instrumentation_last_accessed_at.rb +1 -0
  32. data/examples/mirroring.rb +59 -0
  33. data/examples/strict.rb +18 -0
  34. data/exe/flipper +5 -0
  35. data/flipper-cloud.gemspec +19 -0
  36. data/flipper.gemspec +10 -6
  37. data/lib/flipper/actor.rb +6 -3
  38. data/lib/flipper/adapter.rb +33 -7
  39. data/lib/flipper/adapter_builder.rb +44 -0
  40. data/lib/flipper/adapters/actor_limit.rb +28 -0
  41. data/lib/flipper/adapters/cache_base.rb +143 -0
  42. data/lib/flipper/adapters/dual_write.rb +1 -3
  43. data/lib/flipper/adapters/failover.rb +0 -4
  44. data/lib/flipper/adapters/failsafe.rb +72 -0
  45. data/lib/flipper/adapters/http/client.rb +44 -20
  46. data/lib/flipper/adapters/http/error.rb +1 -1
  47. data/lib/flipper/adapters/http.rb +31 -16
  48. data/lib/flipper/adapters/instrumented.rb +25 -6
  49. data/lib/flipper/adapters/memoizable.rb +33 -21
  50. data/lib/flipper/adapters/memory.rb +81 -46
  51. data/lib/flipper/adapters/operation_logger.rb +17 -78
  52. data/lib/flipper/adapters/poll/poller.rb +2 -0
  53. data/lib/flipper/adapters/poll.rb +37 -0
  54. data/lib/flipper/adapters/pstore.rb +17 -11
  55. data/lib/flipper/adapters/read_only.rb +8 -41
  56. data/lib/flipper/adapters/strict.rb +45 -0
  57. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  58. data/lib/flipper/adapters/sync.rb +0 -4
  59. data/lib/flipper/adapters/wrapper.rb +54 -0
  60. data/lib/flipper/cli.rb +263 -0
  61. data/lib/flipper/cloud/configuration.rb +263 -0
  62. data/lib/flipper/cloud/dsl.rb +27 -0
  63. data/lib/flipper/cloud/message_verifier.rb +95 -0
  64. data/lib/flipper/cloud/middleware.rb +63 -0
  65. data/lib/flipper/cloud/routes.rb +14 -0
  66. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  67. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  68. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  69. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  70. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  71. data/lib/flipper/cloud/telemetry.rb +191 -0
  72. data/lib/flipper/cloud.rb +53 -0
  73. data/lib/flipper/configuration.rb +25 -4
  74. data/lib/flipper/dsl.rb +46 -45
  75. data/lib/flipper/engine.rb +102 -0
  76. data/lib/flipper/errors.rb +3 -20
  77. data/lib/flipper/export.rb +26 -0
  78. data/lib/flipper/exporter.rb +17 -0
  79. data/lib/flipper/exporters/json/export.rb +32 -0
  80. data/lib/flipper/exporters/json/v1.rb +33 -0
  81. data/lib/flipper/expression/builder.rb +73 -0
  82. data/lib/flipper/expression/constant.rb +25 -0
  83. data/lib/flipper/expression.rb +71 -0
  84. data/lib/flipper/expressions/all.rb +11 -0
  85. data/lib/flipper/expressions/any.rb +9 -0
  86. data/lib/flipper/expressions/boolean.rb +9 -0
  87. data/lib/flipper/expressions/comparable.rb +13 -0
  88. data/lib/flipper/expressions/duration.rb +28 -0
  89. data/lib/flipper/expressions/equal.rb +9 -0
  90. data/lib/flipper/expressions/greater_than.rb +9 -0
  91. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/less_than.rb +9 -0
  93. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  94. data/lib/flipper/expressions/not_equal.rb +9 -0
  95. data/lib/flipper/expressions/now.rb +9 -0
  96. data/lib/flipper/expressions/number.rb +9 -0
  97. data/lib/flipper/expressions/percentage.rb +9 -0
  98. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  99. data/lib/flipper/expressions/property.rb +9 -0
  100. data/lib/flipper/expressions/random.rb +9 -0
  101. data/lib/flipper/expressions/string.rb +9 -0
  102. data/lib/flipper/expressions/time.rb +9 -0
  103. data/lib/flipper/feature.rb +87 -26
  104. data/lib/flipper/feature_check_context.rb +10 -6
  105. data/lib/flipper/gate.rb +13 -11
  106. data/lib/flipper/gate_values.rb +5 -18
  107. data/lib/flipper/gates/actor.rb +10 -17
  108. data/lib/flipper/gates/boolean.rb +1 -1
  109. data/lib/flipper/gates/expression.rb +75 -0
  110. data/lib/flipper/gates/group.rb +5 -7
  111. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  112. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  113. data/lib/flipper/identifier.rb +2 -2
  114. data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
  115. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  116. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  117. data/lib/flipper/metadata.rb +7 -1
  118. data/lib/flipper/middleware/memoizer.rb +28 -22
  119. data/lib/flipper/model/active_record.rb +23 -0
  120. data/lib/flipper/poller.rb +118 -0
  121. data/lib/flipper/serializers/gzip.rb +22 -0
  122. data/lib/flipper/serializers/json.rb +17 -0
  123. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  124. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  125. data/lib/flipper/test_help.rb +43 -0
  126. data/lib/flipper/typecast.rb +59 -18
  127. data/lib/flipper/types/actor.rb +13 -13
  128. data/lib/flipper/types/group.rb +4 -4
  129. data/lib/flipper/types/percentage.rb +1 -1
  130. data/lib/flipper/version.rb +11 -1
  131. data/lib/flipper.rb +50 -11
  132. data/lib/generators/flipper/setup_generator.rb +63 -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/failsafe_spec.rb +58 -0
  145. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  146. data/spec/flipper/adapters/http_spec.rb +137 -55
  147. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  148. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  149. data/spec/flipper/adapters/memory_spec.rb +14 -3
  150. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  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 +164 -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 +181 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -73
  167. data/spec/flipper/engine_spec.rb +373 -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 +436 -33
  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 +23 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  203. data/spec/flipper/middleware/memoizer_spec.rb +74 -24
  204. data/spec/flipper/model/active_record_spec.rb +61 -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 +93 -29
  213. data/spec/spec_helper.rb +8 -14
  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 +23 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +19 -2
  223. data/test_rails/system/test_help_test.rb +51 -0
  224. metadata +223 -19
  225. data/lib/flipper/railtie.rb +0 -47
  226. data/spec/flipper/railtie_spec.rb +0 -73
@@ -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,11 @@
1
+ require "flipper/expression"
2
+
3
+ module Flipper
4
+ module Expressions
5
+ class All
6
+ def self.call(*args)
7
+ args.all?
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -96,18 +96,19 @@ module Flipper
96
96
  instrument(:clear) { adapter.clear(self) }
97
97
  end
98
98
 
99
- # Public: Check if a feature is enabled for a thing.
99
+ # Public: Check if a feature is enabled for zero or more actors.
100
100
  #
101
101
  # Returns true if enabled, false if not.
102
- def enabled?(thing = nil)
103
- instrument(:enabled?) do |payload|
104
- values = gate_values
105
- thing = gate(:actor).wrap(thing) unless thing.nil?
106
- payload[:thing] = thing
102
+ def enabled?(*actors)
103
+ actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
104
+ actors = nil if actors.empty?
105
+
106
+ # thing is left for backwards compatibility
107
+ instrument(:enabled?, thing: actors&.first, actors: actors) do |payload|
107
108
  context = FeatureCheckContext.new(
108
109
  feature_name: @name,
109
- values: values,
110
- thing: thing
110
+ values: gate_values,
111
+ actors: actors
111
112
  )
112
113
 
113
114
  if open_gate = gates.detect { |gate| gate.open?(context) }
@@ -119,6 +120,28 @@ module Flipper
119
120
  end
120
121
  end
121
122
 
123
+ # Public: Enables an expression_to_add for a feature.
124
+ #
125
+ # expression - an Expression or Hash that can be converted to an expression.
126
+ #
127
+ # Returns result of enable.
128
+ def enable_expression(expression)
129
+ enable Expression.build(expression)
130
+ end
131
+
132
+ # Public: Add an expression for a feature.
133
+ #
134
+ # expression_to_add - an expression or Hash that can be converted to an expression.
135
+ #
136
+ # Returns result of enable.
137
+ def add_expression(expression_to_add)
138
+ if (current_expression = expression)
139
+ enable current_expression.add(expression_to_add)
140
+ else
141
+ enable expression_to_add
142
+ end
143
+ end
144
+
122
145
  # Public: Enables a feature for an actor.
123
146
  #
124
147
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -159,6 +182,27 @@ module Flipper
159
182
  enable Types::PercentageOfActors.wrap(percentage)
160
183
  end
161
184
 
185
+ # Public: Disables an expression for a feature.
186
+ #
187
+ # expression - an expression or Hash that can be converted to an expression.
188
+ #
189
+ # Returns result of disable.
190
+ def disable_expression
191
+ disable Flipper.all # just need an expression to clear
192
+ end
193
+
194
+ # Public: Remove an expression from a feature. Does nothing if no expression is
195
+ # currently enabled.
196
+ #
197
+ # expression - an Expression or Hash that can be converted to an expression.
198
+ #
199
+ # Returns result of enable or nil (if no expression enabled).
200
+ def remove_expression(expression_to_remove)
201
+ if (current_expression = expression)
202
+ enable current_expression.remove(expression_to_remove)
203
+ end
204
+ end
205
+
162
206
  # Public: Disables a feature for an actor.
163
207
  #
164
208
  # actor - a Flipper::Types::Actor instance or an object that responds
@@ -207,7 +251,7 @@ module Flipper
207
251
 
208
252
  if values.boolean || values.percentage_of_time == 100
209
253
  :on
210
- elsif non_boolean_gates.detect { |gate| gate.enabled?(values[gate.key]) }
254
+ elsif non_boolean_gates.detect { |gate| gate.enabled?(values.send(gate.key)) }
211
255
  :conditional
212
256
  else
213
257
  :off
@@ -232,7 +276,8 @@ module Flipper
232
276
 
233
277
  # Public: Returns the raw gate values stored by the adapter.
234
278
  def gate_values
235
- GateValues.new(adapter.get(self))
279
+ adapter_values = adapter.get(self)
280
+ GateValues.new(adapter_values)
236
281
  end
237
282
 
238
283
  # Public: Get groups enabled for this feature.
@@ -250,6 +295,10 @@ module Flipper
250
295
  Flipper.groups - enabled_groups
251
296
  end
252
297
 
298
+ def expression
299
+ Flipper::Expression.build(expression_value) if expression_value
300
+ end
301
+
253
302
  # Public: Get the adapter value for the groups gate.
254
303
  #
255
304
  # Returns Set of String group names.
@@ -257,6 +306,13 @@ module Flipper
257
306
  gate_values.groups
258
307
  end
259
308
 
309
+ # Public: Get the adapter value for the expression gate.
310
+ #
311
+ # Returns expression.
312
+ def expression_value
313
+ gate_values.expression
314
+ end
315
+
260
316
  # Public: Get the adapter value for the actors gate.
261
317
  #
262
318
  # Returns Set of String flipper_id's.
@@ -290,7 +346,7 @@ module Flipper
290
346
  # Returns an Array of Flipper::Gate instances.
291
347
  def enabled_gates
292
348
  values = gate_values
293
- gates.select { |gate| gate.enabled?(values[gate.key]) }
349
+ gates.select { |gate| gate.enabled?(values.send(gate.key)) }
294
350
  end
295
351
 
296
352
  # Public: Get the names of the enabled gates.
@@ -339,37 +395,42 @@ module Flipper
339
395
  #
340
396
  # Returns an array of gates
341
397
  def gates
342
- @gates ||= [
343
- Gates::Boolean.new,
344
- Gates::Actor.new,
345
- Gates::PercentageOfActors.new,
346
- Gates::PercentageOfTime.new,
347
- Gates::Group.new,
348
- ]
398
+ @gates ||= gates_hash.values.freeze
399
+ end
400
+
401
+ def gates_hash
402
+ @gates_hash ||= {
403
+ boolean: Gates::Boolean.new,
404
+ expression: Gates::Expression.new,
405
+ actor: Gates::Actor.new,
406
+ percentage_of_actors: Gates::PercentageOfActors.new,
407
+ percentage_of_time: Gates::PercentageOfTime.new,
408
+ group: Gates::Group.new,
409
+ }.freeze
349
410
  end
350
411
 
351
412
  # Public: Find a gate by name.
352
413
  #
353
414
  # Returns a Flipper::Gate if found, nil if not.
354
415
  def gate(name)
355
- gates.detect { |gate| gate.name == name.to_sym }
416
+ gates_hash[name.to_sym]
356
417
  end
357
418
 
358
- # Public: Find the gate that protects a thing.
419
+ # Public: Find the gate that protects an actor.
359
420
  #
360
- # thing - The object for which you would like to find a gate
421
+ # actor - The object for which you would like to find a gate
361
422
  #
362
423
  # Returns a Flipper::Gate.
363
- # Raises Flipper::GateNotFound if no gate found for thing
364
- def gate_for(thing)
365
- gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
424
+ # Raises Flipper::GateNotFound if no gate found for actor
425
+ def gate_for(actor)
426
+ gates.detect { |gate| gate.protects?(actor) } || raise(GateNotFound, actor)
366
427
  end
367
428
 
368
429
  private
369
430
 
370
431
  # Private: Instrument a feature operation.
371
- def instrument(operation)
372
- @instrumenter.instrument(InstrumentationName) do |payload|
432
+ def instrument(operation, initial_payload = {})
433
+ @instrumenter.instrument(InstrumentationName, initial_payload) do |payload|
373
434
  payload[:feature_name] = name
374
435
  payload[:operation] = operation
375
436
  payload[:result] = yield(payload) if block_given?