coach 0.2.0 → 0.2.1
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/Gemfile.lock +2 -2
- data/README.md +79 -79
- data/lib/coach/handler.rb +4 -1
- data/lib/coach/middleware.rb +10 -0
- data/lib/coach/notifications.rb +3 -1
- data/lib/coach/version.rb +1 -1
- data/lib/spec/matchers.rb +1 -0
- data/spec/lib/coach/middleware_spec.rb +7 -0
- data/spec/lib/coach/notifications_spec.rb +12 -4
- metadata +19 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3258a468d749d86884a028d51d035966dfa925a2
|
4
|
+
data.tar.gz: c04518e139d26372ee97a8450967d4aa32756989
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 439c250dce80d2ed7f50a69d8c06aaf4d47e01ba3b25f4c368debfc0ab4431c45c8cf470f0f3377cbdb6043e1604c3c731b116e0134903d986865b06f53f9fc2
|
7
|
+
data.tar.gz: 01963b88c9e4886cfd523d0da278958a5db70e08e1f1fb96d2c97ba987c30392fe2581a450a613c9ee0f6427390664ba44f1c4b4b90be10efbb092b360eeb017
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
[](https://travis-ci.org/gocardless/coach)
|
5
5
|
[](https://codeclimate.com/github/gocardless/coach)
|
6
6
|
|
7
|
-
Coach improves your controller code by encouraging
|
7
|
+
Coach improves your controller code by encouraging:
|
8
8
|
|
9
9
|
- **Modularity** - No more tangled `before_filter`'s and interdependent concerns. Build
|
10
10
|
Middleware that does a single job, and does it well.
|
@@ -13,129 +13,129 @@ Coach improves your controller code by encouraging...
|
|
13
13
|
- **Testability** - Test each middleware in isolation, with effortless mocking of test
|
14
14
|
data and natural RSpec matchers.
|
15
15
|
|
16
|
-
##
|
16
|
+
## Coach by example
|
17
17
|
|
18
|
-
|
18
|
+
The best way to see the benefits of Coach is with a demonstration.
|
19
|
+
|
20
|
+
### Mounting an endpoint
|
19
21
|
|
20
22
|
```ruby
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
[ 200, {}, [params[:word]] ]
|
26
|
-
end
|
23
|
+
class HelloWorld < Coach::Middleware
|
24
|
+
def call
|
25
|
+
# Middleware return a Rack response
|
26
|
+
[ 200, {}, ['hello world'] ]
|
27
27
|
end
|
28
28
|
end
|
29
29
|
```
|
30
30
|
|
31
|
-
|
32
|
-
`
|
33
|
-
available inside Middlewares as `params`.
|
31
|
+
So we've created ourselves a piece of middleware, `HelloWorld`. As you'd expect,
|
32
|
+
`HelloWorld` simply outputs the string `'hello world'`.
|
34
33
|
|
35
34
|
In an example Rails app, called `Example`, we can mount this route like so...
|
36
35
|
|
37
36
|
```ruby
|
38
37
|
Example::Application.routes.draw do
|
39
|
-
match "/
|
40
|
-
to: Coach::Handler.new(
|
38
|
+
match "/hello_world",
|
39
|
+
to: Coach::Handler.new(HelloWorld),
|
41
40
|
via: :get
|
42
41
|
end
|
43
42
|
```
|
44
43
|
|
45
|
-
Once
|
46
|
-
respond with `'hello'`.
|
47
|
-
|
48
|
-
## Building chains
|
44
|
+
Once you've booted Rails locally, the following should return `'hello world'`:
|
49
45
|
|
50
|
-
|
51
|
-
|
52
|
-
```ruby
|
53
|
-
module Routes
|
54
|
-
class Secret < Coach::Middleware
|
55
|
-
def call
|
56
|
-
unless User.exists?(id: params[:user_id], password: params[:user_password])
|
57
|
-
return [ 401, {}, ['Access denied'] ]
|
58
|
-
end
|
59
|
-
|
60
|
-
[ 200, {}, ['super-secretness'] ]
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
46
|
+
```sh
|
47
|
+
$ curl -XGET http://localhost:3000/hello_world
|
64
48
|
```
|
65
49
|
|
66
|
-
|
67
|
-
|
50
|
+
### Building chains
|
51
|
+
|
52
|
+
Suppose we didn't want just anybody to see our `HelloWorld` endpoint. In fact, we'd like
|
53
|
+
to lock it down behind some authentication.
|
68
54
|
|
69
|
-
|
70
|
-
|
71
|
-
|
55
|
+
Our request will now have two stages, one where we check authentication details and
|
56
|
+
another where we respond with our secret greeting to the world. Let's split into two
|
57
|
+
pieces, one for each of the two subtasks, allowing us to reuse this authentication flow in
|
58
|
+
other middlewares.
|
72
59
|
|
73
60
|
```ruby
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
return [ 401, {}, ['Access denied'] ]
|
79
|
-
end
|
80
|
-
|
81
|
-
next_middleware.call
|
61
|
+
class Authentication < Coach::Middleware
|
62
|
+
def call
|
63
|
+
unless User.exists?(login: params[:login])
|
64
|
+
return [ 401, {}, ['Access denied'] ]
|
82
65
|
end
|
66
|
+
|
67
|
+
next_middleware.call
|
83
68
|
end
|
84
69
|
end
|
85
70
|
|
86
|
-
|
87
|
-
|
88
|
-
uses Middleware::Authentication
|
71
|
+
class HelloWorld < Coach::Middleware
|
72
|
+
uses Authentication
|
89
73
|
|
90
|
-
|
91
|
-
|
92
|
-
end
|
74
|
+
def call
|
75
|
+
[ 200, {}, ['hello world'] ]
|
93
76
|
end
|
94
77
|
end
|
95
78
|
```
|
96
79
|
|
97
|
-
Here we detach the authentication logic into
|
98
|
-
`
|
99
|
-
|
80
|
+
Here we detach the authentication logic into its own middleware. `HelloWorld` now `uses`
|
81
|
+
`Authentication`, and will only run if it has been called via `next_middleware.call` from
|
82
|
+
authentication.
|
83
|
+
|
84
|
+
Notice we also use `params` just like you would in a normal Rails controller. Every
|
85
|
+
middleware class will have access to a `request` object, which is an instance of
|
86
|
+
`ActionDispatch::Request`.
|
100
87
|
|
101
|
-
|
88
|
+
### Passing data through middleware
|
102
89
|
|
103
|
-
|
104
|
-
|
90
|
+
So far we've demonstrated how Coach can help you break your controller code into modular
|
91
|
+
pieces. The big innovation with Coach, however, is the ability to explicitly pass your
|
92
|
+
data through the middleware chain.
|
93
|
+
|
94
|
+
An example usage here is to create a `HelloUser` endpoint. We want to protect the route by
|
95
|
+
authentication, as we did before, but this time greet the user that is logged in. Making
|
96
|
+
a small modification to the `Authentication` middleware we showed above...
|
105
97
|
|
106
98
|
```ruby
|
107
|
-
|
108
|
-
|
109
|
-
provides :authenticated_user
|
99
|
+
class Authentication < Coach::Middleware
|
100
|
+
provides :user # declare that Authentication provides :user
|
110
101
|
|
111
|
-
|
112
|
-
|
113
|
-
return [ 401, {}, ['Access denied'] ] unless user.exists?
|
102
|
+
def call
|
103
|
+
return [ 401, {}, ['Access denied'] ] unless user.present?
|
114
104
|
|
115
|
-
|
116
|
-
|
117
|
-
|
105
|
+
provide(user: user)
|
106
|
+
next_middleware.call
|
107
|
+
end
|
108
|
+
|
109
|
+
def user
|
110
|
+
@user ||= User.find_by(login: params[:login])
|
118
111
|
end
|
119
112
|
end
|
120
113
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
requires :authenticated_user
|
114
|
+
class HelloUser < Coach::Middleware
|
115
|
+
uses Authentication
|
116
|
+
requires :user # state that HelloUser requires this data
|
125
117
|
|
126
|
-
|
127
|
-
|
128
|
-
|
118
|
+
def call
|
119
|
+
# Can now access `user`, as it's been provided by Authentication
|
120
|
+
[ 200, {}, [ "hello #{user.name}" ] ]
|
129
121
|
end
|
130
122
|
end
|
123
|
+
|
124
|
+
# Inside config/routes.rb
|
125
|
+
Example::Application.routes.draw do
|
126
|
+
match "/hello_user",
|
127
|
+
to: Coach::Handler.new(HelloUser),
|
128
|
+
via: :get
|
129
|
+
end
|
131
130
|
```
|
132
131
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
`AuthenticatedUser`, then mounting would fail with the error...
|
132
|
+
Coach analyses your middleware chains whenever a new `Handler` is created. If any
|
133
|
+
middleware `requires :x` when its chain does not provide `:x`, we'll error out before the
|
134
|
+
app even starts with the error:
|
137
135
|
|
138
|
-
|
136
|
+
```ruby
|
137
|
+
Coach::Errors::MiddlewareDependencyNotMet: HelloUser requires keys [user] that are not provided by the middleware chain
|
138
|
+
```
|
139
139
|
|
140
140
|
This static verification eradicates an entire category of errors that stem from implicitly
|
141
141
|
running code before hitting controller methods. It allows you to be confident that the
|
@@ -213,7 +213,7 @@ router.draw(Routes::Users,
|
|
213
213
|
```
|
214
214
|
|
215
215
|
Default actions that conform to standard REST principles can be easily loaded, with the
|
216
|
-
users resource being mapped to
|
216
|
+
users resource being mapped to:
|
217
217
|
|
218
218
|
| Method | URL | Description |
|
219
219
|
|--------|------------------------------|------------------------------------------------|
|
data/lib/coach/handler.rb
CHANGED
@@ -25,7 +25,10 @@ module Coach
|
|
25
25
|
|
26
26
|
finish = Time.now
|
27
27
|
publish('coach.handler.finish',
|
28
|
-
start, finish, nil,
|
28
|
+
start, finish, nil,
|
29
|
+
start_event.merge(
|
30
|
+
response: { status: response[0] },
|
31
|
+
metadata: context.fetch(:_metadata, {})))
|
29
32
|
|
30
33
|
response
|
31
34
|
end
|
data/lib/coach/middleware.rb
CHANGED
@@ -15,6 +15,10 @@ module Coach
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def self.provides(*new_provided)
|
18
|
+
if new_provided.include?(:_metadata)
|
19
|
+
raise 'Cannot provide :_metadata, Coach uses this internally!'
|
20
|
+
end
|
21
|
+
|
18
22
|
provided.concat(new_provided)
|
19
23
|
provided.uniq!
|
20
24
|
end
|
@@ -76,6 +80,12 @@ module Coach
|
|
76
80
|
end
|
77
81
|
end
|
78
82
|
|
83
|
+
# Adds key-values to metadata, to be published with coach events.
|
84
|
+
def log_metadata(**values)
|
85
|
+
@_context[:_metadata] ||= {}
|
86
|
+
@_context[:_metadata].merge!(values)
|
87
|
+
end
|
88
|
+
|
79
89
|
# Helper to access request params from within middleware
|
80
90
|
delegate :params, to: :request
|
81
91
|
|
data/lib/coach/notifications.rb
CHANGED
@@ -78,10 +78,12 @@ module Coach
|
|
78
78
|
broadcast(event, benchmark)
|
79
79
|
end
|
80
80
|
|
81
|
+
# Receives a handler.finish event, with processed benchmark. Publishes to
|
82
|
+
# coach.request notification.
|
81
83
|
def broadcast(event, benchmark)
|
82
84
|
serialized = RequestSerializer.new(event[:request]).serialize.
|
83
85
|
merge(benchmark.stats).
|
84
|
-
merge(event.slice(:response))
|
86
|
+
merge(event.slice(:response, :metadata))
|
85
87
|
ActiveSupport::Notifications.publish('coach.request', serialized)
|
86
88
|
end
|
87
89
|
end
|
data/lib/coach/version.rb
CHANGED
data/lib/spec/matchers.rb
CHANGED
@@ -5,6 +5,13 @@ describe Coach::Middleware do
|
|
5
5
|
let(:context_) { {} }
|
6
6
|
let(:middleware_obj) { middleware_class.new(context_, nil) }
|
7
7
|
|
8
|
+
describe ".provides" do
|
9
|
+
it "blows up if providing a reserved keyword" do
|
10
|
+
expect { middleware_class.provides(:_metadata) }.
|
11
|
+
to raise_exception(/cannot provide.* coach uses this/i)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
8
15
|
describe ".provides?" do
|
9
16
|
context "given names it does provide" do
|
10
17
|
before { middleware_class.provides(:foo, :bar) }
|
@@ -48,10 +48,18 @@ describe Coach::Notifications do
|
|
48
48
|
expect(middleware_event).not_to be_nil
|
49
49
|
end
|
50
50
|
|
51
|
-
|
52
|
-
handler.call({})
|
53
|
-
|
54
|
-
|
51
|
+
describe "coach.request event" do
|
52
|
+
before { handler.call({}) }
|
53
|
+
|
54
|
+
it "contains all middleware that have been run" do
|
55
|
+
middleware_names = middleware_event[:chain].map { |item| item[:name] }
|
56
|
+
expect(middleware_names).to include(*%w(Terminal A B))
|
57
|
+
end
|
58
|
+
|
59
|
+
it "includes all logged metadata" do
|
60
|
+
expect(middleware_event).
|
61
|
+
to include(metadata: { A: true, B: true, Terminal: true })
|
62
|
+
end
|
55
63
|
end
|
56
64
|
end
|
57
65
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: coach
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GoCardless
|
@@ -14,84 +14,84 @@ dependencies:
|
|
14
14
|
name: actionpack
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - ~>
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - ~>
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - ~>
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '4.2'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - ~>
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '4.2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - ~>
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: 3.2.0
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - ~>
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 3.2.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rspec-its
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - ~>
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: 1.2.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
68
|
version: 1.2.0
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: pry
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- -
|
73
|
+
- - ">="
|
74
74
|
- !ruby/object:Gem::Version
|
75
75
|
version: '0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rubocop
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- -
|
94
|
+
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
description: Controller framework
|
@@ -101,10 +101,10 @@ executables: []
|
|
101
101
|
extensions: []
|
102
102
|
extra_rdoc_files: []
|
103
103
|
files:
|
104
|
-
- .gitignore
|
105
|
-
- .rspec
|
106
|
-
- .rubocop.yml
|
107
|
-
- .travis.yml
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- ".travis.yml"
|
108
108
|
- Gemfile
|
109
109
|
- Gemfile.lock
|
110
110
|
- README.md
|
@@ -139,12 +139,12 @@ require_paths:
|
|
139
139
|
- lib
|
140
140
|
required_ruby_version: !ruby/object:Gem::Requirement
|
141
141
|
requirements:
|
142
|
-
- -
|
142
|
+
- - ">="
|
143
143
|
- !ruby/object:Gem::Version
|
144
144
|
version: '0'
|
145
145
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
146
146
|
requirements:
|
147
|
-
- -
|
147
|
+
- - ">="
|
148
148
|
- !ruby/object:Gem::Version
|
149
149
|
version: '0'
|
150
150
|
requirements: []
|