flipper 0.28.3 → 1.4.2

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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +56 -10
  4. data/.github/workflows/examples.yml +54 -11
  5. data/.github/workflows/release.yml +54 -0
  6. data/.superset/config.json +4 -0
  7. data/CLAUDE.md +93 -0
  8. data/Changelog.md +1 -559
  9. data/Gemfile +17 -8
  10. data/README.md +34 -29
  11. data/Rakefile +2 -2
  12. data/benchmark/typecast_ips.rb +8 -0
  13. data/docs/images/banner.jpg +0 -0
  14. data/docs/images/flipper_cloud.png +0 -0
  15. data/examples/cloud/app.ru +12 -0
  16. data/examples/cloud/backoff_policy.rb +13 -0
  17. data/examples/cloud/basic.rb +22 -0
  18. data/examples/cloud/cloud_setup.rb +20 -0
  19. data/examples/cloud/forked.rb +36 -0
  20. data/examples/cloud/import.rb +17 -0
  21. data/examples/cloud/poll_interval/README.md +111 -0
  22. data/examples/cloud/poll_interval/client.rb +108 -0
  23. data/examples/cloud/poll_interval/server.rb +98 -0
  24. data/examples/cloud/threaded.rb +33 -0
  25. data/examples/dsl.rb +0 -14
  26. data/examples/expressions.rb +237 -0
  27. data/examples/strict.rb +18 -0
  28. data/exe/flipper +5 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +8 -4
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +27 -1
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/actor_limit.rb +54 -0
  35. data/lib/flipper/adapters/cache_base.rb +161 -0
  36. data/lib/flipper/adapters/dual_write.rb +7 -5
  37. data/lib/flipper/adapters/failover.rb +9 -7
  38. data/lib/flipper/adapters/failsafe.rb +2 -6
  39. data/lib/flipper/adapters/http/client.rb +53 -14
  40. data/lib/flipper/adapters/http/error.rb +2 -2
  41. data/lib/flipper/adapters/http.rb +56 -16
  42. data/lib/flipper/adapters/instrumented.rb +2 -6
  43. data/lib/flipper/adapters/memoizable.rb +17 -22
  44. data/lib/flipper/adapters/memory.rb +5 -7
  45. data/lib/flipper/adapters/operation_logger.rb +18 -92
  46. data/lib/flipper/adapters/poll.rb +16 -3
  47. data/lib/flipper/adapters/pstore.rb +18 -12
  48. data/lib/flipper/adapters/read_only.rb +8 -41
  49. data/lib/flipper/adapters/strict.rb +75 -0
  50. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  51. data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
  52. data/lib/flipper/adapters/sync.rb +7 -7
  53. data/lib/flipper/adapters/wrapper.rb +54 -0
  54. data/lib/flipper/cli.rb +314 -0
  55. data/lib/flipper/cloud/configuration.rb +267 -0
  56. data/lib/flipper/cloud/dsl.rb +27 -0
  57. data/lib/flipper/cloud/message_verifier.rb +95 -0
  58. data/lib/flipper/cloud/middleware.rb +63 -0
  59. data/lib/flipper/cloud/migrate.rb +71 -0
  60. data/lib/flipper/cloud/routes.rb +14 -0
  61. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  62. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  63. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  64. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  65. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  66. data/lib/flipper/cloud/telemetry.rb +191 -0
  67. data/lib/flipper/cloud.rb +54 -0
  68. data/lib/flipper/configuration.rb +25 -4
  69. data/lib/flipper/dsl.rb +48 -43
  70. data/lib/flipper/engine.rb +102 -0
  71. data/lib/flipper/export.rb +0 -2
  72. data/lib/flipper/exporters/json/export.rb +1 -1
  73. data/lib/flipper/exporters/json/v1.rb +1 -1
  74. data/lib/flipper/expression/builder.rb +73 -0
  75. data/lib/flipper/expression/constant.rb +25 -0
  76. data/lib/flipper/expression.rb +71 -0
  77. data/lib/flipper/expressions/all.rb +9 -0
  78. data/lib/flipper/expressions/any.rb +9 -0
  79. data/lib/flipper/expressions/boolean.rb +9 -0
  80. data/lib/flipper/expressions/comparable.rb +13 -0
  81. data/lib/flipper/expressions/equal.rb +9 -0
  82. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  83. data/lib/flipper/expressions/greater_than.rb +9 -0
  84. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  85. data/lib/flipper/expressions/less_than.rb +9 -0
  86. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  87. data/lib/flipper/expressions/not_equal.rb +9 -0
  88. data/lib/flipper/expressions/now.rb +9 -0
  89. data/lib/flipper/expressions/number.rb +9 -0
  90. data/lib/flipper/expressions/percentage.rb +9 -0
  91. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  92. data/lib/flipper/expressions/property.rb +9 -0
  93. data/lib/flipper/expressions/random.rb +9 -0
  94. data/lib/flipper/expressions/string.rb +9 -0
  95. data/lib/flipper/expressions/time.rb +16 -0
  96. data/lib/flipper/feature.rb +63 -1
  97. data/lib/flipper/gate.rb +2 -1
  98. data/lib/flipper/gate_values.rb +5 -2
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  101. data/lib/flipper/instrumentation/statsd.rb +4 -2
  102. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  103. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  104. data/lib/flipper/metadata.rb +8 -1
  105. data/lib/flipper/middleware/memoizer.rb +30 -14
  106. data/lib/flipper/model/active_record.rb +23 -0
  107. data/lib/flipper/poller.rb +54 -12
  108. data/lib/flipper/serializers/gzip.rb +22 -0
  109. data/lib/flipper/serializers/json.rb +17 -0
  110. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  111. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  112. data/lib/flipper/test_help.rb +43 -0
  113. data/lib/flipper/typecast.rb +37 -9
  114. data/lib/flipper/types/percentage.rb +1 -1
  115. data/lib/flipper/version.rb +11 -1
  116. data/lib/flipper.rb +61 -8
  117. data/lib/generators/flipper/setup_generator.rb +68 -0
  118. data/lib/generators/flipper/templates/initializer.rb +45 -0
  119. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  120. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  121. data/lib/generators/flipper/update_generator.rb +35 -0
  122. data/package-lock.json +41 -0
  123. data/package.json +10 -0
  124. data/spec/fixtures/environment.rb +1 -0
  125. data/spec/flipper/adapter_builder_spec.rb +72 -0
  126. data/spec/flipper/adapter_spec.rb +21 -0
  127. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  128. data/spec/flipper/adapters/dual_write_spec.rb +15 -2
  129. data/spec/flipper/adapters/failover_spec.rb +12 -0
  130. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  131. data/spec/flipper/adapters/http_spec.rb +375 -74
  132. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  133. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  134. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  135. data/spec/flipper/adapters/poll_spec.rb +41 -0
  136. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  137. data/spec/flipper/adapters/strict_spec.rb +122 -0
  138. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
  139. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  140. data/spec/flipper/adapters/sync_spec.rb +13 -0
  141. data/spec/flipper/cli_spec.rb +217 -0
  142. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  143. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  144. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  145. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  146. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  147. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  148. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  149. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  150. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  151. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  152. data/spec/flipper/cloud_spec.rb +186 -0
  153. data/spec/flipper/configuration_spec.rb +17 -0
  154. data/spec/flipper/dsl_spec.rb +34 -73
  155. data/spec/flipper/engine_spec.rb +374 -0
  156. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  157. data/spec/flipper/expression/builder_spec.rb +248 -0
  158. data/spec/flipper/expression_spec.rb +188 -0
  159. data/spec/flipper/expressions/all_spec.rb +15 -0
  160. data/spec/flipper/expressions/any_spec.rb +15 -0
  161. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  162. data/spec/flipper/expressions/equal_spec.rb +24 -0
  163. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  164. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  165. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  166. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  167. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  168. data/spec/flipper/expressions/now_spec.rb +11 -0
  169. data/spec/flipper/expressions/number_spec.rb +21 -0
  170. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  171. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  172. data/spec/flipper/expressions/property_spec.rb +13 -0
  173. data/spec/flipper/expressions/random_spec.rb +9 -0
  174. data/spec/flipper/expressions/string_spec.rb +11 -0
  175. data/spec/flipper/expressions/time_spec.rb +29 -0
  176. data/spec/flipper/feature_spec.rb +380 -10
  177. data/spec/flipper/gate_values_spec.rb +2 -2
  178. data/spec/flipper/gates/expression_spec.rb +190 -0
  179. data/spec/flipper/identifier_spec.rb +4 -5
  180. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  181. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  182. data/spec/flipper/middleware/memoizer_spec.rb +107 -7
  183. data/spec/flipper/model/active_record_spec.rb +72 -0
  184. data/spec/flipper/poller_spec.rb +347 -4
  185. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  186. data/spec/flipper/serializers/json_spec.rb +13 -0
  187. data/spec/flipper/typecast_spec.rb +43 -7
  188. data/spec/flipper/types/actor_spec.rb +18 -1
  189. data/spec/flipper_integration_spec.rb +247 -16
  190. data/spec/flipper_spec.rb +92 -29
  191. data/spec/spec_helper.rb +24 -17
  192. data/spec/support/actor_names.yml +1 -0
  193. data/spec/support/fail_on_output.rb +8 -0
  194. data/spec/support/fake_backoff_policy.rb +15 -0
  195. data/spec/support/spec_helpers.rb +34 -8
  196. data/test/adapters/actor_limit_test.rb +20 -0
  197. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  198. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  199. data/test_rails/helper.rb +22 -2
  200. data/test_rails/system/test_help_test.rb +52 -0
  201. metadata +139 -75
  202. data/.tool-versions +0 -1
  203. data/lib/flipper/railtie.rb +0 -47
  204. data/spec/flipper/railtie_spec.rb +0 -109
@@ -3,9 +3,6 @@ module Flipper
3
3
  class Failover
4
4
  include ::Flipper::Adapter
5
5
 
6
- # Public: The name of the adapter.
7
- attr_reader :name
8
-
9
6
  # Public: Build a new failover instance.
10
7
  #
11
8
  # primary - The primary flipper adapter.
@@ -16,8 +13,9 @@ module Flipper
16
13
  # primary is updated
17
14
  # :errors - Array of exception types for which to failover
18
15
 
16
+ attr_reader :primary, :secondary
17
+
19
18
  def initialize(primary, secondary, options = {})
20
- @name = :failover
21
19
  @primary = primary
22
20
  @secondary = secondary
23
21
 
@@ -25,6 +23,10 @@ module Flipper
25
23
  @errors = options.fetch(:errors, [ StandardError ])
26
24
  end
27
25
 
26
+ def adapter_stack
27
+ "#{name}(primary: #{@primary.adapter_stack}, secondary: #{@secondary.adapter_stack})"
28
+ end
29
+
28
30
  def features
29
31
  @primary.features
30
32
  rescue *@errors
@@ -43,10 +45,10 @@ module Flipper
43
45
  @secondary.get_multi(features)
44
46
  end
45
47
 
46
- def get_all
47
- @primary.get_all
48
+ def get_all(**kwargs)
49
+ @primary.get_all(**kwargs)
48
50
  rescue *@errors
49
- @secondary.get_all
51
+ @secondary.get_all(**kwargs)
50
52
  end
51
53
 
52
54
  def add(feature)
@@ -3,9 +3,6 @@ module Flipper
3
3
  class Failsafe
4
4
  include ::Flipper::Adapter
5
5
 
6
- # Public: The name of the adapter.
7
- attr_reader :name
8
-
9
6
  # Public: Build a new Failsafe instance.
10
7
  #
11
8
  # adapter - Flipper adapter to guard.
@@ -15,7 +12,6 @@ module Flipper
15
12
  def initialize(adapter, options = {})
16
13
  @adapter = adapter
17
14
  @errors = options.fetch(:errors, [StandardError])
18
- @name = :failsafe
19
15
  end
20
16
 
21
17
  def features
@@ -54,8 +50,8 @@ module Flipper
54
50
  {}
55
51
  end
56
52
 
57
- def get_all
58
- @adapter.get_all
53
+ def get_all(**kwargs)
54
+ @adapter.get_all(**kwargs)
59
55
  rescue *@errors
60
56
  {}
61
57
  end
@@ -7,20 +7,28 @@ module Flipper
7
7
  class Http
8
8
  class Client
9
9
  DEFAULT_HEADERS = {
10
- 'Content-Type' => 'application/json',
11
- 'Accept' => 'application/json',
12
- 'User-Agent' => "Flipper HTTP Adapter v#{VERSION}",
10
+ 'content-type' => 'application/json',
11
+ 'accept' => 'application/json',
12
+ 'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
13
13
  }.freeze
14
14
 
15
15
  HTTPS_SCHEME = "https".freeze
16
16
 
17
+ CLIENT_FRAMEWORKS = {
18
+ rails: -> { Rails.version if defined?(Rails) },
19
+ sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
20
+ hanami: -> { Hanami::VERSION if defined?(Hanami) },
21
+ sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
22
+ good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
23
+ }
24
+
17
25
  attr_reader :uri, :headers
18
26
  attr_reader :basic_auth_username, :basic_auth_password
19
- attr_reader :read_timeout, :open_timeout, :write_timeout, :max_retries, :debug_output
27
+ attr_reader :read_timeout, :open_timeout, :write_timeout
28
+ attr_reader :max_retries, :debug_output
20
29
 
21
30
  def initialize(options = {})
22
31
  @uri = URI(options.fetch(:url))
23
- @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
24
32
  @basic_auth_username = options[:basic_auth_username]
25
33
  @basic_auth_password = options[:basic_auth_password]
26
34
  @read_timeout = options[:read_timeout]
@@ -28,10 +36,28 @@ module Flipper
28
36
  @write_timeout = options[:write_timeout]
29
37
  @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
30
38
  @debug_output = options[:debug_output]
39
+
40
+ @headers = {}
41
+ DEFAULT_HEADERS.each { |key, value| add_header key, value }
42
+ if options[:headers]
43
+ options[:headers].each { |key, value| add_header key, value }
44
+ end
45
+ end
46
+
47
+ def add_header(key, value)
48
+ @headers[normalize_header_key(key)] = value
31
49
  end
32
50
 
33
- def get(path)
34
- perform Net::HTTP::Get, path, @headers
51
+ def get(path, options = {})
52
+ headers = @headers.dup
53
+
54
+ if options[:headers]
55
+ options[:headers].each do |key, value|
56
+ headers[normalize_header_key(key)] = value
57
+ end
58
+ end
59
+
60
+ perform Net::HTTP::Get, path, headers
35
61
  end
36
62
 
37
63
  def post(path, body = nil)
@@ -77,18 +103,23 @@ module Flipper
77
103
 
78
104
  def build_request(http_method, uri, headers, options)
79
105
  request_headers = {
80
- "Client-Language" => "ruby",
81
- "Client-Language-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
82
- "Client-Platform" => RUBY_PLATFORM,
83
- "Client-Engine" => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
84
- "Client-Pid" => Process.pid.to_s,
85
- "Client-Thread" => Thread.current.object_id.to_s,
86
- "Client-Hostname" => Socket.gethostname,
106
+ 'client-language' => "ruby",
107
+ 'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
108
+ 'client-platform' => RUBY_PLATFORM,
109
+ 'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
110
+ 'client-pid' => Process.pid.to_s,
111
+ 'client-thread' => Thread.current.object_id.to_s,
112
+ 'client-hostname' => Socket.gethostname,
87
113
  }.merge(headers)
88
114
 
89
115
  body = options[:body]
90
116
  request = http_method.new(uri.request_uri)
91
117
  request.initialize_http_header(request_headers)
118
+
119
+ client_frameworks.each do |framework, version|
120
+ request.add_field("client-framework", [framework, version].join("="))
121
+ end
122
+
92
123
  request.body = body if body
93
124
 
94
125
  if @basic_auth_username && @basic_auth_password
@@ -97,6 +128,14 @@ module Flipper
97
128
 
98
129
  request
99
130
  end
131
+
132
+ def client_frameworks
133
+ CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
134
+ end
135
+
136
+ def normalize_header_key(key)
137
+ key.to_s.downcase.gsub('_'.freeze, '-'.freeze)
138
+ end
100
139
  end
101
140
  end
102
141
  end
@@ -11,7 +11,7 @@ module Flipper
11
11
  message = "Failed with status: #{response.code}"
12
12
 
13
13
  begin
14
- data = JSON.parse(response.body)
14
+ data = Typecast.from_json(response.body)
15
15
 
16
16
  if error_message = data["message"]
17
17
  message << "\n\n#{data["message"]}"
@@ -20,7 +20,7 @@ module Flipper
20
20
  if more_info = data["more_info"]
21
21
  message << "\n#{data["more_info"]}"
22
22
  end
23
- rescue => exception
23
+ rescue
24
24
  # welp we tried
25
25
  end
26
26
 
@@ -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,16 @@ 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
25
+ @last_get_all_etag = nil
26
+ @last_get_all_result = nil
27
+ @last_get_all_response = nil
28
+ @get_all_mutex = Mutex.new
26
29
  end
27
30
 
28
31
  def get(feature)
29
32
  response = @client.get("/features/#{feature.key}")
30
33
  if response.is_a?(Net::HTTPOK)
31
- parsed_response = JSON.parse(response.body)
34
+ parsed_response = Typecast.from_json(response.body)
32
35
  result_for_feature(feature, parsed_response.fetch('gates'))
33
36
  elsif response.is_a?(Net::HTTPNotFound)
34
37
  default_config
@@ -42,7 +45,7 @@ module Flipper
42
45
  response = @client.get("/features?keys=#{csv_keys}&exclude_gate_names=true")
43
46
  raise Error, response unless response.is_a?(Net::HTTPOK)
44
47
 
45
- parsed_response = JSON.parse(response.body)
48
+ parsed_response = Typecast.from_json(response.body)
46
49
  parsed_features = parsed_response.fetch('features')
47
50
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
48
51
  hash[parsed_feature['key']] = parsed_feature['gates']
@@ -56,30 +59,61 @@ module Flipper
56
59
  result
57
60
  end
58
61
 
59
- def get_all
60
- response = @client.get("/features?exclude_gate_names=true")
62
+ def get_all(cache_bust: false)
63
+ options = {}
64
+ path = "/features?exclude_gate_names=true"
65
+ path += "&_cb=#{Time.now.to_i}" if cache_bust
66
+ etag = @get_all_mutex.synchronize { @last_get_all_etag }
67
+
68
+ if etag && !cache_bust
69
+ options[:headers] = { if_none_match: etag }
70
+ end
71
+
72
+ response = @client.get(path, options)
73
+ @get_all_mutex.synchronize { @last_get_all_response = response }
74
+
75
+ if response.is_a?(Net::HTTPNotModified)
76
+ cached_result = @get_all_mutex.synchronize { @last_get_all_result }
77
+
78
+ if cached_result
79
+ return cached_result
80
+ else
81
+ raise Error, response
82
+ end
83
+ end
84
+
61
85
  raise Error, response unless response.is_a?(Net::HTTPOK)
62
86
 
63
- parsed_response = JSON.parse(response.body)
64
- parsed_features = parsed_response.fetch('features')
87
+ parsed_response = response.body.empty? ? {} : Typecast.from_json(response.body)
88
+ parsed_features = parsed_response['features'] || []
65
89
  gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
66
90
  hash[parsed_feature['key']] = parsed_feature['gates']
67
91
  hash
68
92
  end
69
93
 
70
94
  result = {}
71
- gates_by_key.keys.each do |key|
95
+ gates_by_key.each_key do |key|
72
96
  feature = Feature.new(key, self)
73
97
  result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
74
98
  end
99
+
100
+ @get_all_mutex.synchronize do
101
+ @last_get_all_etag = response['etag'] if response['etag']
102
+ @last_get_all_result = result
103
+ end
104
+
75
105
  result
76
106
  end
77
107
 
108
+ def last_get_all_response
109
+ @get_all_mutex.synchronize { @last_get_all_response }
110
+ end
111
+
78
112
  def features
79
113
  response = @client.get('/features?exclude_gate_names=true')
80
114
  raise Error, response unless response.is_a?(Net::HTTPOK)
81
115
 
82
- parsed_response = JSON.parse(response.body)
116
+ parsed_response = Typecast.from_json(response.body)
83
117
  parsed_response['features'].map { |feature| feature['key'] }.to_set
84
118
  end
85
119
 
@@ -97,7 +131,7 @@ module Flipper
97
131
  end
98
132
 
99
133
  def enable(feature, gate, thing)
100
- body = request_body_for_gate(gate, thing.value.to_s)
134
+ body = request_body_for_gate(gate, thing.value)
101
135
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
102
136
  response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
103
137
  raise Error, response unless response.is_a?(Net::HTTPOK)
@@ -105,7 +139,7 @@ module Flipper
105
139
  end
106
140
 
107
141
  def disable(feature, gate, thing)
108
- body = request_body_for_gate(gate, thing.value.to_s)
142
+ body = request_body_for_gate(gate, thing.value)
109
143
  query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
110
144
  response = case gate.key
111
145
  when :percentage_of_actors, :percentage_of_time
@@ -138,11 +172,13 @@ module Flipper
138
172
  when :boolean
139
173
  {}
140
174
  when :groups
141
- { name: value }
175
+ { name: value.to_s }
142
176
  when :actors
143
- { flipper_id: value }
177
+ { flipper_id: value.to_s }
144
178
  when :percentage_of_actors, :percentage_of_time
145
- { percentage: value }
179
+ { percentage: value.to_s }
180
+ when :expression
181
+ value
146
182
  else
147
183
  raise "#{gate.key} is not a valid flipper gate key"
148
184
  end
@@ -166,13 +202,17 @@ module Flipper
166
202
  case gate.data_type
167
203
  when :boolean, :integer
168
204
  value ? value.to_s : value
205
+ when :json
206
+ value
169
207
  when :set
170
208
  value ? value.to_set : Set.new
171
209
  else
172
- unsupported_data_type(gate.data_type)
210
+ unsupported_data_type gate.data_type
173
211
  end
174
212
  end
175
213
 
214
+ private
215
+
176
216
  def unsupported_data_type(data_type)
177
217
  raise "#{data_type} is not supported by this adapter"
178
218
  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
 
@@ -105,14 +101,14 @@ module Flipper
105
101
  end
106
102
  end
107
103
 
108
- def get_all
104
+ def get_all(**kwargs)
109
105
  default_payload = {
110
106
  operation: :get_all,
111
107
  adapter_name: @adapter.name,
112
108
  }
113
109
 
114
110
  @instrumenter.instrument(InstrumentationName, default_payload) do |payload|
115
- payload[:result] = @adapter.get_all
111
+ payload[:result] = @adapter.get_all(**kwargs)
116
112
  end
117
113
  end
118
114
 
@@ -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
@@ -91,21 +81,21 @@ module Flipper
91
81
  end
92
82
  end
93
83
 
94
- def get_all
84
+ def get_all(**kwargs)
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
103
- response = @adapter.get_all
93
+ response = @adapter.get_all(**kwargs)
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
@@ -113,7 +103,7 @@ module Flipper
113
103
  response.default_proc = ->(memo, key) { memo[key] = default_config }
114
104
  response
115
105
  else
116
- @adapter.get_all
106
+ @adapter.get_all(**kwargs)
117
107
  end
118
108
  end
119
109
 
@@ -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
@@ -60,7 +54,7 @@ module Flipper
60
54
  end
61
55
  end
62
56
 
63
- def get_all
57
+ def get_all(**kwargs)
64
58
  synchronize { Typecast.features_hash(@source) }
65
59
  end
66
60
 
@@ -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
@@ -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