Skip to content

tymate/api-blocks

Repository files navigation

ApiBlocks

Gem Code Climate Inch

ApiBlocks provides simple and consistent Rails API extensions.

Links:

Installation

gem 'api-blocks'

Configuration

In an initializer such as config/initializers/api_blocks.rb you can enable the optional blueprinter and batch-loader integration:

ApiBlocks.configure do |config|
  config.blueprinter.use_batch_loader = true
end

This allows you to use batch-loader in order to avoid n+1 queries when serializing associations in blueprints.

This has some caveats which are documented in association_extractor.rb.

ApiBlocks::Controller

Include ApiBlocks::Controller in your api controller:

class Api::V1::ApplicationController < ActionController::API
  include ApiBlocks::Controller

  pundit_scope :api, :v1
end

Including the module will:

  • Setup ApiBlocks::Responder as a responder.
  • Add the verify_request_format! before_action hook.
  • Setup Pundit, rescue its errors, setup its validation hooks and provide the pundit_scope method.

ApiBlocks::Responder

An ActionController::Responder with better error handling and Dry::Monads::Result support.

Errors are handled for the following cases:

  • The responded resource is an ApplicationRecord subclass and has error.
  • The responded resource is a ActiveRecord::RecordInvalid exception.
  • Otherwise the error is re-raised to be handled through the usual Ruby On Rails error handlers.

In addition, the responder will render resources on POST and PUT rather than returning a redirection.

ApiBlocks::Interactor

It implements a basic interactor base class using dry-transaction and dry-validation under the hood.

It provides to predefined steps:

  • validate_input! which will validate the interactor input according to its schema.
  • database_transaction! an around step that wraps the interactor in an ActiveRecord transaction.

Example:

class Requests::MarkAsRead < ApiBlocks::Interactor
  input do
    schema do
      required(:request).filled(type?: Request)
    end
  end

  around :database_transaction!
  step :validate_input!

  try :update_request!, catch: ActiveRecord::RecordInvalid
  try :create_history_item!, catch: ActiveRecord::RecordInvalid

  def update_request!(request:)
    request.update!(read_at: Time.now.utc)
    request
  end

  def create_history_item!(request)
    request.request_history_items.create!(kind: :read)
    request
  end
end

ApiBlocks::Doorkeeper::Passwords

Implement an API for passwords reset using doorkeeper and devise.

Include the ApiBlocks::Doorkeeper::Passwords::Controller module in your passwords api controller and define the user_model method to return the concerned devise user model.

# app/controllers/api/v1/passwords_controller.rb
class Api::V1::PasswordsController < Api::V1::ApplicationController
  include ApiBlocks::Doorkeeper::Passwords::Controller

  private

  def user_model
    User
  end
end

Then add the approriate routes to your configuration.

# config/routes.rb
Rails.application.routes.draw do
  scope module: :api do
    namespace :v1 do
      resources :passwords, only: %i[create] do
        get :callback, on: :collection
        put :update, on: :collection
      end
    end
  end
end

Include the ApiBlocks::Doorkeeper::ResetPassword module so devise will forward the doorkeeper application to the mailer.

# app/models/user.rb
class User < ApplicationRecord
  include ApiBlocks::Doorkeeper::ResetPassword
end

Include the reset password Doorkeeper::Application extensions.

# config/initializers/doorkeeper.rb

Doorkeeper.configure do
  # ...
end

class ::Doorkeeper::Application < ActiveRecord::Base
  include ApiBlocks::Doorkeeper::Passwords::Application
end

Override your devise mailer #reset_password_instructions method to add the application parameter.

# app/mailers/devise_mailer.rb

class DeviseMailer < Devise::Mailer
  def reset_password_instructions(record, token, application = nil, _opts = {})
    @token = token
    @application = application
  end
end

Update the devise mailer template to link to the callback API.

# app/views/devise/mailer/reset_password_instructions.html.erb
<p><%= link_to "Change my password", callback_v1_passwords_url(reset_password_token: @token) %></p>

Finally, generate the required migrations:

bundle exec rails g api_blocks:doorkeeper:passwords:migration

ApiBlocks::Doorkeeper::Invitations

Implement an API for devise_invitable using doorkeeper.

Include the ApiBlocks::Doorkeeper::Invitations::Controller module in your api controller and define the user_model method to return the concerned devise user model.

# app/controllers/api/v1/invitations_controller.rb
class Api::V1::InvitationsController < Api::V1::ApplicationController
  include ApiBlocks::Doorkeeper::Invitations::Controller

  private

  def user_model
    User
  end
end

Add the approriate routes to your configuration.

# config/routes.rb
Rails.application.routes.draw do
  scope module: :api do
    namespace :v1 do
      resources :invitations, only: %i[create show] do
        get :callback, on: :collection
        put :update, on: :collection
      end
    end
  end
end

Include the invitations Doorkeeper::Application extensions.

# config/initializers/doorkeeper.rb

Doorkeeper.configure do
  # ...
end

class ::Doorkeeper::Application < ActiveRecord::Base
  include ApiBlocks::Doorkeeper::Invitations::Application
end

Override your devise mailer #invitation_instructions method to add the application parameter.

# app/mailers/devise_mailer.rb

class DeviseMailer < Devise::Mailer
  def invitation_instructions(_record, token, application: nil, **_opts)
    @token = token
    @application = application

    super
  end
end

Update the devise mailer template to link to the callback API.

# app/views/devise/mailer/invitation_instructions.html.erb
<p><%= link_to  t("devise.mailer.invitation_instructions.accept"), callback_v1_invitations_url(invitation_token: @token, client_id: @application.uid) %></p>

Finally, generate the required migrations:

bundle exec rails g api_blocks:doorkeeper:invitations:migration

External Resources

License

Licensed under the MIT license, see the separate LICENSE.txt file.