flipper 1.2.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 +2 -1
- data/.github/workflows/examples.yml +1 -1
- data/README.md +2 -0
- data/docs/images/banner.jpg +0 -0
- data/lib/flipper/adapters/actor_limit.rb +28 -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 +36 -17
- 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/engine.rb +5 -5
- 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/typecast.rb +3 -3
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +3 -1
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http_spec.rb +11 -2
- data/spec/flipper/cli_spec.rb +21 -46
- data/spec/flipper/cloud/configuration_spec.rb +2 -1
- data/spec/flipper/cloud/telemetry_spec.rb +52 -0
- data/spec/flipper/cloud_spec.rb +4 -2
- data/spec/flipper/engine_spec.rb +34 -4
- 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/adapters/actor_limit_test.rb +20 -0
- data/test_rails/system/test_help_test.rb +1 -1
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 934e0e50f2aea9b4294ed435a780f5eb55ad8b547df9c80f609022b880a4dd9c
|
4
|
+
data.tar.gz: 8799605783af26860795d336a66d511f35b0f644424aca80cd3f81baaadc6475
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d0e041360e5d15966dd4f4a39be10616aff35dd56a7491297adce42b8d13d3ba7126feaf2d3798194c72fac73afad7e2355379ff51e40481f4a0ee25c49e6c8a
|
7
|
+
data.tar.gz: c16307b9a21775b1db67e9fd99c72c5206e7b0d6e0324e7625f349228757aea1243be676ba838ac0b853a7a48469fe8536da41085982556c0492ad985daf51ca
|
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/README.md
CHANGED
@@ -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/docs/images/banner.jpg
CHANGED
Binary file
|
@@ -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
|
@@ -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)
|
@@ -210,10 +218,12 @@ module Flipper
|
|
210
218
|
end
|
211
219
|
|
212
220
|
def colorize(text, colors)
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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:
|
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)
|