flipper 0.28.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 (195) 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 +52 -10
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -526
  7. data/Gemfile +17 -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/app.ru +12 -0
  14. data/examples/cloud/backoff_policy.rb +13 -0
  15. data/examples/cloud/basic.rb +22 -0
  16. data/examples/cloud/cloud_setup.rb +20 -0
  17. data/examples/cloud/forked.rb +36 -0
  18. data/examples/cloud/import.rb +17 -0
  19. data/examples/cloud/threaded.rb +33 -0
  20. data/examples/dsl.rb +0 -14
  21. data/examples/expressions.rb +213 -0
  22. data/examples/mirroring.rb +59 -0
  23. data/examples/strict.rb +18 -0
  24. data/exe/flipper +5 -0
  25. data/flipper-cloud.gemspec +19 -0
  26. data/flipper.gemspec +8 -4
  27. data/lib/flipper/actor.rb +6 -3
  28. data/lib/flipper/adapter.rb +10 -0
  29. data/lib/flipper/adapter_builder.rb +44 -0
  30. data/lib/flipper/adapters/actor_limit.rb +28 -0
  31. data/lib/flipper/adapters/cache_base.rb +143 -0
  32. data/lib/flipper/adapters/dual_write.rb +1 -3
  33. data/lib/flipper/adapters/failover.rb +0 -4
  34. data/lib/flipper/adapters/failsafe.rb +0 -4
  35. data/lib/flipper/adapters/http/client.rb +40 -12
  36. data/lib/flipper/adapters/http/error.rb +2 -2
  37. data/lib/flipper/adapters/http.rb +19 -14
  38. data/lib/flipper/adapters/instrumented.rb +0 -4
  39. data/lib/flipper/adapters/memoizable.rb +14 -19
  40. data/lib/flipper/adapters/memory.rb +37 -19
  41. data/lib/flipper/adapters/operation_logger.rb +18 -92
  42. data/lib/flipper/adapters/poll.rb +16 -3
  43. data/lib/flipper/adapters/pstore.rb +17 -11
  44. data/lib/flipper/adapters/read_only.rb +8 -41
  45. data/lib/flipper/adapters/strict.rb +45 -0
  46. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  47. data/lib/flipper/adapters/sync.rb +0 -4
  48. data/lib/flipper/adapters/wrapper.rb +54 -0
  49. data/lib/flipper/cli.rb +263 -0
  50. data/lib/flipper/cloud/configuration.rb +266 -0
  51. data/lib/flipper/cloud/dsl.rb +27 -0
  52. data/lib/flipper/cloud/message_verifier.rb +95 -0
  53. data/lib/flipper/cloud/middleware.rb +63 -0
  54. data/lib/flipper/cloud/routes.rb +14 -0
  55. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  56. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  57. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  58. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  59. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  60. data/lib/flipper/cloud/telemetry.rb +191 -0
  61. data/lib/flipper/cloud.rb +53 -0
  62. data/lib/flipper/configuration.rb +25 -4
  63. data/lib/flipper/dsl.rb +47 -42
  64. data/lib/flipper/engine.rb +102 -0
  65. data/lib/flipper/export.rb +0 -2
  66. data/lib/flipper/exporters/json/export.rb +1 -1
  67. data/lib/flipper/exporters/json/v1.rb +1 -1
  68. data/lib/flipper/expression/builder.rb +73 -0
  69. data/lib/flipper/expression/constant.rb +25 -0
  70. data/lib/flipper/expression.rb +71 -0
  71. data/lib/flipper/expressions/all.rb +9 -0
  72. data/lib/flipper/expressions/any.rb +9 -0
  73. data/lib/flipper/expressions/boolean.rb +9 -0
  74. data/lib/flipper/expressions/comparable.rb +13 -0
  75. data/lib/flipper/expressions/duration.rb +28 -0
  76. data/lib/flipper/expressions/equal.rb +9 -0
  77. data/lib/flipper/expressions/greater_than.rb +9 -0
  78. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  79. data/lib/flipper/expressions/less_than.rb +9 -0
  80. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  81. data/lib/flipper/expressions/not_equal.rb +9 -0
  82. data/lib/flipper/expressions/now.rb +9 -0
  83. data/lib/flipper/expressions/number.rb +9 -0
  84. data/lib/flipper/expressions/percentage.rb +9 -0
  85. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  86. data/lib/flipper/expressions/property.rb +9 -0
  87. data/lib/flipper/expressions/random.rb +9 -0
  88. data/lib/flipper/expressions/string.rb +9 -0
  89. data/lib/flipper/expressions/time.rb +9 -0
  90. data/lib/flipper/feature.rb +63 -1
  91. data/lib/flipper/gate.rb +2 -1
  92. data/lib/flipper/gate_values.rb +5 -2
  93. data/lib/flipper/gates/expression.rb +75 -0
  94. data/lib/flipper/instrumentation/log_subscriber.rb +28 -5
  95. data/lib/flipper/instrumentation/statsd.rb +4 -2
  96. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  97. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  98. data/lib/flipper/metadata.rb +8 -1
  99. data/lib/flipper/middleware/memoizer.rb +30 -14
  100. data/lib/flipper/model/active_record.rb +23 -0
  101. data/lib/flipper/poller.rb +10 -9
  102. data/lib/flipper/serializers/gzip.rb +22 -0
  103. data/lib/flipper/serializers/json.rb +17 -0
  104. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  105. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  106. data/lib/flipper/test_help.rb +43 -0
  107. data/lib/flipper/typecast.rb +37 -9
  108. data/lib/flipper/types/percentage.rb +1 -1
  109. data/lib/flipper/version.rb +11 -1
  110. data/lib/flipper.rb +44 -7
  111. data/lib/generators/flipper/setup_generator.rb +68 -0
  112. data/lib/generators/flipper/templates/initializer.rb +45 -0
  113. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  114. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  115. data/lib/generators/flipper/update_generator.rb +35 -0
  116. data/package-lock.json +41 -0
  117. data/package.json +10 -0
  118. data/spec/fixtures/environment.rb +1 -0
  119. data/spec/flipper/adapter_builder_spec.rb +72 -0
  120. data/spec/flipper/adapter_spec.rb +1 -0
  121. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  122. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  123. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  124. data/spec/flipper/adapters/http_spec.rb +135 -74
  125. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  126. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  127. data/spec/flipper/adapters/memory_spec.rb +11 -2
  128. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  129. data/spec/flipper/adapters/poll_spec.rb +41 -0
  130. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  131. data/spec/flipper/adapters/strict_spec.rb +64 -0
  132. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  133. data/spec/flipper/cli_spec.rb +166 -0
  134. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  135. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  136. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  137. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  138. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  139. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  140. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  141. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  142. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  143. data/spec/flipper/cloud_spec.rb +186 -0
  144. data/spec/flipper/configuration_spec.rb +17 -0
  145. data/spec/flipper/dsl_spec.rb +34 -73
  146. data/spec/flipper/engine_spec.rb +374 -0
  147. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  148. data/spec/flipper/expression/builder_spec.rb +248 -0
  149. data/spec/flipper/expression_spec.rb +188 -0
  150. data/spec/flipper/expressions/all_spec.rb +15 -0
  151. data/spec/flipper/expressions/any_spec.rb +15 -0
  152. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  153. data/spec/flipper/expressions/duration_spec.rb +43 -0
  154. data/spec/flipper/expressions/equal_spec.rb +24 -0
  155. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  156. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  157. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  159. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  160. data/spec/flipper/expressions/now_spec.rb +11 -0
  161. data/spec/flipper/expressions/number_spec.rb +21 -0
  162. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  163. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  164. data/spec/flipper/expressions/property_spec.rb +13 -0
  165. data/spec/flipper/expressions/random_spec.rb +9 -0
  166. data/spec/flipper/expressions/string_spec.rb +11 -0
  167. data/spec/flipper/expressions/time_spec.rb +13 -0
  168. data/spec/flipper/feature_spec.rb +380 -10
  169. data/spec/flipper/gate_values_spec.rb +2 -2
  170. data/spec/flipper/gates/expression_spec.rb +108 -0
  171. data/spec/flipper/identifier_spec.rb +4 -5
  172. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  173. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  174. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  175. data/spec/flipper/model/active_record_spec.rb +72 -0
  176. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  177. data/spec/flipper/serializers/json_spec.rb +13 -0
  178. data/spec/flipper/typecast_spec.rb +43 -7
  179. data/spec/flipper/types/actor_spec.rb +18 -1
  180. data/spec/flipper_integration_spec.rb +114 -16
  181. data/spec/flipper_spec.rb +87 -29
  182. data/spec/spec_helper.rb +17 -17
  183. data/spec/support/actor_names.yml +1 -0
  184. data/spec/support/fail_on_output.rb +8 -0
  185. data/spec/support/fake_backoff_policy.rb +15 -0
  186. data/spec/support/spec_helpers.rb +34 -8
  187. data/test/adapters/actor_limit_test.rb +20 -0
  188. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  189. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  190. data/test_rails/helper.rb +22 -2
  191. data/test_rails/system/test_help_test.rb +52 -0
  192. metadata +179 -19
  193. data/.tool-versions +0 -1
  194. data/lib/flipper/railtie.rb +0 -47
  195. data/spec/flipper/railtie_spec.rb +0 -109
@@ -1,8 +1,10 @@
1
1
  require 'set'
2
+ require "flipper/serializers/json"
3
+ require "flipper/serializers/gzip"
2
4
 
3
5
  module Flipper
4
- module Typecast
5
- TruthMap = {
6
+ class Typecast
7
+ TRUTH_MAP = {
6
8
  true => true,
7
9
  1 => true,
8
10
  'true' => true,
@@ -13,7 +15,7 @@ module Flipper
13
15
  #
14
16
  # Returns true or false.
15
17
  def self.to_boolean(value)
16
- !!TruthMap[value]
18
+ !!TRUTH_MAP[value]
17
19
  end
18
20
 
19
21
  # Internal: Convert value to an integer.
@@ -36,17 +38,25 @@ module Flipper
36
38
  raise ArgumentError, "#{value.inspect} cannot be converted to a float"
37
39
  end
38
40
 
39
- # Internal: Convert value to a percentage.
41
+ # Internal: Convert value to a number.
40
42
  #
41
43
  # Returns a Integer or Float representation of the value.
42
44
  # Raises ArgumentError if conversion is not possible.
43
- def self.to_percentage(value)
44
- result_to_f = value.to_f
45
- result_to_i = result_to_f.to_i
46
- result_to_f == result_to_i ? result_to_i : result_to_f
45
+ def self.to_number(value)
46
+ case value
47
+ when Numeric
48
+ value
49
+ when String
50
+ value.include?('.') ? to_float(value) : to_integer(value)
51
+ when NilClass
52
+ 0
53
+ else
54
+ value.to_f
55
+ end
47
56
  rescue NoMethodError
48
- raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
57
+ raise ArgumentError, "#{value.inspect} cannot be converted to a number"
49
58
  end
59
+ singleton_class.send(:alias_method, :to_percentage, :to_number)
50
60
 
51
61
  # Internal: Convert value to a set.
52
62
  #
@@ -71,6 +81,8 @@ module Flipper
71
81
  normalized_value = case value
72
82
  when Array, Set
73
83
  value.to_set
84
+ when Hash
85
+ value
74
86
  else
75
87
  value ? value.to_s : value
76
88
  end
@@ -79,5 +91,21 @@ module Flipper
79
91
  end
80
92
  normalized_source
81
93
  end
94
+
95
+ def self.to_json(source)
96
+ Serializers::Json.serialize(source)
97
+ end
98
+
99
+ def self.from_json(source)
100
+ Serializers::Json.deserialize(source)
101
+ end
102
+
103
+ def self.to_gzip(source)
104
+ Serializers::Gzip.serialize(source)
105
+ end
106
+
107
+ def self.from_gzip(source)
108
+ Serializers::Gzip.deserialize(source)
109
+ end
82
110
  end
83
111
  end
@@ -4,7 +4,7 @@ module Flipper
4
4
  module Types
5
5
  class Percentage < Type
6
6
  def initialize(value)
7
- value = Typecast.to_percentage(value)
7
+ value = Typecast.to_number(value)
8
8
 
9
9
  if value < 0 || value > 100
10
10
  raise ArgumentError,
@@ -1,3 +1,13 @@
1
1
  module Flipper
2
- VERSION = '0.28.0'.freeze
2
+ VERSION = '1.3.6'.freeze
3
+
4
+ REQUIRED_RUBY_VERSION = '2.6'.freeze
5
+ NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
6
+
7
+ REQUIRED_RAILS_VERSION = '5.2'.freeze
8
+ NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
9
+
10
+ def self.deprecated_ruby_version?
11
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
12
+ end
3
13
  end
data/lib/flipper.rb CHANGED
@@ -56,18 +56,50 @@ module Flipper
56
56
  # Public: All the methods delegated to instance. These should match the
57
57
  # interface of Flipper::DSL.
58
58
  def_delegators :instance,
59
- :enabled?, :enable, :disable, :bool, :boolean,
60
- :enable_actor, :disable_actor, :actor,
59
+ :enabled?, :enable, :disable,
60
+ :enable_expression, :disable_expression,
61
+ :expression, :add_expression, :remove_expression,
62
+ :enable_actor, :disable_actor,
61
63
  :enable_group, :disable_group,
62
64
  :enable_percentage_of_actors, :disable_percentage_of_actors,
63
- :actors, :percentage_of_actors,
64
65
  :enable_percentage_of_time, :disable_percentage_of_time,
65
- :time, :percentage_of_time,
66
66
  :features, :feature, :[], :preload, :preload_all,
67
67
  :adapter, :add, :exist?, :remove, :import, :export,
68
- :memoize=, :memoizing?,
68
+ :memoize=, :memoizing?, :read_only?,
69
69
  :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
70
70
 
71
+ def any(*args)
72
+ Expression.build({ Any: args.flatten })
73
+ end
74
+
75
+ def all(*args)
76
+ Expression.build({ All: args.flatten })
77
+ end
78
+
79
+ def constant(value)
80
+ Expression.build(value)
81
+ end
82
+
83
+ def property(name)
84
+ Expression.build({ Property: name })
85
+ end
86
+
87
+ def string(value)
88
+ Expression.build({ String: value })
89
+ end
90
+
91
+ def number(value)
92
+ Expression.build({ Number: value })
93
+ end
94
+
95
+ def boolean(value)
96
+ Expression.build({ Boolean: value })
97
+ end
98
+
99
+ def random(max)
100
+ Expression.build({ Random: max })
101
+ end
102
+
71
103
  # Public: Use this to register a group by name.
72
104
  #
73
105
  # name - The Symbol name of the group.
@@ -142,9 +174,13 @@ end
142
174
 
143
175
  require 'flipper/actor'
144
176
  require 'flipper/adapter'
177
+ require 'flipper/adapters/wrapper'
178
+ require 'flipper/adapters/actor_limit'
179
+ require 'flipper/adapters/instrumented'
145
180
  require 'flipper/adapters/memoizable'
146
181
  require 'flipper/adapters/memory'
147
- require 'flipper/adapters/instrumented'
182
+ require 'flipper/adapters/strict'
183
+ require 'flipper/adapter_builder'
148
184
  require 'flipper/configuration'
149
185
  require 'flipper/dsl'
150
186
  require 'flipper/errors'
@@ -157,6 +193,7 @@ require 'flipper/middleware/memoizer'
157
193
  require 'flipper/middleware/setup_env'
158
194
  require 'flipper/poller'
159
195
  require 'flipper/registry'
196
+ require 'flipper/expression'
160
197
  require 'flipper/type'
161
198
  require 'flipper/types/actor'
162
199
  require 'flipper/types/boolean'
@@ -167,4 +204,4 @@ require 'flipper/types/percentage_of_time'
167
204
  require 'flipper/typecast'
168
205
  require 'flipper/version'
169
206
 
170
- require "flipper/railtie" if defined?(Rails::Railtie)
207
+ require "flipper/engine" if defined?(Rails)
@@ -0,0 +1,68 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module Flipper
4
+ module Generators
5
+ class SetupGenerator < ::Rails::Generators::Base
6
+ desc 'Peform any necessary steps to install Flipper'
7
+ source_paths << File.expand_path('templates', __dir__)
8
+
9
+ class_option :token, type: :string, default: nil, aliases: '-t',
10
+ desc: "Your personal environment token for Flipper Cloud"
11
+
12
+ def generate_initializer
13
+ template 'initializer.rb', 'config/initializers/flipper.rb'
14
+ end
15
+
16
+ def generate_active_record
17
+ invoke 'flipper:active_record' if defined?(Flipper::Adapters::ActiveRecord)
18
+ end
19
+
20
+ def configure_cloud_token
21
+ return unless options[:token]
22
+
23
+ configure_with_dotenv || configure_with_credentials
24
+ end
25
+
26
+ private
27
+
28
+ def configure_with_dotenv
29
+ ['.env.development', '.env.local', '.env'].detect do |file|
30
+ next unless exists?(file)
31
+ append_to_file file, "\nFLIPPER_CLOUD_TOKEN=#{options[:token]}\n"
32
+ end
33
+ end
34
+
35
+ def configure_with_credentials
36
+ return unless exists?("config/credentials.yml.enc") && (ENV["RAILS_MASTER_KEY"] || exists?("config/master.key"))
37
+
38
+ content = "flipper:\n cloud_token: #{options[:token]}\n"
39
+ action InjectIntoEncryptedFile.new(self, Rails.application.credentials, content, after: /\z/)
40
+ end
41
+
42
+ # Check if a file exists in the destination root
43
+ def exists?(path)
44
+ File.exist?(File.expand_path(path, destination_root))
45
+ end
46
+
47
+ # Action to inject content into ActiveSupport::EncryptedFile
48
+ class InjectIntoEncryptedFile < Thor::Actions::InjectIntoFile
49
+ def initialize(base, encrypted_file, data, config)
50
+ @encrypted_file = encrypted_file
51
+ super(base, encrypted_file.content_path, data, config)
52
+ end
53
+
54
+ def content
55
+ @content ||= @encrypted_file.read
56
+ end
57
+
58
+ def replace!(regexp, string, force)
59
+ if force || !replacement_present?
60
+ success = content.gsub!(regexp, string)
61
+ @encrypted_file.write content unless pretend?
62
+ success
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,45 @@
1
+ Rails.application.configure do
2
+ ## Memoization ensures that only one adapter call is made per feature per request.
3
+ ## For more info, see https://www.flippercloud.io/docs/optimization#memoization
4
+ # config.flipper.memoize = true
5
+
6
+ ## Flipper preloads all features before each request, which is recommended if:
7
+ ## * you have a limited number of features (< 100?)
8
+ ## * most of your requests depend on most of your features
9
+ ## * you have limited gate data combined across all features (< 1k enabled gates, like individual actors, across all features)
10
+ ##
11
+ ## For more info, see https://www.flippercloud.io/docs/optimization#preloading
12
+ # config.flipper.preload = true
13
+
14
+ ## Warn or raise an error if an unknown feature is checked
15
+ ## Can be set to `:warn`, `:raise`, or `false`
16
+ # config.flipper.strict = Rails.env.development? && :warn
17
+
18
+ ## Show Flipper checks in logs
19
+ # config.flipper.log = true
20
+
21
+ ## Reconfigure Flipper to use the Memory adapter and disable Cloud in tests
22
+ # config.flipper.test_help = true
23
+
24
+ ## The path that Flipper Cloud will use to sync features
25
+ # config.flipper.cloud_path = "_flipper"
26
+
27
+ ## The instrumenter that Flipper will use. Defaults to ActiveSupport::Notifications.
28
+ # config.flipper.instrumenter = ActiveSupport::Notifications
29
+ end
30
+
31
+ Flipper.configure do |config|
32
+ ## Configure other adapters that you want to use here:
33
+ ## See http://flippercloud.io/docs/adapters
34
+ # config.use Flipper::Adapters::ActiveSupportCacheStore, Rails.cache, expires_in: 5.minutes
35
+ end
36
+
37
+ ## Register a group that can be used for enabling features.
38
+ ##
39
+ ## Flipper.enable_group :my_feature, :admins
40
+ ##
41
+ ## See https://www.flippercloud.io/docs/features#enablement-group
42
+ #
43
+ # Flipper.register(:admins) do |actor|
44
+ # actor.respond_to?(:admin?) && actor.admin?
45
+ # end
@@ -0,0 +1,22 @@
1
+ class CreateFlipperTables < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ create_table :flipper_features do |t|
4
+ t.string :key, null: false
5
+ t.timestamps null: false
6
+ end
7
+ add_index :flipper_features, :key, unique: true
8
+
9
+ create_table :flipper_gates do |t|
10
+ t.string :feature_key, null: false
11
+ t.string :key, null: false
12
+ t.string :value
13
+ t.timestamps null: false
14
+ end
15
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true
16
+ end
17
+
18
+ def down
19
+ drop_table :flipper_gates
20
+ drop_table :flipper_features
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ChangeFlipperGatesValueToText < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ # Ensure this incremental update migration is idempotent
6
+ return unless connection.column_exists? :flipper_gates, :value, :string
7
+
8
+ if index_exists? :flipper_gates, [:feature_key, :key, :value]
9
+ remove_index :flipper_gates, [:feature_key, :key, :value]
10
+ end
11
+ change_column :flipper_gates, :value, :text
12
+ add_index :flipper_gates, [:feature_key, :key, :value], unique: true, length: { value: 255 }
13
+ end
14
+
15
+ def down
16
+ change_column :flipper_gates, :value, :string
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Flipper
7
+ module Generators
8
+ #
9
+ # Rails generator used for updating Flipper in a Rails application.
10
+ # Run it with +bin/rails g flipper:update+ in your console.
11
+ #
12
+ class UpdateGenerator < Rails::Generators::Base
13
+ include ActiveRecord::Generators::Migration
14
+
15
+ TEMPLATES = File.join(File.dirname(__FILE__), 'templates/update')
16
+ source_paths << TEMPLATES
17
+
18
+ # Generates incremental migration files unless they already exist.
19
+ # All migrations should be idempotent e.g. +add_index+ is guarded with +if_index_exists?+
20
+ def update_migration_files
21
+ migration_templates = Dir.children(File.join(TEMPLATES, 'migrations')).sort
22
+ migration_templates.each do |template_file|
23
+ destination_file = template_file.match(/^\d*_(.*\.rb)/)[1] # 01_create_flipper_tables.rb.erb => create_flipper_tables.rb
24
+ migration_template "migrations/#{template_file}", File.join(db_migrate_path, destination_file), skip: true
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def migration_version
31
+ "[#{ActiveRecord::VERSION::STRING.to_f}]"
32
+ end
33
+ end
34
+ end
35
+ end
data/package-lock.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "flipper",
3
+ "lockfileVersion": 3,
4
+ "requires": true,
5
+ "packages": {
6
+ "": {
7
+ "hasInstallScript": true,
8
+ "dependencies": {
9
+ "@popperjs/core": "^2.11.8",
10
+ "bootstrap": "^5.3.3"
11
+ }
12
+ },
13
+ "node_modules/@popperjs/core": {
14
+ "version": "2.11.8",
15
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
16
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
17
+ "funding": {
18
+ "type": "opencollective",
19
+ "url": "https://opencollective.com/popperjs"
20
+ }
21
+ },
22
+ "node_modules/bootstrap": {
23
+ "version": "5.3.3",
24
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
25
+ "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
26
+ "funding": [
27
+ {
28
+ "type": "github",
29
+ "url": "https://github.com/sponsors/twbs"
30
+ },
31
+ {
32
+ "type": "opencollective",
33
+ "url": "https://opencollective.com/bootstrap"
34
+ }
35
+ ],
36
+ "peerDependencies": {
37
+ "@popperjs/core": "^2.11.8"
38
+ }
39
+ }
40
+ }
41
+ }
data/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@popperjs/core": "^2.11.8",
5
+ "bootstrap": "^5.3.3"
6
+ },
7
+ "scripts": {
8
+ "postinstall": "script/vendor-assets"
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ # Placeholder for config/environment.rb
@@ -0,0 +1,72 @@
1
+ RSpec.describe Flipper::AdapterBuilder do
2
+ describe "#initialize" do
3
+ it "instance_eval's block with no arg" do
4
+ called = false
5
+ self_in_block = nil
6
+
7
+ described_class.new do
8
+ called = true
9
+ self_in_block = self
10
+ end
11
+
12
+ expect(self_in_block).to be_instance_of(described_class)
13
+ expect(called).to be(true)
14
+ end
15
+
16
+ it "evals block with arg" do
17
+ called = false
18
+ self_outside_block = self
19
+ self_in_block = nil
20
+
21
+ described_class.new do |arg|
22
+ called = true
23
+ self_in_block = self
24
+ expect(arg).to be_instance_of(described_class)
25
+ end
26
+
27
+ expect(self_in_block).to be(self_outside_block)
28
+ expect(called).to be(true)
29
+ end
30
+ end
31
+
32
+ describe "#use" do
33
+ it "wraps the store adapter with the given adapter" do
34
+ subject.use(Flipper::Adapters::Memoizable)
35
+ subject.use(Flipper::Adapters::Strict, :warn)
36
+
37
+ memoizable_adapter = subject.to_adapter
38
+ strict_adapter = memoizable_adapter.adapter
39
+ memory_adapter = strict_adapter.adapter
40
+
41
+ expect(memoizable_adapter).to be_instance_of(Flipper::Adapters::Memoizable)
42
+ expect(strict_adapter).to be_instance_of(Flipper::Adapters::Strict)
43
+ expect(strict_adapter.handler).to be(:warn)
44
+ expect(memory_adapter).to be_instance_of(Flipper::Adapters::Memory)
45
+ end
46
+
47
+ it "passes block to adapter initializer" do
48
+ expected_block = lambda {}
49
+ adapter_class = double('adapter class')
50
+
51
+ subject.use(adapter_class, &expected_block)
52
+
53
+ expect(adapter_class).to receive(:new) { |&block| expect(block).to be(expected_block) }.and_return(:adapter)
54
+ expect(subject.to_adapter).to be(:adapter)
55
+ end
56
+ end
57
+
58
+ describe "#store" do
59
+ it "defaults to memory adapter" do
60
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
61
+ end
62
+
63
+ it "only saves one store" do
64
+ require "flipper/adapters/pstore"
65
+ subject.store(Flipper::Adapters::PStore)
66
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::PStore)
67
+
68
+ subject.store(Flipper::Adapters::Memory)
69
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
70
+ end
71
+ end
72
+ end
@@ -6,6 +6,7 @@ RSpec.describe Flipper::Adapter do
6
6
  boolean: nil,
7
7
  groups: Set.new,
8
8
  actors: Set.new,
9
+ expression: nil,
9
10
  percentage_of_actors: nil,
10
11
  percentage_of_time: nil,
11
12
  }
@@ -0,0 +1,20 @@
1
+ require "flipper/adapters/actor_limit"
2
+
3
+ RSpec.describe Flipper::Adapters::ActorLimit do
4
+ it_should_behave_like 'a flipper adapter' do
5
+ let(:limit) { 5 }
6
+ let(:adapter) { Flipper::Adapters::ActorLimit.new(Flipper::Adapters::Memory.new, limit) }
7
+
8
+ subject { adapter }
9
+
10
+ describe '#enable' do
11
+ it "fails when limit exceeded" do
12
+ 5.times { |i| feature.enable Flipper::Actor.new("User;#{i}") }
13
+
14
+ expect {
15
+ feature.enable Flipper::Actor.new("User;6")
16
+ }.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -55,14 +55,14 @@ RSpec.describe Flipper::Adapters::DualWrite do
55
55
 
56
56
  it 'updates remote and local for #enable' do
57
57
  feature = sync[:search]
58
- subject.enable feature, feature.gate(:boolean), local.boolean
58
+ subject.enable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(true)
59
59
  expect(remote_adapter.count(:enable)).to be(1)
60
60
  expect(local_adapter.count(:enable)).to be(1)
61
61
  end
62
62
 
63
63
  it 'updates remote and local for #disable' do
64
64
  feature = sync[:search]
65
- subject.disable feature, feature.gate(:boolean), local.boolean(false)
65
+ subject.disable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(false)
66
66
  expect(remote_adapter.count(:disable)).to be(1)
67
67
  expect(local_adapter.count(:disable)).to be(1)
68
68
  end
@@ -0,0 +1,61 @@
1
+ require "flipper/adapters/http/client"
2
+
3
+ RSpec.describe Flipper::Adapters::Http::Client do
4
+ describe "#initialize" do
5
+ it "requires url" do
6
+ expect { described_class.new }.to raise_error(KeyError, "key not found: :url")
7
+ end
8
+
9
+ it "sets default headers" do
10
+ client = described_class.new(url: "http://example.com")
11
+ expect(client.headers).to eq({
12
+ 'content-type' => 'application/json',
13
+ 'accept' => 'application/json',
14
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
15
+ })
16
+ end
17
+
18
+ it "adds custom headers" do
19
+ client = described_class.new(url: "http://example.com", headers: {'custom-header' => 'value'})
20
+ expect(client.headers).to include('custom-header' => 'value')
21
+ end
22
+
23
+ it "overrides default headers with custom headers" do
24
+ client = described_class.new(url: "http://example.com", headers: {'content-type' => 'text/plain'})
25
+ expect(client.headers['content-type']).to eq('text/plain')
26
+ end
27
+ end
28
+
29
+ describe "#add_header" do
30
+ it "can add string header" do
31
+ client = described_class.new(url: "http://example.com")
32
+ client.add_header("key", "value")
33
+ expect(client.headers.fetch("key")).to eq("value")
34
+ end
35
+
36
+ it "standardizes key to lowercase" do
37
+ client = described_class.new(url: "http://example.com")
38
+ client.add_header("Content-Type", "value")
39
+ expect(client.headers.fetch("content-type")).to eq("value")
40
+ end
41
+
42
+ it "standardizes key to dashes" do
43
+ client = described_class.new(url: "http://example.com")
44
+ client.add_header(:content_type, "value")
45
+ expect(client.headers.fetch("content-type")).to eq("value")
46
+ end
47
+
48
+ it "can add symbol header" do
49
+ client = described_class.new(url: "http://example.com")
50
+ client.add_header(:key, "value")
51
+ expect(client.headers.fetch("key")).to eq("value")
52
+ end
53
+
54
+ it "overrides existing header" do
55
+ client = described_class.new(url: "http://example.com")
56
+ client.add_header("key", "value 1")
57
+ client.add_header("key", "value 2")
58
+ expect(client.headers.fetch("key")).to eq("value 2")
59
+ end
60
+ end
61
+ end