Skip to main content

Command Palette

Search for a command to run...

Construindo uma Engine dentro da sua Gem

Published
•4 min read•View as Markdown
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

  • https://guides.rubyonrails.org/engines.html
  • https://github.com/rack/rack
  • https://medium.com/rubybr/ruby-o-que-%C3%A9-uma-gem-d62aafc30724