coach 0.5.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,7 +16,7 @@ module Coach
16
16
 
17
17
  def self.provides(*new_provided)
18
18
  if new_provided.include?(:_metadata)
19
- raise 'Cannot provide :_metadata, Coach uses this internally!'
19
+ raise "Cannot provide :_metadata, Coach uses this internally!"
20
20
  end
21
21
 
22
22
  provided.concat(new_provided)
@@ -73,10 +73,14 @@ module Coach
73
73
  # Use ActiveSupport to instrument the execution of the subsequent chain.
74
74
  def instrument
75
75
  proc do
76
- ActiveSupport::Notifications.
77
- publish('coach.middleware.start', middleware_event)
78
- ActiveSupport::Notifications.
79
- instrument('coach.middleware.finish', middleware_event) { call }
76
+ publish_start
77
+
78
+ if ActiveSupport::Notifications.notifier.listening?("coach.middleware.finish")
79
+ instrument_deprecated { call }
80
+ else
81
+ ActiveSupport::Notifications.
82
+ instrument("finish_middleware.coach", middleware_event) { call }
83
+ end
80
84
  end
81
85
  end
82
86
 
@@ -99,8 +103,31 @@ module Coach
99
103
  def middleware_event
100
104
  {
101
105
  middleware: self.class.name,
102
- request: request
106
+ request: request,
103
107
  }
104
108
  end
109
+
110
+ def publish_start
111
+ if ActiveSupport::Notifications.notifier.listening?("coach.middleware.start")
112
+ ActiveSupport::Deprecation.warn("The 'coach.middleware.start' event has " \
113
+ "been renamed to 'start_middleware.coach' and the old name will be " \
114
+ "removed in a future version.")
115
+ ActiveSupport::Notifications.
116
+ publish("coach.middleware.start", middleware_event)
117
+ end
118
+ ActiveSupport::Notifications.
119
+ publish("start_middleware.coach", middleware_event)
120
+ end
121
+
122
+ def instrument_deprecated(&block)
123
+ ActiveSupport::Deprecation.warn("The 'coach.middleware.finish' event has " \
124
+ "been renamed to 'finish_middleware.coach' and the old name will be " \
125
+ "removed in a future version.")
126
+ ActiveSupport::Notifications.
127
+ instrument("coach.middleware.finish", middleware_event) do
128
+ ActiveSupport::Notifications.
129
+ instrument("finish_middleware.coach", middleware_event, &block)
130
+ end
131
+ end
105
132
  end
106
133
  end
@@ -24,11 +24,13 @@ module Coach
24
24
  end
25
25
 
26
26
  # Assigns the parent for this middleware, allowing config inheritance
27
+ # rubocop:disable Naming/AccessorMethodName
27
28
  def set_parent(parent)
28
29
  @parent = parent
29
30
 
30
31
  self
31
32
  end
33
+ # rubocop:enable Naming/AccessorMethodName
32
34
 
33
35
  # Generates config by either cloning our given config (if it's a hash) else if a
34
36
  # lambda value, then will compute the config by calling the lambda with this
@@ -1,5 +1,5 @@
1
- require_relative 'request_benchmark'
2
- require_relative 'request_serializer'
1
+ require_relative "request_benchmark"
2
+ require_relative "request_serializer"
3
3
 
4
4
  module Coach
5
5
  # By default, Coach will trigger ActiveSupport::Notifications at specific times in a
@@ -8,15 +8,15 @@ module Coach
8
8
  # Notifications is used to coordinate the listening and aggregation of these middleware
9
9
  # notifications, while RequestEvent processes the published data.
10
10
  #
11
- # Once a request has completed, Notifications will emit a 'coach.request' with
11
+ # Once a request has completed, Notifications will emit a 'requst.coach' with
12
12
  # aggregated request data.
13
13
  class Notifications
14
- # Begin processing/emitting 'coach.request's
14
+ # Begin processing/emitting 'request.coach's
15
15
  def self.subscribe!
16
16
  instance.subscribe!
17
17
  end
18
18
 
19
- # Cease to emit 'coach.request's
19
+ # Cease to emit 'request.coach's
20
20
  def self.unsubscribe!
21
21
  instance.unsubscribe!
22
22
  end
@@ -28,15 +28,15 @@ module Coach
28
28
  def subscribe!
29
29
  return if active?
30
30
 
31
- @subscriptions << subscribe('handler.start') do |_, event|
31
+ @subscriptions << subscribe("start_handler") do |_, event|
32
32
  @benchmarks[event[:request].uuid] = RequestBenchmark.new(event[:middleware])
33
33
  end
34
34
 
35
- @subscriptions << subscribe('middleware.finish') do |_name, start, finish, _, event|
35
+ @subscriptions << subscribe("finish_middleware") do |_name, start, finish, _, event|
36
36
  log_middleware_finish(event, start, finish)
37
37
  end
38
38
 
39
- @subscriptions << subscribe('handler.finish') do |_name, start, finish, _, event|
39
+ @subscriptions << subscribe("finish_handler") do |_name, start, finish, _, event|
40
40
  log_handler_finish(event, start, finish)
41
41
  end
42
42
  end
@@ -63,7 +63,10 @@ module Coach
63
63
  end
64
64
 
65
65
  def subscribe(event, &block)
66
- ActiveSupport::Notifications.subscribe("coach.#{event}", &block)
66
+ # New key formats don't include a period. If they do, they're the old deprecated
67
+ # format. No need to warn here since the warnings will show up from elsewhere.
68
+ key = event.include?(".") ? "coach.#{event}" : "#{event}.coach"
69
+ ActiveSupport::Notifications.subscribe(key, &block)
67
70
  end
68
71
 
69
72
  def log_middleware_finish(event, start, finish)
@@ -79,12 +82,17 @@ module Coach
79
82
  end
80
83
 
81
84
  # Receives a handler.finish event, with processed benchmark. Publishes to
82
- # coach.request notification.
85
+ # request.coach notification.
83
86
  def broadcast(event, benchmark)
84
87
  serialized = RequestSerializer.new(event[:request]).serialize.
85
- merge(benchmark.stats).
86
- merge(event.slice(:response, :metadata))
87
- ActiveSupport::Notifications.publish('coach.request', serialized)
88
+ merge(benchmark.stats).
89
+ merge(event.slice(:response, :metadata))
90
+ if ActiveSupport::Notifications.notifier.listening?("coach.request")
91
+ ActiveSupport::Deprecation.warn("The 'coach.request' event has been renamed " \
92
+ "to 'request.coach' and the old name will be removed in a future version.")
93
+ ActiveSupport::Notifications.publish("coach.request", serialized)
94
+ end
95
+ ActiveSupport::Notifications.publish("request.coach", serialized)
88
96
  end
89
97
  end
90
98
  end
@@ -1,6 +1,6 @@
1
1
  module Coach
2
2
  # This class is built to aggregate data during the course of the request. It relies on
3
- # 'coach.middleware.start' and 'coach.middleware.end' events to register the
3
+ # 'start_middleware.coach' and 'finish_middleware.coach' events to register the
4
4
  # start/end of each middleware element, and thereby calculate running times for each.
5
5
  #
6
6
  # Coach::Notifications makes use of this class to produce benchmark data for
@@ -15,7 +15,7 @@ module Coach
15
15
  event = { name: name, start: start, finish: finish }
16
16
 
17
17
  duration_of_children = child_events_for(event).
18
- inject(0) { |total, e| total + e[:duration] }
18
+ inject(0) { |total, e| total + e[:duration] }
19
19
  event[:duration] = (finish - start) - duration_of_children
20
20
 
21
21
  @events.push(event)
@@ -34,7 +34,7 @@ module Coach
34
34
  duration: format_ms(@duration),
35
35
  chain: sorted_chain.map do |event|
36
36
  { name: event[:name], duration: format_ms(event[:duration]) }
37
- end
37
+ end,
38
38
  }
39
39
  end
40
40
 
@@ -8,7 +8,7 @@ module Coach
8
8
  # that will determine how to transform the original header value, otherwise a default
9
9
  # string is used.
10
10
  def self.sanitize_header(header, &rule)
11
- header_rules[header] = rule || ->(value) { '[FILTERED]' }
11
+ header_rules[header] = rule || ->(_value) { "[FILTERED]" }
12
12
  end
13
13
 
14
14
  # Applies sanitizing rules. Expects `header` to be in 'http_header_name' form.
@@ -39,7 +39,7 @@ module Coach
39
39
 
40
40
  # Extra request info
41
41
  headers: filtered_headers,
42
- session_id: @request.remote_ip
42
+ session_id: @request.remote_ip,
43
43
  }
44
44
  end
45
45
 
@@ -47,7 +47,7 @@ module Coach
47
47
 
48
48
  def request_path
49
49
  @request.fullpath
50
- rescue
50
+ rescue StandardError
51
51
  "unknown"
52
52
  end
53
53
 
@@ -5,10 +5,10 @@ module Coach
5
5
  class Router
6
6
  ACTION_TRAITS = {
7
7
  index: { method: :get },
8
- show: { method: :get, url: ':id' },
8
+ show: { method: :get, url: ":id" },
9
9
  create: { method: :post },
10
- update: { method: :put, url: ':id' },
11
- destroy: { method: :delete, url: ':id' }
10
+ update: { method: :put, url: ":id" },
11
+ destroy: { method: :delete, url: ":id" },
12
12
  }.each_value(&:freeze).freeze
13
13
 
14
14
  def initialize(mapper)
@@ -45,9 +45,7 @@ module Coach
45
45
  # ...which will load the default route for `show` and `index`, while also configuring
46
46
  # a refund route.
47
47
  def action_traits(list_of_actions)
48
- if list_of_actions.last.is_a?(Hash)
49
- *list_of_actions, traits = list_of_actions
50
- end
48
+ *list_of_actions, traits = list_of_actions if list_of_actions.last.is_a?(Hash)
51
49
 
52
50
  list_of_actions.reduce(traits || {}) do |memo, action|
53
51
  trait = ACTION_TRAITS.fetch(action) do
@@ -60,7 +58,7 @@ module Coach
60
58
 
61
59
  # Applies trait url to base, removing duplicate /'s
62
60
  def action_url(base, traits)
63
- [base, traits[:url]].compact.join('/').gsub(%r{/+}, '/')
61
+ [base, traits[:url]].compact.join("/").gsub(%r{/+}, "/")
64
62
  end
65
63
 
66
64
  # Turns a snake_case string/symbol into a CamelCase
@@ -1,8 +1,10 @@
1
- require 'rspec/expectations'
2
- require 'coach/middleware'
1
+ require "rspec/expectations"
2
+ require "coach/middleware"
3
3
 
4
4
  # Middleware stubbing ######################################
5
5
 
6
+ # rubocop:disable Metrics/MethodLength
7
+ # rubocop:disable Metrics/AbcSize
6
8
  def build_middleware(name)
7
9
  Class.new(Coach::Middleware) do
8
10
  # To access `name`, we need to use `define_method` instead of `def`
@@ -25,6 +27,8 @@ def build_middleware(name)
25
27
  end
26
28
  end
27
29
  end
30
+ # rubocop:enable Metrics/AbcSize
31
+ # rubocop:enable Metrics/MethodLength
28
32
 
29
33
  def null_middleware
30
34
  double(call: nil)
@@ -39,7 +43,7 @@ RSpec::Matchers.define :respond_with_status do |expected_status|
39
43
  @response[0] == expected_status
40
44
  end
41
45
 
42
- failure_message do |actual|
46
+ failure_message do |_actual|
43
47
  "expected #{@middleware.class.name} to respond with #{expected_status} but got " \
44
48
  "#{@response[0]}"
45
49
  end
@@ -51,7 +55,7 @@ RSpec::Matchers.define :respond_with_body_that_matches do |body_regex|
51
55
  @response_body.match(body_regex)
52
56
  end
53
57
 
54
- failure_message do |actual|
58
+ failure_message do |_actual|
55
59
  "expected that \"#{@response_body}\" would match #{body_regex}"
56
60
  end
57
61
  end
@@ -65,7 +69,7 @@ RSpec::Matchers.define :respond_with_envelope do |envelope, keys = []|
65
69
  expect(@envelope).to match(hash_including(*keys))
66
70
  end
67
71
 
68
- failure_message do |actual|
72
+ failure_message do |_actual|
69
73
  "expected that \"#{@response}\" would have envelope \"#{envelope}\" that matches " \
70
74
  "hash_including(#{keys})"
71
75
  end
@@ -78,7 +82,7 @@ RSpec::Matchers.define :respond_with_header do |header, value_regex|
78
82
  @header_value.match(value_regex)
79
83
  end
80
84
 
81
- failure_message do |actual|
85
+ failure_message do |_actual|
82
86
  "expected #{header} header in response to match #{value_regex} but found " \
83
87
  "\"#{@header_value}\""
84
88
  end
@@ -1,3 +1,3 @@
1
1
  module Coach
2
- VERSION = '0.5.2'.freeze
2
+ VERSION = "1.0.0".freeze
3
3
  end
@@ -5,27 +5,30 @@ require "coach/middleware"
5
5
  require "coach/errors"
6
6
 
7
7
  describe Coach::Handler do
8
+ subject(:handler) { described_class.new(terminal_middleware, handler: true) }
9
+
8
10
  let(:middleware_a) { build_middleware("A") }
9
11
  let(:middleware_b) { build_middleware("B") }
10
12
  let(:middleware_c) { build_middleware("C") }
11
13
  let(:middleware_d) { build_middleware("D") }
12
14
 
13
15
  let(:terminal_middleware) { build_middleware("Terminal") }
14
- subject(:handler) { Coach::Handler.new(terminal_middleware, handler: true) }
15
16
 
16
17
  before { Coach::Notifications.unsubscribe! }
17
18
 
18
19
  describe "#call" do
19
- let(:a_spy) { spy('middleware a') }
20
- let(:b_spy) { spy('middleware b') }
20
+ let(:a_double) { double }
21
+ let(:b_double) { double }
21
22
 
22
- before { terminal_middleware.uses(middleware_a, callback: a_spy) }
23
- before { terminal_middleware.uses(middleware_b, callback: b_spy) }
23
+ before do
24
+ terminal_middleware.uses(middleware_a, callback: a_double)
25
+ terminal_middleware.uses(middleware_b, callback: b_double)
26
+ end
24
27
 
25
28
  it "invokes all middleware in the chain" do
29
+ expect(a_double).to receive(:call)
30
+ expect(b_double).to receive(:call)
26
31
  result = handler.call({})
27
- expect(a_spy).to have_received(:call)
28
- expect(b_spy).to have_received(:call)
29
32
  expect(result).to eq(%w[A{} B{} Terminal{:handler=>true}])
30
33
  end
31
34
  end
@@ -45,9 +48,11 @@ describe Coach::Handler do
45
48
  end
46
49
 
47
50
  context "given a route that includes nested middleware" do
48
- before { middleware_b.uses(middleware_c) }
49
- before { middleware_a.uses(middleware_b) }
50
- before { terminal_middleware.uses(middleware_a) }
51
+ before do
52
+ middleware_b.uses(middleware_c)
53
+ middleware_a.uses(middleware_b)
54
+ terminal_middleware.uses(middleware_a)
55
+ end
51
56
 
52
57
  it "assembles a sequence including all middleware" do
53
58
  expect(sequence).to match_array([middleware_c, middleware_b,
@@ -56,10 +61,12 @@ describe Coach::Handler do
56
61
  end
57
62
 
58
63
  context "when a middleware has been included more than once" do
59
- before { middleware_a.uses(middleware_c) }
60
- before { middleware_b.uses(middleware_c) }
61
- before { terminal_middleware.uses(middleware_a) }
62
- before { terminal_middleware.uses(middleware_b) }
64
+ before do
65
+ middleware_a.uses(middleware_c)
66
+ middleware_b.uses(middleware_c)
67
+ terminal_middleware.uses(middleware_a)
68
+ terminal_middleware.uses(middleware_b)
69
+ end
63
70
 
64
71
  it "only appears once" do
65
72
  expect(sequence).to match_array([middleware_c, middleware_a,
@@ -79,8 +86,10 @@ describe Coach::Handler do
79
86
  end
80
87
 
81
88
  describe "#build_request_chain" do
82
- before { terminal_middleware.uses(middleware_a) }
83
- before { terminal_middleware.uses(middleware_b, b: true) }
89
+ before do
90
+ terminal_middleware.uses(middleware_a)
91
+ terminal_middleware.uses(middleware_b, b: true)
92
+ end
84
93
 
85
94
  let(:root_item) { Coach::MiddlewareItem.new(terminal_middleware) }
86
95
  let(:sequence) { handler.build_sequence(root_item, {}) }
@@ -98,8 +107,10 @@ describe Coach::Handler do
98
107
  end
99
108
 
100
109
  context "with inheriting config" do
101
- before { middleware_b.uses(middleware_c, ->(config) { config.slice(:b) }) }
102
- before { middleware_b.uses(middleware_d) }
110
+ before do
111
+ middleware_b.uses(middleware_c, ->(config) { config.slice(:b) })
112
+ middleware_b.uses(middleware_d)
113
+ end
103
114
 
104
115
  it "calls lambda with parent middlewares config" do
105
116
  expect(handler.build_request_chain(sequence, {}).call).
@@ -111,19 +122,20 @@ describe Coach::Handler do
111
122
  describe "#call" do
112
123
  before { terminal_middleware.uses(middleware_a) }
113
124
 
114
- describe 'notifications' do
115
- before { Coach::Notifications.subscribe! }
116
-
117
- # Prevent RequestSerializer from erroring due to insufficient request mock
125
+ describe "notifications" do
118
126
  before do
127
+ Coach::Notifications.subscribe!
128
+
129
+ # Prevent RequestSerializer from erroring due to insufficient request mock
119
130
  allow(Coach::RequestSerializer).
120
131
  to receive(:new).
121
- and_return(double(serialize: {}))
132
+ and_return(instance_double("Coach::RequestSerializer", serialize: {}))
122
133
  end
123
134
 
124
135
  subject(:coach_events) do
125
136
  events = []
126
- subscription = ActiveSupport::Notifications.subscribe(/coach/) do |name, *args|
137
+ subscription = ActiveSupport::Notifications.
138
+ subscribe(/\.coach$/) do |name, *_args|
127
139
  events << name
128
140
  end
129
141
 
@@ -132,19 +144,17 @@ describe Coach::Handler do
132
144
  events
133
145
  end
134
146
 
135
- it { is_expected.to include('coach.handler.start') }
136
- it { is_expected.to include('coach.middleware.start') }
137
- it { is_expected.to include('coach.request') }
138
- it { is_expected.to include('coach.middleware.finish') }
139
- it { is_expected.to include('coach.handler.finish') }
147
+ it { is_expected.to include("start_handler.coach") }
148
+ it { is_expected.to include("start_middleware.coach") }
149
+ it { is_expected.to include("request.coach") }
150
+ it { is_expected.to include("finish_middleware.coach") }
151
+ it { is_expected.to include("finish_handler.coach") }
140
152
 
141
153
  context "when an exception is raised in the chain" do
142
- let(:explosive_action) { -> { raise "AH" } }
143
- before { terminal_middleware.uses(middleware_a, callback: explosive_action) }
144
-
145
154
  subject(:coach_events) do
146
155
  events = []
147
- subscription = ActiveSupport::Notifications.subscribe(/coach/) do |name, *args|
156
+ subscription = ActiveSupport::Notifications.
157
+ subscribe(/\.coach$/) do |name, *args|
148
158
  events << [name, args.last]
149
159
  end
150
160
 
@@ -157,22 +167,92 @@ describe Coach::Handler do
157
167
  events
158
168
  end
159
169
 
160
- it "should capture the error event with the metadata " do
170
+ let(:explosive_action) { -> { raise "AH" } }
171
+
172
+ before { terminal_middleware.uses(middleware_a, callback: explosive_action) }
173
+
174
+ it "captures the error event with the metadata" do
161
175
  is_expected.
162
- to include(['coach.handler.finish', hash_including(
176
+ to include(["finish_handler.coach", hash_including(
163
177
  response: { status: 500 },
164
- metadata: { A: true }
178
+ metadata: { A: true },
165
179
  )])
166
180
  end
167
181
 
168
- it "should bubble the error to the next handler" do
182
+ it "bubbles the error to the next handler" do
169
183
  expect { handler.call({}) }.to raise_error(StandardError, "AH")
170
184
  end
171
185
  end
186
+
187
+ context "deprecations" do
188
+ subject(:coach_events) do
189
+ events = []
190
+ subscription = ActiveSupport::Notifications.
191
+ subscribe(/^coach\./) do |name, *_args|
192
+ events << name
193
+ end
194
+
195
+ handler.call({})
196
+ ActiveSupport::Notifications.unsubscribe(subscription)
197
+ events
198
+ end
199
+
200
+ let!(:deprecations_silenced) do
201
+ ActiveSupport::Deprecation.silenced
202
+ end
203
+
204
+ before do
205
+ ActiveSupport::Deprecation.silenced = true
206
+ end
207
+
208
+ after do
209
+ ActiveSupport::Deprecation.silenced = deprecations_silenced
210
+ end
211
+
212
+ it { is_expected.to include("coach.handler.start") }
213
+ it { is_expected.to include("coach.middleware.start") }
214
+ it { is_expected.to include("coach.request") }
215
+ it { is_expected.to include("coach.middleware.finish") }
216
+ it { is_expected.to include("coach.handler.finish") }
217
+
218
+ context "when an exception is raised in the chain" do
219
+ subject(:coach_events) do
220
+ events = []
221
+ subscription = ActiveSupport::Notifications.
222
+ subscribe(/^coach\./) do |name, *args|
223
+ events << [name, args.last]
224
+ end
225
+
226
+ begin
227
+ handler.call({})
228
+ rescue
229
+ :continue_anyway
230
+ end
231
+ ActiveSupport::Notifications.unsubscribe(subscription)
232
+ events
233
+ end
234
+
235
+ let(:explosive_action) { -> { raise "AH" } }
236
+
237
+ before { terminal_middleware.uses(middleware_a, callback: explosive_action) }
238
+
239
+ it "captures the error event with the metadata" do
240
+ is_expected.
241
+ to include(["coach.handler.finish", hash_including(
242
+ response: { status: 500 },
243
+ metadata: { A: true },
244
+ )])
245
+ end
246
+
247
+ it "bubbles the error to the next handler" do
248
+ expect { handler.call({}) }.to raise_error(StandardError, "AH")
249
+ end
250
+ end
251
+ end
172
252
  end
173
253
  end
174
254
 
175
255
  describe "#inspect" do
176
- its(:inspect) { is_expected.to eql('#<Coach::Handler[Terminal]>') }
256
+ its(:inspect) { is_expected.to eql("#<Coach::Handler[Terminal]>") }
177
257
  end
178
258
  end