• Stars
    star
    164
  • Rank 221,730 (Top 5 %)
  • Language
    Ruby
  • License
    MIT License
  • Created almost 9 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Alternative controllers with middleware

Coach

Gem Version CircleCI Code Climate

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.

For our policy on compatibility with Ruby and Rails versions, see COMPATIBILITY.md.

Installation

To get started, just add Coach to your Gemfile, and then run bundle:

gem 'coach'

Coach works with Ruby versions 2.6 and onwards.

Coach by example

The best way to see the benefits of Coach is with a demonstration.

Mounting an endpoint

class HelloWorld < Coach::Middleware
  def call
    # Middleware return a Rack response
    [ 200, {}, ['hello world'] ]
  end
end

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...

Example::Application.routes.draw do
  match "/hello_world",
        to: Coach::Handler.new(HelloWorld),
        via: :get
end

Once you've booted Rails locally, the following should return 'hello world':

$ curl -XGET http://localhost:3000/hello_world

Zeitwerk

The new default autoloader in Rails 6+ is Zeitwerk, which removes support for autoloading constants during app boot, which that example would do - either you have to require "hello_world" in your routes file, or avoid referencing the HelloWorld constant until the app has booted. To avoid that, you can instead pass the module or middleware name to Handler.new, for example:

Example::Application.routes.draw do
  match "/hello_world",
        to: Coach::Handler.new("HelloWorld"),
        via: :get

Building chains

Suppose we didn't want just anybody to see our HelloWorld endpoint. In fact, we'd like to lock it down behind some authentication.

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.

class Authentication < Coach::Middleware
  def call
    unless User.exists?(login: params[:login])
      return [ 401, {}, ['Access denied'] ]
    end

    next_middleware.call
  end
end

class HelloWorld < Coach::Middleware
  uses Authentication

  def call
    [ 200, {}, ['hello world'] ]
  end
end

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.

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.

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...

class Authentication < Coach::Middleware
  provides :user  # declare that Authentication provides :user

  def call
    return [ 401, {}, ['Access denied'] ] unless user.present?

    provide(user: user)
    next_middleware.call
  end

  def user
    @user ||= User.find_by(login: params[:login])
  end
end

class HelloUser < Coach::Middleware
  uses Authentication
  requires :user  #ย state that HelloUser requires this data

  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

Coach analyses your middleware chains whenever a new Handler is created, or when the handler is first used if the route is being lazy-loaded (i.e., if you're passing a string name, instead of the route itself). 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: 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.

Configuring middlewares

By making use of middleware config hashes, you can build generalised middlewares that can be configured specifically for the chain that they are used in.

class Logger < Coach::Middleware
  def call
    # Logs the incoming request path, with a configured prefix
    Rails.logger.info("[#{config[:prefix]}] - #{request.path}")
    next_middleware.call
  end
end

class HelloUser < Coach::Middleware
  uses Logger, prefix: 'HelloUser'
  uses Authentication

  def call
    ...
  end
end

The above configures a Logger middleware to prefix it's log entries with 'HelloUser'. This is a contrived example, but at GoCardless we've created middlewares that can act as generalised resource endpoints (show, index, etc) when given the model class and some extra configuration.

Testing

The basic strategy is to test each middleware in isolation, covering all the edge cases, and then create request specs that cover a happy code path, testing each of the middlewares while they work in sequence.

Each middleware is encouraged to rely on data passed through the provide/require syntax exclusively, except in stateful operations (such as database queries). By sticking to this rule, testing becomes as simple as mocking a context hash.

Coach comes with some RSpec matchers to help simplify your testing, however they aren't required by default. You'll need to run require 'coach/rspec', we recommend putting this in your spec/spec_helper.rb or spec/rails_helper.rb file.

require 'spec_helper'

describe "/whoami" do
  let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }

  context "with correct auth details" do
    it "responds with user name" do
      get "/whoami", {}, { 'Authorization' => 'Kryptonite' }
      expect(response.body).to match(/Clark Kent/)
    end
  end
end

describe Routes::Whoami do
  subject(:instance) { described_class.new(context) }
  let(:context) { { authenticated_user: double(name: "Clark Kent") } }

  it { is_expected.to respond_with_body_that_matches(/Clark Kent/) }
end

describe Middleware::AuthenticatedUser do
  subject(:instance) { described_class.new(context) }
  let(:context) do
    { request: instance_double(ActionDispatch::Request, headers: headers) }
  end

  let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }

  context "with valid token" do
    it { is_expected.to call_next_middleware }
    it { is_expected.to provide(authenticated_user: user) }
  end

  context "with invalid token" do
    it { is_expected.to respond_with_status(401) }
    it { is_expected.to respond_with_body_that_matches(/access denied/i) }
  end
end

Routing

For routes that represent resource actions, Coach provides some syntactic sugar to allow concise mapping of endpoint to handler in Rails apps.

# config/routes.rb
Example::Application.routes.draw do
  router = Coach::Router.new(self)
  router.draw(Routes::Users,
              base: "/users",
              actions: [
                :index,
                :show,
                :create,
                :update,
                disable: { method: :post, url: "/:id/actions/disable" }
              ])
end

Default actions that conform to standard REST principles can be easily loaded, with the users resource being mapped to:

Method URL Description
GET /users Index all users
GET /users/:id Get user by ID
POST /users Create new user
PUT /users/:id Update user details
POST /users/:id/actions/disable Custom action routed to the given path suffix

If you're using Zeitwerk, you can pass the name of the module to #draw, instead of the module itself.

# config/routes.rb
Example::Application.routes.draw do
  router = Coach::Router.new(self)
  router.draw("Routes::Users",
              base: "/users",
              actions: [
                :index,
                :show,
                :create,
                :update,
                disable: { method: :post, url: "/:id/actions/disable" }
              ])
end

Rendering

By now you'll probably agree that the rack response format isn't the nicest way to render responses. Coach comes sans renderer, and for a good reason.

We initially built a Coach::Renderer module, but soon realised that doing so would prevent us from open sourcing. Our Renderer was 90% logic specific to the way our APIs function, including handling/formatting of validation errors, logging of unusual events etc.

What worked well for us is a standalone Renderer class that we could require in all our middleware that needed to format responses. This pattern also led to clearer code - consistent with our preference for explicit code, stating Renderer.new_resource(...) is instantly more debuggable than an inherited method on all middlewares.

Instrumentation

Coach uses ActiveSupport::Notifications to issue events that can be used to profile middleware.

Information for how to use ActiveSupports notifications can be found here.

Event Arguments
start_handler.coach event(:middleware, :request)
start_middleware.coach event(:middleware, :request)
finish_middleware.coach start, finish, id, event(:middleware, :request)
finish_handler.coach start, finish, id, event(:middleware, :request)
request.coach event containing request data and benchmarking

Of special interest is request.coach, which publishes statistics on an entire middleware chain and request. This data is particularly useful for logging, and is our solution to Rails process_action.action_controller event emitted on controller requests.

The benchmarking data includes information on how long each middleware took to process, along with the total duration of the chain.

For coach to emit request.coach events, it first needs to be subscribed to handler/middleware events:

Coach::Notifications.subscribe!

# Now you can subscribe to and use request.coach events, e.g.
ActiveSupport::Notifications.subscribe("request.coach") do |_, event|
  Rails.logger.info(event)
end

You can add additional metadata to the notifications published by Coach by calling the log_metadata method from inside your Coach middlewares.

class Tracking < Coach::Middleware
  requires :user

  def call
    log_metadata(user_id: user.id)
    next_middleware.call
  end
end

Coach CLI

As well as the library, the Coach gem comes with a command line tool - coach.

When working in a large codebase that uses Coach, one of the challenges you may run into is understanding the provide/require graph made up of all the middleware chains you've built. While the library enforces the correctness of those chains at boot time, it doesn't help you understand those dependencies. That's where the coach CLI comes in!

Currently, the coach CLI supports two commands.

find-provider

find-provider is the simpler of the two commands. Given the name of a Coach middleware and a value that it requires, it outputs the name of the middleware that provides it.

$ bundle exec coach find-provider HelloUser user
Value `user` is provided to `HelloUser` by:

Authentication

If there are multiple middlewares in the chain that provide the same value, all of them will be listed.

find-chain

find-chain is the more advanced of the two commands, and is most useful in larger codebases. Given the name of a Coach middleware and a value it requires, it outputs the chains of middleware between the specified middleware and the one that provides the required value.

# Note that we've assumed an intermediate middleware - `UserDecorator` exists in this
# example to make the functionality of the command clearer.
$ bundle exec coach find-chain HelloUser user
Value `user` is provided to `HelloUser` by:

HelloUser -> UserDecorator -> Authentication

If there are multiple paths to a middleware that provides that value, all of them will be listed. Similarly, if multiple middlewares provide the same value, all of them will be listed.

Spring integration

Given that the Coach CLI is mostly aimed at large Rails apps using Coach, it would be an oversight for us not to integrate it with Spring.

To enable the use of Spring with the Coach CLI, add the following to config/spring.rb or an equivalent Rails config file.

require "spring/commands/coach"

On GoCardless' main Rails app, using Spring reduces the time to run coach commands from around 15s to 1s.

Future work

While we think the commands we've already built are useful, we do have some ideas to go further, including:

  • Better formatting of provider chains
  • Outputting DOT format files to visualise with Graphviz
  • Editor integrations (e.g. showing the provider chains when hovering a requires statement)

License & Contributing

GoCardless โ™ฅ open source. If you do too, come join us.

More Repositories

1

statesman

A statesmanlike state machine library.
Ruby
1,700
star
2

angularjs-style-guide

AngularJS style guide used at GoCardless
1,446
star
3

business

Ruby business day calculations
Ruby
494
star
4

http-api-design

HTTP Design Guidelines
413
star
5

airflow-dbt

Apache Airflow integration for dbt
Python
367
star
6

es6-angularjs

JavaScript
180
star
7

logjam

a log shipping tool
Go
134
star
8

our-postgresql-setup

PostgreSQL clustering with corosync/pacemaker test environment
Shell
126
star
9

nandi

Fear free PostgreSQL migrations for Rails
Ruby
123
star
10

activerecord-safer_migrations

Safer ActiveRecord migrations for Postgres
Ruby
116
star
11

amqpc

AMQP CLI tool
Go
115
star
12

pgreplay-go

Postgres load testing tool
Go
112
star
13

rspec-activejob

RSpec matchers for testing ActiveJob
Ruby
98
star
14

ibandit

Convert national banking details into IBANs, and vice-versa.
Ruby
95
star
15

gocardless-pro-php

GoCardless Pro PHP Client
PHP
89
star
16

gocardless-legacy-php

The PHP client library for the GoCardless Legacy API
PHP
66
star
17

stolon-pgbouncer

Add-on to stolon for providing zero-downtime failover and PgBouncer integration
Go
59
star
18

gocardless-legacy-ruby

The Ruby client library for the GoCardless API
Ruby
52
star
19

gc-http-factory

A factory for creating $http services in Angular.
JavaScript
44
star
20

draupnir

Anonymised database instances as-a-service
Go
41
star
21

bump

Automated dependency management for Ruby, Python and Javascript
Ruby
35
star
22

gocardless-pro-python

GoCardless Pro Python Client
Python
33
star
23

stubby

Your favourite pretender stubber
JavaScript
29
star
24

gocardless-pro-ruby

GoCardless Pro Ruby Client
Ruby
27
star
25

gocardless-dotnet

GoCardless .NET Client
C#
25
star
26

utopia-getting-started

Sharing a copy of our getting-started tutorial, as a demonstration of how our infrastructure works with utopia
24
star
27

resque-sentry

A Resque failure backend that sends errors to Sentry
Ruby
23
star
28

statesman-events

Event support for Statesman (UNMAINTAINED)
Ruby
23
star
29

prius

Environmentally-friendly application config
Ruby
22
star
30

systemjs-assetgraph

AssetGraph transform for optimizing SystemJS pages for production
JavaScript
22
star
31

gocardless-legacy-python

The Python client library for the GoCardless API
Python
22
star
32

theatre

GoCardless' collection of Kubernetes extensions
Go
20
star
33

gocardless-nodejs

GoCardless Node.js client
TypeScript
19
star
34

javascript-style-guide

The GoCardless JavaScript styleguide
18
star
35

pgsql-cluster-manager

Daemon and migration tool that manages Postgres cluster using etcd/corosync/pacemaker
Go
17
star
36

anony

A small library that defines how ActiveRecord models should be anonymised for deletion purposes.
Ruby
17
star
37

drydock

DryDock is a utility to clean up Docker images
Go
16
star
38

gocardless-pro-java

GoCardless Pro Java Client
Java
15
star
39

ng-gc-components

JavaScript
14
star
40

slo-builder

Templates for building SLOs with Prometheus rules and alerts
Go
14
star
41

legacy-api-docs

Docs for GoCardless legacy API
JavaScript
14
star
42

business-python

Python business day calculations
Python
13
star
43

gocardless-legacy-dotnet

The .NET client library for the GoCardless Legacy API
C#
12
star
44

companies-house-rest

Ruby wrapper for the Companies House REST API.
Ruby
12
star
45

gocardless-legacy-node

The Node.js client library for the GoCardless Legacy API
JavaScript
11
star
46

html-style-guide

How we write HTML at GoCardless
11
star
47

bump-core

The core logic powering Bump
Ruby
11
star
48

atum

Ruby HTTP client generator for APIs represented with JSON schema
Ruby
9
star
49

gocardless-legacy-java

The Java client library for the GoCardless Legacy API
Java
9
star
50

codeigniter-gocardless

The CodeIgniter spark for the GoCardless API
PHP
8
star
51

logsearch

Search Logstash / Elasticsearch logs from the command line
Go
7
star
52

sample-legacy-django-app

A sample Django app demonstrating the use of the GoCardless Legacy API and the Python client.
Python
7
star
53

companies-house-gateway-ruby

Ruby wrapper for the Companies House XML Gateway
Ruby
6
star
54

airflow-looker

A collection of Airflow extensions to provide integration with Looker
Python
6
star
55

uk_phone_numbers

A Ruby library for validating and formatting UK phone numbers.
Ruby
6
star
56

callcredit-ruby

Ruby wrapper for Callcredit's CallValidate API
Ruby
6
star
57

sample-legacy-rails-app

A sample Rails app demonstrating the use of GoCardless Legacy API and the Ruby client.
Ruby
6
star
58

react-dropin

React bindings for the GoCardless Dropin checkout flow
TypeScript
5
star
59

creditsafe-ruby

Ruby library for the Creditsafe SOAP API
Ruby
5
star
60

github-archive

Easy way to archive an entire organisation repos on S3
Go
5
star
61

gocardless-pro-go

Go
5
star
62

simple-swag

Dead simple swagger/openapi docs server
Go
3
star
63

bank-webfont

A webfont of prominent UK banks
CSS
3
star
64

cli-releases

Release repo for the gocardless cli
Dockerfile
3
star
65

gc_ruboconfig

GoCardless Engineering shared rubocop config
Ruby
3
star
66

gocardless-pro-ruby-example

Example of using the GoCardless Pro Ruby client library
HTML
3
star
67

gocardless-legacy-example-django

An example site using the GoCardless Legacy API
Python
3
star
68

belongs-to-one-of

Gem to support activemodel relations where one model can be a child of one of many models
Ruby
3
star
69

coach-demo

Ruby
3
star
70

gocardless-php

Placeholder explaining our PHP API libraries.
2
star
71

prometheus-client-ruby-data-stores-experiments

Ruby
2
star
72

que

A Ruby job queue that uses PostgreSQL's advisory locks for speed and reliability.
Ruby
2
star
73

slackify

Update your Slack status with what you're listening to in Spotify
Elixir
2
star
74

slackify-dot-rb

Take 2: this time without a language barrier
Ruby
1
star
75

gocardless-pro-java-maven-example

An example java app that handles webhooks
Java
1
star
76

open-charities

A Ruby library for querying the OpenCharities database
Ruby
1
star
77

bucket-store

Helper library to access cloud storage services
Ruby
1
star
78

publish-techdocs-action

Action to generate and publish TechDocs
Shell
1
star
79

passfort

Ruby client library for the PassFort API
Ruby
1
star
80

salesforce_wrapper

A wrapper around Restforce, catching exceptions and performing a configurable action with them (e.g. sending an email).
Ruby
1
star
81

gocardless-pro-java-example

Example of using the GoCardless Pro Java client library
Java
1
star
82

homebrew-taps

Ruby
1
star
83

gocardless-legacy-partner-example-ruby

An example GoCardless partner app, written in Sinatra
Ruby
1
star
84

gocardless-pro-php-demo

Pro client PHP demo
PHP
1
star
85

rspec-que

RSpec matchers for testing Que
Ruby
1
star