flipper 1.2.1 → 1.3.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +2 -1
- data/.github/workflows/examples.yml +1 -1
- data/Gemfile +0 -1
- data/README.md +1 -0
- data/lib/flipper/adapters/cache_base.rb +143 -0
- data/lib/flipper/adapters/operation_logger.rb +18 -88
- data/lib/flipper/adapters/read_only.rb +6 -39
- data/lib/flipper/adapters/strict.rb +5 -10
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +38 -15
- data/lib/flipper/cloud/configuration.rb +2 -3
- data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
- data/lib/flipper/cloud/telemetry.rb +10 -2
- 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 +15 -10
- data/lib/flipper/typecast.rb +3 -3
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +1 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/flipper/adapters/http_spec.rb +11 -2
- data/spec/flipper/cli_spec.rb +23 -23
- data/spec/flipper/cloud/configuration_spec.rb +29 -37
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +8 -9
- data/spec/flipper/cloud/telemetry_spec.rb +52 -0
- data/spec/flipper/cloud_spec.rb +6 -5
- data/spec/flipper/engine_spec.rb +39 -49
- data/spec/flipper/middleware/memoizer_spec.rb +7 -4
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/spec_helpers.rb +2 -1
- data/test_rails/system/test_help_test.rb +8 -3
- metadata +9 -5
- data/spec/support/climate_control.rb +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b71c902902ad6e2d1b19483496c257b82a67b0bdf37784c986ac24b41068000e
|
4
|
+
data.tar.gz: df8f571c809de456976c5b8f2adae3e1c2dc443589211a4c83a89130620d8219
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d38bf2b382419e985d9a8bd4f6f650b27d880952f1cecfe2943e3cc2ce72e09843a8bf45b4cd833af40865b5438a3486d7dba72d6c02d197c7a98423d33dd12e
|
7
|
+
data.tar.gz: c67f1b896463545dd60582652d6eaffb7cf4b7e544a7d231bb83a2cc91e2c674c9de13f8a8a4a2e7966fa889792b6a34338782718c49c84a38ace66d7bde9deb
|
data/.github/workflows/ci.yml
CHANGED
@@ -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@
|
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@
|
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/Gemfile
CHANGED
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
|
-
|
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,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
|
-
|
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
|
-
|
20
|
+
super
|
26
21
|
end
|
27
22
|
|
28
23
|
def get_multi(features)
|
29
24
|
features.each { |feature| assert_feature_exists(feature) }
|
30
|
-
|
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
|
-
|
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
|
-
|
49
|
+
ui.error "JSON parse error #{e.message}"
|
50
|
+
ui.trace(e)
|
44
51
|
exit 1
|
45
52
|
rescue ArgumentError => e
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
125
|
+
ui.info help
|
118
126
|
|
119
127
|
if command
|
120
|
-
|
128
|
+
ui.error "Unknown command: #{command}"
|
121
129
|
exit 1
|
122
130
|
end
|
123
131
|
end
|
124
132
|
rescue OptionParser::InvalidOption => e
|
125
|
-
|
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
|
-
|
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)
|
@@ -209,14 +217,29 @@ module Flipper
|
|
209
217
|
"#{count} #{count == 1 ? singular : plural}"
|
210
218
|
end
|
211
219
|
|
212
|
-
def colorize(text,
|
213
|
-
|
220
|
+
def colorize(text, colors)
|
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
|
227
|
+
end
|
214
228
|
end
|
215
229
|
|
216
230
|
def indent(text, spaces)
|
217
231
|
text.gsub(/^/, " " * spaces)
|
218
232
|
end
|
219
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
|
+
|
220
243
|
class Command < OptionParser
|
221
244
|
attr_accessor :description, :load_environment
|
222
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:
|
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
|
-
|
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
|
6
|
+
class Instrumenter
|
7
|
+
attr_reader :instrumenter
|
8
|
+
|
7
9
|
def initialize(cloud_configuration, instrumenter)
|
8
|
-
|
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
|