flipper 0.26.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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  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/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -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/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  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 +166 -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 +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -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 +453 -39
  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 +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -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 +94 -30
  213. data/spec/spec_helper.rb +18 -18
  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 +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
data/lib/flipper.rb CHANGED
@@ -56,28 +56,60 @@ 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
- :adapter, :add, :exist?, :remove, :import,
68
- :memoize=, :memoizing?,
67
+ :adapter, :add, :exist?, :remove, :import, :export,
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.
74
106
  # block - The block that should be used to determine if the group matches a
75
- # given thing.
107
+ # given actor.
76
108
  #
77
109
  # Examples
78
110
  #
79
- # Flipper.register(:admins) { |thing|
80
- # thing.respond_to?(:admin?) && thing.admin?
111
+ # Flipper.register(:admins) { |actor|
112
+ # actor.respond_to?(:admin?) && actor.admin?
81
113
  # }
82
114
  #
83
115
  # Returns a Flipper::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'
@@ -155,7 +191,9 @@ require 'flipper/instrumenters/noop'
155
191
  require 'flipper/identifier'
156
192
  require 'flipper/middleware/memoizer'
157
193
  require 'flipper/middleware/setup_env'
194
+ require 'flipper/poller'
158
195
  require 'flipper/registry'
196
+ require 'flipper/expression'
159
197
  require 'flipper/type'
160
198
  require 'flipper/types/actor'
161
199
  require 'flipper/types/boolean'
@@ -164,5 +202,6 @@ require 'flipper/types/percentage'
164
202
  require 'flipper/types/percentage_of_actors'
165
203
  require 'flipper/types/percentage_of_time'
166
204
  require 'flipper/typecast'
205
+ require 'flipper/version'
167
206
 
168
- 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,46 @@
1
+ {
2
+ "version": 1,
3
+ "features": {
4
+ "search": {
5
+ "boolean": null,
6
+ "actors": [
7
+ "john",
8
+ "another",
9
+ "testing"
10
+ ],
11
+ "percentage_of_actors": null,
12
+ "percentage_of_time": null,
13
+ "groups": [
14
+ "admins"
15
+ ]
16
+ },
17
+ "new_pricing": {
18
+ "boolean": "true",
19
+ "actors": [],
20
+ "percentage_of_actors": null,
21
+ "percentage_of_time": null,
22
+ "groups": []
23
+ },
24
+ "google_analytics_tag": {
25
+ "boolean": null,
26
+ "actors": [],
27
+ "percentage_of_actors": "100",
28
+ "percentage_of_time": null,
29
+ "groups": []
30
+ },
31
+ "help_scout_tag": {
32
+ "boolean": null,
33
+ "actors": [],
34
+ "percentage_of_actors": null,
35
+ "percentage_of_time": "50",
36
+ "groups": []
37
+ },
38
+ "nope": {
39
+ "boolean": null,
40
+ "actors": [],
41
+ "percentage_of_actors": null,
42
+ "percentage_of_time": null,
43
+ "groups": []
44
+ }
45
+ }
46
+ }
@@ -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
  }
@@ -30,9 +31,9 @@ RSpec.describe Flipper::Adapter do
30
31
  end
31
32
 
32
33
  describe '#import' do
33
- it 'returns nothing' do
34
+ it 'returns true' do
34
35
  result = destination_flipper.import(source_flipper)
35
- expect(result).to be(nil)
36
+ expect(result).to be(true)
36
37
  end
37
38
 
38
39
  it 'can import from one adapter to another' do
@@ -114,5 +115,32 @@ RSpec.describe Flipper::Adapter do
114
115
  destination_flipper.import(source_flipper)
115
116
  expect(destination_flipper.features.map(&:key)).to eq([])
116
117
  end
118
+
119
+ it 'can import an export' do
120
+ source_flipper.enable(:search)
121
+ source_flipper.enable(:google_analytics, Flipper::Actor.new("User;1"))
122
+
123
+ destination_flipper.import(source_flipper.export)
124
+
125
+ feature = destination_flipper[:search]
126
+ expect(feature.boolean_value).to be(true)
127
+
128
+ feature = destination_flipper[:google_analytics]
129
+ expect(feature.actors_value).to eq(Set["User;1"])
130
+ end
131
+ end
132
+
133
+ describe "#export" do
134
+ it "exports features" do
135
+ source_flipper.enable(:search)
136
+ export = source_flipper.export
137
+ expect(export.features.dig("search", :boolean)).to eq("true")
138
+ end
139
+
140
+ it "exports with arguments" do
141
+ source_flipper.enable(:search)
142
+ export = source_flipper.export(format: :json, version: 1)
143
+ expect(export.features.dig("search", :boolean)).to eq("true")
144
+ end
117
145
  end
118
146
  end
@@ -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