README.md in coach-0.2.0 vs README.md in coach-0.2.1

- old
+ new

@@ -2,142 +2,142 @@ [![Gem Version](https://badge.fury.io/rb/coach.svg)](http://badge.fury.io/rb/coach) [![Build Status](https://travis-ci.org/gocardless/coach.png?branch=master)](https://travis-ci.org/gocardless/coach) [![Code Climate](https://codeclimate.com/github/gocardless/coach.png)](https://codeclimate.com/github/gocardless/coach) -Coach improves your controller code by encouraging... +Coach improves your controller code by encouraging: - **Modularity** - No more tangled `before_filter`'s and interdependent concerns. Build Middleware that does a single job, and does it well. - **Guarantees** - Work with a simple `provide`/`require` interface to guarantee that your middlewares load data in the right order when you first boot your app. - **Testability** - Test each middleware in isolation, with effortless mocking of test data and natural RSpec matchers. -## Simple endpoint +## Coach by example -Lets start by creating a simple endpoint. +The best way to see the benefits of Coach is with a demonstration. +### Mounting an endpoint + ```ruby -module Routes - class Echo < Coach::Middleware - def call - # All middleware should return rack compliant responses - [ 200, {}, [params[:word]] ] - end +class HelloWorld < Coach::Middleware + def call + # Middleware return a Rack response + [ 200, {}, ['hello world'] ] end end ``` -Any Middlewares have access to the `request` handle, which is an instance of -`ActionDispatch::Request`. This also parses the request params, and these are made -available inside Middlewares as `params`. +So we've created ourselves a piece of middleware, `HelloWorld`. As you'd expect, +`HelloWorld` simply outputs the string `'hello world'`. In an example Rails app, called `Example`, we can mount this route like so... ```ruby Example::Application.routes.draw do - match "/echo/:word", - to: Coach::Handler.new(Routes::Echo), + match "/hello_world", + to: Coach::Handler.new(HelloWorld), via: :get end ``` -Once booting Rails locally, running `curl -XGET http://localhost:3000/echo/hello` should -respond with `'hello'`. +Once you've booted Rails locally, the following should return `'hello world'`: -## Building chains - -Lets try creating a route protected by authentication. - -```ruby -module Routes - class Secret < Coach::Middleware - def call - unless User.exists?(id: params[:user_id], password: params[:user_password]) - return [ 401, {}, ['Access denied'] ] - end - - [ 200, {}, ['super-secretness'] ] - end - end -end +```sh +$ curl -XGET http://localhost:3000/hello_world ``` -The above will verify that a user can be found with the given params, and if it cannot -then will respond with a `401`. +### Building chains -This does what we want it to do, but why should `Secret` know anything about -authentication? This complicates `Secret`'s design and prevents reuse of authentication -logic. +Suppose we didn't want just anybody to see our `HelloWorld` endpoint. In fact, we'd like +to lock it down behind some authentication. -```ruby -module Middleware - class Authentication < Coach::Middleware - def call - unless User.exists?(id: params[:user_id], password: params[:user_password]) - return [ 401, {}, ['Access denied'] ] - end +Our request will now have two stages, one where we check authentication details and +another where we respond with our secret greeting to the world. Let's split into two +pieces, one for each of the two subtasks, allowing us to reuse this authentication flow in +other middlewares. - next_middleware.call +```ruby +class Authentication < Coach::Middleware + def call + unless User.exists?(login: params[:login]) + return [ 401, {}, ['Access denied'] ] end + + next_middleware.call end end -module Routes - class Secret < Coach::Middleware - uses Middleware::Authentication +class HelloWorld < Coach::Middleware + uses Authentication - def call - [ 200, {}, ['super-secretness'] ] - end + def call + [ 200, {}, ['hello world'] ] end end ``` -Here we detach the authentication logic into it's own middleware. `Secret` now `uses` -`Middleware::Authentication`, and will only run if it has been called via -`next_middleware.call` from authentication. +Here we detach the authentication logic into its own middleware. `HelloWorld` now `uses` +`Authentication`, and will only run if it has been called via `next_middleware.call` from +authentication. -## Passing data through middleware +Notice we also use `params` just like you would in a normal Rails controller. Every +middleware class will have access to a `request` object, which is an instance of +`ActionDispatch::Request`. -Now what happens if you have an endpoint that returns the current auth'd users details? We -can maintain the separation of authentication logic and endpoint as below... +### Passing data through middleware +So far we've demonstrated how Coach can help you break your controller code into modular +pieces. The big innovation with Coach, however, is the ability to explicitly pass your +data through the middleware chain. + +An example usage here is to create a `HelloUser` endpoint. We want to protect the route by +authentication, as we did before, but this time greet the user that is logged in. Making +a small modification to the `Authentication` middleware we showed above... + ```ruby -module Middleware - class AuthenticatedUser < Coach::Middleware - provides :authenticated_user +class Authentication < Coach::Middleware + provides :user # declare that Authentication provides :user - def call - user = User.find_by(token: request.headers['Authorization']) - return [ 401, {}, ['Access denied'] ] unless user.exists? + def call + return [ 401, {}, ['Access denied'] ] unless user.present? - provide(authenticated_user: user) - next_middleware.call - end + provide(user: user) + next_middleware.call end + + def user + @user ||= User.find_by(login: params[:login]) + end end -module Routes - class Whoami < Coach::Middleware - uses AuthenticatedUser - requires :authenticated_user +class HelloUser < Coach::Middleware + uses Authentication + requires :user # state that HelloUser requires this data - def call - [ 200, {}, [authenticated_user.name] ] - end + def call + # Can now access `user`, as it's been provided by Authentication + [ 200, {}, [ "hello #{user.name}" ] ] end end + +# Inside config/routes.rb +Example::Application.routes.draw do + match "/hello_user", + to: Coach::Handler.new(HelloUser), + via: :get +end ``` -Each middleware declares what it requires from those that have ran before it, and what it -will provide to those that run after. Whenever a middleware chain is mounted, these -requirements will be verified. In the above, if our `Whoami` middleware had neglected to use -`AuthenticatedUser`, then mounting would fail with the error... +Coach analyses your middleware chains whenever a new `Handler` is created. If any +middleware `requires :x` when its chain does not provide `:x`, we'll error out before the +app even starts with the error: - Coach::Errors::MiddlewareDependencyNotMet: AuthenticatedUser requires keys [authenticated_user] that are not provided by the middleware chain +```ruby +Coach::Errors::MiddlewareDependencyNotMet: HelloUser requires keys [user] that are not provided by the middleware chain +``` This static verification eradicates an entire category of errors that stem from implicitly running code before hitting controller methods. It allows you to be confident that the data you require has been loaded, and makes tracing the origin of that data as simple as looking up the chain. @@ -211,10 +211,10 @@ disable: { method: :post, url: "/:id/actions/disable" } ]) ``` Default actions that conform to standard REST principles can be easily loaded, with the -users resource being mapped to... +users resource being mapped to: | Method | URL | Description | |--------|------------------------------|------------------------------------------------| | `GET` | `/users` | Index all users | | `GET` | `/users/:id` | Get user by ID |