coach 0.5.2 → 1.0.0

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.
@@ -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