flipper 0.22.0 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -0
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +26 -20
- data/.github/workflows/examples.yml +62 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Changelog.md +152 -3
- data/Dockerfile +1 -1
- data/Gemfile +9 -6
- data/README.md +15 -67
- data/Rakefile +4 -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 +19 -0
- data/docker-compose.yml +37 -34
- data/docs/README.md +1 -0
- data/docs/images/banner.jpg +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/dsl.rb +3 -3
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/instrumentation.rb +1 -0
- data/examples/instrumentation_last_accessed_at.rb +38 -0
- data/flipper.gemspec +2 -2
- data/lib/flipper/actor.rb +4 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/dual_write.rb +10 -16
- data/lib/flipper/adapters/failover.rb +83 -0
- data/lib/flipper/adapters/failsafe.rb +76 -0
- data/lib/flipper/adapters/http/client.rb +18 -12
- data/lib/flipper/adapters/http/error.rb +19 -1
- data/lib/flipper/adapters/http.rb +14 -4
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +27 -18
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -0
- data/lib/flipper/adapters/poll.rb +39 -0
- data/lib/flipper/adapters/pstore.rb +2 -5
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -6
- data/lib/flipper/adapters/sync/synchronizer.rb +2 -1
- data/lib/flipper/adapters/sync.rb +9 -15
- data/lib/flipper/dsl.rb +9 -11
- data/lib/flipper/errors.rb +3 -20
- data/lib/flipper/export.rb +26 -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/feature.rb +32 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- 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 +7 -3
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/instrumenters/memory.rb +6 -2
- data/lib/flipper/metadata.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +2 -12
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/railtie.rb +23 -22
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/types/actor.rb +19 -13
- data/lib/flipper/types/group.rb +12 -5
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +6 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/actor_spec.rb +10 -2
- data/spec/flipper/adapter_spec.rb +29 -4
- data/spec/flipper/adapters/dual_write_spec.rb +0 -2
- data/spec/flipper/adapters/failover_spec.rb +129 -0
- data/spec/flipper/adapters/failsafe_spec.rb +58 -0
- data/spec/flipper/adapters/http_spec.rb +64 -6
- data/spec/flipper/adapters/instrumented_spec.rb +28 -12
- data/spec/flipper/adapters/memoizable_spec.rb +30 -12
- data/spec/flipper/adapters/memory_spec.rb +3 -4
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -12
- data/spec/flipper/adapters/pstore_spec.rb +0 -2
- data/spec/flipper/adapters/read_only_spec.rb +0 -1
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +0 -1
- data/spec/flipper/adapters/sync_spec.rb +0 -2
- data/spec/flipper/configuration_spec.rb +0 -1
- data/spec/flipper/dsl_spec.rb +38 -12
- 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/feature_check_context_spec.rb +17 -19
- data/spec/flipper/feature_spec.rb +76 -33
- data/spec/flipper/gate_spec.rb +0 -2
- data/spec/flipper/gate_values_spec.rb +2 -34
- data/spec/flipper/gates/actor_spec.rb +0 -2
- data/spec/flipper/gates/boolean_spec.rb +1 -3
- data/spec/flipper/gates/group_spec.rb +2 -5
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
- data/spec/flipper/identifier_spec.rb +0 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -6
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -1
- data/spec/flipper/instrumenters/memory_spec.rb +18 -1
- data/spec/flipper/instrumenters/noop_spec.rb +14 -8
- data/spec/flipper/middleware/memoizer_spec.rb +0 -23
- data/spec/flipper/middleware/setup_env_spec.rb +0 -2
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/railtie_spec.rb +73 -33
- data/spec/flipper/registry_spec.rb +0 -1
- data/spec/flipper/typecast_spec.rb +82 -4
- data/spec/flipper/types/actor_spec.rb +45 -46
- data/spec/flipper/types/boolean_spec.rb +0 -1
- data/spec/flipper/types/group_spec.rb +2 -3
- data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
- data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
- data/spec/flipper/types/percentage_spec.rb +0 -1
- data/spec/flipper_integration_spec.rb +62 -51
- data/spec/flipper_spec.rb +7 -2
- data/spec/{helper.rb → spec_helper.rb} +4 -2
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +2 -6
- metadata +63 -19
- data/docs/Adapters.md +0 -124
- data/docs/Caveats.md +0 -4
- data/docs/Gates.md +0 -167
- data/docs/Instrumentation.md +0 -27
- data/docs/Optimization.md +0 -137
- data/docs/api/README.md +0 -884
- data/docs/http/README.md +0 -36
- data/docs/read-only/README.md +0 -24
@@ -4,7 +4,7 @@ module Flipper
|
|
4
4
|
module Adapters
|
5
5
|
# Internal: Adapter that wraps another adapter and instruments all adapter
|
6
6
|
# operations.
|
7
|
-
class Instrumented
|
7
|
+
class Instrumented
|
8
8
|
include ::Flipper::Adapter
|
9
9
|
|
10
10
|
# Private: The name of instrumentation events.
|
@@ -24,7 +24,6 @@ module Flipper
|
|
24
24
|
# :instrumenter - What to use to instrument all the things.
|
25
25
|
#
|
26
26
|
def initialize(adapter, options = {})
|
27
|
-
super(adapter)
|
28
27
|
@adapter = adapter
|
29
28
|
@name = :instrumented
|
30
29
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
@@ -146,6 +145,30 @@ module Flipper
|
|
146
145
|
payload[:result] = @adapter.disable(feature, gate, thing)
|
147
146
|
end
|
148
147
|
end
|
148
|
+
|
149
|
+
def import(source)
|
150
|
+
default_payload = {
|
151
|
+
operation: :import,
|
152
|
+
adapter_name: @adapter.name,
|
153
|
+
}
|
154
|
+
|
155
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
156
|
+
payload[:result] = @adapter.import(source)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def export(format: :json, version: 1)
|
161
|
+
default_payload = {
|
162
|
+
operation: :export,
|
163
|
+
adapter_name: @adapter.name,
|
164
|
+
format: format,
|
165
|
+
version: version,
|
166
|
+
}
|
167
|
+
|
168
|
+
@instrumenter.instrument(InstrumentationName, default_payload) do |payload|
|
169
|
+
payload[:result] = @adapter.export(format: format, version: version)
|
170
|
+
end
|
171
|
+
end
|
149
172
|
end
|
150
173
|
end
|
151
174
|
end
|
@@ -5,7 +5,7 @@ module Flipper
|
|
5
5
|
# Internal: Adapter that wraps another adapter with the ability to memoize
|
6
6
|
# adapter calls in memory. Used by flipper dsl and the memoizer middleware
|
7
7
|
# to make it possible to memoize adapter calls for the duration of a request.
|
8
|
-
class Memoizable
|
8
|
+
class Memoizable
|
9
9
|
include ::Flipper::Adapter
|
10
10
|
|
11
11
|
FeaturesKey = :flipper_features
|
@@ -27,7 +27,6 @@ module Flipper
|
|
27
27
|
|
28
28
|
# Public
|
29
29
|
def initialize(adapter, cache = nil)
|
30
|
-
super(adapter)
|
31
30
|
@adapter = adapter
|
32
31
|
@name = :memoizable
|
33
32
|
@cache = cache || {}
|
@@ -45,24 +44,20 @@ module Flipper
|
|
45
44
|
|
46
45
|
# Public
|
47
46
|
def add(feature)
|
48
|
-
|
49
|
-
expire_features_set
|
50
|
-
result
|
47
|
+
@adapter.add(feature).tap { expire_features_set }
|
51
48
|
end
|
52
49
|
|
53
50
|
# Public
|
54
51
|
def remove(feature)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
52
|
+
@adapter.remove(feature).tap do
|
53
|
+
expire_features_set
|
54
|
+
expire_feature(feature)
|
55
|
+
end
|
59
56
|
end
|
60
57
|
|
61
58
|
# Public
|
62
59
|
def clear(feature)
|
63
|
-
|
64
|
-
expire_feature(feature)
|
65
|
-
result
|
60
|
+
@adapter.clear(feature).tap { expire_feature(feature) }
|
66
61
|
end
|
67
62
|
|
68
63
|
# Public
|
@@ -124,16 +119,20 @@ module Flipper
|
|
124
119
|
|
125
120
|
# Public
|
126
121
|
def enable(feature, gate, thing)
|
127
|
-
|
128
|
-
expire_feature(feature)
|
129
|
-
result
|
122
|
+
@adapter.enable(feature, gate, thing).tap { expire_feature(feature) }
|
130
123
|
end
|
131
124
|
|
132
125
|
# Public
|
133
126
|
def disable(feature, gate, thing)
|
134
|
-
|
135
|
-
|
136
|
-
|
127
|
+
@adapter.disable(feature, gate, thing).tap { expire_feature(feature) }
|
128
|
+
end
|
129
|
+
|
130
|
+
def import(source)
|
131
|
+
@adapter.import(source).tap { cache.clear if memoizing? }
|
132
|
+
end
|
133
|
+
|
134
|
+
def export(format: :json, version: 1)
|
135
|
+
@adapter.export(format: format, version: version)
|
137
136
|
end
|
138
137
|
|
139
138
|
# Internal: Turns local caching on/off.
|
@@ -149,6 +148,16 @@ module Flipper
|
|
149
148
|
!!@memoize
|
150
149
|
end
|
151
150
|
|
151
|
+
if RUBY_VERSION >= '3.0'
|
152
|
+
def method_missing(name, *args, **kwargs, &block)
|
153
|
+
@adapter.send name, *args, **kwargs, &block
|
154
|
+
end
|
155
|
+
else
|
156
|
+
def method_missing(name, *args, &block)
|
157
|
+
@adapter.send name, *args, &block
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
152
161
|
private
|
153
162
|
|
154
163
|
def key_for(key)
|
@@ -1,4 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "flipper/adapter"
|
2
|
+
require "flipper/typecast"
|
3
|
+
require 'concurrent/atomic/read_write_lock'
|
2
4
|
|
3
5
|
module Flipper
|
4
6
|
module Adapters
|
@@ -14,86 +16,93 @@ module Flipper
|
|
14
16
|
|
15
17
|
# Public
|
16
18
|
def initialize(source = nil)
|
17
|
-
@source = source
|
19
|
+
@source = Typecast.features_hash(source)
|
18
20
|
@name = :memory
|
21
|
+
@lock = Concurrent::ReadWriteLock.new
|
19
22
|
end
|
20
23
|
|
21
24
|
# Public: The set of known features.
|
22
25
|
def features
|
23
|
-
@source.keys.to_set
|
26
|
+
@lock.with_read_lock { @source.keys }.to_set
|
24
27
|
end
|
25
28
|
|
26
29
|
# Public: Adds a feature to the set of known features.
|
27
30
|
def add(feature)
|
28
|
-
@source[feature.key] ||= default_config
|
31
|
+
@lock.with_write_lock { @source[feature.key] ||= default_config }
|
29
32
|
true
|
30
33
|
end
|
31
34
|
|
32
35
|
# Public: Removes a feature from the set of known features and clears
|
33
36
|
# all the values for the feature.
|
34
37
|
def remove(feature)
|
35
|
-
@source.delete(feature.key)
|
38
|
+
@lock.with_write_lock { @source.delete(feature.key) }
|
36
39
|
true
|
37
40
|
end
|
38
41
|
|
39
42
|
# Public: Clears all the gate values for a feature.
|
40
43
|
def clear(feature)
|
41
|
-
@source[feature.key] = default_config
|
44
|
+
@lock.with_write_lock { @source[feature.key] = default_config }
|
42
45
|
true
|
43
46
|
end
|
44
47
|
|
45
48
|
# Public
|
46
49
|
def get(feature)
|
47
|
-
@source[feature.key] || default_config
|
50
|
+
@lock.with_read_lock { @source[feature.key] } || default_config
|
48
51
|
end
|
49
52
|
|
50
53
|
def get_multi(features)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
@lock.with_read_lock do
|
55
|
+
result = {}
|
56
|
+
features.each do |feature|
|
57
|
+
result[feature.key] = @source[feature.key] || default_config
|
58
|
+
end
|
59
|
+
result
|
54
60
|
end
|
55
|
-
result
|
56
61
|
end
|
57
62
|
|
58
63
|
def get_all
|
59
|
-
@source
|
64
|
+
@lock.with_read_lock { Typecast.features_hash(@source) }
|
60
65
|
end
|
61
66
|
|
62
67
|
# Public
|
63
68
|
def enable(feature, gate, thing)
|
64
|
-
@
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
69
|
+
@lock.with_write_lock do
|
70
|
+
@source[feature.key] ||= default_config
|
71
|
+
|
72
|
+
case gate.data_type
|
73
|
+
when :boolean
|
74
|
+
@source[feature.key] = default_config
|
75
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
76
|
+
when :integer
|
77
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
78
|
+
when :set
|
79
|
+
@source[feature.key][gate.key] << thing.value.to_s
|
80
|
+
else
|
81
|
+
raise "#{gate} is not supported by this adapter yet"
|
82
|
+
end
|
83
|
+
|
84
|
+
true
|
76
85
|
end
|
77
|
-
|
78
|
-
true
|
79
86
|
end
|
80
87
|
|
81
88
|
# Public
|
82
89
|
def disable(feature, gate, thing)
|
83
|
-
@
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
90
|
+
@lock.with_write_lock do
|
91
|
+
@source[feature.key] ||= default_config
|
92
|
+
|
93
|
+
case gate.data_type
|
94
|
+
when :boolean
|
95
|
+
@source[feature.key] = default_config
|
96
|
+
when :integer
|
97
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
98
|
+
when :set
|
99
|
+
@source[feature.key][gate.key].delete thing.value.to_s
|
100
|
+
else
|
101
|
+
raise "#{gate} is not supported by this adapter yet"
|
102
|
+
end
|
103
|
+
|
104
|
+
true
|
94
105
|
end
|
95
|
-
|
96
|
-
true
|
97
106
|
end
|
98
107
|
|
99
108
|
# Public
|
@@ -104,6 +113,14 @@ module Flipper
|
|
104
113
|
]
|
105
114
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
106
115
|
end
|
116
|
+
|
117
|
+
# Public: a more efficient implementation of import for this adapter
|
118
|
+
def import(source)
|
119
|
+
adapter = self.class.from(source)
|
120
|
+
get_all = Typecast.features_hash(adapter.get_all)
|
121
|
+
@lock.with_write_lock { @source.replace(get_all) }
|
122
|
+
true
|
123
|
+
end
|
107
124
|
end
|
108
125
|
end
|
109
126
|
end
|
@@ -5,8 +5,8 @@ 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
|
8
|
+
class OperationLogger
|
9
|
+
include Flipper::Adapter
|
10
10
|
|
11
11
|
class Operation
|
12
12
|
attr_reader :type, :args
|
@@ -18,6 +18,8 @@ module Flipper
|
|
18
18
|
end
|
19
19
|
|
20
20
|
OperationTypes = [
|
21
|
+
:import,
|
22
|
+
:export,
|
21
23
|
:features,
|
22
24
|
:add,
|
23
25
|
:remove,
|
@@ -37,7 +39,6 @@ module Flipper
|
|
37
39
|
|
38
40
|
# Public
|
39
41
|
def initialize(adapter, operations = nil)
|
40
|
-
super(adapter)
|
41
42
|
@adapter = adapter
|
42
43
|
@name = :operation_logger
|
43
44
|
@operations = operations || []
|
@@ -98,6 +99,18 @@ module Flipper
|
|
98
99
|
@adapter.disable(feature, gate, thing)
|
99
100
|
end
|
100
101
|
|
102
|
+
# Public
|
103
|
+
def import(source)
|
104
|
+
@operations << Operation.new(:import, [source])
|
105
|
+
@adapter.import(source)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public
|
109
|
+
def export(format: :json, version: 1)
|
110
|
+
@operations << Operation.new(:export, [format, version])
|
111
|
+
@adapter.export(format: format, version: version)
|
112
|
+
end
|
113
|
+
|
101
114
|
# Public: Count the number of times a certain operation happened.
|
102
115
|
def count(type)
|
103
116
|
type(type).size
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'flipper/adapters/sync/synchronizer'
|
2
|
+
require 'flipper/poller'
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Adapters
|
6
|
+
class Poll
|
7
|
+
extend Forwardable
|
8
|
+
include ::Flipper::Adapter
|
9
|
+
|
10
|
+
# Deprecated
|
11
|
+
Poller = ::Flipper::Poller
|
12
|
+
|
13
|
+
# Public: The name of the adapter.
|
14
|
+
attr_reader :name, :adapter, :poller
|
15
|
+
|
16
|
+
def_delegators :synced_adapter, :features, :get, :get_multi, :get_all, :add, :remove, :clear, :enable, :disable
|
17
|
+
|
18
|
+
def initialize(poller, adapter)
|
19
|
+
@name = :poll
|
20
|
+
@adapter = adapter
|
21
|
+
@poller = poller
|
22
|
+
@last_synced_at = 0
|
23
|
+
@poller.start
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def synced_adapter
|
29
|
+
@poller.start
|
30
|
+
poller_last_synced_at = @poller.last_synced_at.value
|
31
|
+
if poller_last_synced_at > @last_synced_at
|
32
|
+
Flipper::Adapters::Sync::Synchronizer.new(@adapter, @poller.adapter).call
|
33
|
+
@last_synced_at = poller_last_synced_at
|
34
|
+
end
|
35
|
+
@adapter
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -17,14 +17,11 @@ module Flipper
|
|
17
17
|
# Public: The path to where the file is stored.
|
18
18
|
attr_reader :path
|
19
19
|
|
20
|
-
# Public: PStore's thread_safe option.
|
21
|
-
attr_reader :thread_safe
|
22
|
-
|
23
20
|
# Public
|
24
|
-
def initialize(path = 'flipper.pstore', thread_safe =
|
21
|
+
def initialize(path = 'flipper.pstore', thread_safe = true)
|
22
|
+
@name = :pstore
|
25
23
|
@path = path
|
26
24
|
@store = ::PStore.new(path, thread_safe)
|
27
|
-
@name = :pstore
|
28
25
|
end
|
29
26
|
|
30
27
|
# Public: The set of known features.
|
@@ -7,11 +7,6 @@ module Flipper
|
|
7
7
|
# Private: Number of seconds between syncs (default: 10).
|
8
8
|
DEFAULT_INTERVAL = 10
|
9
9
|
|
10
|
-
# Private
|
11
|
-
def self.now
|
12
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
13
|
-
end
|
14
|
-
|
15
10
|
# Public: The Float or Integer number of seconds between invocations of
|
16
11
|
# the wrapped synchronizer.
|
17
12
|
attr_reader :interval
|
@@ -46,7 +41,7 @@ module Flipper
|
|
46
41
|
end
|
47
42
|
|
48
43
|
def now
|
49
|
-
|
44
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
50
45
|
end
|
51
46
|
end
|
52
47
|
end
|
@@ -37,7 +37,8 @@ module Flipper
|
|
37
37
|
# Sync all the gate values.
|
38
38
|
remote_get_all.each do |feature_key, remote_gates_hash|
|
39
39
|
feature = Feature.new(feature_key, @local)
|
40
|
-
|
40
|
+
# Check if feature_key is in hash before accessing to prevent unintended hash modification
|
41
|
+
local_gates_hash = local_get_all.key?(feature_key) ? local_get_all[feature_key] : @local.default_config
|
41
42
|
local_gate_values = GateValues.new(local_gates_hash)
|
42
43
|
remote_gate_values = GateValues.new(remote_gates_hash)
|
43
44
|
FeatureSynchronizer.new(feature, local_gate_values, remote_gate_values).call
|
@@ -58,33 +58,27 @@ module Flipper
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def add(feature)
|
61
|
-
|
62
|
-
@local.add(feature)
|
63
|
-
result
|
61
|
+
@remote.add(feature).tap { @local.add(feature) }
|
64
62
|
end
|
65
63
|
|
66
64
|
def remove(feature)
|
67
|
-
|
68
|
-
@local.remove(feature)
|
69
|
-
result
|
65
|
+
@remote.remove(feature).tap { @local.remove(feature) }
|
70
66
|
end
|
71
67
|
|
72
68
|
def clear(feature)
|
73
|
-
|
74
|
-
@local.clear(feature)
|
75
|
-
result
|
69
|
+
@remote.clear(feature).tap { @local.clear(feature) }
|
76
70
|
end
|
77
71
|
|
78
72
|
def enable(feature, gate, thing)
|
79
|
-
|
80
|
-
|
81
|
-
|
73
|
+
@remote.enable(feature, gate, thing).tap do
|
74
|
+
@local.enable(feature, gate, thing)
|
75
|
+
end
|
82
76
|
end
|
83
77
|
|
84
78
|
def disable(feature, gate, thing)
|
85
|
-
|
86
|
-
|
87
|
-
|
79
|
+
@remote.disable(feature, gate, thing).tap do
|
80
|
+
@local.disable(feature, gate, thing)
|
81
|
+
end
|
88
82
|
end
|
89
83
|
|
90
84
|
private
|
data/lib/flipper/dsl.rb
CHANGED
@@ -10,17 +10,19 @@ module Flipper
|
|
10
10
|
# Private: What is being used to instrument all the things.
|
11
11
|
attr_reader :instrumenter
|
12
12
|
|
13
|
-
def_delegators :@adapter, :memoize=, :memoizing
|
13
|
+
def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
|
14
14
|
|
15
15
|
# Public: Returns a new instance of the DSL.
|
16
16
|
#
|
17
17
|
# adapter - The adapter that this DSL instance should use.
|
18
18
|
# options - The Hash of options.
|
19
19
|
# :instrumenter - What should be used to instrument all the things.
|
20
|
+
# :memoize - Should adapter be wrapped by memoize adapter or not.
|
20
21
|
def initialize(adapter, options = {})
|
21
22
|
@instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
|
22
|
-
|
23
|
-
|
23
|
+
memoize = options.fetch(:memoize, true)
|
24
|
+
adapter = Adapters::Memoizable.new(adapter) if memoize
|
25
|
+
@adapter = adapter
|
24
26
|
@memoized_features = {}
|
25
27
|
end
|
26
28
|
|
@@ -235,12 +237,12 @@ module Flipper
|
|
235
237
|
|
236
238
|
# Public: Wraps an object as a flipper actor.
|
237
239
|
#
|
238
|
-
#
|
240
|
+
# actor - The object that you would like to wrap.
|
239
241
|
#
|
240
242
|
# Returns an instance of Flipper::Types::Actor.
|
241
|
-
# Raises ArgumentError if
|
242
|
-
def actor(
|
243
|
-
Types::Actor.new(
|
243
|
+
# Raises ArgumentError if actor does not respond to `flipper_id`.
|
244
|
+
def actor(actor)
|
245
|
+
Types::Actor.new(actor)
|
244
246
|
end
|
245
247
|
|
246
248
|
# Public: Shortcut for getting a percentage of time instance.
|
@@ -270,10 +272,6 @@ module Flipper
|
|
270
272
|
adapter.features.map { |name| feature(name) }.to_set
|
271
273
|
end
|
272
274
|
|
273
|
-
def import(flipper)
|
274
|
-
adapter.import(flipper.adapter)
|
275
|
-
end
|
276
|
-
|
277
275
|
# Cloud DSL method that does nothing for open source version.
|
278
276
|
def sync
|
279
277
|
end
|
data/lib/flipper/errors.rb
CHANGED
@@ -2,25 +2,16 @@ module Flipper
|
|
2
2
|
# Top level error that all other errors inherit from.
|
3
3
|
class Error < StandardError; end
|
4
4
|
|
5
|
-
# Raised when gate can not be found for
|
5
|
+
# Raised when gate can not be found for an actor.
|
6
6
|
class GateNotFound < Error
|
7
|
-
def initialize(
|
8
|
-
super "Could not find gate for #{
|
7
|
+
def initialize(actor)
|
8
|
+
super "Could not find gate for #{actor.inspect}"
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
12
|
# Raised when attempting to declare a group name that has already been used.
|
13
13
|
class DuplicateGroup < Error; end
|
14
14
|
|
15
|
-
# Raised when default instance not configured but there is an attempt to
|
16
|
-
# use it.
|
17
|
-
class DefaultNotSet < Flipper::Error
|
18
|
-
def initialize(message = nil)
|
19
|
-
warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
|
20
|
-
super
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
15
|
# Raised when an invalid value is set to a configuration property
|
25
16
|
class InvalidConfigurationValue < Flipper::Error
|
26
17
|
def initialize(message = nil)
|
@@ -28,12 +19,4 @@ module Flipper
|
|
28
19
|
super(message || default)
|
29
20
|
end
|
30
21
|
end
|
31
|
-
|
32
|
-
# Raised when accessing a configuration property that has been deprecated
|
33
|
-
class ConfigurationDeprecated < Flipper::Error
|
34
|
-
def initialize(message = nil)
|
35
|
-
default = "The configuration property has been deprecated"
|
36
|
-
super(message || default)
|
37
|
-
end
|
38
|
-
end
|
39
22
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "flipper/adapters/memory"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Export
|
5
|
+
attr_reader :contents, :format, :version
|
6
|
+
|
7
|
+
def initialize(contents:, format: :json, version: 1)
|
8
|
+
@contents = contents
|
9
|
+
@format = format
|
10
|
+
@version = version
|
11
|
+
end
|
12
|
+
|
13
|
+
def features
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def adapter
|
18
|
+
@adapter ||= Flipper::Adapters::Memory.new(features)
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
|
23
|
+
end
|
24
|
+
alias_method :==, :eql?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "flipper/exporters/json/v1"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Exporter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
FORMATTERS = {
|
8
|
+
json: {
|
9
|
+
1 => Flipper::Exporters::Json::V1,
|
10
|
+
}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def build(format: :json, version: 1)
|
14
|
+
FORMATTERS.fetch(format).fetch(version).new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "flipper/export"
|
2
|
+
require "flipper/typecast"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
# Raised when the contents of the export are not valid.
|
8
|
+
class InvalidError < StandardError; end
|
9
|
+
class JsonError < InvalidError; end
|
10
|
+
|
11
|
+
# Internal: JSON export class that knows how to build features hash
|
12
|
+
# from data.
|
13
|
+
class Export < ::Flipper::Export
|
14
|
+
def initialize(contents:, version: 1)
|
15
|
+
super contents: contents, version: version, format: :json
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: The features hash identical to calling get_all on adapter.
|
19
|
+
def features
|
20
|
+
@features ||= begin
|
21
|
+
features = JSON.parse(contents).fetch("features")
|
22
|
+
Typecast.features_hash(features)
|
23
|
+
rescue JSON::ParserError
|
24
|
+
raise JsonError
|
25
|
+
rescue
|
26
|
+
raise InvalidError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "json"
|
2
|
+
require "flipper/exporters/json/export"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
class V1
|
8
|
+
VERSION = 1
|
9
|
+
|
10
|
+
def call(adapter)
|
11
|
+
features = adapter.get_all
|
12
|
+
|
13
|
+
# Convert sets to arrays for json
|
14
|
+
features.each do |feature_key, gates|
|
15
|
+
gates.each do |key, value|
|
16
|
+
case value
|
17
|
+
when Set
|
18
|
+
features[feature_key][key] = value.to_a
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
json = JSON.dump({
|
24
|
+
version: VERSION,
|
25
|
+
features: features,
|
26
|
+
})
|
27
|
+
|
28
|
+
Json::Export.new(contents: json, version: VERSION)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|