Subsecciones

Un Ejemplo Simple: Piedra, Papel, tijeras

[~/rack/rack-rock-paper-scissors(simple)]$ cat -n rps.rb
 1  require 'rack/request'
 2  require 'rack/response'
 3  
 4  module RockPaperScissors
 5    class App 
 6  
 7      def initialize(app = nil)
 8        @app = app
 9        @content_type = :html
10        @defeat = {'rock' => 'scissors', 'paper' => 'rock', 'scissors' => 'paper'}
11        @throws = @defeat.keys
12        @choose = @throws.map { |x| 
13           %Q{ <li><a href="/?choice=#{x}">#{x}</a></li> }
14        }.join("\n")
15        @choose = "<p>\n<ul>\n#{@choose}\n</ul>"
16      end
17  
18      def call(env)
19        req = Rack::Request.new(env)
20  
21        req.env.keys.sort.each { |x| puts "#{x} => #{req.env[x]}" }
22  
23        computer_throw = @throws.sample
24        player_throw = req.GET["choice"]
25        anwser = if !@throws.include?(player_throw)
26            "Choose one of the following:"
27          elsif player_throw == computer_throw
28            "You tied with the computer"
29          elsif computer_throw == @defeat[player_throw]
30            "Nicely done; #{player_throw} beats #{computer_throw}"
31          else
32            "Ouch; #{computer_throw} beats #{player_throw}. Better luck next time!"
33          end
34  
35        res = Rack::Response.new
36        res.write <<-"EOS"
37        <html>
38          <title>rps</title>
39          <body>
40            <h1>
41               #{anwser}
42               #{@choose}
43            </h1>
44          </body>
45        </html>
46        EOS
47        res.finish
48      end # call
49    end   # App
50  end     # RockPaperScissors
51  
52  if $0 == __FILE__
53    require 'rack'
54    require 'rack/showexceptions'
55    Rack::Server.start(
56      :app => Rack::ShowExceptions.new(
57                Rack::Lint.new(
58                  RockPaperScissors::App.new)), 
59      :Port => 9292,
60      :server => 'thin'
61    )
62  end

El Objeto req

El objeto req pertenece a la clase Rack::Request. Tiene un único atributo env:

(rdb:1)  req
#<Rack::Request:0x007f8d735b1410 
@env={
"SERVER_SOFTWARE"=>"thin 1.5.1 codename Straight Razor", 
"SERVER_NAME"=>"0.0.0.0", 
"rack.input"=>#<Rack::Lint::InputWrapper:0x007f8d735776c0 
                @input=#<StringIO:0x007f8d735426a0>>, "rack.version"=>[1, 0], 
                "rack.errors"=>#<Rack::Lint::ErrorWrapper:0x007f8d73577620 @error=#<IO:<STDERR>>
              >, 
"rack.multithread"=>false, 
"rack.multiprocess"=>false, 
"rack.run_once"=>false, 
"REQUEST_METHOD"=>"GET", 
"REQUEST_PATH"=>"/", 
"PATH_INFO"=>"/", 
"REQUEST_URI"=>"/", 
"HTTP_VERSION"=>"HTTP/1.1", 
"HTTP_HOST"=>"0.0.0.0:9292", 
"HTTP_CONNECTION"=>"keep-alive", 
"HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 
"HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.76 Safari/537.36", 
"HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", 
"HTTP_ACCEPT_LANGUAGE"=>"es-ES,es;q=0.8", 
"GATEWAY_INTERFACE"=>"CGI/1.2", 
"SERVER_PORT"=>"9292", 
"QUERY_STRING"=>"", 
"SERVER_PROTOCOL"=>"HTTP/1.1", 
"rack.url_scheme"=>"http", 
"SCRIPT_NAME"=>"", 
"REMOTE_ADDR"=>"127.0.0.1", 
"async.callback"=>#<Method: Thin::Connection#post_process>, 
"async.close"=>#<EventMachine::DefaultDeferrable:0x007f8d735603f8>}>
Cuando llamamos a GET para obtener el valor del parámetro choice:
player_throw = req.GET["choice"]
Si visitamos la página http://0.0.0.0:9292/ el entorno contiene algo como esto:
rdb:1) p @env
{"SERVER_SOFTWARE"=>"thin 1.5.1 codename Straight Razor",
 ...
 "QUERY_STRING"=>"",
 "REQUEST_URI"=>"/"
 ...
}
el código de GET nos da los datos almacenados en QUERY_STRING:
   def GET 
      if @env["rack.request.query_string"] == query_string
        @env["rack.request.query_hash"]
      else
        @env["rack.request.query_string"] = query_string
        @env["rack.request.query_hash"]   = parse_query(query_string)
      end 
    end 

   def query_string;    @env["QUERY_STRING"].to_s                end
si es la primera vez, @env["rack.request.query_string"] está a nil y se ejecuta el else inicializando @env["rack.request.query_string"] y @env["rack.request.query_hash"]

Si por ejemplo visitamos la URL: http://localhost:9292?choice=rock entonces env contendrá:

rdb:1) p env
{ ... 
  "QUERY_STRING"=>"choice=rock", 
  "REQUEST_URI"=>"/?choice=rock", 
  ...
}
Familiaricemonos con algunos de los métodos de Rack::Request:
(rdb:1) req.GET
{"choice"=>"paper"}
(rdb:1)  req.GET["choice"]
"paper"
(rdb:1) req.POST
{}
(rdb:1) req.params
{"choice"=>"paper"}
(rdb:1) req["choice"]
"paper"
(rdb:1) req[:choice]
"paper"
(rdb:1) req.cookies()
{}
(rdb:1) req.get?
true
(rdb:1) req.post?
false
(rdb:1) req.fullpath
"/?choice=paper"
(rdb:1) req.host
"0.0.0.0"
(rdb:1) req.host_with_port
"0.0.0.0:9292"
(rdb:1) req.body
#<Rack::Lint::InputWrapper:0x007f8d7369b5d8 @input=#<StringIO:0x007f8d73690318>>
(rdb:1) req.cookies()
{}
(rdb:1) req.get?
true
(rdb:1) req.post?
false
(rdb:1) req.fullpath
"/?choice=paper"
(rdb:1) req.host
"0.0.0.0"
(rdb:1) req.host_with_port
"0.0.0.0:9292"
(rdb:1) req.ip
"127.0.0.1"
(rdb:1) req.params
{"choice"=>"paper"}
(rdb:1) req.path
"/"
(rdb:1) req.path_info
"/"
(rdb:1) req.port
9292
(rdb:1) req.request_method
"GET"
(rdb:1) req.scheme
"http"
(rdb:1) req.url
"http://0.0.0.0:9292/?choice=paper"
(rdb:1) req.user_agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) 
Chrome/29.0.1547.76 Safari/537.36"
(rdb:1) req.values_at("choice")
["paper"]

Rakefile

[~/rack/rack-rock-paper-scissors(simple)]$ cat Rakefile 
desc "run the server"
task :default do
  sh "ruby rps.rb"
end

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

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

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

  1. curl

Ejecuciones

[~/rack/rack-rock-paper-scissors(simple)]$ rake
ruby rps.rb
>> Thin web server (v1.5.1 codename Straight Razor)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop

[~/rack/rack-rock-paper-scissors(simple)]$ rake rock
curl -v 'http://localhost:9292?choice=rock'
* About to connect() to localhost port 9292 (#0)
*   Trying ::1... Connection refused
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 9292 (#0)
> GET /?choice=rock HTTP/1.1
> User-Agent: curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8x zlib/1.2.5
> Host: localhost:9292
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Length: 332
< Connection: keep-alive
< Server: thin 1.5.1 codename Straight Razor
< 
      <html>
        <title>rps</title>
        <body>
          <h1>
             Nicely done; rock beats scissors
             <p>
<ul>
 <li><a href="/?choice=rock">rock</a></li> 
 <li><a href="/?choice=paper">paper</a></li> 
 <li><a href="/?choice=scissors">scissors</a></li> 
</ul>
          </h1>
        </body>
      </html>
* Connection #0 to host localhost left intact
* Closing connection #0

[~/rack/rack-rock-paper-scissors(simple)]$ rake
ruby rps.rb
>> Thin web server (v1.5.1 codename Straight Razor)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:9292, CTRL+C to stop
GATEWAY_INTERFACE => CGI/1.2
HTTP_ACCEPT => */*
HTTP_HOST => localhost:9292
HTTP_USER_AGENT => curl/7.21.4 (universal-apple-darwin11.0) libcurl/7.21.4 OpenSSL/0.9.8x zlib/1.2.5
HTTP_VERSION => HTTP/1.1
PATH_INFO => /
QUERY_STRING => choice=rock
REMOTE_ADDR => 127.0.0.1
REQUEST_METHOD => GET
REQUEST_PATH => /
REQUEST_URI => /?choice=rock
SCRIPT_NAME => 
SERVER_NAME => localhost
SERVER_PORT => 9292
SERVER_PROTOCOL => HTTP/1.1
SERVER_SOFTWARE => thin 1.5.1 codename Straight Razor
async.callback => #<Method: Thin::Connection#post_process>
async.close => #<EventMachine::DefaultDeferrable:0x007ff4e2bf8e78>
rack.errors => #<Rack::Lint::ErrorWrapper:0x007ff4e2c04b88>
rack.input => #<Rack::Lint::InputWrapper:0x007ff4e2c04c00>
rack.multiprocess => false
rack.multithread => false
rack.run_once => false
rack.url_scheme => http
rack.version => [1, 0]

Véase También

Véase la documentación de las siguientes clases:

  1. Rack::Request
  2. Rack::Response
  3. Rack::Server
  4. Rack::ShowExceptions
  5. Rack::Lint


Práctica: Rock, Paper, Scissors: Debugging

Implemente el ejemplo anterior Rock, Paper, Scissors y ejecútelo con un depurador. Lea la sección Depurando una Ejecución con Ruby 52.1.

Instale la gema debugger. Llame al método debugger en el punto en el que quiere detener la ejecución para inspeccionar el estado del programa. Arranque el servidor y en el navegador visite la página.


Práctica: Añadir Template Haml a Rock, Paper, Scissors

Use Haml para crear un template index.haml en un directorio views.

[~/local/src/ruby/sinatra/rack/rack-rock-paper-scissors(template)]$ tree
.
|--- README
|--- Rakefile
|--- rps.rb
`--- views
    `--- index.haml
El template puede ser usado así:
require 'rack/request'
require 'rack/response'
require 'haml'

module RockPaperScissors
  class App 
     ...

    def call(env)
      ...
      engine = Haml::Engine.new File.open("views/index.haml").read
      res = Rack::Response.new 
      res.write engine.render({}, 
        :answer => answer, 
        :choose => @choose,
        :throws => @throws)
      res.finish
    end # call
  end   # App
end     # RockPaperScissors

Véase:

  1. Haml::Engine

La sintáxis del método render es:

  (String) render(scope = Object.new, locals = {})
También se puede usar como to_html. Procesa el template y retorna el resultado como una cadena.

El parámetro scope es el contexto en el cual se evalúa el template.

Si es un objeto Binding haml lo usa como segundo argumento de Kernel#eval (Véase la sección Bindings (encarpetados) y eval en [*]) en otro caso, haml utiliza #instance_eval.

Nótese que Haml modifica el contexto de la evaluación (bien el objeto ámbito o el objeto self del ámbito del binding). Se extiende Haml::Helpers y se establecen diversas variables de instancia (todas ellas prefijadas con haml_).

Por ejemplo:

s = "foobar"
Haml::Engine.new("%p= upcase").render(s)
produce:
"<p>FOOBAR</p>"

Ahora s extiende Haml::Helpers :

s.respond_to?(:html_attrs) #=> true

Haml::Helpers contiene un conjunto de métodos/utilidades para facilitar distintas tareas. La idea de que estén disponibles en el contexto es para ayudarnos dentro del template. Por ejemplo el método

- (String) escape_once(text)
Escapa las entidades HTML en el texto.

locals es un hash de variables locales que se deja disponible dentro del template. Por ejemplo:

Haml::Engine.new("%p= foo").render(Object.new, :foo => "Hello, world!") 
producirá:
"<p>Hello, world!</p>"

Si se pasa un bloque a render el bloque será ejecutado en aquellos puntos en los que se llama a yield desde el template.

Debido a algunas peculiaridades de Ruby, si el ámbito es un Binding y se proporciona también un bloque, el contexto de la evaluación puede no ser el que el usuario espera.

Parametros:

  1. scope (Binding, Proc, Object) (por defecto: Object.new). El contexto en el que se evalúa el template

  2. locals ({Symbol => Object}) (por defecto: {}). Variables locales que se dejan disponibles en el template
  3. block (#to_proc) Un bloque que será llamado desde el template.

  4. Retorna una String con el template renderizado


Práctica: Añada Hojas de Estilo a Piedra Papel Tijeras

Añada hojas de estilo a la práctica anterior (sección 43.17.2).

  1. Mostramos una posible estructura de ficheros en la que se incluyen hojas de estilo usando bootstrap :
    [~/local/src/ruby/sinatra/rack/rack-rock-paper-scissors(bootstrap)]$ tree
    .
    |--- Gemfile
    |--- Gemfile.lock
    |--- README
    |--- Rakefile
    |--- TODO
    |--- config.ru
    |--- lib
    |   `--- rps.rb
    |--- public
    |   |--- css
    |   |   |--- bootstrap-responsive.css
    |   |   |--- bootstrap-responsive.min.css
    |   |   |--- bootstrap.css
    |   |   `-- bootstrap.min.css
    |   |--- img
    |   |   |--- glyphicons-halflings-white.png
    |   |   |--- glyphicons-halflings.png
    |   |   `-- programming-languages.jpg
    |   `-- js
    |       |--- bootstrap.js
    |       `--- bootstrap.min.js
    |--- rps.rb
    `-- views
        `--- index.haml
    
    6 directories, 18 files
    
  2. El middleware Rack::Static intercepta las peticiones por ficheros estáticos (javascript, imágenes, hojas de estilo, etc.) basandose en los prefijos de las urls pasadas en las opciones y los sirve utilizando un objeto Rack::File. Ejemplos:

        use Rack::Static, :urls => ["/public"]
    
    Servirá todas las peticiones que comiencen por /public desde la carpeta public localizada en el directorio actual (esto es public/*).

    En nuestro jerarquía pondremos en el programa rps.rb:

    builder = Rack::Builder.new do
      use Rack::Static, :urls => ['/public']
      use Rack::ShowExceptions
      use Rack::Lint
    
      run RockPaperScissors::App.new
    end
    
    Rack::Handler::Thin.run builder
    

    y dentro del template haml nos referiremos por ejemplo al fichero javascript como

          %script{:src => "/public/js/bootstrap.js"}
    
    Otro ejemplo:
        use Rack::Static, :urls => ["/css", "/images"], :root => "public"
    
    servirá las peticiones comenzando con /css o /images desde la carpeta public en el directorio actual (esto es public/css/* y public/images/*)

  3. Véase el código en GitHub de Rack::Static
  4. En el template views/index.haml deberá enlazar a las hojas de estilo:
    !!!
    %html{:lang => "en"}
      %head
        %meta{:charset => "utf-8"}/
        %title RPS 
        %link{:href => "/public/css/bootstrap.css", :rel => "stylesheet"}
         %link{:href => "/public/css/bootstrap.css", :rel => "stylesheet"}
    
    y las imágenes como:
    %img(src="/public/img/programming-languages.jpg" width="40%")
    
  5. Rack::File es un middleware que sirve los ficheros debajo del directorio dado, de acuerdo con el path info de la petición Rack. por ejemplo, cuando se usa Rack::File.new("/etc") podremos acceder al fichero passwd como localhost:9292/passwd.
  6. Vease el código en github de Rack::File
  7. Para saber mas de Bootstrap véase la sección [*]

Casiano Rodríguez León
2015-01-25