flipper 1.0.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 (180) 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 +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -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/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -0,0 +1,143 @@
1
+ module Flipper
2
+ module Adapters
3
+ # Base class for caching adapters. Inherit from this and then override
4
+ # cache_fetch, cache_read_multi, cache_write, and cache_delete.
5
+ class CacheBase
6
+ include ::Flipper::Adapter
7
+
8
+ # Public: The adapter being cached.
9
+ attr_reader :adapter
10
+
11
+ # Public: The ActiveSupport::Cache::Store to cache with.
12
+ attr_reader :cache
13
+
14
+ # Public: The ttl for all cached data.
15
+ attr_reader :ttl
16
+
17
+ # Public: The cache key where the set of known features is cached.
18
+ attr_reader :features_cache_key
19
+
20
+ # Public: Alias expires_in to ttl for compatibility.
21
+ alias_method :expires_in, :ttl
22
+
23
+ def initialize(adapter, cache, ttl = 300, prefix: nil)
24
+ @adapter = adapter
25
+ @cache = cache
26
+ @ttl = ttl
27
+
28
+ @cache_version = 'v1'.freeze
29
+ @namespace = "flipper/#{@cache_version}"
30
+ @namespace = @namespace.prepend(prefix) if prefix
31
+ @features_cache_key = "#{@namespace}/features"
32
+ end
33
+
34
+ # Public: Expire the cache for the set of known feature names.
35
+ def expire_features_cache
36
+ cache_delete @features_cache_key
37
+ end
38
+
39
+ # Public: Expire the cache for a given feature.
40
+ def expire_feature_cache(key)
41
+ cache_delete feature_cache_key(key)
42
+ end
43
+
44
+ # Public
45
+ def features
46
+ read_feature_keys
47
+ end
48
+
49
+ # Public
50
+ def add(feature)
51
+ result = @adapter.add(feature)
52
+ expire_features_cache
53
+ result
54
+ end
55
+
56
+ # Public
57
+ def remove(feature)
58
+ result = @adapter.remove(feature)
59
+ expire_features_cache
60
+ expire_feature_cache(feature.key)
61
+ result
62
+ end
63
+
64
+ # Public
65
+ def clear(feature)
66
+ result = @adapter.clear(feature)
67
+ expire_feature_cache(feature.key)
68
+ result
69
+ end
70
+
71
+ # Public
72
+ def get(feature)
73
+ read_feature(feature)
74
+ end
75
+
76
+ # Public
77
+ def get_multi(features)
78
+ read_many_features(features)
79
+ end
80
+
81
+ # Public
82
+ def get_all
83
+ features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
84
+ read_many_features(features)
85
+ end
86
+
87
+ # Public
88
+ def enable(feature, gate, thing)
89
+ result = @adapter.enable(feature, gate, thing)
90
+ expire_feature_cache(feature.key)
91
+ result
92
+ end
93
+
94
+ # Public
95
+ def disable(feature, gate, thing)
96
+ result = @adapter.disable(feature, gate, thing)
97
+ expire_feature_cache(feature.key)
98
+ result
99
+ end
100
+
101
+ # Public: Generate the cache key for a given feature.
102
+ #
103
+ # key - The String or Symbol feature key.
104
+ def feature_cache_key(key)
105
+ "#{@namespace}/feature/#{key}"
106
+ end
107
+
108
+ private
109
+
110
+ # Private: Returns the Set of known feature keys.
111
+ def read_feature_keys
112
+ cache_fetch(@features_cache_key) { @adapter.features }
113
+ end
114
+
115
+ # Private: Read through caching for a single feature.
116
+ def read_feature(feature)
117
+ cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
118
+ end
119
+
120
+ # Private: Given an array of features, attempts to read through cache in
121
+ # as few network calls as possible.
122
+ def read_many_features(features)
123
+ keys = features.map { |feature| feature_cache_key(feature.key) }
124
+ cache_result = cache_read_multi(keys)
125
+ uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
126
+
127
+ if uncached_features.any?
128
+ response = @adapter.get_multi(uncached_features)
129
+ response.each do |key, value|
130
+ cache_write feature_cache_key(key), value
131
+ cache_result[feature_cache_key(key)] = value
132
+ end
133
+ end
134
+
135
+ result = {}
136
+ features.each do |feature|
137
+ result[feature.key] = cache_result[feature_cache_key(feature.key)]
138
+ end
139
+ result
140
+ end
141
+ end
142
+ end
143
+ end
@@ -3,8 +3,7 @@ module Flipper
3
3
  class DualWrite
4
4
  include ::Flipper::Adapter
5
5
 
6
- # Public: The name of the adapter.
7
- attr_reader :name, :local, :remote
6
+ attr_reader :local, :remote
8
7
 
9
8
  # Public: Build a new sync instance.
10
9
  #
@@ -12,7 +11,6 @@ module Flipper
12
11
  # remote - The remote flipper adapter that writes should go to first (in
13
12
  # addition to the local adapter).
14
13
  def initialize(local, remote, options = {})
15
- @name = :dual_write
16
14
  @local = local
17
15
  @remote = remote
18
16
  end
@@ -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.
@@ -17,7 +14,6 @@ module Flipper
17
14
  # :errors - Array of exception types for which to failover
18
15
 
19
16
  def initialize(primary, secondary, options = {})
20
- @name = :failover
21
17
  @primary = primary
22
18
  @secondary = secondary
23
19
 
@@ -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
@@ -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,6 +36,17 @@ 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
+ key = key.to_s.downcase.gsub('_'.freeze, '-'.freeze).freeze
49
+ @headers[key] = value
31
50
  end
32
51
 
33
52
  def get(path)
@@ -77,18 +96,23 @@ module Flipper
77
96
 
78
97
  def build_request(http_method, uri, headers, options)
79
98
  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,
99
+ 'client-language' => "ruby",
100
+ 'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
101
+ 'client-platform' => RUBY_PLATFORM,
102
+ 'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
103
+ 'client-pid' => Process.pid.to_s,
104
+ 'client-thread' => Thread.current.object_id.to_s,
105
+ 'client-hostname' => Socket.gethostname,
87
106
  }.merge(headers)
88
107
 
89
108
  body = options[:body]
90
109
  request = http_method.new(uri.request_uri)
91
110
  request.initialize_http_header(request_headers)
111
+
112
+ client_frameworks.each do |framework, version|
113
+ request.add_field("client-framework", [framework, version].join("="))
114
+ end
115
+
92
116
  request.body = body if body
93
117
 
94
118
  if @basic_auth_username && @basic_auth_password
@@ -97,6 +121,10 @@ module Flipper
97
121
 
98
122
  request
99
123
  end
124
+
125
+ def client_frameworks
126
+ CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
127
+ end
100
128
  end
101
129
  end
102
130
  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,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,15 +59,15 @@ 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)
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
@@ -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