coach 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5b238b46a6cc3ceb9bd972eb675ab185e9782314
4
+ data.tar.gz: 65d9e3c36df29228fa740e1b745a4888b6e7942c
5
+ SHA512:
6
+ metadata.gz: 68a7271f6e510864ed92949c9b4e9d6edb1ed0212138792717d0ed357c0afd2ff95b35ab42a3915e69cc3b37d9653cba9d9c1bdd3d8c9202473dab482abb35be
7
+ data.tar.gz: e4ec8fac9cbcbccd75cef7162604f7b521be4a9b902fe41c5625fb47717aff18a55bbc3c9fe28d37eedd94dcf74bd023edf92f52fd1badcab9182fc8bf846c23
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,123 @@
1
+ # For all options see https://github.com/bbatsov/rubocop/tree/master/config
2
+
3
+ # Limit lines to 90 characters.
4
+ LineLength:
5
+ Max: 90
6
+
7
+ ClassLength:
8
+ Enabled: false
9
+
10
+ ModuleLength:
11
+ Enabled: false
12
+
13
+ # Avoid methods longer than 30 lines of code
14
+ MethodLength:
15
+ CountComments: false # count full line comments?
16
+ Max: 87
17
+
18
+ # Avoid single-line methods.
19
+ SingleLineMethods:
20
+ AllowIfMethodIsEmpty: true
21
+
22
+ StringLiterals:
23
+ Enabled: false
24
+
25
+ GlobalVars:
26
+ Enabled: false # We use them Redis + StatsD (though maybe we shouldn't?)
27
+
28
+ # Wants underscores in all large numbers. Pain in the ass for things like
29
+ # unix timestamps.
30
+ NumericLiterals:
31
+ Enabled: false
32
+
33
+ # Wants you to use the same argument names for every reduce. This seems kinda
34
+ # naff compared to naming them semantically
35
+ SingleLineBlockParams:
36
+ Enabled: false
37
+
38
+ Style/SignalException:
39
+ EnforcedStyle: 'only_raise'
40
+
41
+ # Use trailing rather than leading dots on multi-line call chains
42
+ Style/DotPosition:
43
+ EnforcedStyle: trailing
44
+
45
+ Lint/NestedMethodDefinition:
46
+ Enabled: false
47
+
48
+ Lint/UnusedBlockArgument:
49
+ Enabled: false
50
+
51
+ Metrics/AbcSize:
52
+ Max: 61
53
+
54
+ Metrics/CyclomaticComplexity:
55
+ Max: 10
56
+
57
+ Metrics/PerceivedComplexity:
58
+ Max: 10
59
+
60
+ Style/AccessorMethodName:
61
+ Enabled: false
62
+
63
+ # Allow non-ASCII characters (e.g. £) in comments
64
+ Style/AsciiComments:
65
+ Enabled: false
66
+
67
+ # Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle, SupportedLastArgumentHashStyles.
68
+ Style/AlignHash:
69
+ Enabled: false
70
+
71
+ # Configuration parameters: EnforcedStyle, SupportedStyles, ProceduralMethods, FunctionalMethods, IgnoredMethods.
72
+ Style/BlockDelimiters:
73
+ Enabled: false
74
+
75
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
76
+ Style/ClassAndModuleChildren:
77
+ Enabled: false
78
+
79
+ Style/ClosingParenthesisIndentation:
80
+ Enabled: false
81
+
82
+ # Configuration parameters: Keywords.
83
+ Style/CommentAnnotation:
84
+ Enabled: false
85
+
86
+ Style/Documentation:
87
+ Enabled: false
88
+
89
+ Style/DoubleNegation:
90
+ Enabled: false
91
+
92
+ # Configuration parameters: MinBodyLength.
93
+ Style/GuardClause:
94
+ Enabled: false
95
+
96
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
97
+ Style/IndentHash:
98
+ Enabled: false
99
+
100
+ Style/Lambda:
101
+ Enabled: false
102
+
103
+ # Configuration parameters: EnforcedStyle, SupportedStyles.
104
+ Style/MultilineOperationIndentation:
105
+ Enabled: false
106
+
107
+ # Configuration parameters: NamePrefix, NamePrefixBlacklist.
108
+ Style/PredicateName:
109
+ Enabled: false
110
+
111
+ Style/RedundantSelf:
112
+ Enabled: false
113
+
114
+ Style/SingleSpaceBeforeFirstArg:
115
+ Enabled: false
116
+
117
+ # Configuration parameters: MultiSpaceAllowedForOperators.
118
+ Style/SpaceAroundOperators:
119
+ Enabled: false
120
+
121
+ # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist.
122
+ Style/TrivialAccessors:
123
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,104 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ coach (0.0.0)
5
+ actionpack (~> 4.2)
6
+ activesupport (~> 4.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionpack (4.2.1)
12
+ actionview (= 4.2.1)
13
+ activesupport (= 4.2.1)
14
+ rack (~> 1.6)
15
+ rack-test (~> 0.6.2)
16
+ rails-dom-testing (~> 1.0, >= 1.0.5)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
18
+ actionview (4.2.1)
19
+ activesupport (= 4.2.1)
20
+ builder (~> 3.1)
21
+ erubis (~> 2.7.0)
22
+ rails-dom-testing (~> 1.0, >= 1.0.5)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
24
+ activesupport (4.2.1)
25
+ i18n (~> 0.7)
26
+ json (~> 1.7, >= 1.7.7)
27
+ minitest (~> 5.1)
28
+ thread_safe (~> 0.3, >= 0.3.4)
29
+ tzinfo (~> 1.1)
30
+ ast (2.0.0)
31
+ astrolabe (1.3.0)
32
+ parser (>= 2.2.0.pre.3, < 3.0)
33
+ builder (3.2.2)
34
+ coderay (1.1.0)
35
+ diff-lcs (1.2.5)
36
+ erubis (2.7.0)
37
+ i18n (0.7.0)
38
+ json (1.8.3)
39
+ loofah (2.0.2)
40
+ nokogiri (>= 1.5.9)
41
+ method_source (0.8.2)
42
+ mini_portile (0.6.2)
43
+ minitest (5.7.0)
44
+ nokogiri (1.6.6.2)
45
+ mini_portile (~> 0.6.0)
46
+ parser (2.2.2.5)
47
+ ast (>= 1.1, < 3.0)
48
+ powerpack (0.1.1)
49
+ pry (0.10.1)
50
+ coderay (~> 1.1.0)
51
+ method_source (~> 0.8.1)
52
+ slop (~> 3.4)
53
+ rack (1.6.1)
54
+ rack-test (0.6.3)
55
+ rack (>= 1.0)
56
+ rails-deprecated_sanitizer (1.0.3)
57
+ activesupport (>= 4.2.0.alpha)
58
+ rails-dom-testing (1.0.6)
59
+ activesupport (>= 4.2.0.beta, < 5.0)
60
+ nokogiri (~> 1.6.0)
61
+ rails-deprecated_sanitizer (>= 1.0.1)
62
+ rails-html-sanitizer (1.0.2)
63
+ loofah (~> 2.0)
64
+ rainbow (2.0.0)
65
+ rspec (3.2.0)
66
+ rspec-core (~> 3.2.0)
67
+ rspec-expectations (~> 3.2.0)
68
+ rspec-mocks (~> 3.2.0)
69
+ rspec-core (3.2.3)
70
+ rspec-support (~> 3.2.0)
71
+ rspec-expectations (3.2.1)
72
+ diff-lcs (>= 1.2.0, < 2.0)
73
+ rspec-support (~> 3.2.0)
74
+ rspec-its (1.2.0)
75
+ rspec-core (>= 3.0.0)
76
+ rspec-expectations (>= 3.0.0)
77
+ rspec-mocks (3.2.1)
78
+ diff-lcs (>= 1.2.0, < 2.0)
79
+ rspec-support (~> 3.2.0)
80
+ rspec-support (3.2.2)
81
+ rubocop (0.32.0)
82
+ astrolabe (~> 1.3)
83
+ parser (>= 2.2.2.5, < 3.0)
84
+ powerpack (~> 0.1)
85
+ rainbow (>= 1.99.1, < 3.0)
86
+ ruby-progressbar (~> 1.4)
87
+ ruby-progressbar (1.7.5)
88
+ slop (3.6.0)
89
+ thread_safe (0.3.5)
90
+ tzinfo (1.2.2)
91
+ thread_safe (~> 0.1)
92
+
93
+ PLATFORMS
94
+ ruby
95
+
96
+ DEPENDENCIES
97
+ coach!
98
+ pry
99
+ rspec (~> 3.2.0)
100
+ rspec-its (~> 1.2.0)
101
+ rubocop
102
+
103
+ BUNDLED WITH
104
+ 1.10.3
@@ -0,0 +1,260 @@
1
+ # Coach
2
+ For when Rails has you jumping on the tracks...
3
+
4
+ Coach improves your controller code by encouraging...
5
+
6
+ - **Modularity** - No more tangled `before_filter`'s and interdependent concerns. Build
7
+ Middleware that does a single job, and does it well.
8
+ - **Guarantees** - Work with a simple `provide`/`require` interface to guarantee that the
9
+ data you need in your endpoint is loaded before your app even boots.
10
+ - **Testability** - Test each middleware in isolation, with effortless mocking of test
11
+ data and natural RSpec matchers.
12
+
13
+ ## Simple endpoint
14
+
15
+ Lets start by creating a simple endpoint.
16
+
17
+ ```ruby
18
+ module Routes
19
+ class Echo < Coach::Middleware
20
+ def call
21
+ # All middleware should return rack compliant responses
22
+ [ 200, {}, [params[:word]] ]
23
+ end
24
+ end
25
+ end
26
+ ```
27
+
28
+ Any Middlewares have access to the `request` handle, which is an instance of
29
+ `ActionDispatch::Request`. This also parses the request params, and these are made
30
+ available inside Middlewares as `params`.
31
+
32
+ In an example Rails app, called `Example`, we can mount this route like so...
33
+
34
+ ```ruby
35
+ Example::Application.routes.draw do
36
+ match "/echo/:word",
37
+ to: Coach::Handler.new(Routes::Echo),
38
+ via: :get
39
+ end
40
+ ```
41
+
42
+ Once booting Rails locally, running `curl -XGET http://localhost:3000/echo/hello` should
43
+ respond with `'hello'`.
44
+
45
+ ## Building chains
46
+
47
+ Lets try creating a route protected by authentication.
48
+
49
+ ```ruby
50
+ module Routes
51
+ class Secret < Coach::Middleware
52
+ def call
53
+ unless User.exists?(id: params[:user_id], password: params[:user_password])
54
+ return [ 401, {}, ['Access denied'] ]
55
+ end
56
+
57
+ [ 200, {}, ['super-secretness'] ]
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ The above will verify that a user can be found with the given params, and if it cannot
64
+ then will respond with a `401`.
65
+
66
+ This does what we want it to do, but why should `Secret` know anything about
67
+ authentication? This complicates `Secret`'s design and prevents reuse of authentication
68
+ logic.
69
+
70
+ ```ruby
71
+ module Middleware
72
+ class Authentication < Coach::Middleware
73
+ def call
74
+ unless User.exists?(id: params[:user_id], password: params[:user_password])
75
+ return [ 401, {}, ['Access denied'] ]
76
+ end
77
+
78
+ next_middleware.call
79
+ end
80
+ end
81
+ end
82
+
83
+ module Routes
84
+ class Secret < Coach::Middleware
85
+ uses Middleware::Authentication
86
+
87
+ def call
88
+ [ 200, {}, ['super-secretness'] ]
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ Here we detach the authentication logic into it's own middleware. `Secret` now `uses`
95
+ `Middleware::Authentication`, and will only run if it has been called via
96
+ `next_middleware.call` from authentication.
97
+
98
+ ## Passing data through middleware
99
+
100
+ Now what happens if you have an endpoint that returns the current auth'd users details? We
101
+ can maintain the separation of authentication logic and endpoint as below...
102
+
103
+ ```ruby
104
+ module Middleware
105
+ class AuthenticatedUser < Coach::Middleware
106
+ provides :authenticated_user
107
+
108
+ def call
109
+ user = User.find_by(token: request.headers['Authorization'])
110
+ return [ 401, {}, ['Access denied'] ] unless user.exists?
111
+
112
+ provide(authenticated_user: user)
113
+ next_middleware.call
114
+ end
115
+ end
116
+ end
117
+
118
+ module Routes
119
+ class Whoami < Coach::Middleware
120
+ uses AuthenticatedUser
121
+ requires :authenticated_user
122
+
123
+ def call
124
+ [ 200, {}, [authenticated_user.name] ]
125
+ end
126
+ end
127
+ end
128
+ ```
129
+
130
+ Each middleware declares what it requires from those that have ran before it, and what it
131
+ will provide to those that run after. Whenever a middleware chain is mounted, these
132
+ promises will be verified. In the above, if our `Whoami` middleware had neglected to use
133
+ `AuthenticatedUser`, then mounting would fail with the error...
134
+
135
+ Coach::Errors::MiddlewareDependencyNotMet: AuthenticatedUser requires keys [authenticated_user] that are not provided by the middleware chain
136
+
137
+ This static verification eradicates an entire category of errors that stem from implicitly
138
+ running code before hitting controller methods. It allows you to be confident that the
139
+ data you require has been loaded, and makes tracing the origin of that data as simple as
140
+ looking up the chain.
141
+
142
+ ## Testing
143
+
144
+ The basic strategy is to test each middleware in isolation, covering all the edge cases,
145
+ and then create request specs that cover a happy code path, testing each of the
146
+ middlewares while they work in sequence.
147
+
148
+ Each middleware is encouraged to rely on data passed through the `provide`/`require`
149
+ syntax exclusively, except in stateful operations (such as database queries). By sticking
150
+ to this rule, testing becomes as simple as mocking a `context` hash.
151
+
152
+ ```ruby
153
+ require 'spec_helper'
154
+
155
+ describe "/whoami" do
156
+ let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }
157
+
158
+ context "with correct auth details" do
159
+ it "responds with user name" do
160
+ get "/whoami", {}, { 'Authorization' => 'Kryptonite' }
161
+ expect(response.body).to match(/Clark Kent/)
162
+ end
163
+ end
164
+ end
165
+
166
+ describe Routes::Whoami do
167
+ subject(:instance) { described_class.new(context) }
168
+ let(:context) { { authenticated_user: double(name: "Clark Kent") } }
169
+
170
+ it { is_expected.to respond_with_body_that_matches(/Clark Kent/) }
171
+ end
172
+
173
+ describe Middleware::AuthenticatedUser do
174
+ subject(:instance) { described_class.new(context) }
175
+ let(:context) do
176
+ { request: instance_double(ActionDispatch::Request, headers: headers) }
177
+ end
178
+
179
+ let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }
180
+
181
+ context "with valid token" do
182
+ it { is_expected.to call_next_middleware }
183
+ it { is_expected.to provide(authenticated_user: user) }
184
+ end
185
+
186
+ context "with invalid token" do
187
+ it { is_expected.to respond_with_status(401) }
188
+ it { is_expected.to respond_with_body_that_matches(/access denied/i) }
189
+ end
190
+ end
191
+ ```
192
+
193
+ ## Routing
194
+
195
+ For routes that represent resource actions, Coach provides some syntactic sugar to
196
+ allow concise mapping of endpoint to handler.
197
+
198
+ ```ruby
199
+ router = Coach::Router.new(Example::Application)
200
+
201
+ router.draw(Routes::Users,
202
+ base: "/users",
203
+ actions: [
204
+ :index,
205
+ :show,
206
+ :create,
207
+ :update,
208
+ disable: { method: :post, url: "/:id/actions/disable" }
209
+ ])
210
+ ```
211
+
212
+ Default actions that conform to standard REST principles can be easily loaded, with the
213
+ users resource being mapped to...
214
+
215
+ | Method | URL | Description |
216
+ |--------|------------------------------|------------------------------------------------|
217
+ | `GET` | `/users` | Index all users |
218
+ | `GET` | `/users/:id` | Get user by ID |
219
+ | `POST` | `/users` | Create new user |
220
+ | `PUT` | `/users/:id` | Update user details |
221
+ | `POST` | `/users/:id/actions/disable` | Custom action routed to the given path suffix |
222
+
223
+ ## Rendering
224
+
225
+ By now you'll probably agree that the rack response format isn't the nicest way to render
226
+ responses. Coach comes sans renderer, and for a good reason.
227
+
228
+ We initially built a `Coach::Renderer` module, but soon realised that doing so would
229
+ prevent us from open sourcing. Our `Renderer` was 90% logic specific to the way our APIs
230
+ function, including handling/formatting of validation errors, logging of unusual events
231
+ etc.
232
+
233
+ What worked well for us is a standalone `Renderer` class that we could require in all our
234
+ middleware that needed to format responses. This pattern also led to clearer code -
235
+ consistent with our preference for explicit code, stating `Renderer.new_resource(...)` is
236
+ instantly more debuggable than an inherited method on all middlewares.
237
+
238
+ ## Instrumentation
239
+
240
+ Coach uses `ActiveSupport::Notifications` to issue events that can be used to profile
241
+ middleware.
242
+
243
+ Information for how to use `ActiveSupport`s notifications can be found
244
+ [here](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html).
245
+
246
+
247
+ | Event | Arguments |
248
+ |-------------------------------|------------------------------------------------------- |
249
+ | `coach.handler.start` | `event(:middleware, :request)` |
250
+ | `coach.middleware.start` | `event(:middleware, :request)` |
251
+ | `coach.middleware.finish` | `start`, `finish`, `id`, `event(:middleware, :request)`|
252
+ | `coach.handler.finish` | `start`, `finish`, `id`, `event(:middleware, :request)`|
253
+ | `coach.request` | `event` containing request data and benchmarking |
254
+
255
+ Of special interest is `coach.request`, which publishes statistics on an entire
256
+ middleware chain and request. This data is particularly useful for logging, and is our
257
+ solution to Rails `process_action.action_controller` event emitted on controller requests.
258
+
259
+ The benchmarking data includes information on how long each middleware took to process,
260
+ along with the total duration of the chain.