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.
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(', ')}>"