I'm probably missing something painfully obvious here, but I can't seem to find an answer, or work it out myself. In Sinatra, they have a self.get method, which captures blocks, when a block is called, you're able to use the request variable inside, how is this possible?
module Sinatra
class Base
class Request < Rack::Request
end
attr_accessor :request
def call!(env)
@request = Request.new(env)
end
class << self
def get(path, opts = {}, &block)
...
end
end
end
end
class App < Sinatra::Base
get '/' do
puts request
end
end
Wow. You piqued my curiousity, and sure enough, researching this was facinating. The magic starts in the compile! method defined at: https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1529
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, []
wrapper = block.arity != 0 ?
proc { |a,p| unbound_method.bind(a).call(*p) } :
proc { |a,p| unbound_method.bind(a).call }
wrapper.instance_variable_set(:@route_name, method_name)
[ pattern, keys, conditions, wrapper ]
end
Notice we turn the block passed to get (or any route function) into an unbound method via generate_method (defined a few lines above). We then store a proc which takes two parameters, an object to bind the method to, and a list of arguments, which the method is called with.
Skip ahead to process_route: https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L971
def process_route(pattern, keys, conditions, block = nil, values = [])
route = @request.path_info
route = '/' if route.empty? and not settings.empty_path_info?
return unless match = pattern.match(route)
values += match.captures.to_a.map { |v| force_encoding URI.unescape(v) if v }
if values.any?
original, @params = params, params.merge('splat' => [], 'captures' => values)
keys.zip(values) { |k,v| Array === @params[k] ? @params[k] << v : @params[k] = v if v }
end
catch(:pass) do
conditions.each { |c| throw :pass if c.bind(self).call == false }
block ? block[self, values] : yield(self, values)
end
ensure
@params = original if original
end
Theres a lot going on here, but the key is:
block[self, values]
This calls the stored block above with self, and the appropriate arguments. Thus the unbound method is bound to whatever process_route is bound to( the current self in process_route). And what is process_route bound to? An instance of Sinatra::Base, which as we know has an attribute accessor request that can now be reached in your original block. Tada!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With