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.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +61 -16
- data/.github/workflows/examples.yml +55 -18
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -486
- data/Gemfile +23 -11
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +27 -0
- data/docs/images/banner.jpg +0 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/examples/cloud/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -6
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +30 -17
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +17 -78
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +20 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +266 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +102 -0
- data/lib/flipper/errors.rb +3 -3
- data/lib/flipper/export.rb +24 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +9 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +94 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -5
- data/lib/flipper/metadata.rb +8 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +118 -0
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
- data/lib/flipper/test/shared_adapter_test.rb +101 -58
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +59 -18
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +50 -11
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +138 -55
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +166 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +208 -0
- data/spec/flipper/cloud_spec.rb +186 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -76
- data/spec/flipper/engine_spec.rb +374 -0
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +453 -39
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +94 -30
- data/spec/spec_helper.rb +18 -18
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +203 -20
- data/.github/workflows/release.yml +0 -44
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- 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,
|
60
|
-
:
|
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
|
107
|
+
# given actor.
|
76
108
|
#
|
77
109
|
# Examples
|
78
110
|
#
|
79
|
-
# Flipper.register(:admins) { |
|
80
|
-
#
|
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/
|
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/
|
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
|
data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb
ADDED
@@ -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 @@
|
|
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
|
34
|
+
it 'returns true' do
|
34
35
|
result = destination_flipper.import(source_flipper)
|
35
|
-
expect(result).to be(
|
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),
|
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),
|
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
|