flipper 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +7 -3
  4. data/.github/workflows/examples.yml +27 -5
  5. data/Changelog.md +326 -272
  6. data/Gemfile +4 -4
  7. data/README.md +13 -11
  8. data/benchmark/typecast_ips.rb +8 -0
  9. data/docs/images/flipper_cloud.png +0 -0
  10. data/examples/cloud/backoff_policy.rb +13 -0
  11. data/examples/cloud/cloud_setup.rb +16 -0
  12. data/examples/cloud/forked.rb +7 -2
  13. data/examples/cloud/threaded.rb +15 -18
  14. data/examples/expressions.rb +213 -0
  15. data/examples/strict.rb +18 -0
  16. data/flipper.gemspec +1 -2
  17. data/lib/flipper/actor.rb +6 -3
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapter_builder.rb +44 -0
  20. data/lib/flipper/adapters/dual_write.rb +1 -3
  21. data/lib/flipper/adapters/failover.rb +0 -4
  22. data/lib/flipper/adapters/failsafe.rb +0 -4
  23. data/lib/flipper/adapters/http/client.rb +26 -7
  24. data/lib/flipper/adapters/http/error.rb +1 -1
  25. data/lib/flipper/adapters/http.rb +18 -13
  26. data/lib/flipper/adapters/instrumented.rb +0 -4
  27. data/lib/flipper/adapters/memoizable.rb +14 -19
  28. data/lib/flipper/adapters/memory.rb +4 -6
  29. data/lib/flipper/adapters/operation_logger.rb +0 -4
  30. data/lib/flipper/adapters/poll.rb +1 -3
  31. data/lib/flipper/adapters/pstore.rb +17 -11
  32. data/lib/flipper/adapters/read_only.rb +4 -4
  33. data/lib/flipper/adapters/strict.rb +47 -0
  34. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  35. data/lib/flipper/adapters/sync.rb +0 -4
  36. data/lib/flipper/cloud/configuration.rb +121 -52
  37. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  38. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  39. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  40. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  41. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  42. data/lib/flipper/cloud/telemetry.rb +183 -0
  43. data/lib/flipper/configuration.rb +25 -4
  44. data/lib/flipper/dsl.rb +51 -0
  45. data/lib/flipper/engine.rb +27 -3
  46. data/lib/flipper/exporters/json/export.rb +1 -1
  47. data/lib/flipper/exporters/json/v1.rb +1 -1
  48. data/lib/flipper/expression/builder.rb +73 -0
  49. data/lib/flipper/expression/constant.rb +25 -0
  50. data/lib/flipper/expression.rb +71 -0
  51. data/lib/flipper/expressions/all.rb +11 -0
  52. data/lib/flipper/expressions/any.rb +9 -0
  53. data/lib/flipper/expressions/boolean.rb +9 -0
  54. data/lib/flipper/expressions/comparable.rb +13 -0
  55. data/lib/flipper/expressions/duration.rb +28 -0
  56. data/lib/flipper/expressions/equal.rb +9 -0
  57. data/lib/flipper/expressions/greater_than.rb +9 -0
  58. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  59. data/lib/flipper/expressions/less_than.rb +9 -0
  60. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  61. data/lib/flipper/expressions/not_equal.rb +9 -0
  62. data/lib/flipper/expressions/now.rb +9 -0
  63. data/lib/flipper/expressions/number.rb +9 -0
  64. data/lib/flipper/expressions/percentage.rb +9 -0
  65. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  66. data/lib/flipper/expressions/property.rb +9 -0
  67. data/lib/flipper/expressions/random.rb +9 -0
  68. data/lib/flipper/expressions/string.rb +9 -0
  69. data/lib/flipper/expressions/time.rb +9 -0
  70. data/lib/flipper/feature.rb +55 -0
  71. data/lib/flipper/gate.rb +1 -0
  72. data/lib/flipper/gate_values.rb +5 -2
  73. data/lib/flipper/gates/expression.rb +75 -0
  74. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  75. data/lib/flipper/middleware/memoizer.rb +29 -13
  76. data/lib/flipper/model/active_record.rb +23 -0
  77. data/lib/flipper/poller.rb +1 -1
  78. data/lib/flipper/serializers/gzip.rb +24 -0
  79. data/lib/flipper/serializers/json.rb +19 -0
  80. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  81. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  82. data/lib/flipper/typecast.rb +34 -6
  83. data/lib/flipper/types/percentage.rb +1 -1
  84. data/lib/flipper/version.rb +1 -1
  85. data/lib/flipper.rb +38 -1
  86. data/spec/flipper/adapter_builder_spec.rb +73 -0
  87. data/spec/flipper/adapter_spec.rb +1 -0
  88. data/spec/flipper/adapters/http_spec.rb +39 -5
  89. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  90. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  91. data/spec/flipper/adapters/strict_spec.rb +62 -0
  92. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  93. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  94. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  95. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  96. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  97. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  98. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  99. data/spec/flipper/cloud_spec.rb +12 -12
  100. data/spec/flipper/configuration_spec.rb +17 -0
  101. data/spec/flipper/dsl_spec.rb +39 -0
  102. data/spec/flipper/engine_spec.rb +108 -7
  103. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  104. data/spec/flipper/expression/builder_spec.rb +248 -0
  105. data/spec/flipper/expression_spec.rb +188 -0
  106. data/spec/flipper/expressions/all_spec.rb +15 -0
  107. data/spec/flipper/expressions/any_spec.rb +15 -0
  108. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  109. data/spec/flipper/expressions/duration_spec.rb +43 -0
  110. data/spec/flipper/expressions/equal_spec.rb +24 -0
  111. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  112. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  114. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  115. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  116. data/spec/flipper/expressions/now_spec.rb +11 -0
  117. data/spec/flipper/expressions/number_spec.rb +21 -0
  118. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  119. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  120. data/spec/flipper/expressions/property_spec.rb +13 -0
  121. data/spec/flipper/expressions/random_spec.rb +9 -0
  122. data/spec/flipper/expressions/string_spec.rb +11 -0
  123. data/spec/flipper/expressions/time_spec.rb +13 -0
  124. data/spec/flipper/feature_spec.rb +360 -1
  125. data/spec/flipper/gate_values_spec.rb +2 -2
  126. data/spec/flipper/gates/expression_spec.rb +108 -0
  127. data/spec/flipper/identifier_spec.rb +4 -5
  128. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  129. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  130. data/spec/flipper/model/active_record_spec.rb +61 -0
  131. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  132. data/spec/flipper/serializers/json_spec.rb +13 -0
  133. data/spec/flipper/typecast_spec.rb +43 -7
  134. data/spec/flipper/types/actor_spec.rb +18 -1
  135. data/spec/flipper_integration_spec.rb +102 -4
  136. data/spec/flipper_spec.rb +89 -1
  137. data/spec/spec_helper.rb +5 -0
  138. data/spec/support/actor_names.yml +1 -0
  139. data/spec/support/fake_backoff_policy.rb +15 -0
  140. data/spec/support/spec_helpers.rb +11 -3
  141. metadata +107 -18
  142. data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -10,7 +10,7 @@ module Flipper
10
10
  class Http
11
11
  include Flipper::Adapter
12
12
 
13
- attr_reader :name, :client
13
+ attr_reader :client
14
14
 
15
15
  def initialize(options = {})
16
16
  @client = Client.new(url: options.fetch(:url),
@@ -22,13 +22,12 @@ module Flipper
22
22
  write_timeout: options[:write_timeout],
23
23
  max_retries: options[:max_retries],
24
24
  debug_output: options[:debug_output])
25
- @name = :http
26
25
  end
27
26
 
28
27
  def get(feature)
29
28
  response = @client.get("/features/#{feature.key}")
30
29
  if response.is_a?(Net::HTTPOK)
31
- parsed_response = JSON.parse(response.body)
30
+ parsed_response = Typecast.from_json(response.body)
32
31
  result_for_feature(feature, parsed_response.fetch('gates'))
33
32
  elsif response.is_a?(Net::HTTPNotFound)
34
33
  default_config
@@ -42,7 +41,7 @@ module Flipper
42
41
  response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
43
42
  raise Error, response unless response.is_a?(Net::HTTPOK)
44
43
 
45
- parsed_response = JSON.parse(response.body)
44
+ parsed_response = Typecast.from_json(response.body)
46
45
  parsed_features = parsed_response.fetch('features')
47
46
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
48
47
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -60,7 +59,7 @@ module Flipper
60
59
  response = @client.get("/features?exclude_gate_names=true")
61
60
  raise Error, response unless response.is_a?(Net::HTTPOK)
62
61
 
63
- parsed_response = JSON.parse(response.body)
62
+ parsed_response = Typecast.from_json(response.body)
64
63
  parsed_features = parsed_response.fetch('features')
65
64
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
66
65
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -68,7 +67,7 @@ module Flipper
68
67
  end
69
68
 
70
69
  result = {}
71
- gates_by_key.keys.each do |key|
70
+ gates_by_key.each_key do |key|
72
71
  feature = Feature.new(key, self)
73
72
  result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
74
73
  end
@@ -79,7 +78,7 @@ module Flipper
79
78
  response = @client.get('/features?exclude_gate_names=true')
80
79
  raise Error, response unless response.is_a?(Net::HTTPOK)
81
80
 
82
- parsed_response = JSON.parse(response.body)
81
+ parsed_response = Typecast.from_json(response.body)
83
82
  parsed_response['features'].map { |feature| feature['key'] }.to_set
84
83
  end
85
84
 
@@ -97,7 +96,7 @@ module Flipper
97
96
  end
98
97
 
99
98
  def enable(feature, gate, thing)
100
- body = request_body_for_gate(gate, thing.value.to_s)
99
+ body = request_body_for_gate(gate, thing.value)
101
100
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
102
101
  response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
103
102
  raise Error, response unless response.is_a?(Net::HTTPOK)
@@ -105,7 +104,7 @@ module Flipper
105
104
  end
106
105
 
107
106
  def disable(feature, gate, thing)
108
- body = request_body_for_gate(gate, thing.value.to_s)
107
+ body = request_body_for_gate(gate, thing.value)
109
108
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
110
109
  response = case gate.key
111
110
  when :percentage_of_actors, :percentage_of_time
@@ -138,11 +137,13 @@ module Flipper
138
137
  when :boolean
139
138
  {}
140
139
  when :groups
141
- { name: value }
140
+ { name: value.to_s }
142
141
  when :actors
143
- { flipper_id: value }
142
+ { flipper_id: value.to_s }
144
143
  when :percentage_of_actors, :percentage_of_time
145
- { percentage: value }
144
+ { percentage: value.to_s }
145
+ when :expression
146
+ value
146
147
  else
147
148
  raise "#{gate.key} is not a valid flipper gate key"
148
149
  end
@@ -166,13 +167,17 @@ module Flipper
166
167
  case gate.data_type
167
168
  when :boolean, :integer
168
169
  value ? value.to_s : value
170
+ when :json
171
+ value
169
172
  when :set
170
173
  value ? value.to_set : Set.new
171
174
  else
172
- unsupported_data_type(gate.data_type)
175
+ unsupported_data_type gate.data_type
173
176
  end
174
177
  end
175
178
 
179
+ private
180
+
176
181
  def unsupported_data_type(data_type)
177
182
  raise "#{data_type} is not supported by this adapter"
178
183
  end
@@ -13,9 +13,6 @@ module Flipper
13
13
  # Private: What is used to instrument all the things.
14
14
  attr_reader :instrumenter
15
15
 
16
- # Public: The name of the adapter.
17
- attr_reader :name
18
-
19
16
  # Internal: Initializes a new adapter instance.
20
17
  #
21
18
  # adapter - Vanilla adapter instance to wrap.
@@ -25,7 +22,6 @@ module Flipper
25
22
  #
26
23
  def initialize(adapter, options = {})
27
24
  @adapter = adapter
28
- @name = :instrumented
29
25
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
30
26
  end
31
27
 
@@ -8,35 +8,25 @@ module Flipper
8
8
  class Memoizable
9
9
  include ::Flipper::Adapter
10
10
 
11
- FeaturesKey = :flipper_features
12
- GetAllKey = :all_memoized
13
-
14
11
  # Internal
15
12
  attr_reader :cache
16
13
 
17
- # Public: The name of the adapter.
18
- attr_reader :name
19
-
20
14
  # Internal: The adapter this adapter is wrapping.
21
15
  attr_reader :adapter
22
16
 
23
- # Private
24
- def self.key_for(key)
25
- "feature/#{key}"
26
- end
27
-
28
17
  # Public
29
18
  def initialize(adapter, cache = nil)
30
19
  @adapter = adapter
31
- @name = :memoizable
32
20
  @cache = cache || {}
33
21
  @memoize = false
22
+ @features_key = :flipper_features
23
+ @get_all_key = :all_memoized
34
24
  end
35
25
 
36
26
  # Public
37
27
  def features
38
28
  if memoizing?
39
- cache.fetch(FeaturesKey) { cache[FeaturesKey] = @adapter.features }
29
+ cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
40
30
  else
41
31
  @adapter.features
42
32
  end
@@ -94,9 +84,9 @@ module Flipper
94
84
  def get_all
95
85
  if memoizing?
96
86
  response = nil
97
- if cache[GetAllKey]
87
+ if cache[@get_all_key]
98
88
  response = {}
99
- cache[FeaturesKey].each do |key|
89
+ cache[@features_key].each do |key|
100
90
  response[key] = cache[key_for(key)]
101
91
  end
102
92
  else
@@ -104,8 +94,8 @@ module Flipper
104
94
  response.each do |key, value|
105
95
  cache[key_for(key)] = value
106
96
  end
107
- cache[FeaturesKey] = response.keys.to_set
108
- cache[GetAllKey] = true
97
+ cache[@features_key] = response.keys.to_set
98
+ cache[@get_all_key] = true
109
99
  end
110
100
 
111
101
  # Ensures that looking up other features that do not exist doesn't
@@ -127,6 +117,11 @@ module Flipper
127
117
  @adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
128
118
  end
129
119
 
120
+ # Public
121
+ def read_only?
122
+ @adapter.read_only?
123
+ end
124
+
130
125
  def import(source)
131
126
  @adapter.import(source).tap { cache.clear if memoizing? }
132
127
  end
@@ -161,7 +156,7 @@ module Flipper
161
156
  private
162
157
 
163
158
  def key_for(key)
164
- self.class.key_for(key)
159
+ "feature/#{key}"
165
160
  end
166
161
 
167
162
  def expire_feature(feature)
@@ -169,7 +164,7 @@ module Flipper
169
164
  end
170
165
 
171
166
  def expire_features_set
172
- cache.delete(FeaturesKey) if memoizing?
167
+ cache.delete(@features_key) if memoizing?
173
168
  end
174
169
  end
175
170
  end
@@ -8,15 +8,9 @@ module Flipper
8
8
  class Memory
9
9
  include ::Flipper::Adapter
10
10
 
11
- FeaturesKey = :features
12
-
13
- # Public: The name of the adapter.
14
- attr_reader :name
15
-
16
11
  # Public
17
12
  def initialize(source = nil, threadsafe: true)
18
13
  @source = Typecast.features_hash(source)
19
- @name = :memory
20
14
  @lock = Mutex.new if threadsafe
21
15
  reset
22
16
  end
@@ -77,6 +71,8 @@ module Flipper
77
71
  @source[feature.key][gate.key] = thing.value.to_s
78
72
  when :set
79
73
  @source[feature.key][gate.key] << thing.value.to_s
74
+ when :json
75
+ @source[feature.key][gate.key] = thing.value
80
76
  else
81
77
  raise "#{gate} is not supported by this adapter yet"
82
78
  end
@@ -97,6 +93,8 @@ module Flipper
97
93
  @source[feature.key][gate.key] = thing.value.to_s
98
94
  when :set
99
95
  @source[feature.key][gate.key].delete thing.value.to_s
96
+ when :json
97
+ @source[feature.key].delete(gate.key)
100
98
  else
101
99
  raise "#{gate} is not supported by this adapter yet"
102
100
  end
@@ -34,13 +34,9 @@ module Flipper
34
34
  # Internal: An array of the operations that have happened.
35
35
  attr_reader :operations
36
36
 
37
- # Internal: The name of the adapter.
38
- attr_reader :name
39
-
40
37
  # Public
41
38
  def initialize(adapter, operations = nil)
42
39
  @adapter = adapter
43
- @name = :operation_logger
44
40
  @operations = operations || []
45
41
  end
46
42
 
@@ -10,13 +10,11 @@ module Flipper
10
10
  # Deprecated
11
11
  Poller = ::Flipper::Poller
12
12
 
13
- # Public: The name of the adapter.
14
- attr_reader :name, :adapter, :poller
13
+ attr_reader :adapter, :poller
15
14
 
16
15
  def_delegators :synced_adapter, :features, :get, :get_multi, :get_all, :add, :remove, :clear, :enable, :disable
17
16
 
18
17
  def initialize(poller, adapter)
19
- @name = :poll
20
18
  @adapter = adapter
21
19
  @poller = poller
22
20
  @last_synced_at = 0
@@ -1,3 +1,4 @@
1
+ require 'json'
1
2
  require 'pstore'
2
3
  require 'set'
3
4
  require 'flipper'
@@ -9,19 +10,14 @@ module Flipper
9
10
  class PStore
10
11
  include ::Flipper::Adapter
11
12
 
12
- FeaturesKey = :flipper_features
13
-
14
- # Public: The name of the adapter.
15
- attr_reader :name
16
-
17
13
  # Public: The path to where the file is stored.
18
14
  attr_reader :path
19
15
 
20
16
  # Public
21
17
  def initialize(path = 'flipper.pstore', thread_safe = true)
22
- @name = :pstore
23
18
  @path = path
24
19
  @store = ::PStore.new(path, thread_safe)
20
+ @features_key = :flipper_features
25
21
  end
26
22
 
27
23
  # Public: The set of known features.
@@ -34,7 +30,7 @@ module Flipper
34
30
  # Public: Adds a feature to the set of known features.
35
31
  def add(feature)
36
32
  @store.transaction do
37
- set_add FeaturesKey, feature.key
33
+ set_add @features_key, feature.key
38
34
  end
39
35
  true
40
36
  end
@@ -43,7 +39,7 @@ module Flipper
43
39
  # all the values for the feature.
44
40
  def remove(feature)
45
41
  @store.transaction do
46
- set_delete FeaturesKey, feature.key
42
+ set_delete @features_key, feature.key
47
43
  clear_gates(feature)
48
44
  end
49
45
  true
@@ -88,6 +84,8 @@ module Flipper
88
84
  write key(feature, gate), thing.value.to_s
89
85
  when :set
90
86
  set_add key(feature, gate), thing.value.to_s
87
+ when :json
88
+ write key(feature, gate), Typecast.to_json(thing.value)
91
89
  else
92
90
  raise "#{gate} is not supported by this adapter yet"
93
91
  end
@@ -109,6 +107,10 @@ module Flipper
109
107
  @store.transaction do
110
108
  set_delete key(feature, gate), thing.value.to_s
111
109
  end
110
+ when :json
111
+ @store.transaction do
112
+ delete key(feature, gate)
113
+ end
112
114
  else
113
115
  raise "#{gate} is not supported by this adapter yet"
114
116
  end
@@ -135,7 +137,7 @@ module Flipper
135
137
  end
136
138
 
137
139
  def read_feature_keys
138
- set_members FeaturesKey
140
+ set_members @features_key
139
141
  end
140
142
 
141
143
  def read_many_features(features)
@@ -150,12 +152,16 @@ module Flipper
150
152
  result = {}
151
153
 
152
154
  feature.gates.each do |gate|
155
+ key = key(feature, gate)
153
156
  result[gate.key] =
154
157
  case gate.data_type
155
158
  when :boolean, :integer
156
- read key(feature, gate)
159
+ read key
157
160
  when :set
158
- set_members key(feature, gate)
161
+ set_members key
162
+ when :json
163
+ value = read(key)
164
+ Typecast.from_json(value)
159
165
  else
160
166
  raise "#{gate} is not supported by this adapter yet"
161
167
  end
@@ -12,19 +12,19 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- # Internal: The name of the adapter.
16
- attr_reader :name
17
-
18
15
  # Public
19
16
  def initialize(adapter)
20
17
  @adapter = adapter
21
- @name = :read_only
22
18
  end
23
19
 
24
20
  def features
25
21
  @adapter.features
26
22
  end
27
23
 
24
+ def read_only?
25
+ true
26
+ end
27
+
28
28
  def get(feature)
29
29
  @adapter.get(feature)
30
30
  end
@@ -0,0 +1,47 @@
1
+ module Flipper
2
+ module Adapters
3
+ # An adapter that ensures a feature exists before checking it.
4
+ class Strict
5
+ extend Forwardable
6
+ include ::Flipper::Adapter
7
+ attr_reader :name, :adapter, :handler
8
+
9
+ class NotFound < ::Flipper::Error
10
+ def initialize(name)
11
+ super "Could not find feature #{name.inspect}. Call `Flipper.add(#{name.inspect})` to create it."
12
+ end
13
+ end
14
+
15
+ HANDLERS = {
16
+ raise: ->(feature) { raise NotFound.new(feature.key) },
17
+ warn: ->(feature) { warn NotFound.new(feature.key).message },
18
+ noop: ->(_) { },
19
+ }
20
+
21
+ def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
22
+
23
+ def initialize(adapter, handler = nil, &block)
24
+ @name = :strict
25
+ @adapter = adapter
26
+ @handler = block || HANDLERS.fetch(handler)
27
+ end
28
+
29
+ def get(feature)
30
+ assert_feature_exists(feature)
31
+ @adapter.get(feature)
32
+ end
33
+
34
+ def get_multi(features)
35
+ features.each { |feature| assert_feature_exists(feature) }
36
+ @adapter.get_multi(features)
37
+ end
38
+
39
+ private
40
+
41
+ def assert_feature_exists(feature)
42
+ @handler.call(feature) unless @adapter.features.include?(feature.key)
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -9,6 +9,7 @@ module Flipper
9
9
  class FeatureSynchronizer
10
10
  extend Forwardable
11
11
 
12
+ def_delegator :@local_gate_values, :expression, :local_expression
12
13
  def_delegator :@local_gate_values, :boolean, :local_boolean
13
14
  def_delegator :@local_gate_values, :actors, :local_actors
14
15
  def_delegator :@local_gate_values, :groups, :local_groups
@@ -17,6 +18,7 @@ module Flipper
17
18
  def_delegator :@local_gate_values, :percentage_of_time,
18
19
  :local_percentage_of_time
19
20
 
21
+ def_delegator :@remote_gate_values, :expression, :remote_expression
20
22
  def_delegator :@remote_gate_values, :boolean, :remote_boolean
21
23
  def_delegator :@remote_gate_values, :actors, :remote_actors
22
24
  def_delegator :@remote_gate_values, :groups, :remote_groups
@@ -40,8 +42,9 @@ module Flipper
40
42
  @feature.enable
41
43
  else
42
44
  @feature.disable if local_boolean_enabled?
43
- sync_actors
44
45
  sync_groups
46
+ sync_actors
47
+ sync_expression
45
48
  sync_percentage_of_actors
46
49
  sync_percentage_of_time
47
50
  end
@@ -49,6 +52,12 @@ module Flipper
49
52
 
50
53
  private
51
54
 
55
+ def sync_expression
56
+ return if local_expression == remote_expression
57
+
58
+ @feature.enable_expression remote_expression
59
+ end
60
+
52
61
  def sync_actors
53
62
  remote_actors_added = remote_actors - local_actors
54
63
  remote_actors_added.each do |flipper_id|
@@ -8,9 +8,6 @@ module Flipper
8
8
  class Sync
9
9
  include ::Flipper::Adapter
10
10
 
11
- # Public: The name of the adapter.
12
- attr_reader :name
13
-
14
11
  # Public: The synchronizer that will keep the local and remote in sync.
15
12
  attr_reader :synchronizer
16
13
 
@@ -22,7 +19,6 @@ module Flipper
22
19
  # interval - The Float or Integer number of seconds between syncs from
23
20
  # remote to local. Default value is set in IntervalSynchronizer.
24
21
  def initialize(local, remote, options = {})
25
- @name = :sync
26
22
  @local = local
27
23
  @remote = remote
28
24
  @synchronizer = options.fetch(:synchronizer) do