flipper 1.0.0 → 1.1.0

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