An open API service indexing awesome lists of open source software.

https://github.com/tymate/api-blocks

Simple and consistent rails api extensions
https://github.com/tymate/api-blocks

api dry-transaction dry-validation pundit rails-api responders ruby ruby-on-rails

Last synced: about 1 month ago
JSON representation

Simple and consistent rails api extensions

Awesome Lists containing this project

README

        

[gem]: https://rubygems.org/gems/api-blocks
[code_climate]: https://codeclimate.com/github/tymate/api-blocks
[inch]: https://inch-ci.org/github/tymate/api-blocks?branch=master

# ApiBlocks

[![Gem](https://img.shields.io/gem/v/api-blocks?style=flat-square)][gem]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tymate/api-blocks?style=flat-square)][code_climate]
[![Inch](https://inch-ci.org/github/tymate/api-blocks.svg?branch=master)][inch]

ApiBlocks provides simple and consistent Rails API extensions.

Links:

- [API Documentation](https://www.rubydoc.info/gems/api-blocks/0.1.1)
- [Source Code](https://github.com/tymate/api-blocks)

## Installation

```ruby
gem 'api-blocks'
```

## Configuration

In an initializer such as `config/initializers/api_blocks.rb` you can enable the
optional [blueprinter](https://github.com/procore/blueprinter) and
[batch-loader](https://github.com/exAspArk/batch-loader) integration:

```ruby
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](lib/api_blocks/blueprinter/association_extractor.rb).

## ApiBlocks::Controller

Include `ApiBlocks::Controller` in your api controller:

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

pundit_scope :api, :v1
end
```

Including the module will:

- Setup [ApiBlocks::Responder](#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:

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

```ruby
# 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.

```ruby
# 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.

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

Include the reset password `Doorkeeper::Application` extensions.

```ruby
# 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.

```ruby
# 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.

```erb
# app/views/devise/mailer/reset_password_instructions.html.erb

<%= link_to "Change my password", callback_v1_passwords_url(reset_password_token: @token) %>


```

Finally, generate the required migrations:

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

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

```erb
# app/views/devise/mailer/invitation_instructions.html.erb

<%= link_to t("devise.mailer.invitation_instructions.accept"), callback_v1_invitations_url(invitation_token: @token, client_id: @application.uid) %>


```

Finally, generate the required migrations:

```sh
bundle exec rails g api_blocks:doorkeeper:invitations:migration
```

# External Resources

- [Pundit](https://github.com/varvet/pundit)
- [Responders](https://github.com/plataformatec/responders)
- [Dry Transaction](https://dry-rb.org/gems/dry-transaction/0.13/)
- [Dry Validation](https://dry-rb.org/gems/dry-validation/1.3/)
- [Problem Details](https://github.com/nikushi/problem_details)

# License

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