flipper 0.10.2 → 0.11.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +42 -0
  3. data/.rubocop_todo.yml +188 -0
  4. data/Changelog.md +10 -0
  5. data/Gemfile +6 -3
  6. data/README.md +4 -3
  7. data/Rakefile +13 -13
  8. data/docs/Adapters.md +2 -1
  9. data/docs/DockerCompose.md +6 -3
  10. data/docs/Gates.md +25 -3
  11. data/docs/Optimization.md +27 -5
  12. data/docs/api/README.md +73 -32
  13. data/docs/http/README.md +34 -0
  14. data/docs/read-only/README.md +22 -0
  15. data/examples/percentage_of_actors_group.rb +49 -0
  16. data/flipper.gemspec +15 -15
  17. data/lib/flipper.rb +2 -5
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapters/http.rb +147 -0
  20. data/lib/flipper/adapters/http/client.rb +83 -0
  21. data/lib/flipper/adapters/http/error.rb +14 -0
  22. data/lib/flipper/adapters/instrumented.rb +36 -36
  23. data/lib/flipper/adapters/memoizable.rb +2 -6
  24. data/lib/flipper/adapters/memory.rb +10 -9
  25. data/lib/flipper/adapters/operation_logger.rb +1 -1
  26. data/lib/flipper/adapters/pstore.rb +12 -11
  27. data/lib/flipper/adapters/read_only.rb +6 -6
  28. data/lib/flipper/dsl.rb +1 -3
  29. data/lib/flipper/feature.rb +11 -16
  30. data/lib/flipper/gate.rb +3 -3
  31. data/lib/flipper/gate_values.rb +6 -6
  32. data/lib/flipper/gates/group.rb +2 -2
  33. data/lib/flipper/gates/percentage_of_actors.rb +2 -2
  34. data/lib/flipper/instrumentation/log_subscriber.rb +2 -4
  35. data/lib/flipper/instrumentation/metriks.rb +1 -1
  36. data/lib/flipper/instrumentation/statsd.rb +1 -1
  37. data/lib/flipper/instrumentation/statsd_subscriber.rb +1 -3
  38. data/lib/flipper/instrumentation/subscriber.rb +11 -10
  39. data/lib/flipper/instrumenters/memory.rb +1 -5
  40. data/lib/flipper/instrumenters/noop.rb +1 -1
  41. data/lib/flipper/middleware/memoizer.rb +11 -27
  42. data/lib/flipper/middleware/setup_env.rb +44 -0
  43. data/lib/flipper/registry.rb +8 -10
  44. data/lib/flipper/spec/shared_adapter_specs.rb +45 -67
  45. data/lib/flipper/test/shared_adapter_test.rb +25 -31
  46. data/lib/flipper/typecast.rb +2 -2
  47. data/lib/flipper/types/actor.rb +2 -4
  48. data/lib/flipper/types/group.rb +1 -1
  49. data/lib/flipper/types/percentage.rb +2 -1
  50. data/lib/flipper/version.rb +1 -1
  51. data/spec/fixtures/feature.json +31 -0
  52. data/spec/flipper/adapters/http_spec.rb +148 -0
  53. data/spec/flipper/adapters/instrumented_spec.rb +20 -20
  54. data/spec/flipper/adapters/memoizable_spec.rb +59 -59
  55. data/spec/flipper/adapters/operation_logger_spec.rb +16 -16
  56. data/spec/flipper/adapters/pstore_spec.rb +6 -6
  57. data/spec/flipper/adapters/read_only_spec.rb +28 -34
  58. data/spec/flipper/dsl_spec.rb +73 -84
  59. data/spec/flipper/feature_check_context_spec.rb +27 -27
  60. data/spec/flipper/feature_spec.rb +186 -196
  61. data/spec/flipper/gate_spec.rb +11 -11
  62. data/spec/flipper/gate_values_spec.rb +46 -45
  63. data/spec/flipper/gates/actor_spec.rb +2 -2
  64. data/spec/flipper/gates/boolean_spec.rb +24 -23
  65. data/spec/flipper/gates/group_spec.rb +19 -19
  66. data/spec/flipper/gates/percentage_of_actors_spec.rb +10 -10
  67. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  68. data/spec/flipper/instrumentation/log_subscriber_spec.rb +20 -20
  69. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +20 -20
  70. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +11 -11
  71. data/spec/flipper/instrumenters/memory_spec.rb +5 -5
  72. data/spec/flipper/instrumenters/noop_spec.rb +6 -6
  73. data/spec/flipper/middleware/memoizer_spec.rb +83 -100
  74. data/spec/flipper/middleware/setup_env_spec.rb +76 -0
  75. data/spec/flipper/registry_spec.rb +35 -39
  76. data/spec/flipper/typecast_spec.rb +18 -18
  77. data/spec/flipper/types/actor_spec.rb +30 -29
  78. data/spec/flipper/types/boolean_spec.rb +8 -8
  79. data/spec/flipper/types/group_spec.rb +28 -28
  80. data/spec/flipper/types/percentage_spec.rb +14 -14
  81. data/spec/flipper_spec.rb +61 -54
  82. data/spec/helper.rb +26 -21
  83. data/spec/integration_spec.rb +121 -113
  84. data/spec/support/fake_udp_socket.rb +1 -1
  85. data/spec/support/spec_helpers.rb +32 -4
  86. data/test/adapters/pstore_test.rb +3 -3
  87. data/test/test_helper.rb +1 -1
  88. metadata +20 -5
@@ -64,12 +64,9 @@ module Flipper
64
64
  #
65
65
  # Flipper.group(:admins)
66
66
  #
67
- # Returns the Flipper::Group if group registered.
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.
@@ -11,5 +11,15 @@ module Flipper
11
11
  end
12
12
  result
13
13
  end
14
+
15
+ def default_config
16
+ {
17
+ boolean: nil,
18
+ groups: Set.new,
19
+ actors: Set.new,
20
+ percentage_of_actors: nil,
21
+ percentage_of_time: nil,
22
+ }
23
+ end
14
24
  end
15
25
  end
@@ -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
@@ -0,0 +1,14 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Http
4
+ class Error < StandardError
5
+ attr_reader :response
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ super("Failed with status: #{response.code}")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -34,93 +34,93 @@ module Flipper
34
34
  # Public
35
35
  def features
36
36
  payload = {
37
- :operation => :features,
38
- :adapter_name => @adapter.name,
37
+ operation: :features,
38
+ adapter_name: @adapter.name,
39
39
  }
40
40
 
41
- @instrumenter.instrument(InstrumentationName, payload) { |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
- :operation => :add,
50
- :adapter_name => @adapter.name,
51
- :feature_name => feature.name,
49
+ operation: :add,
50
+ adapter_name: @adapter.name,
51
+ feature_name: feature.name,
52
52
  }
53
53
 
54
- @instrumenter.instrument(InstrumentationName, payload) { |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
- :operation => :remove,
63
- :adapter_name => @adapter.name,
64
- :feature_name => feature.name,
62
+ operation: :remove,
63
+ adapter_name: @adapter.name,
64
+ feature_name: feature.name,
65
65
  }
66
66
 
67
- @instrumenter.instrument(InstrumentationName, payload) { |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
- :operation => :clear,
76
- :adapter_name => @adapter.name,
77
- :feature_name => feature.name,
75
+ operation: :clear,
76
+ adapter_name: @adapter.name,
77
+ feature_name: feature.name,
78
78
  }
79
79
 
80
- @instrumenter.instrument(InstrumentationName, payload) { |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
- :operation => :get,
89
- :adapter_name => @adapter.name,
90
- :feature_name => feature.name,
88
+ operation: :get,
89
+ adapter_name: @adapter.name,
90
+ feature_name: feature.name,
91
91
  }
92
92
 
93
- @instrumenter.instrument(InstrumentationName, payload) { |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
- :operation => :enable,
102
- :adapter_name => @adapter.name,
103
- :feature_name => feature.name,
104
- :gate_name => gate.name,
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) { |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
- :operation => :disable,
116
- :adapter_name => @adapter.name,
117
- :feature_name => feature.name,
118
- :gate_name => gate.name,
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) { |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] = case gate.data_type
54
- when :boolean, :integer
55
- read key(feature, gate)
56
- when :set
57
- set_members key(feature, gate)
58
- else
59
- raise "#{gate} is not supported by this adapter yet"
60
- end
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
- "name=:memory",
100
+ 'name=:memory',
100
101
  "source=#{@source.inspect}",
101
102
  ]
102
103
  "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"