Middleware y la Clase Rack::Builder

We mentioned earlier that between the server and the framework, Rack can be customized to your applications needs using middleware.

The fundamental idea behind Rack middleware is

  1. come between the calling client and the server,
  2. process the HTTP request before sending it to the server, and
  3. processing the HTTP response before returning it to the client.

Motivación para el método use

Si tenemos una app Rack rack_app y dos middlewares con nombres MiddleWare1 y MiddleWare2 que queremos usar, podemos escribir esto:

Rack::Handler::Thin.run Middleware1.new(Middleware2.new(rack_app))
Si necesitamos pasar opciones en el segundo argumento la llamada quedaría mas o menos como esto:
Rack::Handler::Thin.run(
  Middleware1.new(
    Middleware2.new(rack_app, options2),
    options1)
)
Si fueran mas de dos middlewares el correspondiente código se volverá aún mas ilegible y hace mas fácil que metamos la pata cuando queramos hacer algo como - por ejemplo -modificar el orden de los middleware.

La Clase Rack::Builder

La clase Rack::Builder implementa un pequeño DSL para facilitar la construcción de aplicaciones Rack.

Rack::Builder is the thing that glues various Rack middlewares and applications together and convert them into a single entity/rack application.

A good analogy is comparing Rack::Builder object with a stack, where at the very bottom is your actual rack application and all middlewares on top of it, and the whole stack itself is a rack application too.

  1. El método use añade un middleware a la pila
  2. El método run ejecuta una aplicación
  3. El método map construye un Rack::URLMap en la forma apropiada. It mounts a stack of rack application/middleware on the specified path or URI.

Figura 31.1: La pila Rack
[scale=0.6]sinatra/rack_stack.png

Conversión de una Aplicación Rack a Rack::Builder

Dada la aplicación:

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, env.[inspect]] }
Rack::Handler::Mongrel.run infinity, :Port => 9292

Podemos reescribirla:

[~/sinatra/rack/rack-builder/map]$ cat app_builder.rb 
require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [env.inspect]]}
builder = Rack::Builder.new
builder.run infinity
Rack::Handler::Thin.run builder, :Port => 9292
o bien:
[~/sinatra/rack/rack-builder/map]$ cat app_builder2.rb 
require 'rack'

infinity = Proc.new {|env| [200, {"Content-Type" => "text/html"}, [ env.inspect ]] }
builder = Rack::Builder.new do
  run infinity
end
Rack::Handler::Thin.run builder, :Port => 9292

Ejemplo Simple de Uso de Rack::Builder

[~/local/src/ruby/sinatra/rack/rack-builder/simple1]$ cat app.rb 
require 'rack'
require 'rack/server'

app = Rack::Builder.new do
  use Rack::CommonLogger
  use Rack::ShowExceptions
    use Rack::Lint
    map "/chuchu" do
      run lambda { |env| [ 200, {}, ["hello"]] }
    end
    map "/chachi" do
      run lambda { |env| [ 200, {}, ["world"]] }
    end
    run lambda { |env| [ 200, {}, ["everything"]] }
end

Rack::Server.start :app => app

Ejemplo de Uso de Rack::Builder: Dos Middlewares

[~/rack/rack-from-the-beginning(master)]$ cat hello_world.rb 
# hello_world.rb
require 'rack'
require 'rack/server'

class EnsureJsonResponse
  def initialize(app = nil)
    @app = app
  end

  # Set the 'Accept' header to 'application/json' no matter what.
  # Hopefully the next middleware respects the accept header :)
  def call(env)
    env['HTTP_ACCEPT'] = 'application/json'
    puts "env['HTTP_ACCEPT'] = #{env['HTTP_ACCEPT']}"
    @app.call(env) if @app
  end
end

class Timer
  def initialize(app = nil)
    @app = app
  end

  def call(env)
    before = Time.now
    status, headers, body = @app.call(env) if @app

    headers['X-Timing'] = (Time.now - before).to_i.to_s

    [status, headers, body]
  end
end

class HelloWorldApp

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

  def self.call(env)
    [200, {}, ['hello world!']] 
  end
end

# put the timer at the top so it captures everything below it
app = Rack::Builder.new do 
  use Timer # put the timer at the top so it captures everything below it
  use EnsureJsonResponse
  run HelloWorldApp
end

Rack::Server.start :app => app

~/rack/rack-from-the-beginning(master)]$  cat Rakefile 
desc "run the server"
task :default do
  sh "rackup"
end

desc "run the server hello_world.rb"
task :server do
  sh "ruby hello_world.rb"
end

desc "run the client"
task :client do
  sh %q{curl -v 'http://localhost:9292'}
end

desc "run the client for hello_world"
task :client2 do
  sh %q{curl -v 'http://localhost:8080'}
end

[~/rack/rack-from-the-beginning(master)]$ rake server
ruby hello_world.rb
>> Thin web server (v1.5.1 codename Straight Razor)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:8080, CTRL+C to stop

[~/rack/rack-from-the-beginning(master)]$ rake client2
curl -v 'http://localhost:8080'
* About to connect() to localhost port 8080 (#0)
*   Trying ::1... Connection refused
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8y zlib/1.2.5
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Timing: 0
< Connection: close
< Server: thin 1.5.1 codename Straight Razor
< 
* Closing connection #0
hello world!

[~/rack/rack-from-the-beginning(master)]$ rake server
ruby hello_world.rb
>> Thin web server (v1.5.1 codename Straight Razor)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:8080, CTRL+C to stop
env['HTTP_ACCEPT'] = application/json



Subsecciones
Casiano Rodriguez León 2015-01-07