flipper 0.26.0 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
@@ -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
@@ -39,10 +38,10 @@ module Flipper
39
38
 
40
39
  def get_multi(features)
41
40
  csv_keys = features.map(&:key).join(',')
42
- response = @client.get("/features?keys=#{csv_keys}")
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']
@@ -57,18 +56,18 @@ module Flipper
57
56
  end
58
57
 
59
58
  def get_all
60
- response = @client.get("/features")
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)
64
- parsed_features = parsed_response.fetch('features')
62
+ parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
63
+ parsed_features = parsed_response['features'] || []
65
64
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
66
65
  hash[parsed_feature['key']] = parsed_feature['gates']
67
66
  hash
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
@@ -76,10 +75,10 @@ module Flipper
76
75
  end
77
76
 
78
77
  def features
79
- response = @client.get('/features')
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
@@ -123,6 +122,14 @@ module Flipper
123
122
  true
124
123
  end
125
124
 
125
+ def import(source)
126
+ adapter = self.class.from(source)
127
+ export = adapter.export(format: :json, version: 1)
128
+ response = @client.post("/import", export.contents)
129
+ raise Error, response unless response.is_a?(Net::HTTPNoContent)
130
+ true
131
+ end
132
+
126
133
  private
127
134
 
128
135
  def request_body_for_gate(gate, value)
@@ -130,11 +137,13 @@ module Flipper
130
137
  when :boolean
131
138
  {}
132
139
  when :groups
133
- { name: value }
140
+ { name: value.to_s }
134
141
  when :actors
135
- { flipper_id: value }
142
+ { flipper_id: value.to_s }
136
143
  when :percentage_of_actors, :percentage_of_time
137
- { percentage: value }
144
+ { percentage: value.to_s }
145
+ when :expression
146
+ value
138
147
  else
139
148
  raise "#{gate.key} is not a valid flipper gate key"
140
149
  end
@@ -158,13 +167,17 @@ module Flipper
158
167
  case gate.data_type
159
168
  when :boolean, :integer
160
169
  value ? value.to_s : value
170
+ when :json
171
+ value
161
172
  when :set
162
173
  value ? value.to_set : Set.new
163
174
  else
164
- unsupported_data_type(gate.data_type)
175
+ unsupported_data_type gate.data_type
165
176
  end
166
177
  end
167
178
 
179
+ private
180
+
168
181
  def unsupported_data_type(data_type)
169
182
  raise "#{data_type} is not supported by this adapter"
170
183
  end
@@ -4,7 +4,7 @@ module Flipper
4
4
  module Adapters
5
5
  # Internal: Adapter that wraps another adapter and instruments all adapter
6
6
  # operations.
7
- class Instrumented < SimpleDelegator
7
+ class Instrumented
8
8
  include ::Flipper::Adapter
9
9
 
10
10
  # Private: The name of instrumentation events.
@@ -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.
@@ -24,9 +21,7 @@ module Flipper
24
21
  # :instrumenter - What to use to instrument all the things.
25
22
  #
26
23
  def initialize(adapter, options = {})
27
- super(adapter)
28
24
  @adapter = adapter
29
- @name = :instrumented
30
25
  @instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
31
26
  end
32
27
 
@@ -146,6 +141,30 @@ module Flipper
146
141
  payload[:result] = @adapter.disable(feature, gate, thing)
147
142
  end
148
143
  end
144
+
145
+ def import(source)
146
+ default_payload = {
147
+ operation: :import,
148
+ adapter_name: @adapter.name,
149
+ }
150
+
151
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
152
+ payload[:result] = @adapter.import(source)
153
+ end
154
+ end
155
+
156
+ def export(format: :json, version: 1)
157
+ default_payload = {
158
+ operation: :export,
159
+ adapter_name: @adapter.name,
160
+ format: format,
161
+ version: version,
162
+ }
163
+
164
+ @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
165
+ payload[:result] = @adapter.export(format: format, version: version)
166
+ end
167
+ end
149
168
  end
150
169
  end
151
170
  end
@@ -5,39 +5,28 @@ module Flipper
5
5
  # Internal: Adapter that wraps another adapter with the ability to memoize
6
6
  # adapter calls in memory. Used by flipper dsl and the memoizer middleware
7
7
  # to make it possible to memoize adapter calls for the duration of a request.
8
- class Memoizable < SimpleDelegator
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
- super(adapter)
31
19
  @adapter = adapter
32
- @name = :memoizable
33
20
  @cache = cache || {}
34
21
  @memoize = false
22
+ @features_key = :flipper_features
23
+ @get_all_key = :all_memoized
35
24
  end
36
25
 
37
26
  # Public
38
27
  def features
39
28
  if memoizing?
40
- cache.fetch(FeaturesKey) { cache[FeaturesKey] = @adapter.features }
29
+ cache.fetch(@features_key) { cache[@features_key] = @adapter.features }
41
30
  else
42
31
  @adapter.features
43
32
  end
@@ -95,9 +84,9 @@ module Flipper
95
84
  def get_all
96
85
  if memoizing?
97
86
  response = nil
98
- if cache[GetAllKey]
87
+ if cache[@get_all_key]
99
88
  response = {}
100
- cache[FeaturesKey].each do |key|
89
+ cache[@features_key].each do |key|
101
90
  response[key] = cache[key_for(key)]
102
91
  end
103
92
  else
@@ -105,8 +94,8 @@ module Flipper
105
94
  response.each do |key, value|
106
95
  cache[key_for(key)] = value
107
96
  end
108
- cache[FeaturesKey] = response.keys.to_set
109
- cache[GetAllKey] = true
97
+ cache[@features_key] = response.keys.to_set
98
+ cache[@get_all_key] = true
110
99
  end
111
100
 
112
101
  # Ensures that looking up other features that do not exist doesn't
@@ -128,6 +117,19 @@ module Flipper
128
117
  @adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
129
118
  end
130
119
 
120
+ # Public
121
+ def read_only?
122
+ @adapter.read_only?
123
+ end
124
+
125
+ def import(source)
126
+ @adapter.import(source).tap { cache.clear if memoizing? }
127
+ end
128
+
129
+ def export(format: :json, version: 1)
130
+ @adapter.export(format: format, version: version)
131
+ end
132
+
131
133
  # Internal: Turns local caching on/off.
132
134
  #
133
135
  # value - The Boolean that decides if local caching is on.
@@ -141,10 +143,20 @@ module Flipper
141
143
  !!@memoize
142
144
  end
143
145
 
146
+ if RUBY_VERSION >= '3.0'
147
+ def method_missing(name, *args, **kwargs, &block)
148
+ @adapter.send name, *args, **kwargs, &block
149
+ end
150
+ else
151
+ def method_missing(name, *args, &block)
152
+ @adapter.send name, *args, &block
153
+ end
154
+ end
155
+
144
156
  private
145
157
 
146
158
  def key_for(key)
147
- self.class.key_for(key)
159
+ "feature/#{key}"
148
160
  end
149
161
 
150
162
  def expire_feature(feature)
@@ -152,7 +164,7 @@ module Flipper
152
164
  end
153
165
 
154
166
  def expire_features_set
155
- cache.delete(FeaturesKey) if memoizing?
167
+ cache.delete(@features_key) if memoizing?
156
168
  end
157
169
  end
158
170
  end
@@ -1,4 +1,5 @@
1
- require 'set'
1
+ require "flipper/adapter"
2
+ require "flipper/typecast"
2
3
 
3
4
  module Flipper
4
5
  module Adapters
@@ -7,93 +8,99 @@ module Flipper
7
8
  class Memory
8
9
  include ::Flipper::Adapter
9
10
 
10
- FeaturesKey = :features
11
-
12
- # Public: The name of the adapter.
13
- attr_reader :name
14
-
15
11
  # Public
16
- def initialize(source = nil)
17
- @source = source || {}
18
- @name = :memory
12
+ def initialize(source = nil, threadsafe: true)
13
+ @source = Typecast.features_hash(source)
14
+ @lock = Mutex.new if threadsafe
15
+ reset
19
16
  end
20
17
 
21
18
  # Public: The set of known features.
22
19
  def features
23
- @source.keys.to_set
20
+ synchronize { @source.keys }.to_set
24
21
  end
25
22
 
26
23
  # Public: Adds a feature to the set of known features.
27
24
  def add(feature)
28
- @source[feature.key] ||= default_config
25
+ synchronize { @source[feature.key] ||= default_config }
29
26
  true
30
27
  end
31
28
 
32
29
  # Public: Removes a feature from the set of known features and clears
33
30
  # all the values for the feature.
34
31
  def remove(feature)
35
- @source.delete(feature.key)
32
+ synchronize { @source.delete(feature.key) }
36
33
  true
37
34
  end
38
35
 
39
36
  # Public: Clears all the gate values for a feature.
40
37
  def clear(feature)
41
- @source[feature.key] = default_config
38
+ synchronize { @source[feature.key] = default_config }
42
39
  true
43
40
  end
44
41
 
45
42
  # Public
46
43
  def get(feature)
47
- @source[feature.key] || default_config
44
+ synchronize { @source[feature.key] } || default_config
48
45
  end
49
46
 
50
47
  def get_multi(features)
51
- result = {}
52
- features.each do |feature|
53
- result[feature.key] = @source[feature.key] || default_config
48
+ synchronize do
49
+ result = {}
50
+ features.each do |feature|
51
+ result[feature.key] = @source[feature.key] || default_config
52
+ end
53
+ result
54
54
  end
55
- result
56
55
  end
57
56
 
58
57
  def get_all
59
- @source
58
+ synchronize { Typecast.features_hash(@source) }
60
59
  end
61
60
 
62
61
  # Public
63
62
  def enable(feature, gate, thing)
64
- @source[feature.key] ||= default_config
65
-
66
- case gate.data_type
67
- when :boolean
68
- clear(feature)
69
- @source[feature.key][gate.key] = thing.value.to_s
70
- when :integer
71
- @source[feature.key][gate.key] = thing.value.to_s
72
- when :set
73
- @source[feature.key][gate.key] << thing.value.to_s
74
- else
75
- raise "#{gate} is not supported by this adapter yet"
63
+ synchronize do
64
+ @source[feature.key] ||= default_config
65
+
66
+ case gate.data_type
67
+ when :boolean
68
+ @source[feature.key] = default_config
69
+ @source[feature.key][gate.key] = thing.value.to_s
70
+ when :integer
71
+ @source[feature.key][gate.key] = thing.value.to_s
72
+ when :set
73
+ @source[feature.key][gate.key] << thing.value.to_s
74
+ when :json
75
+ @source[feature.key][gate.key] = thing.value
76
+ else
77
+ raise "#{gate} is not supported by this adapter yet"
78
+ end
79
+
80
+ true
76
81
  end
77
-
78
- true
79
82
  end
80
83
 
81
84
  # Public
82
85
  def disable(feature, gate, thing)
83
- @source[feature.key] ||= default_config
84
-
85
- case gate.data_type
86
- when :boolean
87
- clear(feature)
88
- when :integer
89
- @source[feature.key][gate.key] = thing.value.to_s
90
- when :set
91
- @source[feature.key][gate.key].delete thing.value.to_s
92
- else
93
- raise "#{gate} is not supported by this adapter yet"
86
+ synchronize do
87
+ @source[feature.key] ||= default_config
88
+
89
+ case gate.data_type
90
+ when :boolean
91
+ @source[feature.key] = default_config
92
+ when :integer
93
+ @source[feature.key][gate.key] = thing.value.to_s
94
+ when :set
95
+ @source[feature.key][gate.key].delete thing.value.to_s
96
+ when :json
97
+ @source[feature.key].delete(gate.key)
98
+ else
99
+ raise "#{gate} is not supported by this adapter yet"
100
+ end
101
+
102
+ true
94
103
  end
95
-
96
- true
97
104
  end
98
105
 
99
106
  # Public
@@ -104,6 +111,34 @@ module Flipper
104
111
  ]
105
112
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
106
113
  end
114
+
115
+ # Public: a more efficient implementation of import for this adapter
116
+ def import(source)
117
+ adapter = self.class.from(source)
118
+ get_all = Typecast.features_hash(adapter.get_all)
119
+ synchronize { @source.replace(get_all) }
120
+ true
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
107
142
  end
108
143
  end
109
144
  end
@@ -5,102 +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 < SimpleDelegator
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
- :features,
22
- :add,
23
- :remove,
24
- :clear,
25
- :get,
26
- :get_multi,
27
- :get_all,
28
- :enable,
29
- :disable,
30
- ].freeze
31
-
32
20
  # Internal: An array of the operations that have happened.
33
21
  attr_reader :operations
34
22
 
35
- # Internal: The name of the adapter.
36
- attr_reader :name
37
-
38
23
  # Public
39
24
  def initialize(adapter, operations = nil)
40
25
  super(adapter)
41
- @adapter = adapter
42
- @name = :operation_logger
43
26
  @operations = operations || []
44
27
  end
45
28
 
46
- # Public: The set of known features.
47
- def features
48
- @operations << Operation.new(:features, [])
49
- @adapter.features
50
- end
51
-
52
- # Public: Adds a feature to the set of known features.
53
- def add(feature)
54
- @operations << Operation.new(:add, [feature])
55
- @adapter.add(feature)
56
- end
57
-
58
- # Public: Removes a feature from the set of known features and clears
59
- # all the values for the feature.
60
- def remove(feature)
61
- @operations << Operation.new(:remove, [feature])
62
- @adapter.remove(feature)
63
- end
64
-
65
- # Public: Clears all the gate values for a feature.
66
- def clear(feature)
67
- @operations << Operation.new(:clear, [feature])
68
- @adapter.clear(feature)
69
- end
70
-
71
- # Public
72
- def get(feature)
73
- @operations << Operation.new(:get, [feature])
74
- @adapter.get(feature)
75
- end
76
-
77
- # Public
78
- def get_multi(features)
79
- @operations << Operation.new(:get_multi, [features])
80
- @adapter.get_multi(features)
81
- end
82
-
83
- # Public
84
- def get_all
85
- @operations << Operation.new(:get_all, [])
86
- @adapter.get_all
87
- end
88
-
89
- # Public
90
- def enable(feature, gate, thing)
91
- @operations << Operation.new(:enable, [feature, gate, thing])
92
- @adapter.enable(feature, gate, thing)
93
- end
94
-
95
- # Public
96
- def disable(feature, gate, thing)
97
- @operations << Operation.new(:disable, [feature, gate, thing])
98
- @adapter.disable(feature, gate, thing)
99
- end
100
-
101
29
  # Public: Count the number of times a certain operation happened.
102
- def count(type)
103
- type(type).size
30
+ def count(type = nil)
31
+ if type
32
+ type(type).size
33
+ else
34
+ @operations.size
35
+ end
104
36
  end
105
37
 
106
38
  # Public: Get all operations of a certain type.
@@ -122,6 +54,13 @@ module Flipper
122
54
  inspect_id = ::Kernel::format "%x", (object_id * 2)
123
55
  %(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
124
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
125
64
  end
126
65
  end
127
66
  end