How can I extract variables from a ruby snippet

L

Laran Evans

I have this text:

<request>
<post>
<category-id><%= params['post']['category_id'] %></category-id>
<title><%= params['post']['title'] %></title>
<body><%= params['post']['body'] %></body>
<extended-body><%= params['post']['extended_body'] %></extended-
body>
<private><%= params['post']['private'] %></private>
</post>
<% if params['notify'] then %>
<% params['notify'].each do |person_id| %>
<notify><%= person_id %></notify>
<% end %>
<% end %>
<% if params['attachments'] then %>
<% params['attachments'].each do |attachment| %>
<attachments>
<name><%= attachment['name'] %></name>
<file>
<file><%= attachment['file']['temp_id'] %></file>
<content-type><%= attachment['file']['content_type'] %></
content-type>
<original_filename><%= attachment['file']
['original_filename'] %></original-filename>
</file>
</attachments>
<% end %>
<% end %>
</request>

What I need to do is extract all of the variables used in order to
learn the variables, and their structure (array/hash vs. simple value)
used to render the view.

So, using the snippet above, I would learn that there is:

- a "post" variable which is a hash, expected to have the keys
"category_id", "title", "body", "extended_body" and "private".
- a "notify" variable which is an array (indicated by the fact that
it's iterated over with the .each) of simple values (i.e. it's not a
multi-dimensional array, or an array of hashes)
- a "attachments" variable which is an array (again indicated by the
fact that it's iterated over with the .each) of hashes (indicated by
the fact that each element of the "attachments" array is referenced
with hash syntax, i.e. "attachment['name']) and that the value
corresponding to the key "file" is also a hash expected to have the
keys "temp_id", "content_type" and "original_filename".

Is there a tool available to allow me to do this? I haven't been able to
find anything.
 
B

Brian Candler

Laran said:
So, using the snippet above, I would learn that there is:

- a "post" variable which is a hash, expected to have the keys
"category_id", "title", "body", "extended_body" and "private".
- a "notify" variable which is an array (indicated by the fact that
it's iterated over with the .each) of simple values (i.e. it's not a
multi-dimensional array, or an array of hashes)

You can't tell that - lots of objects implement an 'each' iterator, and
there isn't really such thing as a 'simple value' in Ruby anyway. The
number 1 and the Array [1] are both first-class Objects. e.g.
[1, "one"]
[2, "two"]

The full solution to what you want is probably to compile the ERB
template into Ruby, and then compile the Ruby into an sexp (e.g. using
ParseTree or ripper), and then analyse the sexp. This is quite a lot of
work.

Otherwise, you could execute the template using erb, and use
method_missing to pick up attempts to access local variables. From these
return a dummy object, which again uses method_missing to see what
methods have been called on that dummy object. If the method_missing
passes a block then you will need to yield the block with dummy
arguments.

This may be good enough for what you need, but isn't guaranteed to catch
all possible execution paths. e.g. what about

<% if foo %>
<%= foo.xxx %>
<% else %>
<%= bar.yyy %>
<% end %>
?

You are going to find it hard to exercise both branches of this
conditional. Similarly with loops, where the nth iteration may behave
differently to the previous ones.

Rails approaches the problem differently: whenever you render a template
you pass in a hash of 'local_assigns', and it compiles the template
assuming that these are the variables which will be substituted. It then
makes a method signature using this set of variables. If the template is
rendered again and 'local_assigns' contains a different set of variables
to substitute, then it is recompiled. The assumption is that normally
the template will be rendered multiple times using the same set of
local_assigns variables (just with different values, of course)

See actionpack/lib/action_view/renderable.rb
 
R

Robert Klemme

2009/8/18 Brian Candler said:
Otherwise, you could execute the template using erb, and use
method_missing to pick up attempts to access local variables. From these
return a dummy object, which again uses method_missing to see what
methods have been called on that dummy object. If the method_missing
passes a block then you will need to yield the block with dummy
arguments.

I toyed around a bit - this solution is by far not perfect!

#!ruby19 -w

class FillIn < Object
def initialize(name)
@name =3D name.to_s
end

def method_missing(s,*a,&b)
a.map! {|x| x.inspect}

nn =3D case s
when :[]
"#{@name}[#{a.join(', ')}]"
when :[]=3D
v =3D a.slice!(-1, 1)
"#{@name}[#{a.join(', ')}]=3D#{v}"
else
if a.empty?
"#{@name}.#{s}"
else
"#{@name}.#{s}(#{a.join(', ')})"
end
end

puts nn
self.class.new(nn)
end

def to_s
@name
end

alias inspect to_s
end

root =3D FillIn.new "root"
p root

root.foo.bar['help'].length
root.foo.bar[FillIn.new("x")].length
This may be good enough for what you need, but isn't guaranteed to catch
all possible execution paths. e.g. what about

=A0<% if foo %>
=A0 =A0<%=3D foo.xxx %>
=A0<% else %>
=A0 =A0<%=3D bar.yyy %>
=A0<% end %>
?

You are going to find it hard to exercise both branches of this
conditional. Similarly with loops, where the nth iteration may behave
differently to the previous ones.

Correct. I feel somehow reminded of
http://en.wikipedia.org/wiki/Halting_problem :)

Kind regards

robert

--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,066
Latest member
VytoKetoReviews

Latest Threads

Top