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