coach 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c085b582900931acb9b56a21ee03e9b8b1695e8b9e36c23b5286acb8cc949868
4
- data.tar.gz: ab1dd0c12bfcbad8b93832649f96fa3699cc036d392129f4b6e1e8aca36bb72e
3
+ metadata.gz: 9fbde870cfebdaad825256983d4ef36b9b413e9f22495a16ba09be11ff17f61b
4
+ data.tar.gz: ddb38d9c732b21937cd5a9f6ad7c3693531d8973b49939250771b251976054ba
5
5
  SHA512:
6
- metadata.gz: 94525ce01d5c1b0dc75314c47f2901bdd33946fbe63ae847bf0ce6f1a1099e57c1db72f1c146199d0f77462eca826f47d0784aa21eeab41fb8b30aee3aa3dc0c
7
- data.tar.gz: ccd13d3dcb3623c01f6650ce6efb842c892663afaaffa30db8e8781380a8ffcdbc9be809b0d171c224d7c494448cabf7b38d9dae989af1a0c5256bee1a949758
6
+ metadata.gz: eac833c21c43ba291a7e68506d1378d22b64dbc2aeeb2899f02db9ae68b6a9390e7231598ed4197465807f5958decfd3403c02db01526f48d8978366ae279d2d
7
+ data.tar.gz: 8a4b8dbd37680569451d321219e3b157fc202d72a368e9855b477997c8fc42f17a026ca7fe1ce445766f8af55a77f79d1e8a81b1384855b954de15b463e38bf0
data/.circleci/config.yml CHANGED
@@ -1,68 +1,64 @@
1
- version: 2
1
+ ---
2
+ version: 2.1
2
3
 
3
4
  references:
4
- steps: &steps
5
- - checkout
6
-
7
- - type: shell
8
- name: Write RAILS_VERSION to a file so we can use it for caching purposes
9
- command: echo "$RAILS_VERSION" > ~/RAILS_VERSION.txt
10
-
11
- - type: cache-restore
12
- key: coach-bundler-{{ checksum "coach.gemspec" }}-{{ checksum "~/RAILS_VERSION.txt" }}
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
- - type: cache-save
19
- key: coach-bundler-{{ checksum "coach.gemspec" }}-{{ checksum "~/RAILS_VERSION.txt" }}
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
- - type: shell
24
- command: |
25
- bundle exec rspec --profile 10 \
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
- build-ruby24-rails515:
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:2.4
34
+ - image: cimg/ruby:<< parameters.ruby_version >>
39
35
  environment:
40
- - RAILS_VERSION=5.1.5
41
- steps: *steps
42
- build-ruby24-rails4210:
43
- docker:
44
- - image: ruby:2.4
45
- environment:
46
- - RAILS_VERSION=4.2.10
47
- steps: *steps
48
- build-ruby25-rails515:
49
- docker:
50
- - image: ruby:2.5
51
- environment:
52
- - RAILS_VERSION=5.1.5
53
- steps: *steps
54
- build-ruby25-rails4210:
55
- docker:
56
- - image: ruby:2.5
57
- environment:
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
- - build-ruby24-rails515
66
- - build-ruby24-rails4210
67
- - build-ruby25-rails515
68
- - build-ruby25-rails4210
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
+
@@ -0,0 +1,7 @@
1
+ version: 2
2
+
3
+ updates:
4
+ - package-ecosystem: bundler
5
+ directory: "/"
6
+ schedule:
7
+ interval: "daily"
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ inherit_gem:
4
4
  gc_ruboconfig: rubocop.yml
5
5
 
6
6
  AllCops:
7
- TargetRubyVersion: 2.4
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
- No unreleased changes.
5
+ # 3.0.0 / 2021-09-09
6
6
 
7
- # 2.2.0 / 2019-01-08
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.4 and onwards.
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. If any
145
- middleware `requires :x` when its chain does not provide `:x`, we'll error out before the
146
- app even starts with the error:
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
- begin
7
- require File.join(Dir.pwd, "config/environment")
8
- rescue LoadError
9
- puts <<~EOS
10
- Could not load your Rails app
11
- =============================
7
+ module Coach
8
+ class CLI
9
+ extend Commander::Methods
12
10
 
13
- Currently the coach CLI assumes you have a config/environment.rb file that
14
- we can load. We believe this is true of Rails apps in general.
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
- Please raise an issue if that's not the case!
21
+ never_trace!
17
22
 
18
- https://github.com/gocardless/coach/issues
19
- EOS
20
- exit 1
21
- end
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
- Slop.parse do
24
- command "find-provider" do
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
- result = Coach::Cli::ProviderFinder.new(args[0], args[1]).find_provider
32
+ middleware_name, value_name = *args
33
+ raise ArgumentError, "middleware_name and value_name required" unless middleware_name && value_name
30
34
 
31
- puts "Value `#{value_name}` is provided to `#{middleware_name}` by:\n\n"
32
- puts result.to_a.join("\n")
33
- end
34
- end
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
- command "find-chain" do
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
- chains = Coach::Cli::ProviderFinder.new(middleware_name, value_name).find_chain
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
- if chains.size > 1
44
- puts "Value `#{value_name}` is provided to `#{middleware_name}` " \
45
- "by multiple middleware chains:\n\n"
46
- else
47
- puts "Value `#{value_name}` is provided to `#{middleware_name}` by:\n\n"
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
- formatted_chains = chains.map do |chain|
51
- chain.join(" -> ")
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
- puts formatted_chains
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.4"
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
- # TODO: Find another CLI parser that supports subcommands
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", "= 2.9.0"
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 = 500
8
+ STATUS_CODE_FOR_EXCEPTIONS = 0
9
+
10
+ attr_reader :name
9
11
 
10
12
  def initialize(middleware, config = {})
11
- @root_item = MiddlewareItem.new(middleware, config)
12
- validate!
13
- rescue Coach::Errors::MiddlewareDependencyNotMet => e
14
- # Remove noise of validation stack trace, reset to the handler callsite
15
- e.backtrace.clear.concat(Thread.current.backtrace)
16
- raise e
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(@root_item, context)
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
- begin
36
- response = chain.instrument.call
37
- ensure
38
- # We want to populate the response and metadata fields after the middleware
39
- # chain has completed so that the end of the instrumentation can see them. The
40
- # simplest way to do this is pass the event by reference to ActiveSupport, then
41
- # modify the hash to contain this detail before the instrumentation completes.
42
- #
43
- # This way, the last finish_handler.coach event will have all the details.
44
- status = response.try(:first) || STATUS_CODE_FOR_EXCEPTIONS
45
- event.merge!(
46
- response: { status: status },
47
- metadata: context.fetch(:_metadata, {}),
48
- )
49
- end
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[#{@root_item.middleware.name}]>"
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
@@ -64,7 +64,7 @@ module Coach
64
64
  header_value_pairs = @request.filtered_env.map do |key, value|
65
65
  key = if RACK_UNPREFIXED_HEADERS.include?(key)
66
66
  "http_#{key.downcase}"
67
- elsif key.match?(/^HTTP_/)
67
+ elsif key.start_with?("HTTP_")
68
68
  key.downcase
69
69
  end
70
70
 
data/lib/coach/router.rb CHANGED
@@ -17,14 +17,11 @@ module Coach
17
17
  @mapper = mapper
18
18
  end
19
19
 
20
- def draw(routes, base: nil, as: nil, constraints: nil, actions: [])
20
+ def draw(namespace, base: nil, as: nil, constraints: nil, actions: [])
21
21
  action_traits(actions).each do |action, traits|
22
- # Passing false to const_get prevents it searching ancestors until a
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: Handler.new(route),
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("/").gsub(%r{/+}, "/")
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
@@ -15,7 +15,7 @@ def build_middleware(name)
15
15
 
16
16
  def call
17
17
  config[:callback].call if config.include?(:callback)
18
- log_metadata(Hash[name.to_sym, true])
18
+ log_metadata(**{ name.to_sym => true })
19
19
 
20
20
  response = [name + config.except(:callback).inspect.to_s]
21
21
 
data/lib/coach/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Coach
4
- VERSION = "2.2.0"
4
+ VERSION = "3.0.0"
5
5
  end
@@ -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 Metrics/LineLength
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
- let(:a_double) { double }
23
- let(:b_double) { double }
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
- before do
26
- terminal_middleware.uses(middleware_a, callback: a_double)
27
- terminal_middleware.uses(middleware_b, callback: b_double)
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
- it "invokes all middleware in the chain" do
31
- expect(a_double).to receive(:call)
32
- expect(b_double).to receive(:call)
33
- result = handler.call({})
34
- expect(result).to eq(%w[A{} B{} Terminal{:handler=>true}])
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(:resource_routes) do
14
- routes_module = Module.new
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: 2.2.0
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: 2020-01-08 00:00:00.000000000 Z
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: slop
42
+ name: commander
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '3.6'
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: '3.6'
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.9.0
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.9.0
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
- description:
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.4'
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.0.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