Using combination of techniques learned in this book, let's create a class macro (similar to attr_accessor
) that validates attributes.
Rather than giving all classes this class macro, we can include a module.
If the attribute doesn't pass validation, we'll throw an exception.
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
me = Person.new
me.age = 39 # => OK
me.age = 17 # => Exception
Let's take a look at things we'll need to setup this new class macro.
Kernel#eval
takes a string that contains Ruby code and executes it.
array = [1, 2]
element = 3
eval("array << element") # => [1, 2, 3]
When would this be helpful? Let's look at an example below.
REST Client is a simple HTTP client library that comes with an intepreter:
$ restclient http://www.twitter.com
> html_first_chars = get("/")[0..14]
=> "<!DOCTYPE html>"
If we look at the gem's source code, we have top-level methods for HTTP Methods, which delegate to a Resource class:
module RestClient
class Resource
def get(additional_headers={}, &block) # ...
def post(payload, additional_heaers={}, &block) # ...
...
You'd expect the top-level GET method to look somethign like this:
def get(path, *args, &b)
r[path].get(*args, &b)
end
However, the GET method along with other HTTP methods are defined in one go:
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']
POSSIBLE_VERBS.each do |verb|
eval <<-end_eval
def #{verb}(path, *args, &b)
r[path].#{verb}(*args, &b)
end
end_eval
end
A Binding is a whole scope packaged as an object that you can carry around. Later, you can execute code in that scope using eval.
class MyClass
def my_method
@x = 1
binding
end
end
b = MyClass.new.my_method
eval "@x", b # => 1
Ruby provides a predefined constant called TOPLEVEL_BINDING, which is just a binidng for the top-level scope:
class AnotherMethod
def my_method
eval "self", TOPLEVEL_BINDING
end
end
AnotherClass.new_my_method # => main
One gem that makes good use of bindings is Pry. Pry defines an Object#pry
method that opens an interactive session inside the object's scope.
irb is basically a program that parses standard input or file and passes each line to eval
.
Deep within irb's source code:
eval(statements, @binding, file, line)
where
statements
is just a line of Ruby code.@binding
is whole scope packaged into an object to evaluate code in different contextsfile
andline
are used to tweak stack trace in case of excpetions (see below)
# this code raises an exception
x = 1 / 0
If you run irb exception.rb
, you'll get an exception on line 2:
ZeroDivisionError: divided by 0
from exception.rb2:in `/'
When irb calls eval
, it calls it with the current filename and line number. If you were to remove last two arguments:
eval(statements, @binding) # , file, line)
You'd get something more like this:
ZeroDivisionError: divided by 0
from /Users/jimbob/.rvm/rubies/ruby-2.0.0/lib/ruby/2.0.0/irb/workspace.rb:54:in `/'
eval
evalutes a string of code instead of a block. instance_eval
and class_eval
usually evaluate a block, but they can also evaluate a string of code:
array = ['a', 'b', 'c']
x = 'd'
array.instance_eval "self[1] = x"
array # => ['a', 'd', 'c']
You can evaluate string of code or block, but as a rule of thumb, you should use block.
- Strings of code don't always play well with your editor's syntax coloring.
- More difficult to read
- Security issues (code injection)