Created
June 9, 2011 06:26
-
-
Save zspencer/1016180 to your computer and use it in GitHub Desktop.
Code Review of sinatra/base.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# external dependencies | |
require 'rack' | |
require 'tilt' | |
# stdlib dependencies | |
require 'thread' | |
require 'time' | |
require 'uri' | |
# other files we need | |
require 'sinatra/showexceptions' | |
require 'sinatra/version' | |
# z: Interesting how they separate out gem dependencies vs. system level dependencies. | |
# z: not sure what it buys us though | |
module Sinatra | |
# The request object. See Rack::Request for more info: | |
# http://rack.rubyforge.org/doc/classes/Rack/Request.html | |
class Request < Rack::Request | |
# Returns an array of acceptable media types for the response | |
def accept | |
# z: This returns the value of the @env['sinatra.accept'] variable. | |
@env['sinatra.accept'] ||= begin | |
# z: If @env['sinatra.accept'] is not yet set, it executes a block (begin ... end) which | |
# z: sets the sinatra.accept element in the @env array | |
# z: ||= is a language construct that is used to default a variable. | |
# z: You can replace "var = value if var.nil?" into var ||= value | |
entries = @env['HTTP_ACCEPT'].to_s.split(',') | |
# z: The HTTP_ACCEPT header is a comma separated list that indicates what | |
# z: file format the browser prefers to receive from the server. More | |
# z: info: http://www.gethifi.com/blog/browser-rest-http-accept-headers | |
# z: because it's comma separated, we want to split it into an array to make | |
# z: it easier to work with. | |
entries.map { |e| | |
# z: First, we call the map method, which iterates over the elements in entries | |
# z: and invokes the block it is passed on each one. | |
accept_entry(e) | |
# z: accept_entry creates *another* array which looks like: | |
# * [type, [ quality, type.count, some_int_whose_existance_i_dont_grok]] | |
# z: It seems worth extracting to a class to reveal intent better. | |
}.sort_by(&:last).map(&:first) | |
# z: '&' is the unary operator. '&' converts whatever is passed into it into a procedure | |
# z: by calling to_proc on that object. Since Ruby 1.8.7 / 1.9 :properties to_proc | |
# z: executes the method named by the property on the element. | |
# z: in ruby 1.8.7 and 1.9 :property.to_proc yeilds the following block { |e| e.property } | |
# z: One way to read this could be to convert it to: | |
# z: entries.sort_by { |e| e.last }.map { |e| e.first } | |
# z: Also keep in mind order of operations. | |
# z: entries.sort_by { |e| e.last }.map { |e| e.first } is executed | |
# z: entries.sort_by({ |e| e.last }.map { |e| e.first }) | |
# z: not | |
# z: (entries.sort_by { |e| e.last }).map {|e| e.first } | |
end | |
end | |
def preferred_type(*types) | |
# z: The * symbol is called the "splat" operator. It is used here to | |
# z: gather all the arguments for the preferred_types call into an array | |
# z: i.e. preferred_types(:html,:xml:json) means types is [:html, :xml, :json] | |
return accept.first if types.empty? | |
# z: If you call it without a type, it'll just follow the mime type as told by the browser. | |
types.flatten! | |
# z: flatten! takes a multi dimensional array and makes it single dimensional. | |
# z: ! is a ruby convention to state that method modifies its instance variable. | |
# z: given a = [ 1, 2, [3, 4]] | |
# z: when a.flatten! | |
# z: then a becomes [1,2,3,4,5] | |
# z: given a = [ 1, 2, [3, 4]] | |
# z: when a.flatten # => [1, 2, 3, 4] | |
# z: then a stays [1,2, [3,4]] | |
accept.detect do |pattern| | |
# z: detect iterates over a collection and returns the first value which matches. | |
type = types.detect { |t| | |
File.fnmatch(pattern, t) | |
# z: File.fnmatch will basically take a file name/pth type string and match it against a | |
# z: pattern in this case, we are using the different file types we accept, and checking | |
# z: to see which one matches the types passed in. | |
# z: I'm assuming this is used to easily determine which type to serve the client. | |
} | |
return type if type | |
# z: this forces you to pop out of the accept's detect block and return the type, but only | |
# z: if it was matched in the types detect block. | |
# z: may be worth extracting a private method for that top level accept block. | |
end | |
end | |
alias accept? preferred_type | |
# z: alias allows you to call a method with a different name. Way cleaner than creating | |
# z: delegate methods. In this case, calling accept? is the same as calling preferred_type | |
alias secure? ssl? | |
# z: and in this one, it allows you to call ssl? with secure? | |
def forwarded? | |
@env.include? "HTTP_X_FORWARDED_HOST" | |
# z: Checks to see if the client stated that it was forwarded. | |
# z: Several 3rd party add ons disable the sending of this HTTP header. | |
# z: To be honest, I'd be concerned if my app ever had to use this because it would be | |
# z: potentially quite unreliable. But I suppose it's nice to have :) | |
end | |
private | |
# z: This makes all following methods inaccessible publicly. Cool. | |
def accept_entry(entry) | |
type, *options = entry.gsub(/\s/, '').split(';') | |
# z: remember the syntax for an accept string? Basically this splits one into it's | |
# z: component parts | |
quality = 0 # we sort smalles first | |
# z: this comment is actually something of a smell. The logic behind making it | |
# z: sort smallest to biggest is in a different method than this one. Feels like a new class | |
# z: is trying to emerge | |
options.delete_if { |e| | |
# z: cool, didn't know you ocould iterate over a collection and delete things that | |
# z: match a block. This basically deletes the quality from the options string | |
# z: so that it can be put into the accept_entry type, which again feels like an object | |
# z: that is trying to emerge. | |
quality = 1 - e[2..-1].to_f if e.start_with? 'q=' | |
#z: So this string of code is basically just intended to reverse the sort order so that | |
#z: it can sort smallest to largest. Wouldn't it be simpler to just sort biggest to smallest? | |
} | |
[type, [quality, type.count('*'), 1 - options.size]] | |
end | |
end | |
# The response object. See Rack::Response and Rack::ResponseHelpers for | |
# more info: | |
# http://rack.rubyforge.org/doc/classes/Rack/Response.html | |
# http://rack.rubyforge.org/doc/classes/Rack/Response/Helpers.html | |
class Response < Rack::Response | |
def body=(value) | |
value = value.body while value.respond_to? :body and value.body != value | |
@body = value.respond_to?(:to_str) ? [value.to_str] : value | |
end | |
def each | |
block_given? ? super : enum_for(:each) | |
end | |
def finish | |
if body.respond_to? :to_ary and not [204, 304].include?(status.to_i) | |
headers["Content-Length"] = body.inject(0) { |l, p| l + Rack::Utils.bytesize(p) }.to_s | |
end | |
super | |
end | |
end | |
class NotFound < NameError #:nodoc: | |
def code ; 404 ; end | |
end | |
# Methods available to routes, before/after filters, and views. | |
module Helpers | |
# Set or retrieve the response status code. | |
def status(value=nil) | |
response.status = value if value | |
response.status | |
end | |
# Set or retrieve the response body. When a block is given, | |
# evaluation is deferred until the body is read with #each. | |
def body(value=nil, &block) | |
if block_given? | |
def block.each; yield(call) end | |
response.body = block | |
elsif value | |
response.body = value | |
else | |
response.body | |
end | |
end | |
# Halt processing and redirect to the URI provided. | |
def redirect(uri, *args) | |
status 302 | |
# According to RFC 2616 section 14.30, "the field value consists of a | |
# single absolute URI" | |
response['Location'] = uri(uri, settings.absolute_redirects?, settings.prefixed_redirects?) | |
halt(*args) | |
end | |
# Generates the absolute URI for a given path in the app. | |
# Takes Rack routers and reverse proxies into account. | |
def uri(addr = nil, absolute = true, add_script_name = true) | |
return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/ | |
uri = [host = ""] | |
if absolute | |
host << "http#{'s' if request.secure?}://" | |
if request.forwarded? or request.port != (request.secure? ? 443 : 80) | |
host << request.host_with_port | |
else | |
host << request.host | |
end | |
end | |
uri << request.script_name.to_s if add_script_name | |
uri << (addr ? addr : request.path_info).to_s | |
File.join uri | |
end | |
alias url uri | |
alias to uri | |
# Halt processing and return the error status provided. | |
def error(code, body=nil) | |
code, body = 500, code.to_str if code.respond_to? :to_str | |
response.body = body unless body.nil? | |
halt code | |
end | |
# Halt processing and return a 404 Not Found. | |
def not_found(body=nil) | |
error 404, body | |
end | |
# Set multiple response headers with Hash. | |
def headers(hash=nil) | |
response.headers.merge! hash if hash | |
response.headers | |
end | |
# Access the underlying Rack session. | |
def session | |
request.session | |
end | |
# Access shared logger object. | |
def logger | |
request.logger | |
end | |
# Look up a media type by file extension in Rack's mime registry. | |
def mime_type(type) | |
Base.mime_type(type) | |
end | |
# Set the Content-Type of the response body given a media type or file | |
# extension. | |
def content_type(type = nil, params={}) | |
return response['Content-Type'] unless type | |
default = params.delete :default | |
mime_type = mime_type(type) || default | |
fail "Unknown media type: %p" % type if mime_type.nil? | |
mime_type = mime_type.dup | |
unless params.include? :charset or settings.add_charset.all? { |p| not p === mime_type } | |
params[:charset] = params.delete('charset') || settings.default_encoding | |
end | |
params.delete :charset if mime_type.include? 'charset' | |
unless params.empty? | |
mime_type << (mime_type.include?(';') ? ', ' : ';') | |
mime_type << params.map { |kv| kv.join('=') }.join(', ') | |
end | |
response['Content-Type'] = mime_type | |
end | |
# Set the Content-Disposition to "attachment" with the specified filename, | |
# instructing the user agents to prompt to save. | |
def attachment(filename=nil) | |
response['Content-Disposition'] = 'attachment' | |
if filename | |
params = '; filename="%s"' % File.basename(filename) | |
response['Content-Disposition'] << params | |
end | |
end | |
# Use the contents of the file at +path+ as the response body. | |
def send_file(path, opts={}) | |
if opts[:type] or not response['Content-Type'] | |
content_type opts[:type] || File.extname(path), :default => 'application/octet-stream' | |
end | |
if opts[:disposition] == 'attachment' || opts[:filename] | |
attachment opts[:filename] || path | |
elsif opts[:disposition] == 'inline' | |
response['Content-Disposition'] = 'inline' | |
end | |
last_modified opts[:last_modified] if opts[:last_modified] | |
file = Rack::File.new nil | |
file.path = path | |
result = file.serving env | |
result[1].each { |k,v| headers[k] ||= v } | |
halt result[0], result[2] | |
rescue Errno::ENOENT | |
not_found | |
end | |
# Specify response freshness policy for HTTP caches (Cache-Control header). | |
# Any number of non-value directives (:public, :private, :no_cache, | |
# :no_store, :must_revalidate, :proxy_revalidate) may be passed along with | |
# a Hash of value directives (:max_age, :min_stale, :s_max_age). | |
# | |
# cache_control :public, :must_revalidate, :max_age => 60 | |
# => Cache-Control: public, must-revalidate, max-age=60 | |
# | |
# See RFC 2616 / 14.9 for more on standard cache control directives: | |
# http://tools.ietf.org/html/rfc2616#section-14.9.1 | |
def cache_control(*values) | |
if values.last.kind_of?(Hash) | |
hash = values.pop | |
hash.reject! { |k,v| v == false } | |
hash.reject! { |k,v| values << k if v == true } | |
else | |
hash = {} | |
end | |
values = values.map { |value| value.to_s.tr('_','-') } | |
hash.each do |key, value| | |
key = key.to_s.tr('_', '-') | |
value = value.to_i if key == "max-age" | |
values << [key, value].join('=') | |
end | |
response['Cache-Control'] = values.join(', ') if values.any? | |
end | |
# Set the Expires header and Cache-Control/max-age directive. Amount | |
# can be an integer number of seconds in the future or a Time object | |
# indicating when the response should be considered "stale". The remaining | |
# "values" arguments are passed to the #cache_control helper: | |
# | |
# expires 500, :public, :must_revalidate | |
# => Cache-Control: public, must-revalidate, max-age=60 | |
# => Expires: Mon, 08 Jun 2009 08:50:17 GMT | |
# | |
def expires(amount, *values) | |
values << {} unless values.last.kind_of?(Hash) | |
if amount.is_a? Integer | |
time = Time.now + amount.to_i | |
max_age = amount | |
else | |
time = time_for amount | |
max_age = time - Time.now | |
end | |
values.last.merge!(:max_age => max_age) | |
cache_control(*values) | |
response['Expires'] = time.httpdate | |
end | |
# Set the last modified time of the resource (HTTP 'Last-Modified' header) | |
# and halt if conditional GET matches. The +time+ argument is a Time, | |
# DateTime, or other object that responds to +to_time+. | |
# | |
# When the current request includes an 'If-Modified-Since' header that is | |
# equal or later than the time specified, execution is immediately halted | |
# with a '304 Not Modified' response. | |
def last_modified(time) | |
return unless time | |
time = time_for time | |
response['Last-Modified'] = time.httpdate | |
# compare based on seconds since epoch | |
halt 304 if Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i >= time.to_i | |
rescue ArgumentError | |
end | |
# Set the response entity tag (HTTP 'ETag' header) and halt if conditional | |
# GET matches. The +value+ argument is an identifier that uniquely | |
# identifies the current version of the resource. The +kind+ argument | |
# indicates whether the etag should be used as a :strong (default) or :weak | |
# cache validator. | |
# | |
# When the current request includes an 'If-None-Match' header with a | |
# matching etag, execution is immediately halted. If the request method is | |
# GET or HEAD, a '304 Not Modified' response is sent. | |
def etag(value, kind = :strong) | |
raise ArgumentError, ":strong or :weak expected" unless [:strong,:weak].include?(kind) | |
value = '"%s"' % value | |
value = 'W/' + value if kind == :weak | |
response['ETag'] = value | |
# Conditional GET check | |
if etags = env['HTTP_IF_NONE_MATCH'] | |
etags = etags.split(/\s*,\s*/) | |
halt 304 if etags.include?(value) || etags.include?('*') | |
end | |
end | |
# Sugar for redirect (example: redirect back) | |
def back | |
request.referer | |
end | |
private | |
# Ruby 1.8 has no #to_time method. | |
# This can be removed and calls to it replaced with to_time, | |
# if 1.8 support is dropped. | |
def time_for(value) | |
if value.respond_to? :to_time | |
value.to_time | |
elsif value.is_a? Time | |
value | |
elsif value.respond_to? :new_offset | |
# DateTime#to_time does the same on 1.9 | |
d = value.new_offset 0 | |
t = Time.utc d.year, d.mon, d.mday, d.hour, d.min, d.sec + d.sec_fraction | |
t.getlocal | |
elsif value.respond_to? :mday | |
# Date#to_time does the same on 1.9 | |
Time.local(value.year, value.mon, value.mday) | |
elsif value.is_a? Numeric | |
Time.at value | |
else | |
Time.parse value.to_s | |
end | |
rescue ArgumentError => boom | |
raise boom | |
rescue Exception | |
raise ArgumentError, "unable to convert #{value.inspect} to a Time object" | |
end | |
end | |
# Template rendering methods. Each method takes the name of a template | |
# to render as a Symbol and returns a String with the rendered output, | |
# as well as an optional hash with additional options. | |
# | |
# `template` is either the name or path of the template as symbol | |
# (Use `:'subdir/myview'` for views in subdirectories), or a string | |
# that will be rendered. | |
# | |
# Possible options are: | |
# :content_type The content type to use, same arguments as content_type. | |
# :layout If set to false, no layout is rendered, otherwise | |
# the specified layout is used (Ignored for `sass` and `less`) | |
# :layout_engine Engine to use for rendering the layout. | |
# :locals A hash with local variables that should be available | |
# in the template | |
# :scope If set, template is evaluate with the binding of the given | |
# object rather than the application instance. | |
# :views Views directory to use. | |
module Templates | |
module ContentTyped | |
attr_accessor :content_type | |
end | |
def initialize | |
super | |
@default_layout = :layout | |
end | |
def erb(template, options={}, locals={}) | |
render :erb, template, options, locals | |
end | |
def erubis(template, options={}, locals={}) | |
warn "Sinatra::Templates#erubis is deprecated and will be removed, use #erb instead.\n" \ | |
"If you have Erubis installed, it will be used automatically.\n\tfrom #{caller.first}" | |
render :erubis, template, options, locals | |
end | |
def haml(template, options={}, locals={}) | |
render :haml, template, options, locals | |
end | |
def sass(template, options={}, locals={}) | |
options.merge! :layout => false, :default_content_type => :css | |
render :sass, template, options, locals | |
end | |
def scss(template, options={}, locals={}) | |
options.merge! :layout => false, :default_content_type => :css | |
render :scss, template, options, locals | |
end | |
def less(template, options={}, locals={}) | |
options.merge! :layout => false, :default_content_type => :css | |
render :less, template, options, locals | |
end | |
def builder(template=nil, options={}, locals={}, &block) | |
options[:default_content_type] = :xml | |
render_ruby(:builder, template, options, locals, &block) | |
end | |
def liquid(template, options={}, locals={}) | |
render :liquid, template, options, locals | |
end | |
def markdown(template, options={}, locals={}) | |
render :markdown, template, options, locals | |
end | |
def textile(template, options={}, locals={}) | |
render :textile, template, options, locals | |
end | |
def rdoc(template, options={}, locals={}) | |
render :rdoc, template, options, locals | |
end | |
def radius(template, options={}, locals={}) | |
render :radius, template, options, locals | |
end | |
def markaby(template=nil, options={}, locals={}, &block) | |
render_ruby(:mab, template, options, locals, &block) | |
end | |
def coffee(template, options={}, locals={}) | |
options.merge! :layout => false, :default_content_type => :js | |
render :coffee, template, options, locals | |
end | |
def nokogiri(template=nil, options={}, locals={}, &block) | |
options[:default_content_type] = :xml | |
render_ruby(:nokogiri, template, options, locals, &block) | |
end | |
def slim(template, options={}, locals={}) | |
render :slim, template, options, locals | |
end | |
def creole(template, options={}, locals={}) | |
render :creole, template, options, locals | |
end | |
# Calls the given block for every possible template file in views, | |
# named name.ext, where ext is registered on engine. | |
def find_template(views, name, engine) | |
yield ::File.join(views, "#{name}.#{@preferred_extension}") | |
Tilt.mappings.each do |ext, engines| | |
next unless ext != @preferred_extension and engines.include? engine | |
yield ::File.join(views, "#{name}.#{ext}") | |
end | |
end | |
private | |
# logic shared between builder and nokogiri | |
def render_ruby(engine, template, options={}, locals={}, &block) | |
options, template = template, nil if template.is_a?(Hash) | |
template = Proc.new { block } if template.nil? | |
render engine, template, options, locals | |
end | |
def render(engine, data, options={}, locals={}, &block) | |
# merge app-level options | |
options = settings.send(engine).merge(options) if settings.respond_to?(engine) | |
options[:outvar] ||= '@_out_buf' | |
options[:default_encoding] ||= settings.default_encoding | |
# extract generic options | |
locals = options.delete(:locals) || locals || {} | |
views = options.delete(:views) || settings.views || "./views" | |
layout = options.delete(:layout) | |
eat_errors = layout.nil? | |
layout = @default_layout if layout.nil? or layout == true | |
content_type = options.delete(:content_type) || options.delete(:default_content_type) | |
layout_engine = options.delete(:layout_engine) || engine | |
scope = options.delete(:scope) || self | |
# compile and render template | |
layout_was = @default_layout | |
@default_layout = false | |
template = compile_template(engine, data, options, views) | |
output = template.render(scope, locals, &block) | |
@default_layout = layout_was | |
# render layout | |
if layout | |
options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope) | |
catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } } | |
end | |
output.extend(ContentTyped).content_type = content_type if content_type | |
output | |
end | |
def compile_template(engine, data, options, views) | |
eat_errors = options.delete :eat_errors | |
template_cache.fetch engine, data, options do | |
template = Tilt[engine] | |
raise "Template engine not found: #{engine}" if template.nil? | |
case data | |
when Symbol | |
body, path, line = settings.templates[data] | |
if body | |
body = body.call if body.respond_to?(:call) | |
template.new(path, line.to_i, options) { body } | |
else | |
found = false | |
@preferred_extension = engine.to_s | |
find_template(views, data, template) do |file| | |
path ||= file # keep the initial path rather than the last one | |
if found = File.exists?(file) | |
path = file | |
break | |
end | |
end | |
throw :layout_missing if eat_errors and not found | |
template.new(path, 1, options) | |
end | |
when Proc, String | |
body = data.is_a?(String) ? Proc.new { data } : data | |
path, line = settings.caller_locations.first | |
template.new(path, line.to_i, options, &body) | |
else | |
raise ArgumentError | |
end | |
end | |
end | |
end | |
# Base class for all Sinatra applications and middleware. | |
class Base | |
include Rack::Utils | |
include Helpers | |
include Templates | |
attr_accessor :app | |
attr_reader :template_cache | |
def initialize(app=nil) | |
super() | |
@app = app | |
@template_cache = Tilt::Cache.new | |
yield self if block_given? | |
end | |
# Rack call interface. | |
def call(env) | |
dup.call!(env) | |
end | |
attr_accessor :env, :request, :response, :params | |
def call!(env) # :nodoc: | |
@env = env | |
@request = Request.new(env) | |
@response = Response.new | |
@params = indifferent_params(@request.params) | |
template_cache.clear if settings.reload_templates | |
force_encoding(@params) | |
@response['Content-Type'] = nil | |
invoke { dispatch! } | |
invoke { error_block!(response.status) } | |
unless @response['Content-Type'] | |
if body.respond_to? :to_ary and body[0].respond_to? :content_type | |
content_type body[0].content_type | |
else | |
content_type :html | |
end | |
end | |
@response.finish | |
end | |
# Access settings defined with Base.set. | |
def self.settings | |
self | |
end | |
# Access settings defined with Base.set. | |
def settings | |
self.class.settings | |
end | |
def options | |
warn "Sinatra::Base#options is deprecated and will be removed, " \ | |
"use #settings instead.\n\tfrom #{caller.first}" | |
settings | |
end | |
# Exit the current block, halts any further processing | |
# of the request, and returns the specified response. | |
def halt(*response) | |
response = response.first if response.length == 1 | |
throw :halt, response | |
end | |
# Pass control to the next matching route. | |
# If there are no more matching routes, Sinatra will | |
# return a 404 response. | |
def pass(&block) | |
throw :pass, block | |
end | |
# Forward the request to the downstream app -- middleware only. | |
def forward | |
fail "downstream app not set" unless @app.respond_to? :call | |
status, headers, body = @app.call env | |
@response.status = status | |
@response.body = body | |
@response.headers.merge! headers | |
nil | |
end | |
private | |
# Run filters defined on the class and all superclasses. | |
def filter!(type, base = settings) | |
filter! type, base.superclass if base.superclass.respond_to?(:filters) | |
base.filters[type].each { |args| process_route(*args) } | |
end | |
# Run routes defined on the class and all superclasses. | |
def route!(base = settings, pass_block=nil) | |
if routes = base.routes[@request.request_method] | |
routes.each do |pattern, keys, conditions, block| | |
pass_block = process_route(pattern, keys, conditions) do |*args| | |
route_eval { block[*args] } | |
end | |
end | |
end | |
# Run routes defined in superclass. | |
if base.superclass.respond_to?(:routes) | |
return route!(base.superclass, pass_block) | |
end | |
route_eval(&pass_block) if pass_block | |
route_missing | |
end | |
# Run a route block and throw :halt with the result. | |
def route_eval | |
throw :halt, yield | |
end | |
# If the current request matches pattern and conditions, fill params | |
# with keys and call the given block. | |
# Revert params afterwards. | |
# | |
# Returns pass block. | |
def process_route(pattern, keys, conditions, block = nil) | |
@original_params ||= @params | |
route = @request.path_info | |
route = '/' if route.empty? and not settings.empty_path_info? | |
if match = pattern.match(route) | |
values = match.captures.to_a.map { |v| force_encoding URI.decode(v) if v } | |
params = | |
if keys.any? | |
keys.zip(values).inject({}) do |hash,(k,v)| | |
if k == 'splat' | |
(hash[k] ||= []) << v | |
else | |
hash[k] = v | |
end | |
hash | |
end | |
elsif values.any? | |
{'captures' => values} | |
else | |
{} | |
end | |
@params = @original_params.merge(params) | |
@block_params = values | |
catch(:pass) do | |
conditions.each { |c| throw :pass if c.bind(self).call == false } | |
block ? block[self, @block_params] : yield(self, @block_params) | |
end | |
end | |
ensure | |
@params = @original_params | |
end | |
# No matching route was found or all routes passed. The default | |
# implementation is to forward the request downstream when running | |
# as middleware (@app is non-nil); when no downstream app is set, raise | |
# a NotFound exception. Subclasses can override this method to perform | |
# custom route miss logic. | |
def route_missing | |
if @app | |
forward | |
else | |
raise NotFound | |
end | |
end | |
# Attempt to serve static files from public directory. Throws :halt when | |
# a matching file is found, returns nil otherwise. | |
def static! | |
return if (public_dir = settings.public).nil? | |
public_dir = File.expand_path(public_dir) | |
path = File.expand_path(public_dir + unescape(request.path_info)) | |
return unless path.start_with?(public_dir) and File.file?(path) | |
env['sinatra.static_file'] = path | |
send_file path, :disposition => nil | |
end | |
# Enable string or symbol key access to the nested params hash. | |
def indifferent_params(params) | |
params = indifferent_hash.merge(params) | |
params.each do |key, value| | |
next unless value.is_a?(Hash) | |
params[key] = indifferent_params(value) | |
end | |
end | |
# Creates a Hash with indifferent access. | |
def indifferent_hash | |
Hash.new {|hash,key| hash[key.to_s] if Symbol === key } | |
end | |
# Run the block with 'throw :halt' support and apply result to the response. | |
def invoke | |
res = catch(:halt) { yield } | |
res = [res] if Fixnum === res or String === res | |
if Array === res and Fixnum === res.first | |
status(res.shift) | |
body(res.pop) | |
headers(*res) | |
elsif res.respond_to? :each | |
body res | |
end | |
end | |
# Dispatch a request with error handling. | |
def dispatch! | |
static! if settings.static? && (request.get? || request.head?) | |
filter! :before | |
route! | |
rescue ::Exception => boom | |
handle_exception!(boom) | |
ensure | |
filter! :after unless env['sinatra.static_file'] | |
end | |
# Error handling during requests. | |
def handle_exception!(boom) | |
@env['sinatra.error'] = boom | |
dump_errors!(boom) if settings.dump_errors? | |
raise boom if settings.show_exceptions? and settings.show_exceptions != :after_handler | |
@response.status = boom.respond_to?(:code) ? Integer(boom.code) : 500 | |
if @response.status == 404 | |
@response.headers['X-Cascade'] = 'pass' | |
@response.body = ['<h1>Not Found</h1>'] | |
end | |
if res = error_block!(boom.class) | |
res | |
elsif @response.status >= 500 | |
raise boom if settings.raise_errors? or settings.show_exceptions? | |
error_block! Exception | |
end | |
end | |
# Find an custom error block for the key(s) specified. | |
def error_block!(key) | |
base = settings | |
while base.respond_to?(:errors) | |
next base = base.superclass unless args = base.errors[key] | |
return process_route(*args) | |
end | |
return false unless key.respond_to? :superclass and key.superclass < Exception | |
error_block! key.superclass | |
end | |
def dump_errors!(boom) | |
msg = ["#{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t") | |
@env['rack.errors'].puts(msg) | |
end | |
class << self | |
attr_reader :routes, :filters, :templates, :errors | |
# Removes all routes, filters, middleware and extension hooks from the | |
# current class (not routes/filters/... defined by its superclass). | |
def reset! | |
@conditions = [] | |
@routes = {} | |
@filters = {:before => [], :after => []} | |
@errors = {} | |
@middleware = [] | |
@prototype = nil | |
@extensions = [] | |
if superclass.respond_to?(:templates) | |
@templates = Hash.new { |hash,key| superclass.templates[key] } | |
else | |
@templates = {} | |
end | |
end | |
# Extension modules registered on this class and all superclasses. | |
def extensions | |
if superclass.respond_to?(:extensions) | |
(@extensions + superclass.extensions).uniq | |
else | |
@extensions | |
end | |
end | |
# Middleware used in this class and all superclasses. | |
def middleware | |
if superclass.respond_to?(:middleware) | |
superclass.middleware + @middleware | |
else | |
@middleware | |
end | |
end | |
# Sets an option to the given value. If the value is a proc, | |
# the proc will be called every time the option is accessed. | |
def set(option, value = (not_set = true), ignore_setter = false, &block) | |
raise ArgumentError if block and !not_set | |
value, not_set = block, false if block | |
if not_set | |
raise ArgumentError unless option.respond_to?(:each) | |
option.each { |k,v| set(k, v) } | |
return self | |
end | |
if respond_to?("#{option}=") and not ignore_setter | |
return __send__("#{option}=", value) | |
end | |
setter = proc { |val| set option, val, true } | |
getter = proc { value } | |
case value | |
when Proc | |
getter = value | |
when Symbol, Fixnum, FalseClass, TrueClass, NilClass | |
# we have a lot of enable and disable calls, let's optimize those | |
class_eval "def self.#{option}() #{value.inspect} end" | |
getter = nil | |
when Hash | |
setter = proc do |val| | |
val = value.merge val if Hash === val | |
set option, val, true | |
end | |
end | |
(class << self; self; end).class_eval do | |
define_method("#{option}=", &setter) if setter | |
define_method(option, &getter) if getter | |
unless method_defined? "#{option}?" | |
class_eval "def #{option}?() !!#{option} end" | |
end | |
end | |
self | |
end | |
# Same as calling `set :option, true` for each of the given options. | |
def enable(*opts) | |
opts.each { |key| set(key, true) } | |
end | |
# Same as calling `set :option, false` for each of the given options. | |
def disable(*opts) | |
opts.each { |key| set(key, false) } | |
end | |
# Define a custom error handler. Optionally takes either an Exception | |
# class, or an HTTP status code to specify which errors should be | |
# handled. | |
def error(*codes, &block) | |
args = compile! "ERROR", //, block | |
codes = codes.map { |c| Array(c) }.flatten | |
codes << Exception if codes.empty? | |
codes.each { |c| @errors[c] = args } | |
end | |
# Sugar for `error(404) { ... }` | |
def not_found(&block) | |
error 404, &block | |
end | |
# Define a named template. The block must return the template source. | |
def template(name, &block) | |
filename, line = caller_locations.first | |
templates[name] = [block, filename, line.to_i] | |
end | |
# Define the layout template. The block must return the template source. | |
def layout(name=:layout, &block) | |
template name, &block | |
end | |
# Load embeded templates from the file; uses the caller's __FILE__ | |
# when no file is specified. | |
def inline_templates=(file=nil) | |
file = (file.nil? || file == true) ? (caller_files.first || File.expand_path($0)) : file | |
begin | |
io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file) | |
app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2) | |
rescue Errno::ENOENT | |
app, data = nil | |
end | |
if data | |
if app and app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m | |
encoding = $2 | |
else | |
encoding = settings.default_encoding | |
end | |
lines = app.count("\n") + 1 | |
template = nil | |
force_encoding data, encoding | |
data.each_line do |line| | |
lines += 1 | |
if line =~ /^@@\s*(.*\S)\s*$/ | |
template = force_encoding('', encoding) | |
templates[$1.to_sym] = [template, file, lines] | |
elsif template | |
template << line | |
end | |
end | |
end | |
end | |
# Lookup or register a mime type in Rack's mime registry. | |
def mime_type(type, value=nil) | |
return type if type.nil? || type.to_s.include?('/') | |
type = ".#{type}" unless type.to_s[0] == ?. | |
return Rack::Mime.mime_type(type, nil) unless value | |
Rack::Mime::MIME_TYPES[type] = value | |
end | |
# provides all mime types matching type, including deprecated types: | |
# mime_types :html # => ['text/html'] | |
# mime_types :js # => ['application/javascript', 'text/javascript'] | |
def mime_types(type) | |
type = mime_type type | |
type =~ /^application\/(xml|javascript)$/ ? [type, "text/#$1"] : [type] | |
end | |
# Define a before filter; runs before all requests within the same | |
# context as route handlers and may access/modify the request and | |
# response. | |
def before(path = nil, options = {}, &block) | |
add_filter(:before, path, options, &block) | |
end | |
# Define an after filter; runs after all requests within the same | |
# context as route handlers and may access/modify the request and | |
# response. | |
def after(path = nil, options = {}, &block) | |
add_filter(:after, path, options, &block) | |
end | |
# add a filter | |
def add_filter(type, path = nil, options = {}, &block) | |
path, options = //, path if path.respond_to?(:each_pair) | |
filters[type] << compile!(type, path || //, block, options) | |
end | |
# Add a route condition. The route is considered non-matching when the | |
# block returns false. | |
def condition(name = "#{caller.first[/`.*'/]} condition", &block) | |
@conditions << generate_method(name, &block) | |
end | |
private | |
# Condition for matching host name. Parameter might be String or Regexp. | |
def host_name(pattern) | |
condition { pattern === request.host } | |
end | |
# Condition for matching user agent. Parameter should be Regexp. | |
# Will set params[:agent]. | |
def user_agent(pattern) | |
condition do | |
if request.user_agent.to_s =~ pattern | |
@params[:agent] = $~[1..-1] | |
true | |
else | |
false | |
end | |
end | |
end | |
alias_method :agent, :user_agent | |
# Condition for matching mimetypes. Accepts file extensions. | |
def provides(*types) | |
types.map! { |t| mime_types(t) } | |
types.flatten! | |
condition do | |
if type = request.preferred_type(types) | |
content_type(type) | |
true | |
else | |
false | |
end | |
end | |
end | |
public | |
# Defining a `GET` handler also automatically defines | |
# a `HEAD` handler. | |
def get(path, opts={}, &block) | |
conditions = @conditions.dup | |
route('GET', path, opts, &block) | |
@conditions = conditions | |
route('HEAD', path, opts, &block) | |
end | |
def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end | |
def post(path, opts={}, &bk) route 'POST', path, opts, &bk end | |
def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end | |
def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end | |
def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end | |
def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end | |
private | |
def route(verb, path, options={}, &block) | |
# Because of self.options.host | |
host_name(options.delete(:host)) if options.key?(:host) | |
enable :empty_path_info if path == "" and empty_path_info.nil? | |
(@routes[verb] ||= []) << compile!(verb, path, block, options) | |
invoke_hook(:route_added, verb, path, block) | |
end | |
def invoke_hook(name, *args) | |
extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } | |
end | |
def generate_method(method_name, &block) | |
define_method(method_name, &block) | |
method = instance_method method_name | |
remove_method method_name | |
method | |
end | |
def compile!(verb, path, block, options = {}) | |
options.each_pair { |option, args| send(option, *args) } | |
method_name = "#{verb} #{path}" | |
unbound_method = generate_method(method_name, &block) | |
pattern, keys = compile path | |
conditions, @conditions = @conditions, [] | |
[ pattern, keys, conditions, block.arity != 0 ? | |
proc { |a,p| unbound_method.bind(a).call(*p) } : | |
proc { |a,p| unbound_method.bind(a).call } ] | |
end | |
def compile(path) | |
keys = [] | |
if path.respond_to? :to_str | |
special_chars = %w{. + ( ) $} | |
pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c) } | |
pattern.gsub! /((:\w+)|\*)/ do |match| | |
if match == "*" | |
keys << 'splat' | |
"(.*?)" | |
else | |
keys << $2[1..-1] | |
"([^/?#]+)" | |
end | |
end | |
[/^#{pattern}$/, keys] | |
elsif path.respond_to?(:keys) && path.respond_to?(:match) | |
[path, path.keys] | |
elsif path.respond_to?(:names) && path.respond_to?(:match) | |
[path, path.names] | |
elsif path.respond_to? :match | |
[path, keys] | |
else | |
raise TypeError, path | |
end | |
end | |
def encoded(char) | |
enc = URI.encode(char) | |
enc = "(?:#{Regexp.escape enc}|#{URI.encode char, /./})" if enc == char | |
enc = "(?:#{enc}|#{encoded('+')})" if char == " " | |
enc | |
end | |
public | |
# Makes the methods defined in the block and in the Modules given | |
# in `extensions` available to the handlers and templates | |
def helpers(*extensions, &block) | |
class_eval(&block) if block_given? | |
include(*extensions) if extensions.any? | |
end | |
# Register an extension. Alternatively take a block from which an | |
# extension will be created and registered on the fly. | |
def register(*extensions, &block) | |
extensions << Module.new(&block) if block_given? | |
@extensions += extensions | |
extensions.each do |extension| | |
extend extension | |
extension.registered(self) if extension.respond_to?(:registered) | |
end | |
end | |
def development?; environment == :development end | |
def production?; environment == :production end | |
def test?; environment == :test end | |
# Set configuration options for Sinatra and/or the app. | |
# Allows scoping of settings for certain environments. | |
def configure(*envs, &block) | |
yield self if envs.empty? || envs.include?(environment.to_sym) | |
end | |
# Use the specified Rack middleware | |
def use(middleware, *args, &block) | |
@prototype = nil | |
@middleware << [middleware, args, block] | |
end | |
def quit!(server, handler_name) | |
# Use Thin's hard #stop! if available, otherwise just #stop. | |
server.respond_to?(:stop!) ? server.stop! : server.stop | |
STDERR.puts "\n== Sinatra has ended his set (crowd applauds)" unless handler_name =~/cgi/i | |
end | |
# Run the Sinatra app as a self-hosted server using | |
# Thin, Mongrel or WEBrick (in that order) | |
def run!(options={}) | |
set options | |
handler = detect_rack_handler | |
handler_name = handler.name.gsub(/.*::/, '') | |
STDERR.puts "== Sinatra/#{Sinatra::VERSION} has taken the stage " + | |
"on #{port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i | |
handler.run self, :Host => bind, :Port => port do |server| | |
[:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } } | |
set :running, true | |
end | |
rescue Errno::EADDRINUSE => e | |
STDERR.puts "== Someone is already performing on port #{port}!" | |
end | |
# The prototype instance used to process requests. | |
def prototype | |
@prototype ||= new | |
end | |
# Create a new instance without middleware in front of it. | |
alias new! new unless method_defined? :new! | |
# Create a new instance of the class fronted by its middleware | |
# pipeline. The object is guaranteed to respond to #call but may not be | |
# an instance of the class new was called on. | |
def new(*args, &bk) | |
build(Rack::Builder.new, *args, &bk).to_app | |
end | |
# Creates a Rack::Builder instance with all the middleware set up and | |
# an instance of this class as end point. | |
def build(builder, *args, &bk) | |
setup_default_middleware builder | |
setup_middleware builder | |
builder.run new!(*args, &bk) | |
builder | |
end | |
def call(env) | |
synchronize { prototype.call(env) } | |
end | |
private | |
def setup_default_middleware(builder) | |
builder.use ShowExceptions if show_exceptions? | |
builder.use Rack::MethodOverride if method_override? | |
builder.use Rack::Head | |
setup_logging builder | |
setup_sessions builder | |
end | |
def setup_middleware(builder) | |
middleware.each { |c,a,b| builder.use(c, *a, &b) } | |
end | |
def setup_logging(builder) | |
if logging? | |
builder.use Rack::CommonLogger | |
if logging.respond_to? :to_int | |
builder.use Rack::Logger, logging | |
else | |
builder.use Rack::Logger | |
end | |
else | |
builder.use Rack::NullLogger | |
end | |
end | |
def setup_sessions(builder) | |
return unless sessions? | |
options = {} | |
options[:secret] = session_secret if session_secret? | |
options.merge! sessions.to_hash if sessions.respond_to? :to_hash | |
builder.use Rack::Session::Cookie, options | |
end | |
def detect_rack_handler | |
servers = Array(server) | |
servers.each do |server_name| | |
begin | |
return Rack::Handler.get(server_name) | |
rescue LoadError | |
rescue NameError | |
end | |
end | |
fail "Server handler (#{servers.join(',')}) not found." | |
end | |
def inherited(subclass) | |
subclass.reset! | |
subclass.set :app_file, caller_files.first unless subclass.app_file? | |
super | |
end | |
@@mutex = Mutex.new | |
def synchronize(&block) | |
if lock? | |
@@mutex.synchronize(&block) | |
else | |
yield | |
end | |
end | |
public | |
CALLERS_TO_IGNORE = [ # :nodoc: | |
/\/sinatra(\/(base|main|showexceptions))?\.rb$/, # all sinatra code | |
/lib\/tilt.*\.rb$/, # all tilt code | |
/^\(.*\)$/, # generated code | |
/rubygems\/custom_require\.rb$/, # rubygems require hacks | |
/active_support/, # active_support require hacks | |
/bundler(\/runtime)?\.rb/, # bundler require hacks | |
/<internal:/ # internal in ruby >= 1.9.2 | |
] | |
# add rubinius (and hopefully other VM impls) ignore patterns ... | |
CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS) | |
# Like Kernel#caller but excluding certain magic entries and without | |
# line / method information; the resulting array contains filenames only. | |
def caller_files | |
caller_locations. | |
map { |file,line| file } | |
end | |
# Like caller_files, but containing Arrays rather than strings with the | |
# first element being the file, and the second being the line. | |
def caller_locations | |
caller(1). | |
map { |line| line.split(/:(?=\d|in )/)[0,2] }. | |
reject { |file,line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } } | |
end | |
end | |
# Fixes encoding issues by | |
# * defaulting to UTF-8 | |
# * casting params to Encoding.default_external | |
# | |
# The latter might not be necessary if Rack handles it one day. | |
# Keep an eye on Rack's LH #100. | |
def force_encoding(*args) settings.force_encoding(*args) end | |
if defined? Encoding | |
def self.force_encoding(data, encoding = default_encoding) | |
return if data == settings || data.is_a?(Tempfile) | |
if data.respond_to? :force_encoding | |
data.force_encoding(encoding).encode! | |
elsif data.respond_to? :each_value | |
data.each_value { |v| force_encoding(v, encoding) } | |
elsif data.respond_to? :each | |
data.each { |v| force_encoding(v, encoding) } | |
end | |
data | |
end | |
else | |
def self.force_encoding(data, *) data end | |
end | |
reset! | |
set :environment, (ENV['RACK_ENV'] || :development).to_sym | |
set :raise_errors, Proc.new { test? } | |
set :dump_errors, Proc.new { !test? } | |
set :show_exceptions, Proc.new { development? } | |
set :sessions, false | |
set :logging, false | |
set :method_override, false | |
set :default_encoding, "utf-8" | |
set :add_charset, %w[javascript xml xhtml+xml json].map { |t| "application/#{t}" } | |
settings.add_charset << /^text\// | |
# explicitly generating a session secret eagerly to play nice with preforking | |
begin | |
require 'securerandom' | |
set :session_secret, SecureRandom.hex(64) | |
rescue LoadError, NotImplementedError | |
# SecureRandom raises a NotImplementedError if no random device is available | |
set :session_secret, "%064x" % Kernel.rand(2**256-1) | |
end | |
class << self | |
alias_method :methodoverride?, :method_override? | |
alias_method :methodoverride=, :method_override= | |
end | |
set :run, false # start server via at-exit hook? | |
set :running, false # is the built-in server running now? | |
set :server, %w[thin mongrel webrick] | |
set :bind, '0.0.0.0' | |
set :port, 4567 | |
set :absolute_redirects, true | |
set :prefixed_redirects, false | |
set :empty_path_info, nil | |
set :app_file, nil | |
set :root, Proc.new { app_file && File.expand_path(File.dirname(app_file)) } | |
set :views, Proc.new { root && File.join(root, 'views') } | |
set :reload_templates, Proc.new { development? } | |
set :lock, false | |
set :public, Proc.new { root && File.join(root, 'public') } | |
set :static, Proc.new { public && File.exist?(public) } | |
error ::Exception do | |
response.status = 500 | |
content_type 'text/html' | |
'<h1>Internal Server Error</h1>' | |
end | |
configure :development do | |
get '/__sinatra__/:image.png' do | |
filename = File.dirname(__FILE__) + "/images/#{params[:image]}.png" | |
content_type :png | |
send_file filename | |
end | |
error NotFound do | |
content_type 'text/html' | |
(<<-HTML).gsub(/^ {8}/, '') | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style type="text/css"> | |
body { text-align:center;font-family:helvetica,arial;font-size:22px; | |
color:#888;margin:20px} | |
#c {margin:0 auto;width:500px;text-align:left} | |
</style> | |
</head> | |
<body> | |
<h2>Sinatra doesn’t know this ditty.</h2> | |
<img src='#{uri "/__sinatra__/404.png"}'> | |
<div id="c"> | |
Try this: | |
<pre>#{request.request_method.downcase} '#{request.path_info}' do\n "Hello World"\nend</pre> | |
</div> | |
</body> | |
</html> | |
HTML | |
end | |
end | |
end | |
# Execution context for classic style (top-level) applications. All | |
# DSL methods executed on main are delegated to this class. | |
# | |
# The Application class should not be subclassed, unless you want to | |
# inherit all settings, routes, handlers, and error pages from the | |
# top-level. Subclassing Sinatra::Base is highly recommended for | |
# modular applications. | |
class Application < Base | |
set :logging, Proc.new { ! test? } | |
set :method_override, true | |
set :run, Proc.new { ! test? } | |
set :session_secret, Proc.new { super() unless development? } | |
set :app_file, nil | |
def self.register(*extensions, &block) #:nodoc: | |
added_methods = extensions.map {|m| m.public_instance_methods }.flatten | |
Delegator.delegate(*added_methods) | |
super(*extensions, &block) | |
end | |
end | |
# Sinatra delegation mixin. Mixing this module into an object causes all | |
# methods to be delegated to the Sinatra::Application class. Used primarily | |
# at the top-level. | |
module Delegator #:nodoc: | |
def self.delegate(*methods) | |
methods.each do |method_name| | |
define_method(method_name) do |*args, &block| | |
return super(*args, &block) if respond_to? method_name | |
Delegator.target.send(method_name, *args, &block) | |
end | |
private method_name | |
end | |
end | |
delegate :get, :patch, :put, :post, :delete, :head, :options, :template, :layout, | |
:before, :after, :error, :not_found, :configure, :set, :mime_type, | |
:enable, :disable, :use, :development?, :test?, :production?, | |
:helpers, :settings | |
class << self | |
attr_accessor :target | |
end | |
self.target = Application | |
end | |
# Create a new Sinatra application. The block is evaluated in the new app's | |
# class scope. | |
def self.new(base=Base, options={}, &block) | |
base = Class.new(base) | |
base.class_eval(&block) if block_given? | |
base | |
end | |
# Extend the top-level DSL with the modules provided. | |
def self.register(*extensions, &block) | |
Delegator.target.register(*extensions, &block) | |
end | |
# Include the helper modules provided in Sinatra's request context. | |
def self.helpers(*extensions, &block) | |
Delegator.target.helpers(*extensions, &block) | |
end | |
# Use the middleware for classic applications. | |
def self.use(*args, &block) | |
Delegator.target.use(*args, &block) | |
end | |
end |
Also, regarding the splat operator. https://gist.github.com/1016180#L67
It takes the arguments and turns them into an array, if the first argument is an array then the splatted array's first value is that array. e.g.:
def foo *bar
p bar
end
foo 1, 2, 3
# => [1, 2, 3]
foo [1, 2, 3]
# => [[1, 2, 3]]
The method uses flatten on the arguments so that regardless of whether the arguments look like 1, 2, 3
or [1, 2, 3]
, the resulting array will always be [1, 2, 3]
and not [[1, 2, 3]]
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
re https://gist.github.com/1016180#L58, actually it is executed
(entries.sort_by { |e| e.last }).map {|e| e.first }
.z = a.map{|x| x.first}.sort
is equivalent to