Extracting instance variables from a block?

Hello fellow Rubyists,

I am writing a tiny custom web framework, similar in nature to web.py
(http://webpy.org/). However, I'd like someone with better
metaprogramming-foo to help me clean up some syntax a bit.

Here's a _very_ basic abbreviated version of the framework:

class Web
  def initialize
    @pages = {}
    @vars = {}
  end

  # Associates a URI regular expression with a block of code.
  def page(regexp, &action)
    @pages[/^#{regexp}$/] = action
  end

  # Sets a variable for use in the template. See the example below.
  def []=(var, val)
    @vars[var] = val
  end

  # Renders the given ERB template file.
  def render(template)
    o = Object.new
    def o.b; binding; end
    @vars.each { |var, val| o.instance_variable_set("@#{var}", val) }
    print CGI.new.header('text/html')
    ERB.new(File.read(template)).run(o.b)
  end

  # Compares each regexp in @pages to ENV['REQUEST_URI']. If a match
  # is found, calls the associated action block, passing in the match
  # results as arguments.
  def run
    for regexp, action in @pages
      if ENV['REQUEST_URI'] =~ regexp
        action[*$~.to_a[1..-1]]
        return
      end
    end
    # Oh noes! 404 error handling stuff goes here.
  end
end

A simple application would look something like this:

web = Web.new

web.page '/hello/(\w+)' do |name|
  web[:title] = 'Hello'
  web[:name] = name
  web.render 'hello.rhtml'
end

web.run

# Contents of hello.rhtml
<html>
  <title><%= @title %></title>
  <h1>Hello, <%= @name %>!</h1>
  <p>How are you today?</p>
</html>

The Web#run method reads the request URI, and sees if it matches with
any of your defined regexps. If it does, it runs the block of code you
associated that URL with (passing in any needed parameters). So when you
visit http://example.com/hello/_why, it displays a nice welcome message
to _why.

As you can see, you can use web[:foo] = 'bar' to set an instance
variable, @foo, in the template's binding. This is all nice and great,
but the syntax is a bit... crusty. I'd love to do something like this
instead:

web.page '/hello/(\w+)' do |name|
  @title = 'Hello'
  @name = name
  web.render 'hello.rhtml'
end

So, my question: is there any way to capture the instance variables set
in the block, and use them for a template binding elsewhere? Or at least
do something else that gives me my desired results?

Thanks,
Chris

···

--
Posted via http://www.ruby-forum.com/.

Hey Chris-

  Cool little class :wink: Take a look at this, you should be able to use something like it to do what you want.

class Web
   def page(url, &blk)
     @url = url
     instance_eval &blk
   end
end

web = Web.new

web.page '/hello/(\w+)' do |name|
   @title = 'Hello'
   @name = "Ernie"
end

puts web.inspect

=> #<Web:0x32c998 @title="Hello", @name="Ernie", @url="/hello/(\\w+)">

-Ezra

···

On Aug 7, 2006, at 6:08 PM, Chris Eskow wrote:

Hello fellow Rubyists,

I am writing a tiny custom web framework, similar in nature to web.py
(http://webpy.org/\). However, I'd like someone with better
metaprogramming-foo to help me clean up some syntax a bit.

Here's a _very_ basic abbreviated version of the framework:

<snip>

So, my question: is there any way to capture the instance variables set
in the block, and use them for a template binding elsewhere? Or at least
do something else that gives me my desired results?

Thanks,
Chris

--
Posted via http://www.ruby-forum.com/\.

Ezra Zygmuntowicz wrote:

[snip]

class Web
   def page(url, &blk)
     @url = url
     instance_eval &blk
   end
end

web = Web.new

web.page '/hello/(\w+)' do |name|
   @title = 'Hello'
   @name = "Ernie"
end

puts web.inspect

=> #<Web:0x32c998 @title="Hello", @name="Ernie", @url="/hello/(\\w+)">

-Ezra

Thanks for the suggestion (and the compliment), but the problem with
that is that for every page you define, the block of code you associate
it with will be called. So, if you define several pages like so:

web.page('/') { web.render 'home.rhtml' }
web.page('/about') { web.render 'about.rhtml' }
web.page('/hello/(\w+)') { |name| @name = name; web.render 'hello.rhtml'
}

It will render all three templates at once! But it did get me
thinking... What if I instance_eval it within Web#run? For example:

class Web
  def initialize
    #...
    @obj = Object.new
    def @obj.b; binding; end
  end

  def render(template)
    #...
    ERB.new(File.read(template)).run(@obj.b)
  end

  def run
    #...
    @obj.instance_eval(&action)
  end
end

That sort of works nice... But you can't pass in the URL parameters (the
ones that your URL regexps match). Is there some way that you can call a
Proc object (with parameters), but within the context of a specific
object's binding?

Chris

···

--
Posted via http://www.ruby-forum.com/\.

Wait! I got it!

Instead of keeping an object to store the binding in, I can store the
Proc object's binding itself:

class Web
  def initialize
    @pages = {}
    @binding = nil
  end

  def page(regexp, &action)
    @pages[/^#{regexp}$/] = action
  end

  def render(template)
    print CGI.new.header('text/html')
    ERB.new(File.read(template)).run(@binding)
  end

  def run
    for regexp, action in @pages
      if ENV['REQUEST_URI'] =~ regexp
        @binding = action.binding
        action[*$~.to_a[1..-1]]
        return
      end
    end
  end
end

As you can see, once I found the matching page, I store its action's
binding (by calling Proc#binding) into an instance variable (@binding),
right before I actually call the action. Then, when running the ERB
template, I use @binding for template's binding.

Thanks for your help, anyway!

Chris

···

--
Posted via http://www.ruby-forum.com/.