flipper 0.10.2 → 0.11.0.beta1
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/.rubocop.yml +42 -0
- data/.rubocop_todo.yml +188 -0
- data/Changelog.md +10 -0
- data/Gemfile +6 -3
- data/README.md +4 -3
- data/Rakefile +13 -13
- data/docs/Adapters.md +2 -1
- data/docs/DockerCompose.md +6 -3
- data/docs/Gates.md +25 -3
- data/docs/Optimization.md +27 -5
- data/docs/api/README.md +73 -32
- data/docs/http/README.md +34 -0
- data/docs/read-only/README.md +22 -0
- data/examples/percentage_of_actors_group.rb +49 -0
- data/flipper.gemspec +15 -15
- data/lib/flipper.rb +2 -5
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapters/http.rb +147 -0
- data/lib/flipper/adapters/http/client.rb +83 -0
- data/lib/flipper/adapters/http/error.rb +14 -0
- data/lib/flipper/adapters/instrumented.rb +36 -36
- data/lib/flipper/adapters/memoizable.rb +2 -6
- data/lib/flipper/adapters/memory.rb +10 -9
- data/lib/flipper/adapters/operation_logger.rb +1 -1
- data/lib/flipper/adapters/pstore.rb +12 -11
- data/lib/flipper/adapters/read_only.rb +6 -6
- data/lib/flipper/dsl.rb +1 -3
- data/lib/flipper/feature.rb +11 -16
- data/lib/flipper/gate.rb +3 -3
- data/lib/flipper/gate_values.rb +6 -6
- data/lib/flipper/gates/group.rb +2 -2
- data/lib/flipper/gates/percentage_of_actors.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/metriks.rb +1 -1
- data/lib/flipper/instrumentation/statsd.rb +1 -1
- data/lib/flipper/instrumentation/statsd_subscriber.rb +1 -3
- data/lib/flipper/instrumentation/subscriber.rb +11 -10
- data/lib/flipper/instrumenters/memory.rb +1 -5
- data/lib/flipper/instrumenters/noop.rb +1 -1
- data/lib/flipper/middleware/memoizer.rb +11 -27
- data/lib/flipper/middleware/setup_env.rb +44 -0
- data/lib/flipper/registry.rb +8 -10
- data/lib/flipper/spec/shared_adapter_specs.rb +45 -67
- data/lib/flipper/test/shared_adapter_test.rb +25 -31
- data/lib/flipper/typecast.rb +2 -2
- data/lib/flipper/types/actor.rb +2 -4
- data/lib/flipper/types/group.rb +1 -1
- data/lib/flipper/types/percentage.rb +2 -1
- data/lib/flipper/version.rb +1 -1
- data/spec/fixtures/feature.json +31 -0
- data/spec/flipper/adapters/http_spec.rb +148 -0
- data/spec/flipper/adapters/instrumented_spec.rb +20 -20
- data/spec/flipper/adapters/memoizable_spec.rb +59 -59
- data/spec/flipper/adapters/operation_logger_spec.rb +16 -16
- data/spec/flipper/adapters/pstore_spec.rb +6 -6
- data/spec/flipper/adapters/read_only_spec.rb +28 -34
- data/spec/flipper/dsl_spec.rb +73 -84
- data/spec/flipper/feature_check_context_spec.rb +27 -27
- data/spec/flipper/feature_spec.rb +186 -196
- data/spec/flipper/gate_spec.rb +11 -11
- data/spec/flipper/gate_values_spec.rb +46 -45
- data/spec/flipper/gates/actor_spec.rb +2 -2
- data/spec/flipper/gates/boolean_spec.rb +24 -23
- data/spec/flipper/gates/group_spec.rb +19 -19
- data/spec/flipper/gates/percentage_of_actors_spec.rb +10 -10
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +20 -20
- data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +20 -20
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +11 -11
- data/spec/flipper/instrumenters/memory_spec.rb +5 -5
- data/spec/flipper/instrumenters/noop_spec.rb +6 -6
- data/spec/flipper/middleware/memoizer_spec.rb +83 -100
- data/spec/flipper/middleware/setup_env_spec.rb +76 -0
- data/spec/flipper/registry_spec.rb +35 -39
- data/spec/flipper/typecast_spec.rb +18 -18
- data/spec/flipper/types/actor_spec.rb +30 -29
- data/spec/flipper/types/boolean_spec.rb +8 -8
- data/spec/flipper/types/group_spec.rb +28 -28
- data/spec/flipper/types/percentage_spec.rb +14 -14
- data/spec/flipper_spec.rb +61 -54
- data/spec/helper.rb +26 -21
- data/spec/integration_spec.rb +121 -113
- data/spec/support/fake_udp_socket.rb +1 -1
- data/spec/support/spec_helpers.rb +32 -4
- data/test/adapters/pstore_test.rb +3 -3
- data/test/test_helper.rb +1 -1
- metadata +20 -5
data/lib/flipper.rb
CHANGED
@@ -64,12 +64,9 @@ module Flipper
|
|
64
64
|
#
|
65
65
|
# Flipper.group(:admins)
|
66
66
|
#
|
67
|
-
# Returns
|
68
|
-
# Raises Flipper::GroupNotRegistered if group is not registered.
|
67
|
+
# Returns Flipper::Group.
|
69
68
|
def self.group(name)
|
70
|
-
groups_registry.get(name)
|
71
|
-
rescue Registry::KeyNotFound => e
|
72
|
-
raise GroupNotRegistered, "Group #{e.key.inspect} has not been registered"
|
69
|
+
groups_registry.get(name) || Types::Group.new(name)
|
73
70
|
end
|
74
71
|
|
75
72
|
# Internal: Registry of all groups_registry.
|
data/lib/flipper/adapter.rb
CHANGED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'set'
|
4
|
+
require 'flipper'
|
5
|
+
require 'flipper/adapters/http/error'
|
6
|
+
require 'flipper/adapters/http/client'
|
7
|
+
|
8
|
+
module Flipper
|
9
|
+
module Adapters
|
10
|
+
class Http
|
11
|
+
include Flipper::Adapter
|
12
|
+
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@client = Client.new(uri: options.fetch(:uri),
|
17
|
+
headers: options[:headers],
|
18
|
+
basic_auth_username: options[:basic_auth_username],
|
19
|
+
basic_auth_password: options[:basic_auth_password],
|
20
|
+
read_timeout: options[:read_timeout],
|
21
|
+
open_timeout: options[:open_timeout])
|
22
|
+
@name = :http
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(feature)
|
26
|
+
response = @client.get("/features/#{feature.key}")
|
27
|
+
if response.is_a?(Net::HTTPOK)
|
28
|
+
parsed_response = JSON.parse(response.body)
|
29
|
+
result_for_feature(feature, parsed_response.fetch('gates'))
|
30
|
+
elsif response.is_a?(Net::HTTPNotFound)
|
31
|
+
default_config
|
32
|
+
else
|
33
|
+
raise Error, response
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def add(feature)
|
38
|
+
body = JSON.generate(name: feature.key)
|
39
|
+
response = @client.post('/features', body)
|
40
|
+
response.is_a?(Net::HTTPOK)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_multi(features)
|
44
|
+
csv_keys = features.map(&:key).join(',')
|
45
|
+
response = @client.get("/features?keys=#{csv_keys}")
|
46
|
+
raise Error, response unless response.is_a?(Net::HTTPOK)
|
47
|
+
|
48
|
+
parsed_response = JSON.parse(response.body)
|
49
|
+
parsed_features = parsed_response.fetch('features')
|
50
|
+
gates_by_key = parsed_features.each_with_object({}) do |parsed_feature, hash|
|
51
|
+
hash[parsed_feature['key']] = parsed_feature['gates']
|
52
|
+
hash
|
53
|
+
end
|
54
|
+
|
55
|
+
result = {}
|
56
|
+
features.each do |feature|
|
57
|
+
result[feature.key] = result_for_feature(feature, gates_by_key[feature.key])
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def features
|
63
|
+
response = @client.get('/features')
|
64
|
+
raise Error, response unless response.is_a?(Net::HTTPOK)
|
65
|
+
|
66
|
+
parsed_response = JSON.parse(response.body)
|
67
|
+
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove(feature)
|
71
|
+
response = @client.delete("/features/#{feature.key}")
|
72
|
+
response.is_a?(Net::HTTPNoContent)
|
73
|
+
end
|
74
|
+
|
75
|
+
def enable(feature, gate, thing)
|
76
|
+
body = request_body_for_gate(gate, thing.value.to_s)
|
77
|
+
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
78
|
+
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
79
|
+
response.is_a?(Net::HTTPOK)
|
80
|
+
end
|
81
|
+
|
82
|
+
def disable(feature, gate, thing)
|
83
|
+
body = request_body_for_gate(gate, thing.value.to_s)
|
84
|
+
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
85
|
+
response =
|
86
|
+
case gate.key
|
87
|
+
when :percentage_of_actors, :percentage_of_time
|
88
|
+
@client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
89
|
+
else
|
90
|
+
@client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
91
|
+
end
|
92
|
+
response.is_a?(Net::HTTPOK)
|
93
|
+
end
|
94
|
+
|
95
|
+
def clear(feature)
|
96
|
+
response = @client.delete("/features/#{feature.key}/clear")
|
97
|
+
response.is_a?(Net::HTTPNoContent)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def request_body_for_gate(gate, value)
|
103
|
+
data = case gate.key
|
104
|
+
when :boolean
|
105
|
+
{}
|
106
|
+
when :groups
|
107
|
+
{ name: value }
|
108
|
+
when :actors
|
109
|
+
{ flipper_id: value }
|
110
|
+
when :percentage_of_actors, :percentage_of_time
|
111
|
+
{ percentage: value }
|
112
|
+
else
|
113
|
+
raise "#{gate.key} is not a valid flipper gate key"
|
114
|
+
end
|
115
|
+
JSON.generate(data)
|
116
|
+
end
|
117
|
+
|
118
|
+
def result_for_feature(feature, api_gates)
|
119
|
+
api_gates ||= []
|
120
|
+
result = default_config
|
121
|
+
|
122
|
+
feature.gates.each do |gate|
|
123
|
+
api_gate = api_gates.detect { |ag| ag['key'] == gate.key.to_s }
|
124
|
+
result[gate.key] = value_for_gate(gate, api_gate) if api_gate
|
125
|
+
end
|
126
|
+
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
def value_for_gate(gate, api_gate)
|
131
|
+
value = api_gate['value']
|
132
|
+
case gate.data_type
|
133
|
+
when :boolean, :integer
|
134
|
+
value ? value.to_s : value
|
135
|
+
when :set
|
136
|
+
value ? value.to_set : Set.new
|
137
|
+
else
|
138
|
+
unsupported_data_type(gate.data_type)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def unsupported_data_type(data_type)
|
143
|
+
raise "#{data_type} is not supported by this adapter"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'openssl'
|
3
|
+
require 'flipper/version'
|
4
|
+
|
5
|
+
module Flipper
|
6
|
+
module Adapters
|
7
|
+
class Http
|
8
|
+
class Client
|
9
|
+
DEFAULT_HEADERS = {
|
10
|
+
'Content-Type' => 'application/json',
|
11
|
+
'Accept' => 'application/json',
|
12
|
+
'User-Agent' => "Flipper HTTP Adapter v#{VERSION}",
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
HTTPS_SCHEME = "https".freeze
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@uri = URI(options.fetch(:uri))
|
19
|
+
@headers = DEFAULT_HEADERS.merge(options[:headers] || {})
|
20
|
+
@basic_auth_username = options[:basic_auth_username]
|
21
|
+
@basic_auth_password = options[:basic_auth_password]
|
22
|
+
@read_timeout = options[:read_timeout]
|
23
|
+
@open_timeout = options[:open_timeout]
|
24
|
+
end
|
25
|
+
|
26
|
+
def get(path)
|
27
|
+
perform Net::HTTP::Get, path, @headers
|
28
|
+
end
|
29
|
+
|
30
|
+
def post(path, body = nil)
|
31
|
+
perform Net::HTTP::Post, path, @headers, body: body
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(path, body = nil)
|
35
|
+
perform Net::HTTP::Delete, path, @headers, body: body
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def perform(http_method, path, headers = {}, options = {})
|
41
|
+
uri = uri_for_path(path)
|
42
|
+
http = build_http(uri)
|
43
|
+
request = build_request(http_method, uri, headers, options)
|
44
|
+
http.request(request)
|
45
|
+
end
|
46
|
+
|
47
|
+
def uri_for_path(path)
|
48
|
+
uri = @uri.dup
|
49
|
+
path_uri = URI(path)
|
50
|
+
uri.path += path_uri.path
|
51
|
+
uri.query = "#{uri.query}&#{path_uri.query}" if path_uri.query
|
52
|
+
uri
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_http(uri)
|
56
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
57
|
+
http.read_timeout = @read_timeout if @read_timeout
|
58
|
+
http.open_timeout = @open_timeout if @open_timeout
|
59
|
+
|
60
|
+
if uri.scheme == HTTPS_SCHEME
|
61
|
+
http.use_ssl = true
|
62
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
63
|
+
end
|
64
|
+
|
65
|
+
http
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_request(http_method, uri, headers, options)
|
69
|
+
body = options[:body]
|
70
|
+
request = http_method.new(uri.request_uri)
|
71
|
+
request.initialize_http_header(headers) if headers
|
72
|
+
request.body = body if body
|
73
|
+
|
74
|
+
if @basic_auth_username && @basic_auth_password
|
75
|
+
request.basic_auth(@basic_auth_username, @basic_auth_password)
|
76
|
+
end
|
77
|
+
|
78
|
+
request
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -34,93 +34,93 @@ module Flipper
|
|
34
34
|
# Public
|
35
35
|
def features
|
36
36
|
payload = {
|
37
|
-
:
|
38
|
-
:
|
37
|
+
operation: :features,
|
38
|
+
adapter_name: @adapter.name,
|
39
39
|
}
|
40
40
|
|
41
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
41
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
42
42
|
payload[:result] = @adapter.features
|
43
|
-
|
43
|
+
end
|
44
44
|
end
|
45
45
|
|
46
46
|
# Public
|
47
47
|
def add(feature)
|
48
48
|
payload = {
|
49
|
-
:
|
50
|
-
:
|
51
|
-
:
|
49
|
+
operation: :add,
|
50
|
+
adapter_name: @adapter.name,
|
51
|
+
feature_name: feature.name,
|
52
52
|
}
|
53
53
|
|
54
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
54
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
55
55
|
payload[:result] = @adapter.add(feature)
|
56
|
-
|
56
|
+
end
|
57
57
|
end
|
58
58
|
|
59
59
|
# Public
|
60
60
|
def remove(feature)
|
61
61
|
payload = {
|
62
|
-
:
|
63
|
-
:
|
64
|
-
:
|
62
|
+
operation: :remove,
|
63
|
+
adapter_name: @adapter.name,
|
64
|
+
feature_name: feature.name,
|
65
65
|
}
|
66
66
|
|
67
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
67
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
68
68
|
payload[:result] = @adapter.remove(feature)
|
69
|
-
|
69
|
+
end
|
70
70
|
end
|
71
71
|
|
72
72
|
# Public
|
73
73
|
def clear(feature)
|
74
74
|
payload = {
|
75
|
-
:
|
76
|
-
:
|
77
|
-
:
|
75
|
+
operation: :clear,
|
76
|
+
adapter_name: @adapter.name,
|
77
|
+
feature_name: feature.name,
|
78
78
|
}
|
79
79
|
|
80
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
80
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
81
81
|
payload[:result] = @adapter.clear(feature)
|
82
|
-
|
82
|
+
end
|
83
83
|
end
|
84
84
|
|
85
85
|
# Public
|
86
86
|
def get(feature)
|
87
87
|
payload = {
|
88
|
-
:
|
89
|
-
:
|
90
|
-
:
|
88
|
+
operation: :get,
|
89
|
+
adapter_name: @adapter.name,
|
90
|
+
feature_name: feature.name,
|
91
91
|
}
|
92
92
|
|
93
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
93
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
94
94
|
payload[:result] = @adapter.get(feature)
|
95
|
-
|
95
|
+
end
|
96
96
|
end
|
97
97
|
|
98
98
|
# Public
|
99
99
|
def enable(feature, gate, thing)
|
100
100
|
payload = {
|
101
|
-
:
|
102
|
-
:
|
103
|
-
:
|
104
|
-
:
|
101
|
+
operation: :enable,
|
102
|
+
adapter_name: @adapter.name,
|
103
|
+
feature_name: feature.name,
|
104
|
+
gate_name: gate.name,
|
105
105
|
}
|
106
106
|
|
107
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
107
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
108
108
|
payload[:result] = @adapter.enable(feature, gate, thing)
|
109
|
-
|
109
|
+
end
|
110
110
|
end
|
111
111
|
|
112
112
|
# Public
|
113
113
|
def disable(feature, gate, thing)
|
114
114
|
payload = {
|
115
|
-
:
|
116
|
-
:
|
117
|
-
:
|
118
|
-
:
|
115
|
+
operation: :disable,
|
116
|
+
adapter_name: @adapter.name,
|
117
|
+
feature_name: feature.name,
|
118
|
+
gate_name: gate.name,
|
119
119
|
}
|
120
120
|
|
121
|
-
@instrumenter.instrument(InstrumentationName, payload)
|
121
|
+
@instrumenter.instrument(InstrumentationName, payload) do |payload|
|
122
122
|
payload[:result] = @adapter.disable(feature, gate, thing)
|
123
|
-
|
123
|
+
end
|
124
124
|
end
|
125
125
|
end
|
126
126
|
end
|
@@ -120,15 +120,11 @@ module Flipper
|
|
120
120
|
private
|
121
121
|
|
122
122
|
def expire_feature(feature)
|
123
|
-
if memoizing?
|
124
|
-
cache.delete(feature.key)
|
125
|
-
end
|
123
|
+
cache.delete(feature.key) if memoizing?
|
126
124
|
end
|
127
125
|
|
128
126
|
def expire_features_set
|
129
|
-
if memoizing?
|
130
|
-
cache.delete(FeaturesKey)
|
131
|
-
end
|
127
|
+
cache.delete(FeaturesKey) if memoizing?
|
132
128
|
end
|
133
129
|
end
|
134
130
|
end
|
@@ -50,14 +50,15 @@ module Flipper
|
|
50
50
|
result = {}
|
51
51
|
|
52
52
|
feature.gates.each do |gate|
|
53
|
-
result[gate.key] =
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
result[gate.key] =
|
54
|
+
case gate.data_type
|
55
|
+
when :boolean, :integer
|
56
|
+
read key(feature, gate)
|
57
|
+
when :set
|
58
|
+
set_members key(feature, gate)
|
59
|
+
else
|
60
|
+
raise "#{gate} is not supported by this adapter yet"
|
61
|
+
end
|
61
62
|
end
|
62
63
|
|
63
64
|
result
|
@@ -96,7 +97,7 @@ module Flipper
|
|
96
97
|
# Public
|
97
98
|
def inspect
|
98
99
|
attributes = [
|
99
|
-
|
100
|
+
'name=:memory',
|
100
101
|
"source=#{@source.inspect}",
|
101
102
|
]
|
102
103
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|