flipper 0.26.2 → 0.28.3
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/Changelog.md +61 -0
- data/Gemfile +2 -3
- data/benchmark/enabled_multiple_actors_ips.rb +20 -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/mirroring.rb +59 -0
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/http.rb +11 -3
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +19 -2
- data/lib/flipper/adapters/memory.rb +40 -16
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/dsl.rb +5 -9
- data/lib/flipper/errors.rb +3 -3
- 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 +12 -10
- data/lib/flipper/feature_check_context.rb +8 -4
- data/lib/flipper/gate.rb +12 -11
- data/lib/flipper/gates/actor.rb +11 -8
- data/lib/flipper/gates/group.rb +4 -2
- data/lib/flipper/gates/percentage_of_actors.rb +4 -5
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/poller.rb +1 -1
- 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 +17 -0
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +5 -4
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_spec.rb +29 -2
- data/spec/flipper/adapters/http_spec.rb +25 -3
- data/spec/flipper/adapters/instrumented_spec.rb +28 -10
- data/spec/flipper/adapters/memoizable_spec.rb +30 -10
- data/spec/flipper/adapters/memory_spec.rb +11 -2
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
- data/spec/flipper/dsl_spec.rb +25 -8
- 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 +5 -5
- data/spec/flipper/feature_spec.rb +76 -32
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
- data/spec/flipper/typecast_spec.rb +79 -0
- data/spec/flipper/types/actor_spec.rb +45 -45
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +62 -50
- data/spec/flipper_spec.rb +7 -1
- data/spec/support/skippable.rb +18 -0
- metadata +20 -2
data/lib/flipper/errors.rb
CHANGED
@@ -2,10 +2,10 @@ 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
|
|
@@ -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
|
data/lib/flipper/feature.rb
CHANGED
@@ -96,17 +96,19 @@ module Flipper
|
|
96
96
|
instrument(:clear) { adapter.clear(self) }
|
97
97
|
end
|
98
98
|
|
99
|
-
# Public: Check if a feature is enabled for
|
99
|
+
# Public: Check if a feature is enabled for zero or more actors.
|
100
100
|
#
|
101
101
|
# Returns true if enabled, false if not.
|
102
|
-
def enabled?(
|
103
|
-
|
102
|
+
def enabled?(*actors)
|
103
|
+
actors = actors.flatten.compact.map { |actor| Types::Actor.wrap(actor) }
|
104
|
+
actors = nil if actors.empty?
|
104
105
|
|
105
|
-
|
106
|
+
# thing is left for backwards compatibility
|
107
|
+
instrument(:enabled?, thing: actors&.first, actors: actors) do |payload|
|
106
108
|
context = FeatureCheckContext.new(
|
107
109
|
feature_name: @name,
|
108
110
|
values: gate_values,
|
109
|
-
|
111
|
+
actors: actors
|
110
112
|
)
|
111
113
|
|
112
114
|
if open_gate = gates.detect { |gate| gate.open?(context) }
|
@@ -359,14 +361,14 @@ module Flipper
|
|
359
361
|
gates_hash[name.to_sym]
|
360
362
|
end
|
361
363
|
|
362
|
-
# Public: Find the gate that protects
|
364
|
+
# Public: Find the gate that protects an actor.
|
363
365
|
#
|
364
|
-
#
|
366
|
+
# actor - The object for which you would like to find a gate
|
365
367
|
#
|
366
368
|
# Returns a Flipper::Gate.
|
367
|
-
# Raises Flipper::GateNotFound if no gate found for
|
368
|
-
def gate_for(
|
369
|
-
gates.detect { |gate| gate.protects?(
|
369
|
+
# Raises Flipper::GateNotFound if no gate found for actor
|
370
|
+
def gate_for(actor)
|
371
|
+
gates.detect { |gate| gate.protects?(actor) } || raise(GateNotFound, actor)
|
370
372
|
end
|
371
373
|
|
372
374
|
private
|
@@ -7,13 +7,17 @@ module Flipper
|
|
7
7
|
# gates for the feature.
|
8
8
|
attr_reader :values
|
9
9
|
|
10
|
-
# Public: The
|
11
|
-
attr_reader :
|
10
|
+
# Public: The actors we want to know if a feature is enabled for.
|
11
|
+
attr_reader :actors
|
12
12
|
|
13
|
-
def initialize(feature_name:, values:,
|
13
|
+
def initialize(feature_name:, values:, actors:)
|
14
14
|
@feature_name = feature_name
|
15
15
|
@values = values
|
16
|
-
@
|
16
|
+
@actors = actors
|
17
|
+
end
|
18
|
+
|
19
|
+
def actors?
|
20
|
+
!@actors.nil? && !@actors.empty?
|
17
21
|
end
|
18
22
|
|
19
23
|
# Public: Convenience method for groups value like Feature has.
|
data/lib/flipper/gate.rb
CHANGED
@@ -18,28 +18,29 @@ module Flipper
|
|
18
18
|
raise 'Not implemented'
|
19
19
|
end
|
20
20
|
|
21
|
-
def enabled?(
|
21
|
+
def enabled?(value)
|
22
22
|
raise 'Not implemented'
|
23
23
|
end
|
24
24
|
|
25
|
-
# Internal: Check if a gate is open for
|
25
|
+
# Internal: Check if a gate is open for one or more actors. Implemented
|
26
|
+
# in subclass.
|
26
27
|
#
|
27
|
-
# Returns true if gate open for
|
28
|
-
def open?(
|
28
|
+
# Returns true if gate open for any actor, false if not.
|
29
|
+
def open?(actors, value, options = {})
|
29
30
|
false
|
30
31
|
end
|
31
32
|
|
32
|
-
# Internal: Check if a gate is protects
|
33
|
+
# Internal: Check if a gate is protects an actor. Implemented in subclass.
|
33
34
|
#
|
34
|
-
# Returns true if gate protects
|
35
|
-
def protects?(
|
35
|
+
# Returns true if gate protects actor, false if not.
|
36
|
+
def protects?(actor)
|
36
37
|
false
|
37
38
|
end
|
38
39
|
|
39
|
-
# Internal: Allows gate to wrap
|
40
|
-
# types so adapters always get
|
41
|
-
def wrap(
|
42
|
-
|
40
|
+
# Internal: Allows gate to wrap actor using one of the supported flipper
|
41
|
+
# types so adapters always get someactor that responds to value.
|
42
|
+
def wrap(actor)
|
43
|
+
actor
|
43
44
|
end
|
44
45
|
|
45
46
|
# Public: Pretty string version for debugging.
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -19,20 +19,23 @@ module Flipper
|
|
19
19
|
!value.empty?
|
20
20
|
end
|
21
21
|
|
22
|
-
# Internal: Checks if the gate is open for
|
22
|
+
# Internal: Checks if the gate is open for an actor.
|
23
23
|
#
|
24
|
-
# Returns true if gate open for
|
24
|
+
# Returns true if gate open for actor, false if not.
|
25
25
|
def open?(context)
|
26
|
-
return false
|
27
|
-
|
26
|
+
return false unless context.actors?
|
27
|
+
|
28
|
+
context.actors.any? do |actor|
|
29
|
+
context.values.actors.include?(actor.value)
|
30
|
+
end
|
28
31
|
end
|
29
32
|
|
30
|
-
def wrap(
|
31
|
-
Types::Actor.wrap(
|
33
|
+
def wrap(actor)
|
34
|
+
Types::Actor.wrap(actor)
|
32
35
|
end
|
33
36
|
|
34
|
-
def protects?(
|
35
|
-
Types::Actor.wrappable?(
|
37
|
+
def protects?(actor)
|
38
|
+
Types::Actor.wrappable?(actor)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
data/lib/flipper/gates/group.rb
CHANGED
@@ -23,10 +23,12 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
25
|
def open?(context)
|
26
|
-
return false
|
26
|
+
return false unless context.actors?
|
27
27
|
|
28
28
|
context.values.groups.any? do |name|
|
29
|
-
|
29
|
+
context.actors.any? do |actor|
|
30
|
+
Flipper.group(name).match?(actor, context)
|
31
|
+
end
|
30
32
|
end
|
31
33
|
end
|
32
34
|
|
@@ -26,13 +26,12 @@ module Flipper
|
|
26
26
|
SCALING_FACTOR = 1_000
|
27
27
|
private_constant :SCALING_FACTOR
|
28
28
|
|
29
|
-
# Internal: Checks if the gate is open for
|
29
|
+
# Internal: Checks if the gate is open for one or more actors.
|
30
30
|
#
|
31
|
-
# Returns true if gate open for
|
31
|
+
# Returns true if gate open for any actors, false if not.
|
32
32
|
def open?(context)
|
33
|
-
return false
|
34
|
-
|
35
|
-
id = "#{context.feature_name}#{context.thing.value}"
|
33
|
+
return false unless context.actors?
|
34
|
+
id = "#{context.feature_name}#{context.actors.map(&:value).sort.join}"
|
36
35
|
Zlib.crc32(id) % (100 * SCALING_FACTOR) < context.values.percentage_of_actors * SCALING_FACTOR
|
37
36
|
end
|
38
37
|
|
data/lib/flipper/identifier.rb
CHANGED
@@ -10,7 +10,7 @@ module Flipper
|
|
10
10
|
# Example Output
|
11
11
|
#
|
12
12
|
# flipper[:search].enabled?(user)
|
13
|
-
# # Flipper feature(search) enabled? false (1.2ms) [
|
13
|
+
# # Flipper feature(search) enabled? false (1.2ms) [ actors=... ]
|
14
14
|
#
|
15
15
|
# Returns nothing.
|
16
16
|
def feature_operation(event)
|
@@ -20,15 +20,19 @@ module Flipper
|
|
20
20
|
gate_name = event.payload[:gate_name]
|
21
21
|
operation = event.payload[:operation]
|
22
22
|
result = event.payload[:result]
|
23
|
-
thing = event.payload[:thing]
|
24
23
|
|
25
24
|
description = "Flipper feature(#{feature_name}) #{operation} #{result.inspect}"
|
26
|
-
|
25
|
+
|
26
|
+
details = if event.payload.key?(:actors)
|
27
|
+
"actors=#{event.payload[:actors].inspect}"
|
28
|
+
else
|
29
|
+
"thing=#{event.payload[:thing].inspect}"
|
30
|
+
end
|
27
31
|
|
28
32
|
details += " gate_name=#{gate_name}" unless gate_name.nil?
|
29
33
|
|
30
34
|
name = '%s (%.1fms)' % [description, event.duration]
|
31
|
-
debug " #{
|
35
|
+
debug " #{color_name(name)} [ #{details} ]"
|
32
36
|
end
|
33
37
|
|
34
38
|
# Logs an adapter operation. If operation is for a feature, then that
|
@@ -60,12 +64,27 @@ module Flipper
|
|
60
64
|
details = "result=#{result.inspect}"
|
61
65
|
|
62
66
|
name = '%s (%.1fms)' % [description, event.duration]
|
63
|
-
debug " #{
|
67
|
+
debug " #{color_name(name)} [ #{details} ]"
|
64
68
|
end
|
65
69
|
|
66
70
|
def logger
|
67
71
|
self.class.logger
|
68
72
|
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# Rails 7.1 changed the signature of this function.
|
77
|
+
# Checking if > 7.0.99 rather than >= 7.1 so that 7.1 pre-release versions are included.
|
78
|
+
COLOR_OPTIONS = if Rails.gem_version > Gem::Version.new('7.0.99')
|
79
|
+
{ bold: true }.freeze
|
80
|
+
else
|
81
|
+
true
|
82
|
+
end
|
83
|
+
private_constant :COLOR_OPTIONS
|
84
|
+
|
85
|
+
def color_name(name)
|
86
|
+
color(name, CYAN, COLOR_OPTIONS)
|
87
|
+
end
|
69
88
|
end
|
70
89
|
end
|
71
90
|
|
@@ -45,7 +45,6 @@ module Flipper
|
|
45
45
|
gate_name = @payload[:gate_name]
|
46
46
|
operation = strip_trailing_question_mark(@payload[:operation])
|
47
47
|
result = @payload[:result]
|
48
|
-
thing = @payload[:thing]
|
49
48
|
|
50
49
|
update_timer "flipper.feature_operation.#{operation}"
|
51
50
|
|
@@ -72,6 +71,14 @@ module Flipper
|
|
72
71
|
update_timer "flipper.adapter.#{adapter_name}.#{operation}"
|
73
72
|
end
|
74
73
|
|
74
|
+
def update_poller_metrics
|
75
|
+
# noop
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_synchronizer_call_metrics
|
79
|
+
# noop
|
80
|
+
end
|
81
|
+
|
75
82
|
QUESTION_MARK = '?'.freeze
|
76
83
|
|
77
84
|
# Private
|
data/lib/flipper/poller.rb
CHANGED
@@ -28,7 +28,7 @@ module Flipper
|
|
28
28
|
@remote_adapter = options.fetch(:remote_adapter)
|
29
29
|
@interval = options.fetch(:interval, 10).to_f
|
30
30
|
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
31
|
-
@adapter = Adapters::Memory.new
|
31
|
+
@adapter = Adapters::Memory.new(nil, threadsafe: true)
|
32
32
|
|
33
33
|
if @interval < 1
|
34
34
|
warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
|
@@ -33,7 +33,15 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
33
33
|
expect(subject.class.ancestors).to include(Flipper::Adapter)
|
34
34
|
end
|
35
35
|
|
36
|
+
it 'knows how to get adapter from source' do
|
37
|
+
adapter = Flipper::Adapters::Memory.new
|
38
|
+
flipper = Flipper.new(adapter)
|
39
|
+
expect(subject.class.from(adapter).class.ancestors).to include(Flipper::Adapter)
|
40
|
+
expect(subject.class.from(flipper).class.ancestors).to include(Flipper::Adapter)
|
41
|
+
end
|
42
|
+
|
36
43
|
it 'returns correct default values for the gates if none are enabled' do
|
44
|
+
expect(subject.get(feature)).to eq(subject.class.default_config)
|
37
45
|
expect(subject.get(feature)).to eq(subject.default_config)
|
38
46
|
end
|
39
47
|
|
@@ -304,4 +312,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
304
312
|
subject.enable(feature, boolean_gate, flipper.boolean(true))
|
305
313
|
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
306
314
|
end
|
315
|
+
|
316
|
+
it 'can import and export' do
|
317
|
+
adapter = Flipper::Adapters::Memory.new
|
318
|
+
source_flipper = Flipper.new(adapter)
|
319
|
+
source_flipper.enable(:stats)
|
320
|
+
export = adapter.export
|
321
|
+
|
322
|
+
# some adapters cannot import so if they return false lets assert it
|
323
|
+
# didn't happen
|
324
|
+
if subject.import(export)
|
325
|
+
expect(flipper[:stats]).to be_enabled
|
326
|
+
else
|
327
|
+
expect(flipper[:stats]).not_to be_enabled
|
328
|
+
end
|
329
|
+
end
|
307
330
|
end
|
@@ -34,7 +34,16 @@ module Flipper
|
|
34
34
|
assert_includes @adapter.class.ancestors, Flipper::Adapter
|
35
35
|
end
|
36
36
|
|
37
|
+
def test_knows_how_to_get_adapter_from_source
|
38
|
+
adapter = Flipper::Adapters::Memory.new
|
39
|
+
flipper = Flipper.new(adapter)
|
40
|
+
|
41
|
+
assert_includes adapter.class.from(adapter).class.ancestors, Flipper::Adapter
|
42
|
+
assert_includes adapter.class.from(flipper).class.ancestors, Flipper::Adapter
|
43
|
+
end
|
44
|
+
|
37
45
|
def test_returns_correct_default_values_for_gates_if_none_are_enabled
|
46
|
+
assert_equal @adapter.class.default_config, @adapter.get(@feature)
|
38
47
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
39
48
|
end
|
40
49
|
|
@@ -300,6 +309,21 @@ module Flipper
|
|
300
309
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
|
301
310
|
assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
|
302
311
|
end
|
312
|
+
|
313
|
+
def test_can_import_and_export
|
314
|
+
adapter = Flipper::Adapters::Memory.new
|
315
|
+
source_flipper = Flipper.new(adapter)
|
316
|
+
source_flipper.enable(:stats)
|
317
|
+
export = adapter.export
|
318
|
+
|
319
|
+
# some adapters cannot import so if they return false lets assert it
|
320
|
+
# didn't happen
|
321
|
+
if @adapter.import(export)
|
322
|
+
assert @flipper[:stats].enabled?
|
323
|
+
else
|
324
|
+
refute @flipper[:stats].enabled?
|
325
|
+
end
|
326
|
+
end
|
303
327
|
end
|
304
328
|
end
|
305
329
|
end
|
data/lib/flipper/typecast.rb
CHANGED
@@ -62,5 +62,22 @@ module Flipper
|
|
62
62
|
raise ArgumentError, "#{value.inspect} cannot be converted to a set"
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
def self.features_hash(source)
|
67
|
+
normalized_source = {}
|
68
|
+
(source || {}).each do |feature_key, gates|
|
69
|
+
normalized_source[feature_key] ||= {}
|
70
|
+
gates.each do |gate_key, value|
|
71
|
+
normalized_value = case value
|
72
|
+
when Array, Set
|
73
|
+
value.to_set
|
74
|
+
else
|
75
|
+
value ? value.to_s : value
|
76
|
+
end
|
77
|
+
normalized_source[feature_key][gate_key.to_sym] = normalized_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
normalized_source
|
81
|
+
end
|
65
82
|
end
|
66
83
|
end
|
data/lib/flipper/types/actor.rb
CHANGED
@@ -1,35 +1,35 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Types
|
3
3
|
class Actor < Type
|
4
|
-
def self.wrappable?(
|
5
|
-
return false if
|
6
|
-
|
4
|
+
def self.wrappable?(actor)
|
5
|
+
return false if actor.nil?
|
6
|
+
actor.respond_to?(:flipper_id)
|
7
7
|
end
|
8
8
|
|
9
|
-
attr_reader :
|
9
|
+
attr_reader :actor
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
raise ArgumentError, '
|
11
|
+
def initialize(actor)
|
12
|
+
raise ArgumentError, 'actor cannot be nil' if actor.nil?
|
13
13
|
|
14
|
-
unless
|
15
|
-
raise ArgumentError, "#{
|
14
|
+
unless actor.respond_to?(:flipper_id)
|
15
|
+
raise ArgumentError, "#{actor.inspect} must respond to flipper_id, but does not"
|
16
16
|
end
|
17
17
|
|
18
|
-
@
|
19
|
-
@value =
|
18
|
+
@actor = actor
|
19
|
+
@value = actor.flipper_id.to_s
|
20
20
|
end
|
21
21
|
|
22
22
|
def respond_to?(*args)
|
23
|
-
super || @
|
23
|
+
super || @actor.respond_to?(*args)
|
24
24
|
end
|
25
25
|
|
26
26
|
if RUBY_VERSION >= '3.0'
|
27
27
|
def method_missing(name, *args, **kwargs, &block)
|
28
|
-
@
|
28
|
+
@actor.send name, *args, **kwargs, &block
|
29
29
|
end
|
30
30
|
else
|
31
31
|
def method_missing(name, *args, &block)
|
32
|
-
@
|
32
|
+
@actor.send name, *args, &block
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
data/lib/flipper/types/group.rb
CHANGED
@@ -16,16 +16,16 @@ module Flipper
|
|
16
16
|
@block = block
|
17
17
|
@single_argument = call_with_no_context?(@block)
|
18
18
|
else
|
19
|
-
@block = ->(
|
19
|
+
@block = ->(actor, context) { false }
|
20
20
|
@single_argument = false
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def match?(
|
24
|
+
def match?(actor, context)
|
25
25
|
if @single_argument
|
26
|
-
@block.call(
|
26
|
+
@block.call(actor)
|
27
27
|
else
|
28
|
-
@block.call(
|
28
|
+
@block.call(actor, context)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
@@ -64,7 +64,7 @@ module Flipper
|
|
64
64
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
65
65
|
:time, :percentage_of_time,
|
66
66
|
:features, :feature, :[], :preload, :preload_all,
|
67
|
-
:adapter, :add, :exist?, :remove, :import,
|
67
|
+
:adapter, :add, :exist?, :remove, :import, :export,
|
68
68
|
:memoize=, :memoizing?,
|
69
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
70
70
|
|
@@ -72,12 +72,12 @@ module Flipper
|
|
72
72
|
#
|
73
73
|
# name - The Symbol name of the group.
|
74
74
|
# block - The block that should be used to determine if the group matches a
|
75
|
-
# given
|
75
|
+
# given actor.
|
76
76
|
#
|
77
77
|
# Examples
|
78
78
|
#
|
79
|
-
# Flipper.register(:admins) { |
|
80
|
-
#
|
79
|
+
# Flipper.register(:admins) { |actor|
|
80
|
+
# actor.respond_to?(:admin?) && actor.admin?
|
81
81
|
# }
|
82
82
|
#
|
83
83
|
# Returns a Flipper::Group.
|
@@ -165,5 +165,6 @@ require 'flipper/types/percentage'
|
|
165
165
|
require 'flipper/types/percentage_of_actors'
|
166
166
|
require 'flipper/types/percentage_of_time'
|
167
167
|
require 'flipper/typecast'
|
168
|
+
require 'flipper/version'
|
168
169
|
|
169
170
|
require "flipper/railtie" if defined?(Rails::Railtie)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"version": 1,
|
3
|
+
"features": {
|
4
|
+
"search": {
|
5
|
+
"boolean": null,
|
6
|
+
"actors": [
|
7
|
+
"john",
|
8
|
+
"another",
|
9
|
+
"testing"
|
10
|
+
],
|
11
|
+
"percentage_of_actors": null,
|
12
|
+
"percentage_of_time": null,
|
13
|
+
"groups": [
|
14
|
+
"admins"
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"new_pricing": {
|
18
|
+
"boolean": "true",
|
19
|
+
"actors": [],
|
20
|
+
"percentage_of_actors": null,
|
21
|
+
"percentage_of_time": null,
|
22
|
+
"groups": []
|
23
|
+
},
|
24
|
+
"google_analytics_tag": {
|
25
|
+
"boolean": null,
|
26
|
+
"actors": [],
|
27
|
+
"percentage_of_actors": "100",
|
28
|
+
"percentage_of_time": null,
|
29
|
+
"groups": []
|
30
|
+
},
|
31
|
+
"help_scout_tag": {
|
32
|
+
"boolean": null,
|
33
|
+
"actors": [],
|
34
|
+
"percentage_of_actors": null,
|
35
|
+
"percentage_of_time": "50",
|
36
|
+
"groups": []
|
37
|
+
},
|
38
|
+
"nope": {
|
39
|
+
"boolean": null,
|
40
|
+
"actors": [],
|
41
|
+
"percentage_of_actors": null,
|
42
|
+
"percentage_of_time": null,
|
43
|
+
"groups": []
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|