{"id":13880078,"url":"https://github.com/gocardless/coach","last_synced_at":"2025-05-15T11:02:34.962Z","repository":{"id":33665383,"uuid":"37317984","full_name":"gocardless/coach","owner":"gocardless","description":"Alternative controllers with middleware","archived":false,"fork":false,"pushed_at":"2025-03-24T17:59:23.000Z","size":237,"stargazers_count":165,"open_issues_count":6,"forks_count":11,"subscribers_count":93,"default_branch":"master","last_synced_at":"2025-05-15T11:01:56.355Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"atom/language-ruby","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gocardless.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-06-12T11:17:36.000Z","updated_at":"2025-03-24T17:59:28.000Z","dependencies_parsed_at":"2025-02-05T19:11:15.517Z","dependency_job_id":"ba31991e-585d-44c3-9bc7-71f56c253601","html_url":"https://github.com/gocardless/coach","commit_stats":{"total_commits":154,"total_committers":29,"mean_commits":5.310344827586207,"dds":0.8766233766233766,"last_synced_commit":"302485b0c0e248d7deec9d3e325e60cdb24126c4"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fcoach","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fcoach/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fcoach/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gocardless%2Fcoach/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gocardless","download_url":"https://codeload.github.com/gocardless/coach/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254328385,"owners_count":22052632,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-06T08:02:46.403Z","updated_at":"2025-05-15T11:02:34.883Z","avatar_url":"https://github.com/gocardless.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Coach\n\n[![Gem Version](https://badge.fury.io/rb/coach.svg)](http://badge.fury.io/rb/coach)\n\nCoach improves your controller code by encouraging:\n\n- **Modularity** - No more tangled `before_filter`'s and interdependent concerns. Build\n  Middleware that does a single job, and does it well.\n- **Guarantees** - Work with a simple `provide`/`require` interface to guarantee that your\n  middlewares load data in the right order when you first boot your app.\n- **Testability** - Test each middleware in isolation, with effortless mocking of test\n  data and natural RSpec matchers.\n\nFor our policy on compatibility with Ruby and Rails versions, see [COMPATIBILITY.md](docs/COMPATIBILITY.md).\n\n# Installation\n\nTo get started, just add Coach to your `Gemfile`, and then run `bundle`:\n\n```ruby\ngem 'coach'\n```\n\nCoach works with Ruby versions 3.1 and onwards.\n\n## Coach by example\n\nThe best way to see the benefits of Coach is with a demonstration.\n\n### Mounting an endpoint\n\n```ruby\nclass HelloWorld \u003c Coach::Middleware\n  def call\n    # Middleware return a Rack response\n    [ 200, {}, ['hello world'] ]\n  end\nend\n```\n\nSo we've created ourselves a piece of middleware, `HelloWorld`. As you'd expect,\n`HelloWorld` simply outputs the string `'hello world'`.\n\nIn an example Rails app, called `Example`, we can mount this route like so...\n\n```ruby\nExample::Application.routes.draw do\n  match \"/hello_world\",\n        to: Coach::Handler.new(HelloWorld),\n        via: :get\nend\n```\n\nOnce you've booted Rails locally, the following should return `'hello world'`:\n\n```sh\n$ curl -XGET http://localhost:3000/hello_world\n```\n\n### Zeitwerk\n\nThe new default autoloader in Rails 6+ is [Zeitwerk](https://github.com/fxn/zeitwerk), which removes\nsupport for autoloading constants during app boot, which that example would do - either you have to\n`require \"hello_world\"` in your routes file, or avoid referencing the `HelloWorld` constant until\nthe app has booted. To avoid that, you can instead pass the module or middleware _name_ to\n`Handler.new`, for example:\n\n```ruby\nExample::Application.routes.draw do\n  match \"/hello_world\",\n        to: Coach::Handler.new(\"HelloWorld\"),\n        via: :get\n```\n\n### Building chains\n\nSuppose we didn't want just anybody to see our `HelloWorld` endpoint. In fact, we'd like\nto lock it down behind some authentication.\n\nOur request will now have two stages, one where we check authentication details and\nanother where we respond with our secret greeting to the world. Let's split into two\npieces, one for each of the two subtasks, allowing us to reuse this authentication flow in\nother middlewares.\n\n```ruby\nclass Authentication \u003c Coach::Middleware\n  def call\n    unless User.exists?(login: params[:login])\n      return [ 401, {}, ['Access denied'] ]\n    end\n\n    next_middleware.call\n  end\nend\n\nclass HelloWorld \u003c Coach::Middleware\n  uses Authentication\n\n  def call\n    [ 200, {}, ['hello world'] ]\n  end\nend\n```\n\nHere we detach the authentication logic into its own middleware. `HelloWorld` now `uses`\n`Authentication`, and will only run if it has been called via `next_middleware.call` from\nauthentication.\n\nNotice we also use `params` just like you would in a normal Rails controller. Every\nmiddleware class will have access to a `request` object, which is an instance of\n`ActionDispatch::Request`.\n\n### Passing data through middleware\n\nSo far we've demonstrated how Coach can help you break your controller code into modular\npieces. The big innovation with Coach, however, is the ability to explicitly pass your\ndata through the middleware chain.\n\nAn example usage here is to create a `HelloUser` endpoint. We want to protect the route by\nauthentication, as we did before, but this time greet the user that is logged in. Making\na small modification to the `Authentication` middleware we showed above...\n\n```ruby\nclass Authentication \u003c Coach::Middleware\n  provides :user  # declare that Authentication provides :user\n\n  def call\n    return [ 401, {}, ['Access denied'] ] unless user.present?\n\n    provide(user: user)\n    next_middleware.call\n  end\n\n  def user\n    @user ||= User.find_by(login: params[:login])\n  end\nend\n\nclass HelloUser \u003c Coach::Middleware\n  uses Authentication\n  requires :user  # state that HelloUser requires this data\n\n  def call\n    # Can now access `user`, as it's been provided by Authentication\n    [ 200, {}, [ \"hello #{user.name}\" ] ]\n  end\nend\n\n# Inside config/routes.rb\nExample::Application.routes.draw do\n  match \"/hello_user\",\n        to: Coach::Handler.new(\"HelloUser\"),\n        via: :get\nend\n```\n\nCoach analyses your middleware chains whenever a new `Handler` is created, or when the handler is\nfirst used if the route is being lazy-loaded (i.e., if you're passing a string name, instead of the\nroute itself). If any middleware `requires :x` when its chain does not provide `:x`, we'll error out\nbefore the app even starts with the error:\n\n```ruby\nCoach::Errors::MiddlewareDependencyNotMet: HelloUser requires keys [user] that are not provided by the middleware chain\n```\n\nThis static verification eradicates an entire category of errors that stem from implicitly\nrunning code before hitting controller methods. It allows you to be confident that the\ndata you require has been loaded, and makes tracing the origin of that data as simple as\nlooking up the chain.\n\n## Configuring middlewares\n\nBy making use of middleware config hashes, you can build generalised middlewares that can\nbe configured specifically for the chain that they are used in.\n\n```ruby\nclass Logger \u003c Coach::Middleware\n  def call\n    # Logs the incoming request path, with a configured prefix\n    Rails.logger.info(\"[#{config[:prefix]}] - #{request.path}\")\n    next_middleware.call\n  end\nend\n\nclass HelloUser \u003c Coach::Middleware\n  uses Logger, prefix: 'HelloUser'\n  uses Authentication\n\n  def call\n    ...\n  end\nend\n```\n\nThe above configures a `Logger` middleware to prefix it's log entries with `'HelloUser'`.\nThis is a contrived example, but at GoCardless we've created middlewares that can act as\ngeneralised resource endpoints (show, index, etc) when given the model class and some\nextra configuration.\n\n## Testing\n\nThe basic strategy is to test each middleware in isolation, covering all the edge cases,\nand then create request specs that cover a happy code path, testing each of the\nmiddlewares while they work in sequence.\n\nEach middleware is encouraged to rely on data passed through the `provide`/`require`\nsyntax exclusively, except in stateful operations (such as database queries). By sticking\nto this rule, testing becomes as simple as mocking a `context` hash.\n\nCoach comes with some RSpec matchers to help simplify your testing, however they aren't\nrequired by default. You'll need to run `require 'coach/rspec'`, we recommend putting this\nin your `spec/spec_helper.rb` or `spec/rails_helper.rb` file.\n\n```ruby\nrequire 'spec_helper'\n\ndescribe \"/whoami\" do\n  let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }\n\n  context \"with correct auth details\" do\n    it \"responds with user name\" do\n      get \"/whoami\", {}, { 'Authorization' =\u003e 'Kryptonite' }\n      expect(response.body).to match(/Clark Kent/)\n    end\n  end\nend\n\ndescribe Routes::Whoami do\n  subject(:instance) { described_class.new(context) }\n  let(:context) { { authenticated_user: double(name: \"Clark Kent\") } }\n\n  it { is_expected.to respond_with_body_that_matches(/Clark Kent/) }\nend\n\ndescribe Middleware::AuthenticatedUser do\n  subject(:instance) { described_class.new(context) }\n  let(:context) do\n    { request: instance_double(ActionDispatch::Request, headers: headers) }\n  end\n\n  let(:user) { FactoryGirl.create(:user, name: 'Clark Kent', token: 'Kryptonite') }\n\n  context \"with valid token\" do\n    it { is_expected.to call_next_middleware }\n    it { is_expected.to provide(authenticated_user: user) }\n  end\n\n  context \"with invalid token\" do\n    it { is_expected.to respond_with_status(401) }\n    it { is_expected.to respond_with_body_that_matches(/access denied/i) }\n  end\nend\n```\n\n## Routing\n\nFor routes that represent resource actions, Coach provides some syntactic sugar to\nallow concise mapping of endpoint to handler in Rails apps.\n\n```ruby\n# config/routes.rb\nExample::Application.routes.draw do\n  router = Coach::Router.new(self)\n  router.draw(Routes::Users,\n              base: \"/users\",\n              actions: [\n                :index,\n                :show,\n                :create,\n                :update,\n                disable: { method: :post, url: \"/:id/actions/disable\" }\n              ])\nend\n```\n\nDefault actions that conform to standard REST principles can be easily loaded, with the\nusers resource being mapped to:\n\n| Method | URL                          | Description                                   |\n| ------ | ---------------------------- | --------------------------------------------- |\n| `GET`  | `/users`                     | Index all users                               |\n| `GET`  | `/users/:id`                 | Get user by ID                                |\n| `POST` | `/users`                     | Create new user                               |\n| `PUT`  | `/users/:id`                 | Update user details                           |\n| `POST` | `/users/:id/actions/disable` | Custom action routed to the given path suffix |\n\nIf you're using Zeitwerk, you can pass the name of the module to `#draw`, instead of the module\nitself.\n\n```ruby\n# config/routes.rb\nExample::Application.routes.draw do\n  router = Coach::Router.new(self)\n  router.draw(\"Routes::Users\",\n              base: \"/users\",\n              actions: [\n                :index,\n                :show,\n                :create,\n                :update,\n                disable: { method: :post, url: \"/:id/actions/disable\" }\n              ])\nend\n```\n\n## Rendering\n\nBy now you'll probably agree that the rack response format isn't the nicest way to render\nresponses. Coach comes sans renderer, and for a good reason.\n\nWe initially built a `Coach::Renderer` module, but soon realised that doing so would\nprevent us from open sourcing. Our `Renderer` was 90% logic specific to the way our APIs\nfunction, including handling/formatting of validation errors, logging of unusual events\netc.\n\nWhat worked well for us is a standalone `Renderer` class that we could require in all our\nmiddleware that needed to format responses. This pattern also led to clearer code -\nconsistent with our preference for explicit code, stating `Renderer.new_resource(...)` is\ninstantly more debuggable than an inherited method on all middlewares.\n\n## Instrumentation\n\nCoach uses `ActiveSupport::Notifications` to issue events that can be used to profile\nmiddleware.\n\nInformation for how to use `ActiveSupport`s notifications can be found\n[here](http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html).\n\n| Event                     | Arguments                                               |\n| ------------------------- | ------------------------------------------------------- |\n| `start_handler.coach`     | `event(:middleware, :request)`                          |\n| `start_middleware.coach`  | `event(:middleware, :request)`                          |\n| `finish_middleware.coach` | `start`, `finish`, `id`, `event(:middleware, :request)` |\n| `finish_handler.coach`    | `start`, `finish`, `id`, `event(:middleware, :request)` |\n| `request.coach`           | `event` containing request data and benchmarking        |\n\nOf special interest is `request.coach`, which publishes statistics on an entire\nmiddleware chain and request. This data is particularly useful for logging, and is our\nsolution to Rails `process_action.action_controller` event emitted on controller requests.\n\nThe benchmarking data includes information on how long each middleware took to process,\nalong with the total duration of the chain.\n\nFor coach to emit `request.coach` events, it first needs to be subscribed to handler/middleware events:\n\n```ruby\nCoach::Notifications.subscribe!\n\n# Now you can subscribe to and use request.coach events, e.g.\nActiveSupport::Notifications.subscribe(\"request.coach\") do |_, event|\n  Rails.logger.info(event)\nend\n```\n\nYou can add additional metadata to the notifications published by Coach by calling the\n`log_metadata` method from inside your Coach middlewares.\n\n```ruby\nclass Tracking \u003c Coach::Middleware\n  requires :user\n\n  def call\n    log_metadata(user_id: user.id)\n    next_middleware.call\n  end\nend\n```\n\n# Coach CLI\n\nAs well as the library, the Coach gem comes with a command line tool - `coach`.\n\nWhen working in a large codebase that uses Coach, one of the challenges you may run into\nis understanding the `provide`/`require` graph made up of all the middleware chains you've\nbuilt. While the library enforces the correctness of those chains at boot time, it doesn't\nhelp you understand those dependencies. That's where the `coach` CLI comes in!\n\nCurrently, the `coach` CLI supports two commands.\n\n## `find-provider`\n\n`find-provider` is the simpler of the two commands. Given the name of a Coach middleware\nand a value that it requires, it outputs the name of the middleware that provides it.\n\n```bash\n$ bundle exec coach find-provider HelloUser user\nValue `user` is provided to `HelloUser` by:\n\nAuthentication\n```\n\nIf there are multiple middlewares in the chain that provide the same value, all of them\nwill be listed.\n\n## `find-chain`\n\n`find-chain` is the more advanced of the two commands, and is most useful in larger\ncodebases. Given the name of a Coach middleware and a value it requires, it outputs the\nchains of middleware between the specified middleware and the one that provides the\nrequired value.\n\n```bash\n# Note that we've assumed an intermediate middleware - `UserDecorator` exists in this\n# example to make the functionality of the command clearer.\n$ bundle exec coach find-chain HelloUser user\nValue `user` is provided to `HelloUser` by:\n\nHelloUser -\u003e UserDecorator -\u003e Authentication\n```\n\nIf there are multiple paths to a middleware that provides that value, all of them will be\nlisted. Similarly, if multiple middlewares provide the same value, all of them will be\nlisted.\n\n## Spring integration\n\nGiven that the Coach CLI is mostly aimed at large Rails apps using Coach, it would be an\noversight for us not to integrate it with [Spring](https://github.com/rails/spring/).\n\nTo enable the use of Spring with the Coach CLI, add the following to `config/spring.rb` or\nan equivalent Rails config file.\n\n```ruby\nrequire \"spring/commands/coach\"\n```\n\nOn GoCardless' main Rails app, using Spring reduces the time to run `coach` commands from\naround 15s to 1s.\n\n## Future work\n\nWhile we think the commands we've already built are useful, we do have some ideas to go\nfurther, including:\n\n- Better formatting of provider chains\n- Outputting DOT format files to visualise with Graphviz\n- Editor integrations (e.g. showing the provider chains when hovering a `requires`\n  statement)\n\n# License \u0026 Contributing\n\n- Coach is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n- Bug reports and pull requests are welcome on GitHub at https://github.com/gocardless/coach.\n\nGoCardless ♥ open source. If you do too, come [join us](https://gocardless.com/about/jobs).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fcoach","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgocardless%2Fcoach","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgocardless%2Fcoach/lists"}