coach 2.2.0 → 3.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +49 -53
- data/.github/dependabot.yml +7 -0
- data/.rubocop.yml +4 -1
- data/.ruby-version +1 -0
- data/CHANGELOG.md +20 -3
- data/README.md +40 -5
- data/bin/coach +77 -38
- data/coach.gemspec +4 -5
- data/lib/coach/handler.rb +47 -30
- data/lib/coach/request_serializer.rb +1 -1
- data/lib/coach/router.rb +18 -7
- data/lib/coach/rspec.rb +1 -1
- data/lib/coach/version.rb +1 -1
- data/spec/lib/coach/cli/provider_finder_spec.rb +1 -1
- data/spec/lib/coach/handler_spec.rb +123 -75
- data/spec/lib/coach/router_spec.rb +23 -7
- data/spec/spec_helper.rb +4 -0
- data/spec/support/routes.rb +41 -0
- metadata +33 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9fbde870cfebdaad825256983d4ef36b9b413e9f22495a16ba09be11ff17f61b
|
4
|
+
data.tar.gz: ddb38d9c732b21937cd5a9f6ad7c3693531d8973b49939250771b251976054ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eac833c21c43ba291a7e68506d1378d22b64dbc2aeeb2899f02db9ae68b6a9390e7231598ed4197465807f5958decfd3403c02db01526f48d8978366ae279d2d
|
7
|
+
data.tar.gz: 8a4b8dbd37680569451d321219e3b157fc202d72a368e9855b477997c8fc42f17a026ca7fe1ce445766f8af55a77f79d1e8a81b1384855b954de15b463e38bf0
|
data/.circleci/config.yml
CHANGED
@@ -1,68 +1,64 @@
|
|
1
|
-
|
1
|
+
---
|
2
|
+
version: 2.1
|
2
3
|
|
3
4
|
references:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
- run: gem install bundler -v 1.11.2
|
15
|
-
|
16
|
-
- run: bundle install --path vendor/bundle
|
5
|
+
bundle_install: &bundle_install
|
6
|
+
run:
|
7
|
+
name: Bundle
|
8
|
+
command: |
|
9
|
+
gem install bundler --no-document && \
|
10
|
+
bundle config set no-cache 'true' && \
|
11
|
+
bundle config set jobs '4' && \
|
12
|
+
bundle config set retry '3' && \
|
13
|
+
bundle install
|
17
14
|
|
18
|
-
|
19
|
-
|
15
|
+
cache_bundle: &cache_bundle
|
16
|
+
save_cache:
|
17
|
+
key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "coach.gemspec" }}-{{ checksum "Gemfile" }}
|
20
18
|
paths:
|
21
19
|
- vendor/bundle
|
22
20
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
--format RspecJunitFormatter \
|
27
|
-
--out /tmp/test-results/rspec.xml \
|
28
|
-
--format progress \
|
29
|
-
spec
|
30
|
-
|
31
|
-
- type: store_test_results
|
32
|
-
path: /tmp/test-results
|
21
|
+
restore_bundle: &restore_bundle
|
22
|
+
restore_cache:
|
23
|
+
key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "coach.gemspec" }}-{{ checksum "Gemfile" }}
|
33
24
|
|
34
|
-
- run: bundle exec rubocop
|
35
25
|
jobs:
|
36
|
-
|
26
|
+
rspec:
|
27
|
+
working_directory: /mnt/ramdisk
|
28
|
+
parameters:
|
29
|
+
ruby_version:
|
30
|
+
type: string
|
31
|
+
rails_version:
|
32
|
+
type: string
|
37
33
|
docker:
|
38
|
-
- image: ruby
|
34
|
+
- image: cimg/ruby:<< parameters.ruby_version >>
|
39
35
|
environment:
|
40
|
-
|
41
|
-
steps:
|
42
|
-
|
43
|
-
|
44
|
-
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
-
|
57
|
-
|
58
|
-
- RAILS_VERSION=4.2.10
|
59
|
-
steps: *steps
|
36
|
+
CIRCLE_TEST_REPORTS: /tmp/circle_artifacts/
|
37
|
+
steps:
|
38
|
+
- add_ssh_keys
|
39
|
+
- checkout
|
40
|
+
- *restore_bundle
|
41
|
+
- *bundle_install
|
42
|
+
- *cache_bundle
|
43
|
+
- run:
|
44
|
+
name: Run specs
|
45
|
+
command: |
|
46
|
+
bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile --format progress --format RspecJunitFormatter -o /tmp/circle_artifacts/rspec.xml
|
47
|
+
- run:
|
48
|
+
name: "Rubocop"
|
49
|
+
command: bundle exec rubocop --extra-details --display-style-guide --parallel --force-exclusion
|
50
|
+
- store_artifacts:
|
51
|
+
path: /tmp/circle_artifacts/
|
52
|
+
- store_test_results:
|
53
|
+
path: /tmp/circle_artifacts/
|
60
54
|
|
61
55
|
workflows:
|
62
56
|
version: 2
|
63
57
|
tests:
|
64
58
|
jobs:
|
65
|
-
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
- rspec:
|
60
|
+
matrix:
|
61
|
+
parameters:
|
62
|
+
ruby_version: ["2.6", "2.7", "3.0"]
|
63
|
+
rails_version: ["5.2.6", "6.0.4", "6.1.4"]
|
64
|
+
|
data/.rubocop.yml
CHANGED
@@ -4,7 +4,7 @@ inherit_gem:
|
|
4
4
|
gc_ruboconfig: rubocop.yml
|
5
5
|
|
6
6
|
AllCops:
|
7
|
-
TargetRubyVersion:
|
7
|
+
TargetRubyVersion: 3.0
|
8
8
|
|
9
9
|
Metrics/MethodLength:
|
10
10
|
Max: 15
|
@@ -26,3 +26,6 @@ Naming/MethodParameterName:
|
|
26
26
|
# These are some custom names that we want to allow, since they aren't
|
27
27
|
# uncommunicative - they're actually rather meaningful!
|
28
28
|
- as
|
29
|
+
|
30
|
+
Gemspec/RequiredRubyVersion:
|
31
|
+
Enabled: False
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.2
|
data/CHANGELOG.md
CHANGED
@@ -2,9 +2,27 @@
|
|
2
2
|
|
3
3
|
# Unreleased
|
4
4
|
|
5
|
-
|
5
|
+
# 3.0.0 / 2021-09-09
|
6
6
|
|
7
|
-
#
|
7
|
+
* [#105](https://github.com/gocardless/coach/pull/105) Add support for module/class names to be
|
8
|
+
passed to `Handler.new` and `Router#draw` to allow routes to be defined without loading the routes
|
9
|
+
constants, which makes using Zeitwerk much easier.
|
10
|
+
|
11
|
+
* Add Ruby 3 support
|
12
|
+
|
13
|
+
## Breaking changes
|
14
|
+
|
15
|
+
* Remove Ruby 2.4 and 2.5 support
|
16
|
+
|
17
|
+
# 2.3.0 / 2020-04-29
|
18
|
+
|
19
|
+
* [#90](https://github.com/gocardless/coach/pull/90) Instrument status `0` instead of `500` when Coach::Handler catches an exception
|
20
|
+
|
21
|
+
# 2.2.1 / 2020-02-06
|
22
|
+
|
23
|
+
* [#85](https://github.com/gocardless/coach/pull/85) Uses [commander](https://github.com/commander-rb/commander) for CLI option parsing
|
24
|
+
|
25
|
+
# 2.2.0 / 2020-01-08
|
8
26
|
|
9
27
|
* [#68](https://github.com/gocardless/coach/pull/68) Add `coach` CLI and add
|
10
28
|
documentation to README
|
@@ -135,4 +153,3 @@ consistent with other libraries. The previous names will work for now, but will
|
|
135
153
|
|
136
154
|
Initial public release. This library is in beta until it hits 1.0, so backwards
|
137
155
|
incompatible changes may be made in minor version releases.
|
138
|
-
|
data/README.md
CHANGED
@@ -23,7 +23,7 @@ To get started, just add Coach to your `Gemfile`, and then run `bundle`:
|
|
23
23
|
gem 'coach'
|
24
24
|
```
|
25
25
|
|
26
|
-
Coach works with Ruby versions 2.
|
26
|
+
Coach works with Ruby versions 2.6 and onwards.
|
27
27
|
|
28
28
|
## Coach by example
|
29
29
|
|
@@ -59,6 +59,21 @@ Once you've booted Rails locally, the following should return `'hello world'`:
|
|
59
59
|
$ curl -XGET http://localhost:3000/hello_world
|
60
60
|
```
|
61
61
|
|
62
|
+
### Zeitwerk
|
63
|
+
|
64
|
+
The new default autoloader in Rails 6+ is [Zeitwerk](https://github.com/fxn/zeitwerk), which removes
|
65
|
+
support for autoloading constants during app boot, which that example would do - either you have to
|
66
|
+
`require "hello_world"` in your routes file, or avoid referencing the `HelloWorld` constant until
|
67
|
+
the app has booted. To avoid that, you can instead pass the module or middleware _name_ to
|
68
|
+
`Handler.new`, for example:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
Example::Application.routes.draw do
|
72
|
+
match "/hello_world",
|
73
|
+
to: Coach::Handler.new("HelloWorld"),
|
74
|
+
via: :get
|
75
|
+
```
|
76
|
+
|
62
77
|
### Building chains
|
63
78
|
|
64
79
|
Suppose we didn't want just anybody to see our `HelloWorld` endpoint. In fact, we'd like
|
@@ -136,14 +151,15 @@ end
|
|
136
151
|
# Inside config/routes.rb
|
137
152
|
Example::Application.routes.draw do
|
138
153
|
match "/hello_user",
|
139
|
-
to: Coach::Handler.new(HelloUser),
|
154
|
+
to: Coach::Handler.new("HelloUser"),
|
140
155
|
via: :get
|
141
156
|
end
|
142
157
|
```
|
143
158
|
|
144
|
-
Coach analyses your middleware chains whenever a new `Handler` is created
|
145
|
-
|
146
|
-
|
159
|
+
Coach analyses your middleware chains whenever a new `Handler` is created, or when the handler is
|
160
|
+
first used if the route is being lazy-loaded (i.e., if you're passing a string name, instead of the
|
161
|
+
route itself). If any middleware `requires :x` when its chain does not provide `:x`, we'll error out
|
162
|
+
before the app even starts with the error:
|
147
163
|
|
148
164
|
```ruby
|
149
165
|
Coach::Errors::MiddlewareDependencyNotMet: HelloUser requires keys [user] that are not provided by the middleware chain
|
@@ -270,6 +286,25 @@ users resource being mapped to:
|
|
270
286
|
| `PUT` | `/users/:id` | Update user details |
|
271
287
|
| `POST` | `/users/:id/actions/disable` | Custom action routed to the given path suffix |
|
272
288
|
|
289
|
+
If you're using Zeitwerk, you can pass the name of the module to `#draw`, instead of the module
|
290
|
+
itself.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
# config/routes.rb
|
294
|
+
Example::Application.routes.draw do
|
295
|
+
router = Coach::Router.new(self)
|
296
|
+
router.draw("Routes::Users",
|
297
|
+
base: "/users",
|
298
|
+
actions: [
|
299
|
+
:index,
|
300
|
+
:show,
|
301
|
+
:create,
|
302
|
+
:update,
|
303
|
+
disable: { method: :post, url: "/:id/actions/disable" }
|
304
|
+
])
|
305
|
+
end
|
306
|
+
```
|
307
|
+
|
273
308
|
## Rendering
|
274
309
|
|
275
310
|
By now you'll probably agree that the rack response format isn't the nicest way to render
|
data/bin/coach
CHANGED
@@ -1,57 +1,96 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require "slop"
|
4
3
|
require "coach/cli/provider_finder"
|
4
|
+
require "coach/version"
|
5
|
+
require "commander"
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
puts <<~EOS
|
10
|
-
Could not load your Rails app
|
11
|
-
=============================
|
7
|
+
module Coach
|
8
|
+
class CLI
|
9
|
+
extend Commander::Methods
|
12
10
|
|
13
|
-
|
14
|
-
|
11
|
+
class << self
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
13
|
+
def run
|
14
|
+
program :version, Coach::VERSION
|
15
|
+
program :description,
|
16
|
+
"Coach's CLI to help understand the provide/require graph made up of " \
|
17
|
+
"all the middleware chains you've built using Coach.\n" \
|
18
|
+
" More information at: https://github.com/gocardless/coach#coach-cli."
|
19
|
+
program :help_formatter, :compact
|
15
20
|
|
16
|
-
|
21
|
+
never_trace!
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
23
|
+
command "find-provider" do |c|
|
24
|
+
c.syntax = "bundle exec coach find-provider"
|
25
|
+
c.description =
|
26
|
+
"Given the name of a Coach middleware and a value that it requires, it " \
|
27
|
+
"outputs the name of the middleware that provides it."
|
22
28
|
|
23
|
-
|
24
|
-
|
25
|
-
run do |_, args|
|
26
|
-
middleware_name, value_name = *args
|
27
|
-
raise ArgumentError, "middleware_name and value_name required" unless middleware_name && value_name
|
29
|
+
c.action do |args, _|
|
30
|
+
load_config_environment
|
28
31
|
|
29
|
-
|
32
|
+
middleware_name, value_name = *args
|
33
|
+
raise ArgumentError, "middleware_name and value_name required" unless middleware_name && value_name
|
30
34
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
result = Coach::Cli::ProviderFinder.new(args[0], args[1]).find_provider
|
36
|
+
|
37
|
+
puts "Value `#{value_name}` is provided to `#{middleware_name}` by:\n\n"
|
38
|
+
puts result.to_a.join("\n")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
command "find-chain" do |c|
|
43
|
+
c.syntax = "bundle exec coach find-chain"
|
44
|
+
c.description =
|
45
|
+
"Given the name of a Coach middleware and a value it requires, " \
|
46
|
+
"it outputs the chains of middleware between the specified middleware " \
|
47
|
+
"and the one that provides the required value."
|
48
|
+
|
49
|
+
c.action do |args, _|
|
50
|
+
load_config_environment
|
51
|
+
|
52
|
+
middleware_name, value_name = *args
|
53
|
+
raise ArgumentError, "middleware_name and value_name required" unless middleware_name && value_name
|
35
54
|
|
36
|
-
|
37
|
-
run do |_, args|
|
38
|
-
middleware_name, value_name = *args
|
39
|
-
raise ArgumentError, "middleware_name and value_name required" unless middleware_name && value_name
|
55
|
+
chains = Coach::Cli::ProviderFinder.new(middleware_name, value_name).find_chain
|
40
56
|
|
41
|
-
|
57
|
+
if chains.size > 1
|
58
|
+
puts "Value `#{value_name}` is provided to `#{middleware_name}` " \
|
59
|
+
"by multiple middleware chains:\n\n"
|
60
|
+
else
|
61
|
+
puts "Value `#{value_name}` is provided to `#{middleware_name}` by:\n\n"
|
62
|
+
end
|
42
63
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
64
|
+
formatted_chains = chains.map do |chain|
|
65
|
+
chain.join(" -> ")
|
66
|
+
end.join("\n---\n")
|
67
|
+
|
68
|
+
puts formatted_chains
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
run!
|
48
73
|
end
|
74
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
75
|
+
|
76
|
+
def load_config_environment
|
77
|
+
require File.join(Dir.pwd, "config/environment")
|
78
|
+
rescue LoadError
|
79
|
+
puts <<~ERR
|
80
|
+
Could not load your Rails app
|
81
|
+
=============================
|
49
82
|
|
50
|
-
|
51
|
-
|
52
|
-
end.join("\n---\n")
|
83
|
+
Currently the coach CLI assumes you have a config/environment.rb file that
|
84
|
+
we can load. We believe this is true of Rails apps in general.
|
53
85
|
|
54
|
-
|
86
|
+
Please raise an issue if that's not the case!
|
87
|
+
|
88
|
+
https://github.com/gocardless/coach/issues
|
89
|
+
ERR
|
90
|
+
exit 1
|
91
|
+
end
|
55
92
|
end
|
56
93
|
end
|
57
94
|
end
|
95
|
+
|
96
|
+
Coach::CLI.run
|
data/coach.gemspec
CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.homepage = "https://github.com/gocardless/coach"
|
13
13
|
spec.email = %w[developers@gocardless.com]
|
14
14
|
spec.license = "MIT"
|
15
|
-
spec.required_ruby_version = ">= 2.
|
15
|
+
spec.required_ruby_version = ">= 2.6"
|
16
16
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0")
|
18
18
|
spec.test_files = spec.files.grep(%r{^spec/})
|
@@ -21,13 +21,12 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.add_dependency "actionpack", ">= 4.2"
|
23
23
|
spec.add_dependency "activesupport", ">= 4.2"
|
24
|
-
|
25
|
-
# Slop v4 got rid of them :(
|
26
|
-
spec.add_dependency "slop", "~> 3.6"
|
24
|
+
spec.add_dependency "commander", "~> 4.5"
|
27
25
|
|
28
|
-
spec.add_development_dependency "gc_ruboconfig", "
|
26
|
+
spec.add_development_dependency "gc_ruboconfig", "~> 2.18.0"
|
29
27
|
spec.add_development_dependency "pry", "~> 0.10"
|
30
28
|
spec.add_development_dependency "rspec", "~> 3.2"
|
31
29
|
spec.add_development_dependency "rspec-its", "~> 1.2"
|
32
30
|
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
|
31
|
+
spec.add_development_dependency "rubocop", "~> 1.12"
|
33
32
|
end
|
data/lib/coach/handler.rb
CHANGED
@@ -5,19 +5,22 @@ require "active_support/core_ext/object/try"
|
|
5
5
|
|
6
6
|
module Coach
|
7
7
|
class Handler
|
8
|
-
STATUS_CODE_FOR_EXCEPTIONS =
|
8
|
+
STATUS_CODE_FOR_EXCEPTIONS = 0
|
9
|
+
|
10
|
+
attr_reader :name
|
9
11
|
|
10
12
|
def initialize(middleware, config = {})
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
@config = config
|
14
|
+
if middleware.is_a?(String)
|
15
|
+
@name = middleware
|
16
|
+
else
|
17
|
+
@middleware = middleware
|
18
|
+
@name = middleware.name
|
19
|
+
# This triggers validation of the middleware chain, to raise any errors early on.
|
20
|
+
root_item
|
21
|
+
end
|
17
22
|
end
|
18
23
|
|
19
|
-
# Run validation on the root of the middleware chain
|
20
|
-
delegate :validate!, to: :@root_item
|
21
24
|
delegate :publish, :instrument, :notifier, to: ActiveSupport::Notifications
|
22
25
|
|
23
26
|
# The Rack interface to handler - builds a middleware chain based on
|
@@ -25,28 +28,31 @@ module Coach
|
|
25
28
|
# rubocop:disable Metrics/MethodLength
|
26
29
|
def call(env)
|
27
30
|
context = { request: ActionDispatch::Request.new(env) }
|
28
|
-
sequence = build_sequence(
|
31
|
+
sequence = build_sequence(root_item, context)
|
29
32
|
chain = build_request_chain(sequence, context)
|
30
33
|
|
31
34
|
event = build_event(context)
|
32
35
|
|
33
36
|
publish("start_handler.coach", event.dup)
|
34
37
|
instrument("finish_handler.coach", event) do
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
38
|
+
response = chain.instrument.call
|
39
|
+
rescue StandardError => e
|
40
|
+
raise
|
41
|
+
ensure
|
42
|
+
# We want to populate the response and metadata fields after the middleware
|
43
|
+
# chain has completed so that the end of the instrumentation can see them. The
|
44
|
+
# simplest way to do this is pass the event by reference to ActiveSupport, then
|
45
|
+
# modify the hash to contain this detail before the instrumentation completes.
|
46
|
+
#
|
47
|
+
# This way, the last finish_handler.coach event will have all the details.
|
48
|
+
status = response.try(:first) || STATUS_CODE_FOR_EXCEPTIONS
|
49
|
+
event.merge!(
|
50
|
+
response: {
|
51
|
+
status: status,
|
52
|
+
exception: e,
|
53
|
+
}.compact,
|
54
|
+
metadata: context.fetch(:_metadata, {}),
|
55
|
+
)
|
50
56
|
end
|
51
57
|
end
|
52
58
|
# rubocop:enable Metrics/MethodLength
|
@@ -71,11 +77,25 @@ module Coach
|
|
71
77
|
end
|
72
78
|
|
73
79
|
def inspect
|
74
|
-
"#<Coach::Handler[#{
|
80
|
+
"#<Coach::Handler[#{name}]>"
|
75
81
|
end
|
76
82
|
|
77
83
|
private
|
78
84
|
|
85
|
+
attr_reader :config
|
86
|
+
|
87
|
+
def root_item
|
88
|
+
@root_item ||= MiddlewareItem.new(middleware, config).tap(&:validate!)
|
89
|
+
rescue Coach::Errors::MiddlewareDependencyNotMet => e
|
90
|
+
# Remove noise of validation stack trace, reset to the handler callsite
|
91
|
+
e.backtrace.clear.concat(Thread.current.backtrace)
|
92
|
+
raise e
|
93
|
+
end
|
94
|
+
|
95
|
+
def middleware
|
96
|
+
@middleware ||= ActiveSupport::Dependencies.constantize(name)
|
97
|
+
end
|
98
|
+
|
79
99
|
# Remove middleware that have been included multiple times with the same
|
80
100
|
# config, leaving only the first instance
|
81
101
|
def dedup_sequence(sequence)
|
@@ -84,10 +104,7 @@ module Coach
|
|
84
104
|
|
85
105
|
# Event to send with notifications
|
86
106
|
def build_event(context)
|
87
|
-
{
|
88
|
-
middleware: @root_item.middleware.name,
|
89
|
-
request: context[:request],
|
90
|
-
}
|
107
|
+
{ middleware: name, request: context[:request] }
|
91
108
|
end
|
92
109
|
end
|
93
110
|
end
|
data/lib/coach/router.rb
CHANGED
@@ -17,14 +17,11 @@ module Coach
|
|
17
17
|
@mapper = mapper
|
18
18
|
end
|
19
19
|
|
20
|
-
def draw(
|
20
|
+
def draw(namespace, base: nil, as: nil, constraints: nil, actions: [])
|
21
21
|
action_traits(actions).each do |action, traits|
|
22
|
-
|
23
|
-
# match is found. Without this, globally defined constants such as
|
24
|
-
# `Process` will be picked up before consts that need to be autoloaded.
|
25
|
-
route = routes.const_get(camel(action), false)
|
22
|
+
handler = build_handler(namespace, action)
|
26
23
|
match(action_url(base, traits),
|
27
|
-
to:
|
24
|
+
to: handler,
|
28
25
|
via: traits[:method],
|
29
26
|
as: as,
|
30
27
|
constraints: constraints)
|
@@ -37,6 +34,20 @@ module Coach
|
|
37
34
|
|
38
35
|
private
|
39
36
|
|
37
|
+
def build_handler(namespace, action)
|
38
|
+
action_name = camel(action)
|
39
|
+
|
40
|
+
if namespace.is_a?(String)
|
41
|
+
route_name = "#{namespace}::#{action_name}"
|
42
|
+
Handler.new(route_name)
|
43
|
+
else
|
44
|
+
# Passing false to const_get prevents it searching ancestors until a
|
45
|
+
# match is found. Without this, globally defined constants such as
|
46
|
+
# `Process` will be picked up before consts that need to be autoloaded.
|
47
|
+
Handler.new(namespace.const_get(action_name, false))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
40
51
|
# Receives an array of symbols that represent actions for which the default traits
|
41
52
|
# should be used, and then lastly an optional trait configuration.
|
42
53
|
#
|
@@ -60,7 +71,7 @@ module Coach
|
|
60
71
|
|
61
72
|
# Applies trait url to base, removing duplicate /'s
|
62
73
|
def action_url(base, traits)
|
63
|
-
[base, traits[:url]].compact.join("/").
|
74
|
+
[base, traits[:url]].compact.join("/").squeeze("/")
|
64
75
|
end
|
65
76
|
|
66
77
|
# Turns a snake_case string/symbol into a CamelCase
|
data/lib/coach/rspec.rb
CHANGED
data/lib/coach/version.rb
CHANGED
@@ -201,7 +201,7 @@ describe Coach::Cli::ProviderFinder do
|
|
201
201
|
expect(provider_finder.find_chain).
|
202
202
|
to eq([
|
203
203
|
%w[FirstProvidingMiddleware FirstIntermediateMiddleware RequiringMiddleware],
|
204
|
-
%w[SecondProvidingMiddleware SecondIntermediateMiddleware RequiringMiddleware], # rubocop:disable
|
204
|
+
%w[SecondProvidingMiddleware SecondIntermediateMiddleware RequiringMiddleware], # rubocop:disable Layout/LineLength
|
205
205
|
].to_set)
|
206
206
|
end
|
207
207
|
end
|
@@ -19,19 +19,132 @@ describe Coach::Handler do
|
|
19
19
|
before { Coach::Notifications.unsubscribe! }
|
20
20
|
|
21
21
|
describe "#call" do
|
22
|
-
|
23
|
-
|
22
|
+
context "with multiple middleware" do
|
23
|
+
let(:a_double) { double.as_null_object }
|
24
|
+
let(:b_double) { double.as_null_object }
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
|
26
|
+
before do
|
27
|
+
terminal_middleware.uses(middleware_a, callback: a_double)
|
28
|
+
terminal_middleware.uses(middleware_b, callback: b_double)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "invokes all middleware in the chain" do
|
32
|
+
expect(a_double).to receive(:call)
|
33
|
+
expect(b_double).to receive(:call)
|
34
|
+
result = handler.call({})
|
35
|
+
expect(result).to eq(%w[A{} B{} Terminal{:handler=>true}])
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with an invalid chain" do
|
39
|
+
before { terminal_middleware.requires(:not_available) }
|
40
|
+
|
41
|
+
it "raises an error" do
|
42
|
+
expect { handler }.to raise_error(Coach::Errors::MiddlewareDependencyNotMet)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "lazy-loading the middleware" do
|
47
|
+
subject(:handler) { described_class.new(terminal_middleware.name, handler: true) }
|
48
|
+
|
49
|
+
before do
|
50
|
+
allow(ActiveSupport::Dependencies).to receive(:constantize).and_call_original
|
51
|
+
allow(ActiveSupport::Dependencies).to receive(:constantize).
|
52
|
+
with(terminal_middleware.name).
|
53
|
+
and_return(terminal_middleware)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "does not load the route when initialized" do
|
57
|
+
expect(ActiveSupport::Dependencies).
|
58
|
+
to_not receive(:constantize).with(terminal_middleware.name)
|
59
|
+
|
60
|
+
handler
|
61
|
+
end
|
62
|
+
|
63
|
+
it "calls through the middleware chain" do
|
64
|
+
expect(a_double).to receive(:call)
|
65
|
+
expect(b_double).to receive(:call)
|
66
|
+
|
67
|
+
result = handler.call({})
|
68
|
+
|
69
|
+
expect(result).to eq(%w[A{} B{} Terminal{:handler=>true}])
|
70
|
+
end
|
71
|
+
|
72
|
+
context "with an invalid chain" do
|
73
|
+
before { terminal_middleware.requires(:not_available) }
|
74
|
+
|
75
|
+
it "does not raise on initialize" do
|
76
|
+
expect { handler }.to_not raise_error
|
77
|
+
end
|
78
|
+
|
79
|
+
it "raises on first call" do
|
80
|
+
expect { handler.call({}) }.
|
81
|
+
to raise_error(Coach::Errors::MiddlewareDependencyNotMet)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
28
85
|
end
|
29
86
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
87
|
+
describe "notifications" do
|
88
|
+
subject(:coach_events) do
|
89
|
+
events = []
|
90
|
+
subscription = ActiveSupport::Notifications.
|
91
|
+
subscribe(/\.coach$/) { |name, *_args| events << name }
|
92
|
+
|
93
|
+
handler.call({})
|
94
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
95
|
+
events
|
96
|
+
end
|
97
|
+
|
98
|
+
before do
|
99
|
+
terminal_middleware.uses(middleware_a)
|
100
|
+
|
101
|
+
Coach::Notifications.subscribe!
|
102
|
+
|
103
|
+
# Prevent RequestSerializer from erroring due to insufficient request mock
|
104
|
+
allow(Coach::RequestSerializer).
|
105
|
+
to receive(:new).
|
106
|
+
and_return(instance_double("Coach::RequestSerializer", serialize: {}))
|
107
|
+
end
|
108
|
+
|
109
|
+
it { is_expected.to include("start_handler.coach") }
|
110
|
+
it { is_expected.to include("start_middleware.coach") }
|
111
|
+
it { is_expected.to include("request.coach") }
|
112
|
+
it { is_expected.to include("finish_middleware.coach") }
|
113
|
+
it { is_expected.to include("finish_handler.coach") }
|
114
|
+
|
115
|
+
context "when an exception is raised in the chain" do
|
116
|
+
subject(:coach_events) do
|
117
|
+
events = []
|
118
|
+
subscription = ActiveSupport::Notifications.
|
119
|
+
subscribe(/\.coach$/) do |name, *args|
|
120
|
+
events << [name, args.last]
|
121
|
+
end
|
122
|
+
|
123
|
+
begin
|
124
|
+
handler.call({})
|
125
|
+
rescue StandardError
|
126
|
+
:continue_anyway
|
127
|
+
end
|
128
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
129
|
+
events
|
130
|
+
end
|
131
|
+
|
132
|
+
let(:explosive_action) { -> { raise "AH" } }
|
133
|
+
|
134
|
+
before { terminal_middleware.uses(middleware_a, callback: explosive_action) }
|
135
|
+
|
136
|
+
it "captures the error event with the metadata and nil status" do
|
137
|
+
expect(coach_events).
|
138
|
+
to include(["finish_handler.coach", hash_including(
|
139
|
+
response: { status: 0, exception: instance_of(RuntimeError) },
|
140
|
+
metadata: { A: true },
|
141
|
+
)])
|
142
|
+
end
|
143
|
+
|
144
|
+
it "bubbles the error to the next handler" do
|
145
|
+
expect { handler.call({}) }.to raise_error(StandardError, "AH")
|
146
|
+
end
|
147
|
+
end
|
35
148
|
end
|
36
149
|
end
|
37
150
|
|
@@ -121,71 +234,6 @@ describe Coach::Handler do
|
|
121
234
|
end
|
122
235
|
end
|
123
236
|
|
124
|
-
describe "#call" do
|
125
|
-
before { terminal_middleware.uses(middleware_a) }
|
126
|
-
|
127
|
-
describe "notifications" do
|
128
|
-
subject(:coach_events) do
|
129
|
-
events = []
|
130
|
-
subscription = ActiveSupport::Notifications.
|
131
|
-
subscribe(/\.coach$/) { |name, *_args| events << name }
|
132
|
-
|
133
|
-
handler.call({})
|
134
|
-
ActiveSupport::Notifications.unsubscribe(subscription)
|
135
|
-
events
|
136
|
-
end
|
137
|
-
|
138
|
-
before do
|
139
|
-
Coach::Notifications.subscribe!
|
140
|
-
|
141
|
-
# Prevent RequestSerializer from erroring due to insufficient request mock
|
142
|
-
allow(Coach::RequestSerializer).
|
143
|
-
to receive(:new).
|
144
|
-
and_return(instance_double("Coach::RequestSerializer", serialize: {}))
|
145
|
-
end
|
146
|
-
|
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") }
|
152
|
-
|
153
|
-
context "when an exception is raised in the chain" do
|
154
|
-
subject(:coach_events) do
|
155
|
-
events = []
|
156
|
-
subscription = ActiveSupport::Notifications.
|
157
|
-
subscribe(/\.coach$/) do |name, *args|
|
158
|
-
events << [name, args.last]
|
159
|
-
end
|
160
|
-
|
161
|
-
begin
|
162
|
-
handler.call({})
|
163
|
-
rescue StandardError
|
164
|
-
:continue_anyway
|
165
|
-
end
|
166
|
-
ActiveSupport::Notifications.unsubscribe(subscription)
|
167
|
-
events
|
168
|
-
end
|
169
|
-
|
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
|
175
|
-
expect(coach_events).
|
176
|
-
to include(["finish_handler.coach", hash_including(
|
177
|
-
response: { status: 500 },
|
178
|
-
metadata: { A: true },
|
179
|
-
)])
|
180
|
-
end
|
181
|
-
|
182
|
-
it "bubbles the error to the next handler" do
|
183
|
-
expect { handler.call({}) }.to raise_error(StandardError, "AH")
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
237
|
describe "#inspect" do
|
190
238
|
its(:inspect) { is_expected.to eql("#<Coach::Handler[Terminal]>") }
|
191
239
|
end
|
@@ -10,13 +10,8 @@ describe Coach::Router do
|
|
10
10
|
|
11
11
|
let(:mapper) { instance_double("ActionDispatch::Routing::Mapper") }
|
12
12
|
|
13
|
-
let(:
|
14
|
-
|
15
|
-
%i[Index Show Create Update Destroy Refund].each do |class_name|
|
16
|
-
routes_module.const_set(class_name, Class.new)
|
17
|
-
end
|
18
|
-
routes_module
|
19
|
-
end
|
13
|
+
let(:routes) { %i[Index Show Create Update Destroy Refund] }
|
14
|
+
let(:resource_routes) { Routes::Thing }
|
20
15
|
|
21
16
|
before do
|
22
17
|
allow(Coach::Handler).to receive(:new) { |route| route }
|
@@ -84,6 +79,27 @@ describe Coach::Router do
|
|
84
79
|
it "raises NameError" do
|
85
80
|
expect { draw }.to raise_error(NameError)
|
86
81
|
end
|
82
|
+
|
83
|
+
context "when passing a string for the namespace" do
|
84
|
+
subject(:router) { described_class.new(mapper) }
|
85
|
+
|
86
|
+
let(:resource_routes) { Routes::Thing.name }
|
87
|
+
|
88
|
+
before do
|
89
|
+
allow(ActiveSupport::Dependencies).to receive(:constantize).and_call_original
|
90
|
+
|
91
|
+
routes.each do |class_name|
|
92
|
+
allow(ActiveSupport::Dependencies).to receive(:constantize).
|
93
|
+
with("Routes::Thing::#{class_name}").
|
94
|
+
and_return(Routes::Thing.const_get(class_name))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
it "does not raise an error" do
|
99
|
+
expect(mapper).to receive(:match).with("/resource/:id/process", anything)
|
100
|
+
expect { draw }.to_not raise_error
|
101
|
+
end
|
102
|
+
end
|
87
103
|
end
|
88
104
|
end
|
89
105
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,6 +5,10 @@ require "pry"
|
|
5
5
|
require "coach"
|
6
6
|
require "coach/rspec"
|
7
7
|
|
8
|
+
Dir[Pathname(__FILE__).dirname.join("support", "**", "*.rb")].
|
9
|
+
sort.
|
10
|
+
each { |path| require path }
|
11
|
+
|
8
12
|
RSpec.configure do |config|
|
9
13
|
config.expect_with :rspec do |expectations|
|
10
14
|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Routes
|
4
|
+
module Thing
|
5
|
+
class Index < Coach::Middleware
|
6
|
+
def call
|
7
|
+
[200, {}, []]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Show < Coach::Middleware
|
12
|
+
def call
|
13
|
+
[200, {}, []]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Create < Coach::Middleware
|
18
|
+
def call
|
19
|
+
[200, {}, []]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Update < Coach::Middleware
|
24
|
+
def call
|
25
|
+
[200, {}, []]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Destroy < Coach::Middleware
|
30
|
+
def call
|
31
|
+
[200, {}, []]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Refund < Coach::Middleware
|
36
|
+
def call
|
37
|
+
[200, {}, []]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coach
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GoCardless
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -39,33 +39,33 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: commander
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '4.5'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '4.5'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: gc_ruboconfig
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- -
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 2.
|
61
|
+
version: 2.18.0
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- -
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 2.
|
68
|
+
version: 2.18.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,7 +122,21 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: 0.4.0
|
125
|
-
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.12'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.12'
|
139
|
+
description:
|
126
140
|
email:
|
127
141
|
- developers@gocardless.com
|
128
142
|
executables:
|
@@ -131,10 +145,12 @@ extensions: []
|
|
131
145
|
extra_rdoc_files: []
|
132
146
|
files:
|
133
147
|
- ".circleci/config.yml"
|
148
|
+
- ".github/dependabot.yml"
|
134
149
|
- ".gitignore"
|
135
150
|
- ".rspec"
|
136
151
|
- ".rubocop.yml"
|
137
152
|
- ".rubocop_todo.yml"
|
153
|
+
- ".ruby-version"
|
138
154
|
- CHANGELOG.md
|
139
155
|
- Gemfile
|
140
156
|
- LICENSE.txt
|
@@ -166,11 +182,12 @@ files:
|
|
166
182
|
- spec/lib/coach/request_serializer_spec.rb
|
167
183
|
- spec/lib/coach/router_spec.rb
|
168
184
|
- spec/spec_helper.rb
|
185
|
+
- spec/support/routes.rb
|
169
186
|
homepage: https://github.com/gocardless/coach
|
170
187
|
licenses:
|
171
188
|
- MIT
|
172
189
|
metadata: {}
|
173
|
-
post_install_message:
|
190
|
+
post_install_message:
|
174
191
|
rdoc_options: []
|
175
192
|
require_paths:
|
176
193
|
- lib
|
@@ -178,15 +195,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
178
195
|
requirements:
|
179
196
|
- - ">="
|
180
197
|
- !ruby/object:Gem::Version
|
181
|
-
version: '2.
|
198
|
+
version: '2.6'
|
182
199
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
183
200
|
requirements:
|
184
201
|
- - ">="
|
185
202
|
- !ruby/object:Gem::Version
|
186
203
|
version: '0'
|
187
204
|
requirements: []
|
188
|
-
rubygems_version: 3.
|
189
|
-
signing_key:
|
205
|
+
rubygems_version: 3.2.22
|
206
|
+
signing_key:
|
190
207
|
specification_version: 4
|
191
208
|
summary: Alternative controllers built with middleware
|
192
209
|
test_files:
|
@@ -199,3 +216,4 @@ test_files:
|
|
199
216
|
- spec/lib/coach/request_serializer_spec.rb
|
200
217
|
- spec/lib/coach/router_spec.rb
|
201
218
|
- spec/spec_helper.rb
|
219
|
+
- spec/support/routes.rb
|