flipper 0.28.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 (195) 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 +52 -10
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -526
  7. data/Gemfile +17 -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/app.ru +12 -0
  14. data/examples/cloud/backoff_policy.rb +13 -0
  15. data/examples/cloud/basic.rb +22 -0
  16. data/examples/cloud/cloud_setup.rb +20 -0
  17. data/examples/cloud/forked.rb +36 -0
  18. data/examples/cloud/import.rb +17 -0
  19. data/examples/cloud/threaded.rb +33 -0
  20. data/examples/dsl.rb +0 -14
  21. data/examples/expressions.rb +213 -0
  22. data/examples/mirroring.rb +59 -0
  23. data/examples/strict.rb +18 -0
  24. data/exe/flipper +5 -0
  25. data/flipper-cloud.gemspec +19 -0
  26. data/flipper.gemspec +8 -4
  27. data/lib/flipper/actor.rb +6 -3
  28. data/lib/flipper/adapter.rb +10 -0
  29. data/lib/flipper/adapter_builder.rb +44 -0
  30. data/lib/flipper/adapters/actor_limit.rb +28 -0
  31. data/lib/flipper/adapters/cache_base.rb +143 -0
  32. data/lib/flipper/adapters/dual_write.rb +1 -3
  33. data/lib/flipper/adapters/failover.rb +0 -4
  34. data/lib/flipper/adapters/failsafe.rb +0 -4
  35. data/lib/flipper/adapters/http/client.rb +40 -12
  36. data/lib/flipper/adapters/http/error.rb +2 -2
  37. data/lib/flipper/adapters/http.rb +19 -14
  38. data/lib/flipper/adapters/instrumented.rb +0 -4
  39. data/lib/flipper/adapters/memoizable.rb +14 -19
  40. data/lib/flipper/adapters/memory.rb +37 -19
  41. data/lib/flipper/adapters/operation_logger.rb +18 -92
  42. data/lib/flipper/adapters/poll.rb +16 -3
  43. data/lib/flipper/adapters/pstore.rb +17 -11
  44. data/lib/flipper/adapters/read_only.rb +8 -41
  45. data/lib/flipper/adapters/strict.rb +45 -0
  46. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  47. data/lib/flipper/adapters/sync.rb +0 -4
  48. data/lib/flipper/adapters/wrapper.rb +54 -0
  49. data/lib/flipper/cli.rb +263 -0
  50. data/lib/flipper/cloud/configuration.rb +266 -0
  51. data/lib/flipper/cloud/dsl.rb +27 -0
  52. data/lib/flipper/cloud/message_verifier.rb +95 -0
  53. data/lib/flipper/cloud/middleware.rb +63 -0
  54. data/lib/flipper/cloud/routes.rb +14 -0
  55. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  56. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  57. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  58. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  59. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  60. data/lib/flipper/cloud/telemetry.rb +191 -0
  61. data/lib/flipper/cloud.rb +53 -0
  62. data/lib/flipper/configuration.rb +25 -4
  63. data/lib/flipper/dsl.rb +47 -42
  64. data/lib/flipper/engine.rb +102 -0
  65. data/lib/flipper/export.rb +0 -2
  66. data/lib/flipper/exporters/json/export.rb +1 -1
  67. data/lib/flipper/exporters/json/v1.rb +1 -1
  68. data/lib/flipper/expression/builder.rb +73 -0
  69. data/lib/flipper/expression/constant.rb +25 -0
  70. data/lib/flipper/expression.rb +71 -0
  71. data/lib/flipper/expressions/all.rb +9 -0
  72. data/lib/flipper/expressions/any.rb +9 -0
  73. data/lib/flipper/expressions/boolean.rb +9 -0
  74. data/lib/flipper/expressions/comparable.rb +13 -0
  75. data/lib/flipper/expressions/duration.rb +28 -0
  76. data/lib/flipper/expressions/equal.rb +9 -0
  77. data/lib/flipper/expressions/greater_than.rb +9 -0
  78. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  79. data/lib/flipper/expressions/less_than.rb +9 -0
  80. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  81. data/lib/flipper/expressions/not_equal.rb +9 -0
  82. data/lib/flipper/expressions/now.rb +9 -0
  83. data/lib/flipper/expressions/number.rb +9 -0
  84. data/lib/flipper/expressions/percentage.rb +9 -0
  85. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  86. data/lib/flipper/expressions/property.rb +9 -0
  87. data/lib/flipper/expressions/random.rb +9 -0
  88. data/lib/flipper/expressions/string.rb +9 -0
  89. data/lib/flipper/expressions/time.rb +9 -0
  90. data/lib/flipper/feature.rb +63 -1
  91. data/lib/flipper/gate.rb +2 -1
  92. data/lib/flipper/gate_values.rb +5 -2
  93. data/lib/flipper/gates/expression.rb +75 -0
  94. data/lib/flipper/instrumentation/log_subscriber.rb +28 -5
  95. data/lib/flipper/instrumentation/statsd.rb +4 -2
  96. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  97. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  98. data/lib/flipper/metadata.rb +8 -1
  99. data/lib/flipper/middleware/memoizer.rb +30 -14
  100. data/lib/flipper/model/active_record.rb +23 -0
  101. data/lib/flipper/poller.rb +10 -9
  102. data/lib/flipper/serializers/gzip.rb +22 -0
  103. data/lib/flipper/serializers/json.rb +17 -0
  104. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  105. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  106. data/lib/flipper/test_help.rb +43 -0
  107. data/lib/flipper/typecast.rb +37 -9
  108. data/lib/flipper/types/percentage.rb +1 -1
  109. data/lib/flipper/version.rb +11 -1
  110. data/lib/flipper.rb +44 -7
  111. data/lib/generators/flipper/setup_generator.rb +68 -0
  112. data/lib/generators/flipper/templates/initializer.rb +45 -0
  113. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  114. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  115. data/lib/generators/flipper/update_generator.rb +35 -0
  116. data/package-lock.json +41 -0
  117. data/package.json +10 -0
  118. data/spec/fixtures/environment.rb +1 -0
  119. data/spec/flipper/adapter_builder_spec.rb +72 -0
  120. data/spec/flipper/adapter_spec.rb +1 -0
  121. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  122. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  123. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  124. data/spec/flipper/adapters/http_spec.rb +135 -74
  125. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  126. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  127. data/spec/flipper/adapters/memory_spec.rb +11 -2
  128. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  129. data/spec/flipper/adapters/poll_spec.rb +41 -0
  130. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  131. data/spec/flipper/adapters/strict_spec.rb +64 -0
  132. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  133. data/spec/flipper/cli_spec.rb +166 -0
  134. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  135. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  136. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  137. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  138. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  139. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  140. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  141. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  142. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  143. data/spec/flipper/cloud_spec.rb +186 -0
  144. data/spec/flipper/configuration_spec.rb +17 -0
  145. data/spec/flipper/dsl_spec.rb +34 -73
  146. data/spec/flipper/engine_spec.rb +374 -0
  147. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  148. data/spec/flipper/expression/builder_spec.rb +248 -0
  149. data/spec/flipper/expression_spec.rb +188 -0
  150. data/spec/flipper/expressions/all_spec.rb +15 -0
  151. data/spec/flipper/expressions/any_spec.rb +15 -0
  152. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  153. data/spec/flipper/expressions/duration_spec.rb +43 -0
  154. data/spec/flipper/expressions/equal_spec.rb +24 -0
  155. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  156. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  157. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  159. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  160. data/spec/flipper/expressions/now_spec.rb +11 -0
  161. data/spec/flipper/expressions/number_spec.rb +21 -0
  162. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  163. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  164. data/spec/flipper/expressions/property_spec.rb +13 -0
  165. data/spec/flipper/expressions/random_spec.rb +9 -0
  166. data/spec/flipper/expressions/string_spec.rb +11 -0
  167. data/spec/flipper/expressions/time_spec.rb +13 -0
  168. data/spec/flipper/feature_spec.rb +380 -10
  169. data/spec/flipper/gate_values_spec.rb +2 -2
  170. data/spec/flipper/gates/expression_spec.rb +108 -0
  171. data/spec/flipper/identifier_spec.rb +4 -5
  172. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  173. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  174. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  175. data/spec/flipper/model/active_record_spec.rb +72 -0
  176. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  177. data/spec/flipper/serializers/json_spec.rb +13 -0
  178. data/spec/flipper/typecast_spec.rb +43 -7
  179. data/spec/flipper/types/actor_spec.rb +18 -1
  180. data/spec/flipper_integration_spec.rb +114 -16
  181. data/spec/flipper_spec.rb +87 -29
  182. data/spec/spec_helper.rb +17 -17
  183. data/spec/support/actor_names.yml +1 -0
  184. data/spec/support/fail_on_output.rb +8 -0
  185. data/spec/support/fake_backoff_policy.rb +15 -0
  186. data/spec/support/spec_helpers.rb +34 -8
  187. data/test/adapters/actor_limit_test.rb +20 -0
  188. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  189. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  190. data/test_rails/helper.rb +22 -2
  191. data/test_rails/system/test_help_test.rb +52 -0
  192. metadata +179 -19
  193. data/.tool-versions +0 -1
  194. data/lib/flipper/railtie.rb +0 -47
  195. data/spec/flipper/railtie_spec.rb +0 -109
@@ -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
@@ -1,6 +1,5 @@
1
1
  require "flipper/adapter"
2
2
  require "flipper/typecast"
3
- require 'concurrent/atomic/read_write_lock'
4
3
 
5
4
  module Flipper
6
5
  module Adapters
@@ -9,49 +8,44 @@ module Flipper
9
8
  class Memory
10
9
  include ::Flipper::Adapter
11
10
 
12
- FeaturesKey = :features
13
-
14
- # Public: The name of the adapter.
15
- attr_reader :name
16
-
17
11
  # Public
18
- def initialize(source = nil)
12
+ def initialize(source = nil, threadsafe: true)
19
13
  @source = Typecast.features_hash(source)
20
- @name = :memory
21
- @lock = Concurrent::ReadWriteLock.new
14
+ @lock = Mutex.new if threadsafe
15
+ reset
22
16
  end
23
17
 
24
18
  # Public: The set of known features.
25
19
  def features
26
- @lock.with_read_lock { @source.keys }.to_set
20
+ synchronize { @source.keys }.to_set
27
21
  end
28
22
 
29
23
  # Public: Adds a feature to the set of known features.
30
24
  def add(feature)
31
- @lock.with_write_lock { @source[feature.key] ||= default_config }
25
+ synchronize { @source[feature.key] ||= default_config }
32
26
  true
33
27
  end
34
28
 
35
29
  # Public: Removes a feature from the set of known features and clears
36
30
  # all the values for the feature.
37
31
  def remove(feature)
38
- @lock.with_write_lock { @source.delete(feature.key) }
32
+ synchronize { @source.delete(feature.key) }
39
33
  true
40
34
  end
41
35
 
42
36
  # Public: Clears all the gate values for a feature.
43
37
  def clear(feature)
44
- @lock.with_write_lock { @source[feature.key] = default_config }
38
+ synchronize { @source[feature.key] = default_config }
45
39
  true
46
40
  end
47
41
 
48
42
  # Public
49
43
  def get(feature)
50
- @lock.with_read_lock { @source[feature.key] } || default_config
44
+ synchronize { @source[feature.key] } || default_config
51
45
  end
52
46
 
53
47
  def get_multi(features)
54
- @lock.with_read_lock do
48
+ synchronize do
55
49
  result = {}
56
50
  features.each do |feature|
57
51
  result[feature.key] = @source[feature.key] || default_config
@@ -61,12 +55,12 @@ module Flipper
61
55
  end
62
56
 
63
57
  def get_all
64
- @lock.with_read_lock { Typecast.features_hash(@source) }
58
+ synchronize { Typecast.features_hash(@source) }
65
59
  end
66
60
 
67
61
  # Public
68
62
  def enable(feature, gate, thing)
69
- @lock.with_write_lock do
63
+ synchronize do
70
64
  @source[feature.key] ||= default_config
71
65
 
72
66
  case gate.data_type
@@ -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
@@ -87,7 +83,7 @@ module Flipper
87
83
 
88
84
  # Public
89
85
  def disable(feature, gate, thing)
90
- @lock.with_write_lock do
86
+ synchronize do
91
87
  @source[feature.key] ||= default_config
92
88
 
93
89
  case gate.data_type
@@ -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
@@ -118,9 +116,29 @@ module Flipper
118
116
  def import(source)
119
117
  adapter = self.class.from(source)
120
118
  get_all = Typecast.features_hash(adapter.get_all)
121
- @lock.with_write_lock { @source.replace(get_all) }
119
+ synchronize { @source.replace(get_all) }
122
120
  true
123
121
  end
122
+
123
+ private
124
+
125
+ def reset
126
+ @pid = Process.pid
127
+ @lock&.unlock if @lock&.locked?
128
+ end
129
+
130
+ def forked?
131
+ @pid != Process.pid
132
+ end
133
+
134
+ def synchronize(&block)
135
+ if @lock
136
+ reset if forked?
137
+ @lock.synchronize(&block)
138
+ else
139
+ block.call
140
+ end
141
+ end
124
142
  end
125
143
  end
126
144
  end
@@ -5,115 +5,34 @@ module Flipper
5
5
  # Public: Adapter that wraps another adapter and stores the operations.
6
6
  #
7
7
  # Useful in tests to verify calls and such. Never use outside of testing.
8
- class OperationLogger
9
- include Flipper::Adapter
8
+ class OperationLogger < Wrapper
10
9
 
11
10
  class Operation
12
- attr_reader :type, :args
11
+ attr_reader :type, :args, :kwargs
13
12
 
14
- def initialize(type, args)
13
+ def initialize(type, args, kwargs = {})
15
14
  @type = type
16
15
  @args = args
16
+ @kwargs = kwargs
17
17
  end
18
18
  end
19
19
 
20
- OperationTypes = [
21
- :import,
22
- :export,
23
- :features,
24
- :add,
25
- :remove,
26
- :clear,
27
- :get,
28
- :get_multi,
29
- :get_all,
30
- :enable,
31
- :disable,
32
- ].freeze
33
-
34
20
  # Internal: An array of the operations that have happened.
35
21
  attr_reader :operations
36
22
 
37
- # Internal: The name of the adapter.
38
- attr_reader :name
39
-
40
23
  # Public
41
24
  def initialize(adapter, operations = nil)
42
- @adapter = adapter
43
- @name = :operation_logger
25
+ super(adapter)
44
26
  @operations = operations || []
45
27
  end
46
28
 
47
- # Public: The set of known features.
48
- def features
49
- @operations << Operation.new(:features, [])
50
- @adapter.features
51
- end
52
-
53
- # Public: Adds a feature to the set of known features.
54
- def add(feature)
55
- @operations << Operation.new(:add, [feature])
56
- @adapter.add(feature)
57
- end
58
-
59
- # Public: Removes a feature from the set of known features and clears
60
- # all the values for the feature.
61
- def remove(feature)
62
- @operations << Operation.new(:remove, [feature])
63
- @adapter.remove(feature)
64
- end
65
-
66
- # Public: Clears all the gate values for a feature.
67
- def clear(feature)
68
- @operations << Operation.new(:clear, [feature])
69
- @adapter.clear(feature)
70
- end
71
-
72
- # Public
73
- def get(feature)
74
- @operations << Operation.new(:get, [feature])
75
- @adapter.get(feature)
76
- end
77
-
78
- # Public
79
- def get_multi(features)
80
- @operations << Operation.new(:get_multi, [features])
81
- @adapter.get_multi(features)
82
- end
83
-
84
- # Public
85
- def get_all
86
- @operations << Operation.new(:get_all, [])
87
- @adapter.get_all
88
- end
89
-
90
- # Public
91
- def enable(feature, gate, thing)
92
- @operations << Operation.new(:enable, [feature, gate, thing])
93
- @adapter.enable(feature, gate, thing)
94
- end
95
-
96
- # Public
97
- def disable(feature, gate, thing)
98
- @operations << Operation.new(:disable, [feature, gate, thing])
99
- @adapter.disable(feature, gate, thing)
100
- end
101
-
102
- # Public
103
- def import(source)
104
- @operations << Operation.new(:import, [source])
105
- @adapter.import(source)
106
- end
107
-
108
- # Public
109
- def export(format: :json, version: 1)
110
- @operations << Operation.new(:export, [format, version])
111
- @adapter.export(format: format, version: version)
112
- end
113
-
114
29
  # Public: Count the number of times a certain operation happened.
115
- def count(type)
116
- type(type).size
30
+ def count(type = nil)
31
+ if type
32
+ type(type).size
33
+ else
34
+ @operations.size
35
+ end
117
36
  end
118
37
 
119
38
  # Public: Get all operations of a certain type.
@@ -135,6 +54,13 @@ module Flipper
135
54
  inspect_id = ::Kernel::format "%x", (object_id * 2)
136
55
  %(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
137
56
  end
57
+
58
+ private
59
+
60
+ def wrap(method, *args, **kwargs, &block)
61
+ @operations << Operation.new(method, args, kwargs)
62
+ block.call
63
+ end
138
64
  end
139
65
  end
140
66
  end
@@ -10,16 +10,29 @@ 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
21
+
22
+ # If the adapter is empty, we need to sync before starting the poller.
23
+ # Yes, this will block the main thread, but that's better than thinking
24
+ # nothing is enabled.
25
+ if adapter.features.empty?
26
+ begin
27
+ @poller.sync
28
+ rescue
29
+ # TODO: Warn here that it's possible that no data has been synced
30
+ # and flags are being evaluated without flag data being present
31
+ # until a sync completes. We rescue to avoid flipper being down
32
+ # causing your processes to crash.
33
+ end
34
+ end
35
+
23
36
  @poller.start
24
37
  end
25
38
 
@@ -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
@@ -3,8 +3,8 @@ require 'flipper'
3
3
  module Flipper
4
4
  module Adapters
5
5
  # Public: Adapter that wraps another adapter and raises for any writes.
6
- class ReadOnly
7
- include ::Flipper::Adapter
6
+ class ReadOnly < Wrapper
7
+ WRITE_METHODS = %i[add remove clear enable disable]
8
8
 
9
9
  class WriteAttempted < Error
10
10
  def initialize(message = nil)
@@ -12,49 +12,16 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- # Internal: The name of the adapter.
16
- attr_reader :name
17
-
18
- # Public
19
- def initialize(adapter)
20
- @adapter = adapter
21
- @name = :read_only
22
- end
23
-
24
- def features
25
- @adapter.features
26
- end
27
-
28
- def get(feature)
29
- @adapter.get(feature)
30
- end
31
-
32
- def get_multi(features)
33
- @adapter.get_multi(features)
15
+ def read_only?
16
+ true
34
17
  end
35
18
 
36
- def get_all
37
- @adapter.get_all
38
- end
19
+ private
39
20
 
40
- def add(_feature)
41
- raise WriteAttempted
42
- end
43
-
44
- def remove(_feature)
45
- raise WriteAttempted
46
- end
47
-
48
- def clear(_feature)
49
- raise WriteAttempted
50
- end
51
-
52
- def enable(_feature, _gate, _thing)
53
- raise WriteAttempted
54
- end
21
+ def wrap(method, *args, **kwargs)
22
+ raise WriteAttempted if WRITE_METHODS.include?(method)
55
23
 
56
- def disable(_feature, _gate, _thing)
57
- raise WriteAttempted
24
+ yield
58
25
  end
59
26
  end
60
27
  end
@@ -0,0 +1,45 @@
1
+ module Flipper
2
+ module Adapters
3
+ # An adapter that ensures a feature exists before checking it.
4
+ class Strict < Wrapper
5
+ attr_reader :handler
6
+
7
+ class NotFound < ::Flipper::Error
8
+ def initialize(name)
9
+ super "Could not find feature #{name.inspect}. Call `Flipper.add(#{name.inspect})` to create it."
10
+ end
11
+ end
12
+
13
+ def initialize(adapter, handler = nil, &block)
14
+ super(adapter)
15
+ @handler = block || handler
16
+ end
17
+
18
+ def get(feature)
19
+ assert_feature_exists(feature)
20
+ super
21
+ end
22
+
23
+ def get_multi(features)
24
+ features.each { |feature| assert_feature_exists(feature) }
25
+ super
26
+ end
27
+
28
+ private
29
+
30
+ def assert_feature_exists(feature)
31
+ return if @adapter.features.include?(feature.key)
32
+
33
+ case handler
34
+ when Proc then handler.call(feature)
35
+ when :warn then warn NotFound.new(feature.key).message
36
+ when :noop, false, nil
37
+ # noop
38
+ else # truthy or :raise
39
+ raise NotFound.new(feature.key)
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ 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