Dryer Routes

Dryer Routes is a gem allows for request and response types to be added to a Rails routes file via dry-validation contracts

Installation

add the following to you gemfile

gem "dryer_routes"

Usage

Add this code to config/initializers/dry_routes.rb

RouteRegistry = Dryer::Routes::Registry.new

And then in config/routes.rb you can register your app's routes, eg.

Rails.application.routes.draw do
  RouteRegistry.register(
    {
      controller: UsersController,
      url: "/users",
      actions: {
        create: {
          method: :post,
          request_contract: Contracts::Users::Post::Request,
          response_contracts: {
            200 => Contracts::Users::Post::Response,
          }
        }
      }
    },
    {
      controller: SessionsController,
      url: "/sessions",
      actions: {
        create: {
          method: :post,
          request_contract: Contracts::Sessions::Post::Request,
          response_contracts: {
            200 => Contracts::Sessions::Post::Response,
          }
        }
      }
    }
  )
  RouteRegistry.to_rails_routes(self)
end

Features

This gem helps organize and enforce typing for your routes, all relevant data can be accessed through the gem keeping your code dry

Easy to find route metadata

request contracts: RouteRegistry.users.create.request_contract

route url: RouteRegistry.users.create.url

response contracts: RouteRegistry.users.create.response_contracts._200

Generating types in controller tests

class UsersControllerIntegreationTest < ActionDispatch::IntegrationTest
  test "POST 200 - successfully creating a user" do
    request = Dryer::Factories::BuildFromContract.call(
      RouteRegistry.users.create.request_contract
    )
    post RouteRegistry.users.create.url, params: request.as_json

    assert_response :success

    assert_empty RouteRegistry.users.create.response_contracts._200.new.call(
      JSON.parse(response.body)
    ).errors
  end
end

Shameless plug for my other gem dryer_factories

Enforcing types in Controllers

By adding an around_action to ApplicationController, a controller's requests and responses can be validated automatically eg:

class ApplicationController < ActionController::Base
  include Dry::Monads[:result]

  around_action :validate_request_and_response

  def validate_request
    request_errors = RouteRegistry.validate_request(request)
    if request_errors.empty?
      @validated_request_body = Dry::Monads::Success(
        RouteRegistry.get_validated_values(request)
      )
    else
      @validated_request_body = Dry::Monads::Failure(request_errors)
    end
  end
  attr_reader :validated_request_body

  def validate_response
    response_errors = RouteRegistry.validate_response(
      controller: request.controller_class,
      method: request.request_method_symbol,
      status: response.status,
      body: JSON.parse(response.body)
    )
    if !response_errors.empty?
      Rails.logger.error("
        #{request.controller_class}##{request.request_method_symbol}
        response errors: #{response_errors}
      ")
    end
    response
  end

  def validate_request_and_response
    validate_request
    if validated_request_body.success?
      yield
      validate_response
    else
      render json: {errors: validated_request_body.failure.to_h}, status: :bad_request
    end
  end
end

Allowing the controller to look like

class UsersController < ApplicationController
  def create
    validated_request_body.bind do |body|
      # Do stuff
    end
  end
end

where body will only contain the keys specified by the contract.

Development

This gem is set up to be developed using Nix and ruby_gem_dev_shell Once you have nix installed you can run make env to enter the development environment and then make to see the list of available commands

Contributing

Please create a github issue to report any problems using the Gem. Thanks for your help in making testing easier for everyone!

Versioning

Dryer Routes follows Semantic Versioning 2.0 as defined at https://semver.org.

License

This code is free to use under the terms of the MIT license.