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
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,63 @@
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
+
8
+ class_option :token, type: :string, default: nil, aliases: '-t',
9
+ desc: "Your personal environment token for Flipper Cloud"
10
+
11
+ def generate_active_record
12
+ invoke 'flipper:active_record' if defined?(Flipper::Adapters::ActiveRecord)
13
+ end
14
+
15
+ def configure_cloud_token
16
+ return unless options[:token]
17
+
18
+ configure_with_dotenv || configure_with_credentials
19
+ end
20
+
21
+ private
22
+
23
+ def configure_with_dotenv
24
+ ['.env.development', '.env.local', '.env'].detect do |file|
25
+ next unless exists?(file)
26
+ append_to_file file, "\nFLIPPER_CLOUD_TOKEN=#{options[:token]}\n"
27
+ end
28
+ end
29
+
30
+ def configure_with_credentials
31
+ return unless exists?("config/credentials.yml.enc") && (ENV["RAILS_MASTER_KEY"] || exists?("config/master.key"))
32
+
33
+ content = "flipper:\n cloud_token: #{options[:token]}\n"
34
+ action InjectIntoEncryptedFile.new(self, Rails.application.credentials, content, after: /\z/)
35
+ end
36
+
37
+ # Check if a file exists in the destination root
38
+ def exists?(path)
39
+ File.exist?(File.expand_path(path, destination_root))
40
+ end
41
+
42
+ # Action to inject content into ActiveSupport::EncryptedFile
43
+ class InjectIntoEncryptedFile < Thor::Actions::InjectIntoFile
44
+ def initialize(base, encrypted_file, data, config)
45
+ @encrypted_file = encrypted_file
46
+ super(base, encrypted_file.content_path, data, config)
47
+ end
48
+
49
+ def content
50
+ @content ||= @encrypted_file.read
51
+ end
52
+
53
+ def replace!(regexp, string, force)
54
+ if force || !replacement_present?
55
+ success = content.gsub!(regexp, string)
56
+ @encrypted_file.write content unless pretend?
57
+ success
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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,58 @@
1
+ require 'flipper/adapters/failsafe'
2
+
3
+ RSpec.describe Flipper::Adapters::Failsafe do
4
+ subject { described_class.new(memory_adapter, options) }
5
+
6
+ let(:memory_adapter) { Flipper::Adapters::Memory.new }
7
+ let(:options) { {} }
8
+ let(:flipper) { Flipper.new(subject) }
9
+
10
+ it_should_behave_like 'a flipper adapter'
11
+
12
+ context 'when disaster strikes' do
13
+ before do
14
+ expect(flipper[feature.name].enable).to be(true)
15
+
16
+ (subject.methods - Object.methods).each do |method_name|
17
+ allow(memory_adapter).to receive(method_name).and_raise(IOError)
18
+ end
19
+ end
20
+
21
+ let(:feature) { Flipper::Feature.new(:my_feature, subject) }
22
+
23
+ it { expect(subject.features).to eq(Set.new) }
24
+ it { expect(feature.add).to eq(false) }
25
+ it { expect(feature.remove).to eq(false) }
26
+ it { expect(feature.clear).to eq(false) }
27
+ it { expect(subject.get(feature)).to eq({}) }
28
+ it { expect(subject.get_multi([feature])).to eq({}) }
29
+ it { expect(subject.get_all).to eq({}) }
30
+ it { expect(feature.enable).to eq(false) }
31
+ it { expect(feature.disable).to eq(false) }
32
+
33
+ context 'when used via Flipper' do
34
+ it { expect(flipper.features).to eq(Set.new) }
35
+ it { expect(flipper[feature.name].enabled?).to eq(false) }
36
+ it { expect(flipper[feature.name].enable).to eq(false) }
37
+ it { expect(flipper[feature.name].disable).to eq(false) }
38
+ end
39
+
40
+ context 'when there is a syntax error' do
41
+ let(:test) { flipper[feature.name].enabled? }
42
+
43
+ before do
44
+ expect(memory_adapter).to receive(:get).and_raise(SyntaxError)
45
+ end
46
+
47
+ it 'does not catch this type of error' do
48
+ expect { test }.to raise_error(SyntaxError)
49
+ end
50
+
51
+ context 'when configured to catch SyntaxError' do
52
+ let(:options) { { errors: [SyntaxError] } }
53
+
54
+ it { expect(test).to eq(false) }
55
+ end
56
+ end
57
+ end
58
+ 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