Construindo uma Engine dentro da sua Gem

Atualmente estou trabalhando no time de Devtools na RD Station e nosso time é responsável por melhorar o fluxo de trabalho dos desenvolvedores.

Uma das tarefas recentes que nós entregamos foi de disponibilizar uma gem que dentre algumas funcionalidades entregava uma engine para facilitar a vida do pessoal. Bora ver como fazer isso?

Uma rails engine, para quem não sabe, nada mais é que uma "mini aplicação" rails, que permite que você adicione funcionalidades na aplicação principal (Rotas, serviços e outras coisas). Criar uma engine é bem simples:

rails plugin new yoda-say --mountable

Apenas executando isso uma nova gem será gerada com todos os arquivos necessários para você sair usando. Eis que surge o primeiro problema:

Isso funciona bem para uma gem nova, não para uma que já existe (que era o meu caso).

Bob Esponja sentado triste na mesa

Para seguir com o exemplo, vamos criar uma gem (sem ser uma engine):

rails plugin new yoda-say

ps: Nesse caso eu optei por criar especificamente um plugin rails para facilitar o exemplo

A diferença é que ela não tem o parâmetro mountable no final.

Agora vamos criar a classe que vai ser a nossa engine de fato e importar ela:

# lib/yoda/say/engine.rb

module Yoda
  module Say
    class Engine < ::Rails::Engine
    end
  end
end

# lib/yoda/say.rb
require "yoda/say/engine"

Isso é suficiente para nossa engine estar funcionando. O unico problema é que ela não faz nada. Homem esfregando as temporas

Agora vamos adicionar algumas coisas necessárias para nossa engine fazer alguma coisa:

# config/routes.rb
Yoda::Say::Engine.routes.draw do
  get '/', to: 'home#index'
end

# app/controllers/home_controller.rb
class HomeController < ActionController::Base
  def index
    render json: { yoda_say: '' }
  end
end

Pronto, agora sim. Se você quiser testar essa etapa, pode colocar na sua app rails dentro do arquivo de rotas:

Rails.application.routes.draw do
  mount Yoda::Say::Engine => '/yoda'
end

Mas como a gente adiciona testes? Mulher dando um sorriso e ficando chocada em seguida

Acontece que quando nós geramos a nossa gem (como plugin rails) foi criada uma aplicação boba para testarmos (ela está em test/dummy), então só precisamos mudar algumas coisas nela:

# test/dummy/config/routes.rb
Rails.application.routes.draw do
  mount Yoda::Say::Engine => '/yoda'
end

# test/controllers/home_controller_test.rb
require 'test_helper'

class HomeControllerTest < ActiveSupport::TestCase
  include Rack::Test::Methods

  def app
    Rails.application
  end

  test 'succeeds' do
    get '/yoda'
    assert last_response.ok?
    assert last_response.body == '{"yoda_say":""}'
  end
end

Se você executar rake test vai estar tudo funcionando. barney stinson fazendo sinal de joia dentro de um carro

O Plot Twist

Você não precisa de uma Engine.

Joe Cruz se virando com um olhar de estranhamento

Não, você não leu errado.

O que acontece é simples, apesar da Rails Engine ser uma coisa muito legal, ela carrega o "peso do Rails" junto dela (o que em alguns casos é o que nós queremos, então tudo bem).

Além disso ela só pode ser plugada em uma aplicação Rails, o que pode ser um problema caso você queira oferecer isso para alguém que usa Sinatra por exemplo.

A alternativa é muito simples, nós vamos usar uma Rack App! Bora ver como isso fica.

Primeiro vamos criar a nossa App:

# lib/yoda/say/app.rb
module Yoda
  module Say
    class App

      def self.call(env)
        request = Rack::Request.new(env)
        [status, headers, body]
      end

    end
  end
end

A aplicação é bem simples, você recebe um request, processa ele (nós vamos fazer isso no controller) e devolve um array com três posições: status (número), header(hash) e body (array).

Vamos ajustar nossa aplicação para ela funcionar corretamente:

# lib/yoda/say/app.rb

require_relative 'home_controller'

module Yoda
  module Say
    class App

      def self.call(env)
        request = Rack::Request.new(env)
        ::HomeController.index(request)
      end

    end
  end
end

# lib/yoda/say/home_controller.rb
class HomeController
  def self.index(_request)
    status = 200
    headers = {}
    body = [{ yoda_say: '' }.to_json]
    [status, headers, body]
  end
end

Pronto, nossa aplicação está funcional agora. Antes de rodar o teste, precisamos alterar a rota da nossa app dummy em test/dummy/config/routes.rb para mount Yoda::Say::App => '/yoda'.

Dito isso, podemos deletar o arquivo da engine e as definições de rotas que já não são mais necessárias.

Pra fechar

Nesse tutorial você viu duas maneiras de entregar recursos avançados dentro da sua aplicação principal através de uma gem. A primeira parte foi com uma Rails Engine e na sequência mudamos tudo para usar uma Rack App.

Você pode estar se perguntando, como eu escolho qual usar?

Tem muito a ver com a sua necessidade, usar o Rack é bem simples além de permitir integrar facilmente com vários frameworks, mas quando seu caso de uso é mais complexo você pode acabar tendo que desenvolver muitas coisas que já existem numa aplicação Rails. Por exemplo, se você precisar de duas ou três rotas, cache e alguns outros detalhes, vale muito mais a pena você já começar usando uma engine.

Você pode consultar o código fonte no meu Github Qualquer coisa me manda uma mensagem.

Dúvidas? Gostou? Me acha um idiota?

Comenta ai!!

Angeliski

Referências