flipper 1.1.2 → 1.3.0
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/workflows/ci.yml +9 -2
- data/.github/workflows/examples.yml +8 -2
- data/Changelog.md +1 -647
- data/Gemfile +3 -2
- data/README.md +3 -1
- data/Rakefile +2 -2
- data/docs/images/banner.jpg +0 -0
- data/exe/flipper +5 -0
- data/flipper.gemspec +5 -1
- data/lib/flipper/adapters/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/http/client.rb +25 -16
- data/lib/flipper/adapters/operation_logger.rb +18 -88
- data/lib/flipper/adapters/read_only.rb +6 -39
- data/lib/flipper/adapters/strict.rb +16 -18
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +9 -4
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
- data/lib/flipper/cloud/telemetry/submitter.rb +2 -2
- data/lib/flipper/cloud/telemetry.rb +10 -2
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/engine.rb +32 -17
- data/lib/flipper/instrumentation/log_subscriber.rb +12 -3
- data/lib/flipper/metadata.rb +3 -1
- data/lib/flipper/poller.rb +6 -5
- data/lib/flipper/serializers/gzip.rb +3 -5
- data/lib/flipper/serializers/json.rb +3 -5
- data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
- data/lib/flipper/test/shared_adapter_test.rb +17 -17
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +3 -3
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +3 -1
- data/lib/generators/flipper/setup_generator.rb +63 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +1 -2
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +102 -76
- data/spec/flipper/adapters/strict_spec.rb +11 -9
- data/spec/flipper/cli_spec.rb +164 -0
- data/spec/flipper/cloud/configuration_spec.rb +35 -36
- data/spec/flipper/cloud/dsl_spec.rb +5 -5
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +24 -24
- data/spec/flipper/cloud/telemetry_spec.rb +53 -1
- data/spec/flipper/cloud_spec.rb +10 -9
- data/spec/flipper/engine_spec.rb +140 -58
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +9 -2
- data/spec/flipper/middleware/memoizer_spec.rb +7 -4
- data/spec/flipper_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/spec_helpers.rb +12 -5
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
- data/test_rails/system/test_help_test.rb +51 -0
- metadata +31 -9
- data/spec/support/climate_control.rb +0 -7
data/Gemfile
CHANGED
@@ -13,7 +13,7 @@ gem 'rspec', '~> 3.0'
|
|
13
13
|
gem 'rack-test'
|
14
14
|
gem 'rackup'
|
15
15
|
gem 'sqlite3', "~> #{ENV['SQLITE3_VERSION'] || '1.4.1'}"
|
16
|
-
gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.
|
16
|
+
gem 'rails', "~> #{ENV['RAILS_VERSION'] || '7.1'}"
|
17
17
|
gem 'minitest', '~> 5.18'
|
18
18
|
gem 'minitest-documentation'
|
19
19
|
gem 'webmock'
|
@@ -24,9 +24,10 @@ gem 'stackprof'
|
|
24
24
|
gem 'benchmark-ips'
|
25
25
|
gem 'stackprof-webnav'
|
26
26
|
gem 'flamegraph'
|
27
|
-
gem 'climate_control'
|
28
27
|
gem 'mysql2'
|
29
28
|
gem 'pg'
|
29
|
+
gem 'cuprite'
|
30
|
+
gem 'puma'
|
30
31
|
|
31
32
|
group(:guard) do
|
32
33
|
gem 'guard'
|
data/README.md
CHANGED
@@ -99,7 +99,7 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
|
|
99
99
|
|
100
100
|
1. Update the version to be whatever it should be and commit.
|
101
101
|
2. `script/release`
|
102
|
-
3.
|
102
|
+
3. Create a new [GitHub Release](https://github.com/flippercloud/flipper/releases/new)
|
103
103
|
|
104
104
|
## Brought To You By
|
105
105
|
|
@@ -111,3 +111,5 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
|
|
111
111
|
|  | [@alexwheeler](https://github.com/alexwheeler) | api |
|
112
112
|
|  | [@thetimbanks](https://github.com/thetimbanks) | ui |
|
113
113
|
|  | [@lazebny](https://github.com/lazebny) | docker |
|
114
|
+
|  | [@pagertree](https://github.com/pagertree) | sponsor |
|
115
|
+
|  | [@kdaigle](https://github.com/kdaigle) | sponsor |
|
data/Rakefile
CHANGED
@@ -27,7 +27,8 @@ end
|
|
27
27
|
|
28
28
|
require 'rspec/core/rake_task'
|
29
29
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
30
|
-
t.rspec_opts = %w(--color
|
30
|
+
t.rspec_opts = %w(--color)
|
31
|
+
t.verbose = false
|
31
32
|
end
|
32
33
|
|
33
34
|
namespace :spec do
|
@@ -41,7 +42,6 @@ end
|
|
41
42
|
Rake::TestTask.new do |t|
|
42
43
|
t.libs = %w(lib test)
|
43
44
|
t.pattern = 'test/**/*_test.rb'
|
44
|
-
t.options = '--documentation'
|
45
45
|
t.warning = false
|
46
46
|
end
|
47
47
|
|
data/docs/images/banner.jpg
CHANGED
Binary file
|
data/exe/flipper
ADDED
data/flipper.gemspec
CHANGED
@@ -6,7 +6,7 @@ plugin_files = []
|
|
6
6
|
plugin_test_files = []
|
7
7
|
|
8
8
|
Dir['flipper-*.gemspec'].map do |gemspec|
|
9
|
-
spec =
|
9
|
+
spec = Gem::Specification.load(gemspec)
|
10
10
|
plugin_files << spec.files
|
11
11
|
plugin_test_files << spec.files
|
12
12
|
end
|
@@ -27,6 +27,8 @@ Gem::Specification.new do |gem|
|
|
27
27
|
gem.homepage = 'https://www.flippercloud.io/docs'
|
28
28
|
gem.license = 'MIT'
|
29
29
|
|
30
|
+
gem.bindir = "exe"
|
31
|
+
gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
|
30
32
|
gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
|
31
33
|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
|
32
34
|
gem.name = 'flipper'
|
@@ -35,4 +37,6 @@ Gem::Specification.new do |gem|
|
|
35
37
|
gem.metadata = Flipper::METADATA
|
36
38
|
|
37
39
|
gem.add_dependency 'concurrent-ruby', '< 2'
|
40
|
+
|
41
|
+
gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
|
38
42
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
class ActorLimit < Wrapper
|
4
|
+
LimitExceeded = Class.new(Flipper::Error)
|
5
|
+
|
6
|
+
attr_reader :limit
|
7
|
+
|
8
|
+
def initialize(adapter, limit = 100)
|
9
|
+
super(adapter)
|
10
|
+
@limit = limit
|
11
|
+
end
|
12
|
+
|
13
|
+
def enable(feature, gate, resource)
|
14
|
+
if gate.is_a?(Flipper::Gates::Actor) && over_limit?(feature)
|
15
|
+
raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def over_limit?(feature)
|
24
|
+
feature.actors_value.size >= @limit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
# Base class for caching adapters. Inherit from this and then override
|
4
|
+
# cache_fetch, cache_read_multi, cache_write, and cache_delete.
|
5
|
+
class CacheBase
|
6
|
+
include ::Flipper::Adapter
|
7
|
+
|
8
|
+
# Public: The adapter being cached.
|
9
|
+
attr_reader :adapter
|
10
|
+
|
11
|
+
# Public: The ActiveSupport::Cache::Store to cache with.
|
12
|
+
attr_reader :cache
|
13
|
+
|
14
|
+
# Public: The ttl for all cached data.
|
15
|
+
attr_reader :ttl
|
16
|
+
|
17
|
+
# Public: The cache key where the set of known features is cached.
|
18
|
+
attr_reader :features_cache_key
|
19
|
+
|
20
|
+
# Public: Alias expires_in to ttl for compatibility.
|
21
|
+
alias_method :expires_in, :ttl
|
22
|
+
|
23
|
+
def initialize(adapter, cache, ttl = 300, prefix: nil)
|
24
|
+
@adapter = adapter
|
25
|
+
@cache = cache
|
26
|
+
@ttl = ttl
|
27
|
+
|
28
|
+
@cache_version = 'v1'.freeze
|
29
|
+
@namespace = "flipper/#{@cache_version}"
|
30
|
+
@namespace = @namespace.prepend(prefix) if prefix
|
31
|
+
@features_cache_key = "#{@namespace}/features"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Expire the cache for the set of known feature names.
|
35
|
+
def expire_features_cache
|
36
|
+
cache_delete @features_cache_key
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Expire the cache for a given feature.
|
40
|
+
def expire_feature_cache(key)
|
41
|
+
cache_delete feature_cache_key(key)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public
|
45
|
+
def features
|
46
|
+
read_feature_keys
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public
|
50
|
+
def add(feature)
|
51
|
+
result = @adapter.add(feature)
|
52
|
+
expire_features_cache
|
53
|
+
result
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public
|
57
|
+
def remove(feature)
|
58
|
+
result = @adapter.remove(feature)
|
59
|
+
expire_features_cache
|
60
|
+
expire_feature_cache(feature.key)
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
# Public
|
65
|
+
def clear(feature)
|
66
|
+
result = @adapter.clear(feature)
|
67
|
+
expire_feature_cache(feature.key)
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
# Public
|
72
|
+
def get(feature)
|
73
|
+
read_feature(feature)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public
|
77
|
+
def get_multi(features)
|
78
|
+
read_many_features(features)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Public
|
82
|
+
def get_all
|
83
|
+
features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
|
84
|
+
read_many_features(features)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Public
|
88
|
+
def enable(feature, gate, thing)
|
89
|
+
result = @adapter.enable(feature, gate, thing)
|
90
|
+
expire_feature_cache(feature.key)
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
# Public
|
95
|
+
def disable(feature, gate, thing)
|
96
|
+
result = @adapter.disable(feature, gate, thing)
|
97
|
+
expire_feature_cache(feature.key)
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public: Generate the cache key for a given feature.
|
102
|
+
#
|
103
|
+
# key - The String or Symbol feature key.
|
104
|
+
def feature_cache_key(key)
|
105
|
+
"#{@namespace}/feature/#{key}"
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Private: Returns the Set of known feature keys.
|
111
|
+
def read_feature_keys
|
112
|
+
cache_fetch(@features_cache_key) { @adapter.features }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Private: Read through caching for a single feature.
|
116
|
+
def read_feature(feature)
|
117
|
+
cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Private: Given an array of features, attempts to read through cache in
|
121
|
+
# as few network calls as possible.
|
122
|
+
def read_many_features(features)
|
123
|
+
keys = features.map { |feature| feature_cache_key(feature.key) }
|
124
|
+
cache_result = cache_read_multi(keys)
|
125
|
+
uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
|
126
|
+
|
127
|
+
if uncached_features.any?
|
128
|
+
response = @adapter.get_multi(uncached_features)
|
129
|
+
response.each do |key, value|
|
130
|
+
cache_write feature_cache_key(key), value
|
131
|
+
cache_result[feature_cache_key(key)] = value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
result = {}
|
136
|
+
features.each do |feature|
|
137
|
+
result[feature.key] = cache_result[feature_cache_key(feature.key)]
|
138
|
+
end
|
139
|
+
result
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -7,26 +7,28 @@ module Flipper
|
|
7
7
|
class Http
|
8
8
|
class Client
|
9
9
|
DEFAULT_HEADERS = {
|
10
|
-
'
|
11
|
-
'
|
12
|
-
'
|
10
|
+
'content-type' => 'application/json',
|
11
|
+
'accept' => 'application/json',
|
12
|
+
'user-agent' => "Flipper HTTP Adapter v#{VERSION}",
|
13
13
|
}.freeze
|
14
14
|
|
15
15
|
HTTPS_SCHEME = "https".freeze
|
16
16
|
|
17
17
|
CLIENT_FRAMEWORKS = {
|
18
|
-
rails:
|
19
|
-
sinatra:
|
20
|
-
hanami:
|
18
|
+
rails: -> { Rails.version if defined?(Rails) },
|
19
|
+
sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
|
20
|
+
hanami: -> { Hanami::VERSION if defined?(Hanami) },
|
21
|
+
sidekiq: -> { Sidekiq::VERSION if defined?(Sidekiq) },
|
22
|
+
good_job: -> { GoodJob::VERSION if defined?(GoodJob) },
|
21
23
|
}
|
22
24
|
|
23
25
|
attr_reader :uri, :headers
|
24
26
|
attr_reader :basic_auth_username, :basic_auth_password
|
25
|
-
attr_reader :read_timeout, :open_timeout, :write_timeout
|
27
|
+
attr_reader :read_timeout, :open_timeout, :write_timeout
|
28
|
+
attr_reader :max_retries, :debug_output
|
26
29
|
|
27
30
|
def initialize(options = {})
|
28
31
|
@uri = URI(options.fetch(:url))
|
29
|
-
@headers = DEFAULT_HEADERS.merge(options[:headers] || {})
|
30
32
|
@basic_auth_username = options[:basic_auth_username]
|
31
33
|
@basic_auth_password = options[:basic_auth_password]
|
32
34
|
@read_timeout = options[:read_timeout]
|
@@ -34,9 +36,16 @@ module Flipper
|
|
34
36
|
@write_timeout = options[:write_timeout]
|
35
37
|
@max_retries = options.key?(:max_retries) ? options[:max_retries] : 0
|
36
38
|
@debug_output = options[:debug_output]
|
39
|
+
|
40
|
+
@headers = {}
|
41
|
+
DEFAULT_HEADERS.each { |key, value| add_header key, value }
|
42
|
+
if options[:headers]
|
43
|
+
options[:headers].each { |key, value| add_header key, value }
|
44
|
+
end
|
37
45
|
end
|
38
46
|
|
39
47
|
def add_header(key, value)
|
48
|
+
key = key.to_s.downcase.gsub('_'.freeze, '-'.freeze).freeze
|
40
49
|
@headers[key] = value
|
41
50
|
end
|
42
51
|
|
@@ -87,13 +96,13 @@ module Flipper
|
|
87
96
|
|
88
97
|
def build_request(http_method, uri, headers, options)
|
89
98
|
request_headers = {
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
99
|
+
'client-language' => "ruby",
|
100
|
+
'client-language-version' => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
|
101
|
+
'client-platform' => RUBY_PLATFORM,
|
102
|
+
'client-engine' => defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
|
103
|
+
'client-pid' => Process.pid.to_s,
|
104
|
+
'client-thread' => Thread.current.object_id.to_s,
|
105
|
+
'client-hostname' => Socket.gethostname,
|
97
106
|
}.merge(headers)
|
98
107
|
|
99
108
|
body = options[:body]
|
@@ -101,7 +110,7 @@ module Flipper
|
|
101
110
|
request.initialize_http_header(request_headers)
|
102
111
|
|
103
112
|
client_frameworks.each do |framework, version|
|
104
|
-
request.add_field("
|
113
|
+
request.add_field("client-framework", [framework, version].join("="))
|
105
114
|
end
|
106
115
|
|
107
116
|
request.body = body if body
|
@@ -5,111 +5,34 @@ module Flipper
|
|
5
5
|
# Public: Adapter that wraps another adapter and stores the operations.
|
6
6
|
#
|
7
7
|
# Useful in tests to verify calls and such. Never use outside of testing.
|
8
|
-
class OperationLogger
|
9
|
-
include Flipper::Adapter
|
8
|
+
class OperationLogger < Wrapper
|
10
9
|
|
11
10
|
class Operation
|
12
|
-
attr_reader :type, :args
|
11
|
+
attr_reader :type, :args, :kwargs
|
13
12
|
|
14
|
-
def initialize(type, args)
|
13
|
+
def initialize(type, args, kwargs = {})
|
15
14
|
@type = type
|
16
15
|
@args = args
|
16
|
+
@kwargs = kwargs
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
OperationTypes = [
|
21
|
-
:import,
|
22
|
-
:export,
|
23
|
-
:features,
|
24
|
-
:add,
|
25
|
-
:remove,
|
26
|
-
:clear,
|
27
|
-
:get,
|
28
|
-
:get_multi,
|
29
|
-
:get_all,
|
30
|
-
:enable,
|
31
|
-
:disable,
|
32
|
-
].freeze
|
33
|
-
|
34
20
|
# Internal: An array of the operations that have happened.
|
35
21
|
attr_reader :operations
|
36
22
|
|
37
23
|
# Public
|
38
24
|
def initialize(adapter, operations = nil)
|
39
|
-
|
25
|
+
super(adapter)
|
40
26
|
@operations = operations || []
|
41
27
|
end
|
42
28
|
|
43
|
-
# Public: The set of known features.
|
44
|
-
def features
|
45
|
-
@operations << Operation.new(:features, [])
|
46
|
-
@adapter.features
|
47
|
-
end
|
48
|
-
|
49
|
-
# Public: Adds a feature to the set of known features.
|
50
|
-
def add(feature)
|
51
|
-
@operations << Operation.new(:add, [feature])
|
52
|
-
@adapter.add(feature)
|
53
|
-
end
|
54
|
-
|
55
|
-
# Public: Removes a feature from the set of known features and clears
|
56
|
-
# all the values for the feature.
|
57
|
-
def remove(feature)
|
58
|
-
@operations << Operation.new(:remove, [feature])
|
59
|
-
@adapter.remove(feature)
|
60
|
-
end
|
61
|
-
|
62
|
-
# Public: Clears all the gate values for a feature.
|
63
|
-
def clear(feature)
|
64
|
-
@operations << Operation.new(:clear, [feature])
|
65
|
-
@adapter.clear(feature)
|
66
|
-
end
|
67
|
-
|
68
|
-
# Public
|
69
|
-
def get(feature)
|
70
|
-
@operations << Operation.new(:get, [feature])
|
71
|
-
@adapter.get(feature)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Public
|
75
|
-
def get_multi(features)
|
76
|
-
@operations << Operation.new(:get_multi, [features])
|
77
|
-
@adapter.get_multi(features)
|
78
|
-
end
|
79
|
-
|
80
|
-
# Public
|
81
|
-
def get_all
|
82
|
-
@operations << Operation.new(:get_all, [])
|
83
|
-
@adapter.get_all
|
84
|
-
end
|
85
|
-
|
86
|
-
# Public
|
87
|
-
def enable(feature, gate, thing)
|
88
|
-
@operations << Operation.new(:enable, [feature, gate, thing])
|
89
|
-
@adapter.enable(feature, gate, thing)
|
90
|
-
end
|
91
|
-
|
92
|
-
# Public
|
93
|
-
def disable(feature, gate, thing)
|
94
|
-
@operations << Operation.new(:disable, [feature, gate, thing])
|
95
|
-
@adapter.disable(feature, gate, thing)
|
96
|
-
end
|
97
|
-
|
98
|
-
# Public
|
99
|
-
def import(source)
|
100
|
-
@operations << Operation.new(:import, [source])
|
101
|
-
@adapter.import(source)
|
102
|
-
end
|
103
|
-
|
104
|
-
# Public
|
105
|
-
def export(format: :json, version: 1)
|
106
|
-
@operations << Operation.new(:export, [format, version])
|
107
|
-
@adapter.export(format: format, version: version)
|
108
|
-
end
|
109
|
-
|
110
29
|
# Public: Count the number of times a certain operation happened.
|
111
|
-
def count(type)
|
112
|
-
type
|
30
|
+
def count(type = nil)
|
31
|
+
if type
|
32
|
+
type(type).size
|
33
|
+
else
|
34
|
+
@operations.size
|
35
|
+
end
|
113
36
|
end
|
114
37
|
|
115
38
|
# Public: Get all operations of a certain type.
|
@@ -131,6 +54,13 @@ module Flipper
|
|
131
54
|
inspect_id = ::Kernel::format "%x", (object_id * 2)
|
132
55
|
%(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
|
133
56
|
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def wrap(method, *args, **kwargs, &block)
|
61
|
+
@operations << Operation.new(method, args, kwargs)
|
62
|
+
block.call
|
63
|
+
end
|
134
64
|
end
|
135
65
|
end
|
136
66
|
end
|
@@ -3,8 +3,8 @@ require 'flipper'
|
|
3
3
|
module Flipper
|
4
4
|
module Adapters
|
5
5
|
# Public: Adapter that wraps another adapter and raises for any writes.
|
6
|
-
class ReadOnly
|
7
|
-
|
6
|
+
class ReadOnly < Wrapper
|
7
|
+
WRITE_METHODS = %i[add remove clear enable disable]
|
8
8
|
|
9
9
|
class WriteAttempted < Error
|
10
10
|
def initialize(message = nil)
|
@@ -12,49 +12,16 @@ module Flipper
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
# Public
|
16
|
-
def initialize(adapter)
|
17
|
-
@adapter = adapter
|
18
|
-
end
|
19
|
-
|
20
|
-
def features
|
21
|
-
@adapter.features
|
22
|
-
end
|
23
|
-
|
24
15
|
def read_only?
|
25
16
|
true
|
26
17
|
end
|
27
18
|
|
28
|
-
|
29
|
-
@adapter.get(feature)
|
30
|
-
end
|
31
|
-
|
32
|
-
def get_multi(features)
|
33
|
-
@adapter.get_multi(features)
|
34
|
-
end
|
35
|
-
|
36
|
-
def get_all
|
37
|
-
@adapter.get_all
|
38
|
-
end
|
39
|
-
|
40
|
-
def add(_feature)
|
41
|
-
raise WriteAttempted
|
42
|
-
end
|
43
|
-
|
44
|
-
def remove(_feature)
|
45
|
-
raise WriteAttempted
|
46
|
-
end
|
47
|
-
|
48
|
-
def clear(_feature)
|
49
|
-
raise WriteAttempted
|
50
|
-
end
|
19
|
+
private
|
51
20
|
|
52
|
-
def
|
53
|
-
raise WriteAttempted
|
54
|
-
end
|
21
|
+
def wrap(method, *args, **kwargs)
|
22
|
+
raise WriteAttempted if WRITE_METHODS.include?(method)
|
55
23
|
|
56
|
-
|
57
|
-
raise WriteAttempted
|
24
|
+
yield
|
58
25
|
end
|
59
26
|
end
|
60
27
|
end
|
@@ -1,10 +1,8 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Adapters
|
3
3
|
# An adapter that ensures a feature exists before checking it.
|
4
|
-
class Strict
|
5
|
-
|
6
|
-
include ::Flipper::Adapter
|
7
|
-
attr_reader :name, :adapter, :handler
|
4
|
+
class Strict < Wrapper
|
5
|
+
attr_reader :handler
|
8
6
|
|
9
7
|
class NotFound < ::Flipper::Error
|
10
8
|
def initialize(name)
|
@@ -12,34 +10,34 @@ module Flipper
|
|
12
10
|
end
|
13
11
|
end
|
14
12
|
|
15
|
-
HANDLERS = {
|
16
|
-
raise: ->(feature) { raise NotFound.new(feature.key) },
|
17
|
-
warn: ->(feature) { warn NotFound.new(feature.key).message },
|
18
|
-
noop: ->(_) { },
|
19
|
-
}
|
20
|
-
|
21
|
-
def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
|
22
|
-
|
23
13
|
def initialize(adapter, handler = nil, &block)
|
24
|
-
|
25
|
-
@
|
26
|
-
@handler = block || HANDLERS.fetch(handler)
|
14
|
+
super(adapter)
|
15
|
+
@handler = block || handler
|
27
16
|
end
|
28
17
|
|
29
18
|
def get(feature)
|
30
19
|
assert_feature_exists(feature)
|
31
|
-
|
20
|
+
super
|
32
21
|
end
|
33
22
|
|
34
23
|
def get_multi(features)
|
35
24
|
features.each { |feature| assert_feature_exists(feature) }
|
36
|
-
|
25
|
+
super
|
37
26
|
end
|
38
27
|
|
39
28
|
private
|
40
29
|
|
41
30
|
def assert_feature_exists(feature)
|
42
|
-
|
31
|
+
return if @adapter.features.include?(feature.key)
|
32
|
+
|
33
|
+
case handler
|
34
|
+
when Proc then handler.call(feature)
|
35
|
+
when :warn then warn NotFound.new(feature.key).message
|
36
|
+
when :noop, false, nil
|
37
|
+
# noop
|
38
|
+
else # truthy or :raise
|
39
|
+
raise NotFound.new(feature.key)
|
40
|
+
end
|
43
41
|
end
|
44
42
|
|
45
43
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
# A base class for any adapter that wraps another adapter. By default, all methods
|
4
|
+
# delegate to the wrapped adapter. Implement `#wrap` to customize the behavior of
|
5
|
+
# all delegated methods, or override individual methods as needed.
|
6
|
+
class Wrapper
|
7
|
+
include Flipper::Adapter
|
8
|
+
|
9
|
+
METHODS = [
|
10
|
+
:import,
|
11
|
+
:export,
|
12
|
+
:features,
|
13
|
+
:add,
|
14
|
+
:remove,
|
15
|
+
:clear,
|
16
|
+
:get,
|
17
|
+
:get_multi,
|
18
|
+
:get_all,
|
19
|
+
:enable,
|
20
|
+
:disable,
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
attr_reader :adapter
|
24
|
+
|
25
|
+
def initialize(adapter)
|
26
|
+
@adapter = adapter
|
27
|
+
end
|
28
|
+
|
29
|
+
METHODS.each do |method|
|
30
|
+
if RUBY_VERSION >= '3.0'
|
31
|
+
define_method(method) do |*args, **kwargs|
|
32
|
+
wrap(method, *args, **kwargs) { @adapter.public_send(method, *args, **kwargs) }
|
33
|
+
end
|
34
|
+
else
|
35
|
+
define_method(method) do |*args|
|
36
|
+
wrap(method, *args) { @adapter.public_send(method, *args) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Override this method to customize the behavior of all delegated methods, and just yield to
|
42
|
+
# the block to call the wrapped adapter.
|
43
|
+
if RUBY_VERSION >= '3.0'
|
44
|
+
def wrap(method, *args, **kwargs, &block)
|
45
|
+
block.call
|
46
|
+
end
|
47
|
+
else
|
48
|
+
def wrap(method, *args, &block)
|
49
|
+
block.call
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|