flipper 0.24.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +45 -14
  5. data/.github/workflows/examples.yml +39 -16
  6. data/Changelog.md +2 -443
  7. data/Gemfile +19 -11
  8. data/README.md +31 -27
  9. data/Rakefile +6 -4
  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/instrumentation.rb +1 -0
  31. data/examples/instrumentation_last_accessed_at.rb +1 -0
  32. data/examples/mirroring.rb +59 -0
  33. data/examples/strict.rb +18 -0
  34. data/exe/flipper +5 -0
  35. data/flipper-cloud.gemspec +19 -0
  36. data/flipper.gemspec +10 -6
  37. data/lib/flipper/actor.rb +6 -3
  38. data/lib/flipper/adapter.rb +33 -7
  39. data/lib/flipper/adapter_builder.rb +44 -0
  40. data/lib/flipper/adapters/actor_limit.rb +28 -0
  41. data/lib/flipper/adapters/cache_base.rb +143 -0
  42. data/lib/flipper/adapters/dual_write.rb +1 -3
  43. data/lib/flipper/adapters/failover.rb +0 -4
  44. data/lib/flipper/adapters/failsafe.rb +72 -0
  45. data/lib/flipper/adapters/http/client.rb +44 -20
  46. data/lib/flipper/adapters/http/error.rb +1 -1
  47. data/lib/flipper/adapters/http.rb +31 -16
  48. data/lib/flipper/adapters/instrumented.rb +25 -6
  49. data/lib/flipper/adapters/memoizable.rb +33 -21
  50. data/lib/flipper/adapters/memory.rb +81 -46
  51. data/lib/flipper/adapters/operation_logger.rb +17 -78
  52. data/lib/flipper/adapters/poll/poller.rb +2 -0
  53. data/lib/flipper/adapters/poll.rb +37 -0
  54. data/lib/flipper/adapters/pstore.rb +17 -11
  55. data/lib/flipper/adapters/read_only.rb +8 -41
  56. data/lib/flipper/adapters/strict.rb +45 -0
  57. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  58. data/lib/flipper/adapters/sync.rb +0 -4
  59. data/lib/flipper/adapters/wrapper.rb +54 -0
  60. data/lib/flipper/cli.rb +263 -0
  61. data/lib/flipper/cloud/configuration.rb +263 -0
  62. data/lib/flipper/cloud/dsl.rb +27 -0
  63. data/lib/flipper/cloud/message_verifier.rb +95 -0
  64. data/lib/flipper/cloud/middleware.rb +63 -0
  65. data/lib/flipper/cloud/routes.rb +14 -0
  66. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  67. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  68. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  69. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  70. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  71. data/lib/flipper/cloud/telemetry.rb +191 -0
  72. data/lib/flipper/cloud.rb +53 -0
  73. data/lib/flipper/configuration.rb +25 -4
  74. data/lib/flipper/dsl.rb +46 -45
  75. data/lib/flipper/engine.rb +102 -0
  76. data/lib/flipper/errors.rb +3 -20
  77. data/lib/flipper/export.rb +26 -0
  78. data/lib/flipper/exporter.rb +17 -0
  79. data/lib/flipper/exporters/json/export.rb +32 -0
  80. data/lib/flipper/exporters/json/v1.rb +33 -0
  81. data/lib/flipper/expression/builder.rb +73 -0
  82. data/lib/flipper/expression/constant.rb +25 -0
  83. data/lib/flipper/expression.rb +71 -0
  84. data/lib/flipper/expressions/all.rb +11 -0
  85. data/lib/flipper/expressions/any.rb +9 -0
  86. data/lib/flipper/expressions/boolean.rb +9 -0
  87. data/lib/flipper/expressions/comparable.rb +13 -0
  88. data/lib/flipper/expressions/duration.rb +28 -0
  89. data/lib/flipper/expressions/equal.rb +9 -0
  90. data/lib/flipper/expressions/greater_than.rb +9 -0
  91. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/less_than.rb +9 -0
  93. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  94. data/lib/flipper/expressions/not_equal.rb +9 -0
  95. data/lib/flipper/expressions/now.rb +9 -0
  96. data/lib/flipper/expressions/number.rb +9 -0
  97. data/lib/flipper/expressions/percentage.rb +9 -0
  98. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  99. data/lib/flipper/expressions/property.rb +9 -0
  100. data/lib/flipper/expressions/random.rb +9 -0
  101. data/lib/flipper/expressions/string.rb +9 -0
  102. data/lib/flipper/expressions/time.rb +9 -0
  103. data/lib/flipper/feature.rb +87 -26
  104. data/lib/flipper/feature_check_context.rb +10 -6
  105. data/lib/flipper/gate.rb +13 -11
  106. data/lib/flipper/gate_values.rb +5 -18
  107. data/lib/flipper/gates/actor.rb +10 -17
  108. data/lib/flipper/gates/boolean.rb +1 -1
  109. data/lib/flipper/gates/expression.rb +75 -0
  110. data/lib/flipper/gates/group.rb +5 -7
  111. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  112. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  113. data/lib/flipper/identifier.rb +2 -2
  114. data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
  115. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  116. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  117. data/lib/flipper/metadata.rb +7 -1
  118. data/lib/flipper/middleware/memoizer.rb +28 -22
  119. data/lib/flipper/model/active_record.rb +23 -0
  120. data/lib/flipper/poller.rb +118 -0
  121. data/lib/flipper/serializers/gzip.rb +22 -0
  122. data/lib/flipper/serializers/json.rb +17 -0
  123. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  124. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  125. data/lib/flipper/test_help.rb +43 -0
  126. data/lib/flipper/typecast.rb +59 -18
  127. data/lib/flipper/types/actor.rb +13 -13
  128. data/lib/flipper/types/group.rb +4 -4
  129. data/lib/flipper/types/percentage.rb +1 -1
  130. data/lib/flipper/version.rb +11 -1
  131. data/lib/flipper.rb +50 -11
  132. data/lib/generators/flipper/setup_generator.rb +63 -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/failsafe_spec.rb +58 -0
  145. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  146. data/spec/flipper/adapters/http_spec.rb +137 -55
  147. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  148. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  149. data/spec/flipper/adapters/memory_spec.rb +14 -3
  150. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  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 +164 -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 +181 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -73
  167. data/spec/flipper/engine_spec.rb +373 -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 +436 -33
  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 +23 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  203. data/spec/flipper/middleware/memoizer_spec.rb +74 -24
  204. data/spec/flipper/model/active_record_spec.rb +61 -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 +93 -29
  213. data/spec/spec_helper.rb +8 -14
  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 +23 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +19 -2
  223. data/test_rails/system/test_help_test.rb +51 -0
  224. metadata +223 -19
  225. data/lib/flipper/railtie.rb +0 -47
  226. data/spec/flipper/railtie_spec.rb +0 -73
@@ -0,0 +1,18 @@
1
+ require 'bundler/setup'
2
+ require 'flipper'
3
+
4
+ adapter = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
5
+ flipper = Flipper.new(adapter)
6
+
7
+ begin
8
+ puts "Checking :unknown_feature, which should raise an error."
9
+ flipper.enabled?(:unknown_feature)
10
+ warn "An error was not raised, but should have been"
11
+ exit 1
12
+ rescue Flipper::Adapters::Strict::NotFound => exception
13
+ puts "Ok, the exepcted error was raised: #{exception.message}"
14
+ end
15
+
16
+ puts "Flipper.add(:new_feature)"
17
+ flipper.add(:new_feature)
18
+ puts "Flipper.enabled?(:new_feature) => #{flipper.enabled?(:new_feature)}"
data/exe/flipper ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "flipper/cli"
4
+
5
+ Flipper::CLI.run(ARGV)
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/flipper/version', __FILE__)
3
+ require File.expand_path('../lib/flipper/metadata', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ['John Nunemaker']
7
+ gem.email = 'support@flippercloud.io'
8
+ gem.summary = '[DEPRECATED] This gem has been merged into the `flipper` gem'
9
+ gem.license = 'MIT'
10
+ gem.homepage = 'https://www.flippercloud.io'
11
+
12
+ gem.files = [ 'lib/flipper-cloud.rb', 'lib/flipper/version.rb' ]
13
+ gem.name = 'flipper-cloud'
14
+ gem.require_paths = ['lib']
15
+ gem.version = Flipper::VERSION
16
+ gem.metadata = Flipper::METADATA
17
+
18
+ gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
19
+ end
data/flipper.gemspec CHANGED
@@ -6,14 +6,13 @@ plugin_files = []
6
6
  plugin_test_files = []
7
7
 
8
8
  Dir['flipper-*.gemspec'].map do |gemspec|
9
- spec = eval(File.read(gemspec))
9
+ spec = Gem::Specification.load(gemspec)
10
10
  plugin_files << spec.files
11
11
  plugin_test_files << spec.files
12
12
  end
13
13
 
14
14
  ignored_files = plugin_files
15
15
  ignored_files << Dir['script/*']
16
- ignored_files << '.travis.yml'
17
16
  ignored_files << '.gitignore'
18
17
  ignored_files << 'Guardfile'
19
18
  ignored_files.flatten!.uniq!
@@ -23,16 +22,21 @@ ignored_test_files.flatten!.uniq!
23
22
 
24
23
  Gem::Specification.new do |gem|
25
24
  gem.authors = ['John Nunemaker']
26
- gem.email = ['nunemaker@gmail.com']
27
- gem.summary = 'Feature flipper for ANYTHING'
28
- gem.homepage = 'https://github.com/jnunemaker/flipper'
25
+ gem.email = 'support@flippercloud.io'
26
+ gem.summary = 'Beautiful, performant feature flags for Ruby and Rails.'
27
+ gem.homepage = 'https://www.flippercloud.io/docs'
29
28
  gem.license = 'MIT'
30
29
 
31
- gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
30
+ gem.bindir = "exe"
31
+ gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
32
32
  gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
33
33
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
34
34
  gem.name = 'flipper'
35
35
  gem.require_paths = ['lib']
36
36
  gem.version = Flipper::VERSION
37
37
  gem.metadata = Flipper::METADATA
38
+
39
+ gem.add_dependency 'concurrent-ruby', '< 2'
40
+
41
+ gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
38
42
  end
data/lib/flipper/actor.rb CHANGED
@@ -2,14 +2,17 @@
2
2
  # to Flipper::Feature#enabled?.
3
3
  module Flipper
4
4
  class Actor
5
- attr_reader :flipper_id
5
+ attr_reader :flipper_id, :flipper_properties
6
6
 
7
- def initialize(flipper_id)
7
+ def initialize(flipper_id, flipper_properties = {})
8
8
  @flipper_id = flipper_id
9
+ @flipper_properties = flipper_properties
9
10
  end
10
11
 
11
12
  def eql?(other)
12
- self.class.eql?(other.class) && @flipper_id == other.flipper_id
13
+ self.class.eql?(other.class) &&
14
+ @flipper_id == other.flipper_id &&
15
+ @flipper_properties == other.flipper_properties
13
16
  end
14
17
  alias_method :==, :eql?
15
18
 
@@ -1,7 +1,3 @@
1
- require "set"
2
- require "flipper/feature"
3
- require "flipper/adapters/sync/synchronizer"
4
-
5
1
  module Flipper
6
2
  # Adding a module include so we have some hooks for stuff down the road
7
3
  module Adapter
@@ -16,10 +12,20 @@ module Flipper
16
12
  boolean: nil,
17
13
  groups: Set.new,
18
14
  actors: Set.new,
15
+ expression: nil,
19
16
  percentage_of_actors: nil,
20
17
  percentage_of_time: nil,
21
18
  }
22
19
  end
20
+
21
+ def from(source)
22
+ return source if source.is_a?(Flipper::Adapter)
23
+ source.adapter
24
+ end
25
+ end
26
+
27
+ def read_only?
28
+ false
23
29
  end
24
30
 
25
31
  # Public: Get all features and gate values in one call. Defaults to one call
@@ -43,14 +49,34 @@ module Flipper
43
49
 
44
50
  # Public: Ensure that adapter is in sync with source adapter provided.
45
51
  #
46
- # Returns result of Synchronizer#call.
47
- def import(source_adapter)
48
- Adapters::Sync::Synchronizer.new(self, source_adapter, raise: true).call
52
+ # source - The source dsl, adapter or export to import.
53
+ #
54
+ # Returns true if successful.
55
+ def import(source)
56
+ Adapters::Sync::Synchronizer.new(self, self.class.from(source), raise: true).call
57
+ true
58
+ end
59
+
60
+ # Public: Exports the adapter in a given format for a given format version.
61
+ #
62
+ # Returns a Flipper::Export instance.
63
+ def export(format: :json, version: 1)
64
+ Flipper::Exporter.build(format: format, version: version).call(self)
49
65
  end
50
66
 
51
67
  # Public: Default config for a feature's gate values.
52
68
  def default_config
53
69
  self.class.default_config
54
70
  end
71
+
72
+ # Public: default name of the adapter
73
+ def name
74
+ @name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
75
+ end
55
76
  end
56
77
  end
78
+
79
+ require "set"
80
+ require "flipper/exporter"
81
+ require "flipper/feature"
82
+ require "flipper/adapters/sync/synchronizer"
@@ -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
@@ -0,0 +1,28 @@
1
+ module Flipper
2
+ module Adapters
3
+ class ActorLimit < Wrapper
4
+ LimitExceeded = Class.new(Flipper::Error)
5
+
6
+ attr_reader :limit
7
+
8
+ def initialize(adapter, limit = 100)
9
+ super(adapter)
10
+ @limit = limit
11
+ end
12
+
13
+ def enable(feature, gate, resource)
14
+ if gate.is_a?(Flipper::Gates::Actor) && over_limit?(feature)
15
+ raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def over_limit?(feature)
24
+ feature.actors_value.size >= @limit
25
+ end
26
+ end
27
+ end
28
+ end
@@ -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
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
 
@@ -0,0 +1,72 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Failsafe
4
+ include ::Flipper::Adapter
5
+
6
+ # Public: Build a new Failsafe instance.
7
+ #
8
+ # adapter - Flipper adapter to guard.
9
+ # options - Hash of options:
10
+ # :errors - Array of exception types for which to fail safe
11
+
12
+ def initialize(adapter, options = {})
13
+ @adapter = adapter
14
+ @errors = options.fetch(:errors, [StandardError])
15
+ end
16
+
17
+ def features
18
+ @adapter.features
19
+ rescue *@errors
20
+ Set.new
21
+ end
22
+
23
+ def add(feature)
24
+ @adapter.add(feature)
25
+ rescue *@errors
26
+ false
27
+ end
28
+
29
+ def remove(feature)
30
+ @adapter.remove(feature)
31
+ rescue *@errors
32
+ false
33
+ end
34
+
35
+ def clear(feature)
36
+ @adapter.clear(feature)
37
+ rescue *@errors
38
+ false
39
+ end
40
+
41
+ def get(feature)
42
+ @adapter.get(feature)
43
+ rescue *@errors
44
+ {}
45
+ end
46
+
47
+ def get_multi(features)
48
+ @adapter.get_multi(features)
49
+ rescue *@errors
50
+ {}
51
+ end
52
+
53
+ def get_all
54
+ @adapter.get_all
55
+ rescue *@errors
56
+ {}
57
+ end
58
+
59
+ def enable(feature, gate, thing)
60
+ @adapter.enable(feature, gate, thing)
61
+ rescue *@errors
62
+ false
63
+ end
64
+
65
+ def disable(feature, gate, thing)
66
+ @adapter.disable(feature, gate, thing)
67
+ rescue *@errors
68
+ false
69
+ end
70
+ end
71
+ end
72
+ end
@@ -7,22 +7,46 @@ 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
+
25
+ attr_reader :uri, :headers
26
+ attr_reader :basic_auth_username, :basic_auth_password
27
+ attr_reader :read_timeout, :open_timeout, :write_timeout
28
+ attr_reader :max_retries, :debug_output
29
+
17
30
  def initialize(options = {})
18
31
  @uri = URI(options.fetch(:url))
19
- @headers = DEFAULT_HEADERS.merge(options[:headers] || {})
20
32
  @basic_auth_username = options[:basic_auth_username]
21
33
  @basic_auth_password = options[:basic_auth_password]
22
34
  @read_timeout = options[:read_timeout]
23
35
  @open_timeout = options[:open_timeout]
24
36
  @write_timeout = options[:write_timeout]
37
+ @max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
25
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
26
50
  end
27
51
 
28
52
  def get(path)
@@ -58,7 +82,8 @@ module Flipper
58
82
  http = Net::HTTP.new(uri.host, uri.port)
59
83
  http.read_timeout = @read_timeout if @read_timeout
60
84
  http.open_timeout = @open_timeout if @open_timeout
61
- apply_write_timeout(http)
85
+ http.max_retries = @max_retries if @max_retries
86
+ http.write_timeout = @write_timeout if @write_timeout
62
87
  http.set_debug_output(@debug_output) if @debug_output
63
88
 
64
89
  if uri.scheme == HTTPS_SCHEME
@@ -71,18 +96,23 @@ module Flipper
71
96
 
72
97
  def build_request(http_method, uri, headers, options)
73
98
  request_headers = {
74
- "Client-Language" => "ruby",
75
- "Client-Language-Version" => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
76
- "Client-Platform" => RUBY_PLATFORM,
77
- "Client-Engine" => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
78
- "Client-Pid" => Process.pid.to_s,
79
- "Client-Thread" => Thread.current.object_id.to_s,
80
- "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,
81
106
  }.merge(headers)
82
107
 
83
108
  body = options[:body]
84
109
  request = http_method.new(uri.request_uri)
85
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
+
86
116
  request.body = body if body
87
117
 
88
118
  if @basic_auth_username && @basic_auth_password
@@ -92,14 +122,8 @@ module Flipper
92
122
  request
93
123
  end
94
124
 
95
- def apply_write_timeout(http)
96
- if @write_timeout
97
- if RUBY_VERSION >= '2.6.0'
98
- http.write_timeout = @write_timeout
99
- else
100
- Kernel.warn("Warning: option :write_timeout requires Ruby version 2.6.0 or later")
101
- end
102
- end
125
+ def client_frameworks
126
+ CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
103
127
  end
104
128
  end
105
129
  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"]}"