flipper 1.2.2 → 1.3.0.pre

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ff734c4221d1ea2694fdf4e9e463085f60242e698b8702389c53ccc6a17e87b
4
- data.tar.gz: 5666852d9f7416d30919a188c5e3c46cb202d66d332c482aba56c3d496437282
3
+ metadata.gz: b71c902902ad6e2d1b19483496c257b82a67b0bdf37784c986ac24b41068000e
4
+ data.tar.gz: df8f571c809de456976c5b8f2adae3e1c2dc443589211a4c83a89130620d8219
5
5
  SHA512:
6
- metadata.gz: c48de5bfe83ff53d969d496f96a3df9335af8f1fe8d41559158930c9ae3dd94b91f245f7b4cfbceea1c85ab453a4dd94057e4a46e3f928d9b27ddc417f04c718
7
- data.tar.gz: 2ed4dd3a74bbf07fe289d57b9545e073527971e5ab7f393ee96ea8f31904a9a550db4cda0ba492f080a14b1eb8b4982eb680e88b1c2bcfeea2e74e032207b1f4
6
+ metadata.gz: d38bf2b382419e985d9a8bd4f6f650b27d880952f1cecfe2943e3cc2ce72e09843a8bf45b4cd833af40865b5438a3486d7dba72d6c02d197c7a98423d33dd12e
7
+ data.tar.gz: c67f1b896463545dd60582652d6eaffb7cf4b7e544a7d231bb83a2cc91e2c674c9de13f8a8a4a2e7966fa889792b6a34338782718c49c84a38ace66d7bde9deb
@@ -26,6 +26,7 @@ jobs:
26
26
  --health-timeout 5s
27
27
  --health-retries 5
28
28
  strategy:
29
+ fail-fast: false
29
30
  matrix:
30
31
  ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
31
32
  rails: ['5.2', '6.0.0', '6.1.0', '7.0.0', '7.1.0']
@@ -75,7 +76,7 @@ jobs:
75
76
  - name: Check out repository code
76
77
  uses: actions/checkout@v4
77
78
  - name: Do some action caching
78
- uses: actions/cache@v3
79
+ uses: actions/cache@v4
79
80
  with:
80
81
  path: vendor/bundle
81
82
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
@@ -58,7 +58,7 @@ jobs:
58
58
  - name: Check out repository code
59
59
  uses: actions/checkout@v4
60
60
  - name: Do some action caching
61
- uses: actions/cache@v3
61
+ uses: actions/cache@v4
62
62
  with:
63
63
  path: vendor/bundle
64
64
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
data/README.md CHANGED
@@ -111,3 +111,4 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
111
111
  | ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
112
112
  | ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
113
113
  | ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
114
+ | ![@pagertree](https://avatars.githubusercontent.com/u/24941240?s=64) | [@pagertree](https://github.com/pagertree) | sponsor |
@@ -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
@@ -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
- @adapter = adapter
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(type).size
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
- include ::Flipper::Adapter
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
- def get(feature)
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 enable(_feature, _gate, _thing)
53
- raise WriteAttempted
54
- end
21
+ def wrap(method, *args, **kwargs)
22
+ raise WriteAttempted if WRITE_METHODS.include?(method)
55
23
 
56
- def disable(_feature, _gate, _thing)
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
- extend Forwardable
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,22 +10,19 @@ module Flipper
12
10
  end
13
11
  end
14
12
 
15
- def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
16
-
17
13
  def initialize(adapter, handler = nil, &block)
18
- @name = :strict
19
- @adapter = adapter
14
+ super(adapter)
20
15
  @handler = block || handler
21
16
  end
22
17
 
23
18
  def get(feature)
24
19
  assert_feature_exists(feature)
25
- @adapter.get(feature)
20
+ super
26
21
  end
27
22
 
28
23
  def get_multi(features)
29
24
  features.each { |feature| assert_feature_exists(feature) }
30
- @adapter.get_multi(features)
25
+ super
31
26
  end
32
27
 
33
28
  private
@@ -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
data/lib/flipper/cli.rb CHANGED
@@ -9,7 +9,9 @@ module Flipper
9
9
  # Path to the local Rails application's environment configuration.
10
10
  DEFAULT_REQUIRE = "./config/environment"
11
11
 
12
- def initialize
12
+ attr_accessor :shell
13
+
14
+ def initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new)
13
15
  super
14
16
 
15
17
  # Program is always flipper, no matter how it's invoked
@@ -17,6 +19,10 @@ module Flipper
17
19
  @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
18
20
  @commands = {}
19
21
 
22
+ # Extend whatever shell to support output redirection
23
+ @shell = shell.extend(ShellOutput)
24
+ shell.redirect(stdout: stdout, stderr: stderr)
25
+
20
26
  %w[enable disable].each do |action|
21
27
  command action do |c|
22
28
  c.banner = "Usage: #{c.program_name} [options] <feature>"
@@ -40,10 +46,12 @@ module Flipper
40
46
  begin
41
47
  values << Flipper::Expression.build(JSON.parse(expression))
42
48
  rescue JSON::ParserError => e
43
- warn "JSON parse error: #{e.message}"
49
+ ui.error "JSON parse error #{e.message}"
50
+ ui.trace(e)
44
51
  exit 1
45
52
  rescue ArgumentError => e
46
- warn "Invalid expression: #{e.message}"
53
+ ui.error "Invalid expression: #{e.message}"
54
+ ui.trace(e)
47
55
  exit 1
48
56
  end
49
57
  end
@@ -57,7 +65,7 @@ module Flipper
57
65
  values.each { |value| f.send(action, value) }
58
66
  end
59
67
 
60
- puts feature_details(f)
68
+ ui.info feature_details(f)
61
69
  end
62
70
  end
63
71
  end
@@ -65,21 +73,21 @@ module Flipper
65
73
  command 'list' do |c|
66
74
  c.description = "List defined features"
67
75
  c.action do
68
- puts feature_summary(Flipper.features)
76
+ ui.info feature_summary(Flipper.features)
69
77
  end
70
78
  end
71
79
 
72
80
  command 'show' do |c|
73
81
  c.description = "Show a defined feature"
74
82
  c.action do |feature|
75
- puts feature_details(Flipper.feature(feature))
83
+ ui.info feature_details(Flipper.feature(feature))
76
84
  end
77
85
  end
78
86
 
79
87
  command 'help' do |c|
80
88
  c.load_environment = false
81
89
  c.action do |command = nil|
82
- puts command ? @commands[command].help : help
90
+ ui.info command ? @commands[command].help : help
83
91
  end
84
92
  end
85
93
 
@@ -89,7 +97,7 @@ module Flipper
89
97
 
90
98
  # Options available on all commands
91
99
  on_tail('-h', '--help', 'Print help message') do
92
- puts help
100
+ ui.info help
93
101
  exit
94
102
  end
95
103
 
@@ -114,15 +122,15 @@ module Flipper
114
122
  load_environment! if @commands[command].load_environment
115
123
  @commands[command].run(args)
116
124
  else
117
- puts help
125
+ ui.info help
118
126
 
119
127
  if command
120
- warn "Unknown command: #{command}"
128
+ ui.error "Unknown command: #{command}"
121
129
  exit 1
122
130
  end
123
131
  end
124
132
  rescue OptionParser::InvalidOption => e
125
- warn e.message
133
+ ui.error e.message
126
134
  exit 1
127
135
  end
128
136
 
@@ -138,7 +146,7 @@ module Flipper
138
146
  # Ensure all of flipper gets loaded if it hasn't already.
139
147
  require 'flipper'
140
148
  rescue LoadError => e
141
- warn e.message
149
+ ui.error e.message
142
150
  exit 1
143
151
  end
144
152
 
@@ -170,7 +178,7 @@ module Flipper
170
178
  end
171
179
 
172
180
  colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
173
- end
181
+ end.join("\n")
174
182
  end
175
183
 
176
184
  def feature_details(feature)
@@ -210,10 +218,12 @@ module Flipper
210
218
  end
211
219
 
212
220
  def colorize(text, colors)
213
- if defined?(Bundler)
214
- Bundler.ui.add_color(text, *colors)
215
- else
216
- text
221
+ ui.add_color(text, *colors)
222
+ end
223
+
224
+ def ui
225
+ @ui ||= Bundler::UI::Shell.new.tap do |ui|
226
+ ui.shell = shell
217
227
  end
218
228
  end
219
229
 
@@ -221,6 +231,15 @@ module Flipper
221
231
  text.gsub(/^/, " " * spaces)
222
232
  end
223
233
 
234
+ # Redirect the shell's output to the given stdout and stderr streams
235
+ module ShellOutput
236
+ attr_reader :stdout, :stderr
237
+
238
+ def redirect(stdout: $stdout, stderr: $stderr)
239
+ @stdout, @stderr = stdout, stderr
240
+ end
241
+ end
242
+
224
243
  class Command < OptionParser
225
244
  attr_accessor :description, :load_environment
226
245
 
@@ -174,7 +174,7 @@ module Flipper
174
174
  end
175
175
 
176
176
  def setup_log(options)
177
- set_option :logging_enabled, options, default: true, typecast: :boolean
177
+ set_option :logging_enabled, options, default: false, typecast: :boolean
178
178
  set_option :logger, options, from_env: false, default: -> {
179
179
  if logging_enabled
180
180
  Logger.new(STDOUT)
@@ -214,8 +214,7 @@ module Flipper
214
214
  Telemetry.instance_for(self)
215
215
  }
216
216
 
217
- # This is alpha. Don't use this unless you are me. And you are not me.
218
- set_option :telemetry_enabled, options, default: false, typecast: :boolean
217
+ set_option :telemetry_enabled, options, default: true, typecast: :boolean
219
218
  instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
220
219
  @instrumenter = if telemetry_enabled
221
220
  Telemetry::Instrumenter.new(self, instrumenter)
@@ -3,9 +3,11 @@ require "delegate"
3
3
  module Flipper
4
4
  module Cloud
5
5
  class Telemetry
6
- class Instrumenter < SimpleDelegator
6
+ class Instrumenter
7
+ attr_reader :instrumenter
8
+
7
9
  def initialize(cloud_configuration, instrumenter)
8
- super instrumenter
10
+ @instrumenter = instrumenter
9
11
  @cloud_configuration = cloud_configuration
10
12
  end
11
13
 
@@ -14,12 +16,6 @@ module Flipper
14
16
  @cloud_configuration.telemetry.record(name, payload)
15
17
  return_value
16
18
  end
17
-
18
- private
19
-
20
- def instrumenter
21
- __getobj__
22
- end
23
19
  end
24
20
  end
25
21
  end
@@ -160,8 +160,16 @@ module Flipper
160
160
  # thus may have a telemetry-interval header for us to respect.
161
161
  response ||= error.response if error && error.respond_to?(:response)
162
162
 
163
- if response && interval = response["telemetry-interval"]
164
- self.interval = interval.to_f
163
+ if response
164
+ if Flipper::Typecast.to_boolean(response["telemetry-shutdown"])
165
+ debug "action=telemetry_shutdown message=The server has requested that telemetry be shut down."
166
+ stop
167
+ return
168
+ end
169
+
170
+ if interval = response["telemetry-interval"]
171
+ self.interval = interval.to_f
172
+ end
165
173
  end
166
174
  rescue => error
167
175
  error "action=post_to_cloud error=#{error.inspect}"