Se você quer entender rapidamente o que é um Middleware, pense em um filtro. Algo que permite que todas as requisições e respostas passem por ele, permitindo assim, que ele altere-as como bem entender. Middlewares devem possuir o mesmo método call de qualquer outro objeto Rack, porém em seu construtor, eles devem receber outra aplicação como argumento. Esta é uma maneira simples de empilhar diversas aplicações. Se você vem de outras linguagens como Java ou C# e já leu sobre Design Patterns, deve estar reconhecendo o padrão Decorator. A idéia é semelhante, mas com Ruby as coisas se tornam muito mais simples.

Nosso primeiro middleware

Se pegarmos o exemplo simples do HelloWorld apresentado no início desta sequência de posts sobre Rack, podemos acomplar um middleware a ele, adicionando funcionalidades extras à aplicação original.

Primeiros, vamos criar nosso Middleware. O exemplo é muito simples. Após imprimirmos um “Hello World!”, vamos informar a hora.

class Middleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, response = @app.call(env)
    response_body = ""
    response.each { |part| response_body += part }
    response_body += " --- #{Time.now}"
    headers["Content-Length"] = response_body.length.to_s
    [status, headers, [response_body]]
  end
end

Não se assuste se você não entender tudo de cara. A parte mais complicada é cocatenar a informação de tempo na resposta, e isso é feito usando um each, devido ao fato de o response poder ser qualquer objeto que responda a esse método. Além disso, para que nossa resposta seja válida, é interessante colocar no cabeçalho HTTP o comprimento do nosso body.

Mas além do middleware, devemos dizer que nossa aplicação original quer usar ele. E isso deve ser feito em um arquivo .ru:

#config.ru
require 'rack'
require 'middleware'
require 'hello_world'      

use Middleware
run HelloWorld.new

Agora rodando “rackup config.ru” na linha de comando, teremos o seguinte no nosso servidor:


Hello World! — 2009-10-05 14:05:58 -0300

Outros Exemplos de Middlewares

Na primeira parte dessa série de artigos, citei o projeto rack-contrib, que consistia em uma coleção de Middlewares criados pela comunidade e disponibilizado em um único projeto. Além de serem de extrema utilidade no desenvolvimento, eles também servem como um ótimo objeto de estudo para quem quer aprender sobre o funcionamento de um Middleware. Se olharmos o código do exemplo garbagecollection.rb, veremos que o funcionamento é extremamente simples, consistindo apenas de uma chamada forçada do Garbage Collector a cada request na aplicação.

module Rack
  # Forces garbage collection after each request.
  class GarbageCollector
    def initialize(app)
      @app = app
    end

    def call(env)
      @app.call(env)
    ensure
      GC.start
    end
  end
end

Sem entrar no mérito da utilidade do exemplo, é interessante notar a simplicidade do código. Em outro exemplo, podemos ver integração fácil do código do Google Analytics em sua aplicação a com 2 linhas:

require "rack/google_analytics"
use Rack::GoogleAnalytics, :web_property_id => "UA-000000-1"

E o código, propriamente dito, é algo simples:

module Rack #:nodoc:
  class GoogleAnalytics < Struct.new :app, :options

    def call env
      status, headers, response = app.call(env)

      if headers["Content-Type"] =~ /text\/html|application\/xhtml\+xml/
        body = ""
        response.each { |part| body << part }
        index = body.rindex("")
        if index
          body.insert(index, tracking_code(options[:web_property_id]))
          headers["Content-Length"] = body.length.to_s
          response = [body]
        end
      end

      [status, headers, response]
    end

    private

      # Returns JS to be embeded. This takes one argument, a Web Property ID
      # (aka UA number).
      def tracking_code web_property_id
        return <<-EOF


EOF
      end

  end
end

Um outro exemplo interessante é o Middleware CommonLogger, incorporado ao código do projeto Rack. É uma implementação de logs para aplicações Rack, baseada no projeto Commons Logging da Apache . Primeiro, vejamos o código para depois analisar-mos seu funcionamento.


module Rack
  # Rack::CommonLogger forwards every request to an +app+ given, and
  # logs a line in the Apache common log format to the +logger+, or
  # rack.errors by default.
  class CommonLogger
    # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
    # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 -
    # %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
    FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}

    def initialize(app, logger=nil)
      @app = app
      @logger = logger
    end

    def call(env)
      began_at = Time.now
      status, header, body = @app.call(env)
      log(env, status, header, began_at)
      [status, header, body]
    end

    private

    def log(env, status, header, began_at)
      now = Time.now
      length = extract_content_length(header)

      logger = @logger || env['rack.errors']
      logger.write FORMAT % [
        env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
        env["REMOTE_USER"] || "-",
        now.strftime("%d/%b/%Y %H:%M:%S"),
        env["REQUEST_METHOD"],
        env["PATH_INFO"],
        env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
        env["HTTP_VERSION"],
        status.to_s[0..3],
        length,
        now - began_at ]
    end

    def extract_content_length(headers)
      headers.each do |key, value|
        if key.downcase == 'content-length'
          return value.to_s == '0' ? '-' : value
        end
      end
      '-'
    end
  end
end

Como podemos ver, este é outro exemplo simples de um Middleware muito útil. No método call, é criado um registro simples do momento em que a aplicação principal é chamada, e um método de log pega todas as informações recebidas e formata em uma string que será impressa no servidor. Apesar de parecer difícil de entender a primeira vista, o método log apenas coleta e formata informações contidas no objeto env para adicionar ao log.

Outros Middleware

Existem outros ótimos exemplos de Middlewares pela rede. Optei por citar os mais simples aqui por questões didáticas. Dentre os que eu poderia citar como fundamentais, estão o rack-cache do Ryan Tomayko, que é utilizado largamente em aplicações desenvolvidas com Rails. Posso citar também o “encolhedor” de javascript Javascript Minifier, o criador de responses JSON “Rack::JSONP” também é um bom exemplo, bem como “Rack::MailExceptions” e outros contidos no projeto rack-contrib.

Até o próximo post!