Friday: Code That Writes Code - Metaprogramming Ruby - Metaprogramming Ruby 2: Program Like the Ruby Pros (2014)

Metaprogramming Ruby 2: Program Like the Ruby Pros (2014)

Part 1. Metaprogramming Ruby

Chapter 6. Friday: Code That Writes Code

So far you’ve seen many wonderful metaprogramming spells—but it’s possible that the meaning of the “m” word has only become fuzzier for you. The fact is, the original definition of metaprogramming as “writing code that writes code” doesn’t fit every technique described in this book.

Rather than look for an updated, Wikipedia-worthy definition, we can accept that metaprogramming is not a single approach that you can define in a short sentence. It’s more like a heterogeneous bag of tricks that all happen to revolve around the Ruby object model. And like any other bag of tricks, metaprogramming really comes into its own when you start blending many of those tricks together.

Today you’ll learn a few new tricks you can add to that bag, including one that quite literally “writes code.” But even better, you’ll see how you can seamlessly mix and match many tricks to solve a difficult coding challenge.

Coding Your Way to the Weekend

Where your boss challenges you and Bill to write better code than she can.

After such an eventful week, you’re looking forward to a relaxing Friday. But as soon as you sit down with Bill and your cup of coffee, your boss appears.

“You guys did a good job this week,” she says. “Looking over your code, I got so excited about metaprogramming that I decided to learn it myself. But last night I got stuck on a difficult coding problem. Can you help me?”

Having a boss who used to be a programmer and still likes to get her hands dirty can sometimes make your life harder. But you’re new at this job, and you can’t say no when your boss is asking for your help.

The Boss’ Challenge

A few days ago, your boss learned about the attr_accessor method that you read about in The attr_accessor() Example. Now she’s using attr_accessor all the time to generate her objects’ attributes. While she was at it, your boss also came up with the idea of writing her own Class Macro (Class Macro), similar to attr_accessor, which generates a validated attribute. “I call it attr_checked,” she says.

Your boss explains how this attr_checked method should work, pointing out that it should take the name of the attribute, as well as a block. The block is used for validation. If you assign a value to the attribute and the block doesn’t return true for that value, then you get a runtime exception.

Your boss’ first requirement is an attr_checked Class Macro, and she explains her secondary requirement: “I don’t want this attr_checked method to be available to each and every class, because I don’t like the idea of cluttering standard classes with my own methods. Instead, a class should gain access to attr_checked only when it includes a CheckedAttributes module.” She provides this example:

class Person

*

include CheckedAttributes

attr_checked :age do |v|

v >= 18

end

end

me = Person.new

me.age = 39 # OK

me.age = 12 # Exception

Your task today is to write CheckedAttributes and attr_checked for your boss.

A Development Plan

The boss’ challenge is a bit too much to handle in a single burst of coding. You’ll get to a solution in small steps.

Instead of engaging in pair programming, Bill proposes sharing roles: he’ll manage the development, and you’ll write the code. While you wonder what “managing the development” actually means, Bill quickly lists the steps you’ll take:

1. Write a Kernel Method (Kernel Method) named add_checked_attribute using eval to add a super-simple validated attribute to a class.

2. Refactor add_checked_attribute to remove eval.

3. Validate attributes through a block.

4. Change add_checked_attribute to a Class Macro (Class Macro) named attr_checked that’s available to all classes.

5. Write a module adding attr_checked to selected classes through a hook.

“Aren’t we supposed to work as a pair?” you ask. “I don’t even understand these steps.”

“Don’t worry,” Bill says. “You really only need to learn two things before you start developing: one is a method named eval, and the other is the concept of a Hook Method.” He vows to tell you everything you need to know about eval, because eval is necessary for the first development step. You will deal with Hook Methods later.

Kernel#eval

Where you learn that, when it comes right down to it, code is just text.

You already learned about instance_eval and class_eval (in instance_eval(), and class_eval(), respectively). Now you can get acquainted with the third member of the *eval family—a Kernel Method (Kernel Method) that’s simply named eval. Kernel#eval is the most straightforward of the three *eval methods. Instead of a block, it takes a string that contains Ruby code—a Spell: String of Code for short. Kernel#eval executes the code in the string and returns the result:

ctwc/simple_eval.rb

array = [10, 20]

element = 30

eval("array << element") # => [10, 20, 30]

Executing a literal string of Ruby code is a pretty pointless exercise, but the power of eval becomes apparent when you compute your Strings of Code on the fly. Here’s an example.

The REST Client Example

REST Client (installed with gem install rest-client) is a simple HTTP client library. It includes an interpreter where you can issue regular Ruby commands together with HTTP methods such as get:

=>

restclient http://www.twitter.com

> html_first_chars = get("/")[0..14]

=> "<!DOCTYPE html>"

If you look in the gem’s source, you will see that get and the three other basic HTTP methods are defined on the Resource class:

gems/rest-client-1.6.7/lib/restclient/resource.rb

module RestClient

class Resource

def get(additional_headers={}, &block) # ...

def post(payload, additional_headers={}, &block) # ...

def put(payload, additional_headers={}, &block) # ...

def delete(additional_headers={}, &block) # ...

To make get and its siblings available in the interpreter, REST Client defines four top-level methods that delegate to the methods of a Resource at a specific URL. For example, here is how the top-level get delegates to a Resource (returned by the r method):

def get(path, *args, &b)

r[path].get(*args, &b)

end

You might expect to find this definition of get in the source code, together with similar definitions for put, post, and delete. However, here comes a twist. Instead of defining the four methods separately, REST Client defines all of them in one shot by creating and evaluating four Strings of Code (String of Code) in a loop:

gems/rest-client-1.6.7/bin/restclient

POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|

eval <<-end_eval

def #{m}(path, *args, &b)

r[path].#{m}(*args, &b)

end

end_eval

end

The code above uses an exotic syntax known as a here document, or heredoc for short. What you’re seeing after the eval is just a regular Ruby string, although it’s not delimited by the usual quotes. Instead, it starts with a <<- sequence followed by an arbitrary termination sequence—in this case, end_eval. The string ends on the first line that contains only the termination sequence, so this particular string spans the lines from the def to the first end included. The code uses regular string substitution to generate and eval four Strings of Code, one each for the definitions of get, put,post, and delete.

Most Strings of Code feature some kind of string substitution, as in the example above. For an alternate way to use eval, you can evaluate arbitrary Strings of Code from an external source, effectively building your own simple Ruby interpreter.

If you want to use Kernel#eval to its fullest potential, you should also learn about the Binding class.

Binding Objects

A Binding is a whole scope packaged as an object. The idea is that you can create a Binding to capture the local scope and carry it around. Later, you can execute code in that scope by using the Binding object in conjunction with eval.

You can create a Binding with the Kernel#binding method:

ctwc/bindings.rb

class MyClass

def my_method

@x = 1

binding

end

end

b = MyClass.new.my_method

You can think of Binding objects as “purer” forms of closures than blocks because these objects contain a scope but don’t contain code. You can evaluate code in the captured scope by passing the Binding as an additional argument to eval:

eval "@x", b # => 1

Ruby also provides a predefined constant named TOPLEVEL_BINDING, which is just a Binding of the top-level scope. You can use it to access the top-level scope from anywhere in your program:

class AnotherClass

def my_method

eval "self", TOPLEVEL_BINDING

end

end

AnotherClass.new.my_method # => main

One gem that makes good use of bindings is Pry, which you met in The Pry Example. Pry defines an Object#pry method that opens an interactive session inside the object’s scope, similar to what irb does with nested sessions. You can use this function as a debugger of sorts: instead of setting a breakpoint, you add a line to your code that calls pry on the current bindings, as shown in the following code.

# code...

require "pry"; binding.pry

# more code...

The call to binding.pry opens a Ruby interpreter in the current bindings, right inside the running process. From there, you can read and change your variables at will. When you want to exit the interpreter, just type exit to continue running the program. Thanks to this feature, Pry is a great alternative to traditional debuggers.

Pry is not the only command-line interpreter that uses bindings. Let’s also look at irb, the default Ruby command-line utility.

The irb Example

At its core, irb is just a simple program that parses the standard input or a file and passes each line to eval. (This type of program is sometimes called a Spell: Code Processor.) Here’s the line that calls eval, deep within irb’s source code, in a file named workspace.rb:

eval(statements, @binding, file, line)

The statements argument is just a line of Ruby code. But what about those three additional arguments to eval? Let’s go through them.

The first optional argument to eval is a Binding, and irb can change this argument to evaluate code in different contexts. This happens, for example, when you open a nested irb session on a specific object, by typing irb followed by the name of an object in an existing irb session. As a result, your next commands will be evaluated in the context of the object, similar to what instance_eval does.

What about file and line, the remaining two optional arguments to eval? These arguments are used to tweak the stack trace in case of exceptions. You can see how they work by writing a Ruby program that raises an exception:

ctwc/exception.rb

# this file raises an Exception on the second line

x = 1 / 0

You can process this program with irb by typing irb exception.rb at the prompt. If you do that, you’ll get an exception on line 2 of exception.rb:

<=

ZeroDivisionError: divided by 0

from exception.rb:2:in `/'

When irb calls eval, it calls it with the filename and line number it’s currently processing. That’s why you get the right information in the exception’s stack trace. Just for fun, you can hack irb’s source and remove the last two arguments from the call to eval (remember to undo the change afterward):

eval(statements, @binding) # , file, line)

Run irb exception.rb now, and the exception reports the file and line where eval is called:

<=

ZeroDivisionError: divided by 0

from /Users/nusco/.rvm/rubies/ruby-2.0.0/lib/ruby/2.0.0/irb/workspace.rb:54:in `/'

This kind of hacking of the stack trace is especially useful when you write Code Processors—but consider using it everywhere you evaluate a String of Code (String of Code) so you can get a better stack trace in case of an exception.

Strings of Code vs. Blocks

In Kernel#eval, you learned that eval is a special case in the eval* family: it evaluates a String of Code (String of Code) instead of a block, like both class_eval and instance_eval do. However, this is not the whole truth. Although it’s true that eval always requires a string, instance_eval andclass_eval can take either a String of Code or a block.

This shouldn’t come as a big surprise. After all, code in a string is not that different from code in a block. Strings of Code can even access local variables like blocks do:

array = ['a', 'b', 'c']

x = 'd'

array.instance_eval "self[1] = x"

array # => ["a", "d", "c"]

Because a block and a String of Code are so similar, in many cases you have the option of using either one. Which one should you choose? The short answer is that you should probably avoid Strings of Code whenever you have an alternative. Let’s see why.

The Trouble with eval()

Strings of Code are powerful, no doubt about that. But with great power comes great responsibility—and danger.

To start with, Strings of Code don’t always play well with your editor’s syntax coloring and autocompletion. Even when they do get along with everyone, Strings of Code tend to be difficult to read and modify. Also, Ruby won’t report a syntax error in a String of Code until that string is evaluated, potentially resulting in brittle programs that fail unexpectedly at runtime.

Thankfully, these annoyances are minor compared to the biggest issue with eval: security. This particular problem calls for a more detailed explanation.

Code Injection

Assume that, like most people, you have trouble remembering what each of the umpteen methods of Array does. As a speedy way to refresh your memory, you can write an eval-based utility that allows you to call a method on a sample array and view the result (call it the array explorer):

ctwc/array_explorer.rb

def explore_array(method)

code = "['a', 'b', 'c'].#{method}"

puts "Evaluating: #{code}"

eval code

end

loop { p explore_array(gets()) }

The infinite loop on the last line collects strings from the standard input and feeds these strings to explore_array. In turn, explore_array turns the strings into method calls on a sample array. For example, if you feed the string "revert()" to explore_array, the method will evaluate the string "[’a’, ’b’, ’c’].revert()". It’s time to try out this utility:

=>

find_index("b")

<=

Evaluating: ['a', 'b', 'c'].find_index("b")

1

=>

map! {|e| e.next }

<=

Evaluating: ['a', 'b', 'c'].map! {|e| e.next }

["b", "c", "d"]

Now imagine that, being a sharing kind of person, you decide to make this program widely available on the web. You hack together a quick web page, and—presto!—you have a site where people can call array methods and see the results. (To sound like a proper startup, you might call this site “Arry” or maybe “MeThood.”) Your wonderful site takes the Internet by storm, until a sneaky user feeds it a string like this:

=>

object_id; Dir.glob("*")

<=

['a', 'b', 'c'].object_id; Dir.glob("*") => [your own private information here]

The input is an inconsequential call to the array, followed by a command that lists all the files in your program’s directory. Oh, the horror! Your malicious user can now execute arbitrary code on your computer—code that does something terrible like wipe your hard disk clean or post your love letters to your entire address book. This kind of exploit is called a code injection attack.

Defending Yourself from Code Injection

How can you protect your code from code injection? You might parse all Strings of Code (String of Code) to identify operations that are potentially dangerous. This approach may prove ineffective, though, because there are so many possible ways to write malicious code. Trying to outsmart a determined hacker can be dangerous to both your computer and your ego.

When it comes to code injection, some strings are safer than others. Only strings that derive from an external source can contain malicious code, so you might simply limit your use of eval to those strings that you wrote yourself. This is the case in The REST Client Example. In more complicated cases, however, it can be surprisingly difficult to track which strings come from where.

With all these challenges, some programmers advocate banning eval altogether. Programmers tend to be paranoid about anything that might possibly go wrong, so this eval ban turns out to be a pretty popular choice. (Actually, we’re not paranoid. It’s the government putting something in the tap water that makes us feel that way.)

If you do away with eval, you’ll have to look for alternative techniques on a case-by-case basis. For an example, look back at the eval in The REST Client Example. You could replace it with a Dynamic Method (Dynamic Method) and Dynamic Dispatch (Dynamic Dispatch):

ctwc/rest_client_without_eval.rb

POSSIBLE_VERBS.each do |m|

define_method m do |path, *args, &b|

r[path].send(m, *args, &b)

end

end

In the same way, you could rewrite the Array Explorer utility from Code Injection, by using a Dynamic Dispatch in place of eval:

ctwc/array_explorer_without_eval.rb

def explore_array(method, *arguments)

['a', 'b', 'c'].send(method, *arguments)

end

Still, there are times when you might just miss eval. For example, this latest, safer version of the Array Explorer wouldn’t allow your web user to call a method that takes a block. If you want to describe a Ruby block in a web interface, you need to allow the user to insert arbitrary Strings of Code.

It’s not easy to hit the sweet spot between too much eval and no eval at all. If you don’t want to abstain from eval completely, Ruby does provide some features that make it somewhat safer. Let’s take a look at them.

Tainted Objects and Safe Levels

Ruby automatically marks potentially unsafe objects—in particular, objects that come from external sources—as tainted. Tainted objects include strings that your program reads from web forms, files, the command line, or even a system variable. Every time you create a new string by manipulating tainted strings, the result is itself tainted. Here’s an example program that checks whether an object is tainted by calling its tainted? method:

ctwc/tainted_objects.rb

# read user input

user_input = "User input: #{gets()}"

puts user_input.tainted?

=>

x = 1

<=

true

If you had to check every string for taintedness, then you wouldn’t be in a much better position than if you had simply tracked unsafe strings on your own. But Ruby also provides the notion of safe levels, which complement tainted objects nicely. When you set a safe level (which you can do by assigning a value to the $SAFE global variable), you disallow certain potentially dangerous operations.

You can choose from four safe levels, from the default 0 (“hippie commune,” where you can hug trees and format hard disks) to 3 (“military dictatorship,” where every object you create is tainted by default). A safe level of 2, for example, disallows most file-related operations. Any safe level greater than 0 also causes Ruby to flat-out refuse to evaluate tainted strings:

$SAFE = 1

user_input = "User input: #{gets()}"

eval user_input

=>

x = 1

<=

SecurityError: Insecure operation - eval

Ruby 2.0 and earlier also had a safe level of 4 that didn’t even allow you to exit the program freely. For various reasons, this extreme safe level turned out to be not as secure as people assumed it would be, so it has been removed in Ruby 2.1.

To fine-tune safety, you can explicitly remove the taintedness on Strings of Code before you evaluate them (you can do that by calling Object#untaint) and then rely on safe levels to disallow dangerous operations such as disk access.

By using safe levels carefully, you can write a controlled environment for eval. Such an environment is called a Spell: Sandbox. Let’s take a look at a Sandbox taken from a real-life library.

The ERB Example

The ERB standard library is the default Ruby template system. This library is a Code Processor (Code Processor) that you can use to embed Ruby into any file, such as this template containing a snippet of HTML:

ctwc/template.rhtml

<p><strong>Wake up!</strong> It's a nice sunny <%= Time.new.strftime("%A") %>.</p>

The special <%= ... > tag contains embedded Ruby code. When you pass this template through ERB, the code is evaluated:

ctwc/erb_example.rb

require 'erb'

erb = ERB.new(File.read('template.rhtml'))

erb.run

<=

<p><strong>Wake up!</strong> It's a nice sunny Friday.</p>

Somewhere in ERB’s source, there must be a method that takes a snippet of Ruby code extracted from the template and passes it to eval. Sure enough, here it is:

class ERB

def result(b=new_toplevel)

if @safe_level

proc {

$SAFE = @safe_level

eval(@src, b, (@filename || '(erb)'), 0)

}.call

else

eval(@src, b, (@filename || '(erb)'), 0)

end

end

#...

new_toplevel is a method that returns a copy of TOPLEVEL_BINDING. The @src instance variable carries the content of a code tag, and the @safe_level instance variable contains the safe level required by the user. If no safe level is set, the content of the tag is simply evaluated. Otherwise, ERB builds a quick Sandbox (Sandbox): it makes sure that the global safe level is exactly what the user asked for and also uses a Proc as a Clean Room (Clean Room) to execute the code in a separate scope. (Note that the new value of $SAFE applies only inside the Proc. Contrary to what happens with other global variables, the Ruby interpreter takes care to reset $SAFE to its former value after the call.)

“Now,” Bill says, finally wrapping up his long explanation, “you know about eval and how dangerous it can be. But eval is great to get code up and running quickly. That’s why you can use this method as a first step to solve your original problem: writing the attribute generator for the boss.”

Kernel#eval() and Kernel#load()

Ruby has methods like Kernel#load and Kernel#require that take the name of a source file and execute code from that file. If you think about it, evaluating a file is not that different from evaluating a string. This means load and require are somewhat similar to eval. Although these methods are not really part of the *eval family, you can think of them as first cousins.

You can usually control the content of your files, so you don’t have as many security concerns with load and require as you do with eval. Still, safe levels higher than 1 do put some limitations on importing files. For example, a safe level of 2 or higher prevents you from using load with a tainted filename.

Quiz: Checked Attributes (Step 1)

Where you take your first step toward solving the boss’ challenge, with Bill looking over your shoulder.

You and Bill look back at the first two steps of your development plan:

1. Write a Kernel Method (Kernel Method) named add_checked_attribute using eval to add a super-simple validated attribute to a class.

2. Refactor add_checked_attribute to remove eval.

Focus on the first step. The add_checked_attribute method should generate a reader method and a writer method, pretty much like attr_accessor does. However, add_checked_attribute should differ from attr_accessor in three ways. First, while attr_accessor is a Class Macro (Class Macro),add_checked_attribute is supposed to be a simple Kernel Method (Kernel Method). Second, attr_accessor is written in C, while add_checked_attribute should use plain Ruby (and a String of Code (String of Code)). Finally, add_checked_attribute should add one basic example of validation to the attribute: the attribute will raise a runtime exception if you assign it either nil or false. (You’ll deal with flexible validation down the road.)

These requirements are expressed more clearly in a test suite:

ctwc/checked_attributes/eval.rb

require 'test/unit'

class Person; end

class TestCheckedAttribute < Test::Unit::TestCase

def setup

add_checked_attribute(Person, :age)

@bob = Person.new

end

def test_accepts_valid_values

@bob.age = 20

assert_equal 20, @bob.age

end

def test_refuses_nil_values

assert_raises RuntimeError, 'Invalid attribute' do

@bob.age = nil

end

end

def test_refuses_false_values

assert_raises RuntimeError, 'Invalid attribute' do

@bob.age = false

end

end

end

# Here is the method that you should implement.

def add_checked_attribute(klass, attribute)

# ...

end

(The reference to the class in add_checked_attribute is called klass because class is a reserved word in Ruby.)

Can you implement add_checked_attribute and pass the test?

Before You Solve This Quiz…

You need to generate an attribute like attr_accessor does. You might appreciate a short review of attr_accessor, which we talked about first in The attr_accessor() Example. When you tell attr_accessor that you want an attribute named, say, :my_attr, it generates two Mimic Methods (Mimic Method) like the following:

def my_attr

@my_attr

end

def my_attr=(value)

@my_attr = value

end

Quiz Solution

Here’s a solution:

def add_checked_attribute(klass, attribute)

*

eval "

*

class #{klass}

*

def #{attribute}=(value)

*

raise 'Invalid attribute' unless value

*

@#{attribute} = value

*

end

*

*

def #{attribute}()

*

@#{attribute}

*

end

*

end

*

"

end

Here’s the String of Code (String of Code) that gets evaluated when you call add_checked_attribute(String, :my_attr):

class String

def my_attr=(value)

raise 'Invalid attribute' unless value

@my_attr = value

end

def my_attr()

@my_attr

end

end

The String class is treated as an Open Class (Open Class), and it gets two new methods. These methods are almost identical to those that would be generated by attr_accessor, with an additional check that raises an exception if you call my_attr= with either nil or false.

“That was a good start,” Bill says. “But remember our plan. We only used eval to pass the unit tests quickly; we don’t want to stick with eval for the real solution. This takes us to step 2.”

Quiz: Checked Attributes (Step 2)

Where you make your code eval-free.

You glance at the development plan. Your next step is refactoring add_checked_attribute and replacing eval with regular Ruby methods.

You may be wondering why the obsession with removing eval. How can add_checked_attribute be a target for a code injection attack if it’s meant to be used only by you and your teammates? The problem is, you never know whether this method might be exposed to the world some time in the future. Besides, if you rewrite the same method without using Strings of Code (String of Code), it will only get clearer and more elegant for human readers, and less confusing for tools like syntax higlighters. These considerations are reason enough to go forward and drop eval altogether.

Can you refactor add_checked_attribute with the same method signature and the same unit tests but using standard Ruby methods in place of eval? Be forewarned that to solve this quiz, you’ll have to do some research. You’ll probably need to dig through the Ruby standard libraries for methods that can replace the operations in the current String of Code. You’ll also need to manage scope carefully so that the attribute is defined in the context of the right class. (Hint: remember Flat Scopes (Flat Scope)?)

Quiz Solution

To define methods in a class, you need to get into that class’s scope. The previous version of add_checked_attribute did that by using an Open Class (Open Class) inside a String of Code. If you remove eval, you cannot use the class keyword anymore, because class won’t accept a variable for the class name. Instead, you can get into the class’s scope with class_eval.

ctwc/checked_attributes/no_eval.rb

def add_checked_attribute(klass, attribute)

*

klass.class_eval do

*

# ...

*

end

end

You’re in the class now, and you can define the reader and writer methods. Previously, you did that by using the def keyword in the String of Code. Again, you can no longer use def, because you won’t know the names of the methods until runtime. In place of def, you can use Dynamic Methods (Dynamic Method):

def add_checked_attribute(klass, attribute)

klass.class_eval do

*

define_method "#{attribute}=" do |value|

*

# ...

*

end

*

*

define_method attribute do

*

# ...

*

end

end

end

The previous code defines two Mimic Methods (Mimic Method) that are supposed to read and write an instance variable. How can the code do this without evaluating a String of Code? If you browse through Ruby’s documentation, you’ll find a few methods that manipulate instance variables, including Object#instance_variable_get and Object#instance_variable_set. Let’s use them:

def add_checked_attribute(klass, attribute)

klass.class_eval do

define_method "#{attribute}=" do |value|

*

raise 'Invalid attribute' unless value

*

instance_variable_set("@#{attribute}", value)

end

define_method attribute do

*

instance_variable_get "@#{attribute}"

end

end

end

“That’s it,” Bill says. “We now have a method that enters a class scope and defines instance methods that manipulate instance variables, and there’s no string-based eval to speak of. Now that our code is both working and eval-free, we can move on to the third step in our development plan.”

Quiz: Checked Attributes (Step 3)

Where you sprinkle some flexibility over today’s project.

To solve the boss’ challenge, you and Bill still need to implement a few important features. One of these features is described in the third step of your development plan: “validate attributes through a block.” Right now, your generated attribute raises an exception if you assign it nil or false. But it’s supposed to support flexible validation through a block.

Because this step changes the interface of add_checked_attribute, it also calls for an update of the test suite. Bill replaces the two test cases that checked for nil or false attributes with a single new test case:

ctwc/checked_attributes/block.rb

require 'test/unit'

class Person; end

class TestCheckedAttribute < Test::Unit::TestCase

def setup

*

add_checked_attribute(Person, :age) {|v| v >= 18 }

@bob = Person.new

end

def test_accepts_valid_values

@bob.age = 20

assert_equal 20, @bob.age

end

*

def test_refuses_invalid_values

*

assert_raises RuntimeError, 'Invalid attribute' do

*

@bob.age = 17

*

end

*

end

end

*

def add_checked_attribute(klass, attribute, &validation)

# ... (The code here doesn't pass the test. Modify it.)

end

Can you modify add_checked_attribute so that it passes the new tests?

Quiz Solution

You can pass the tests and solve the quiz by changing a couple of lines in add_checked_attribute:

*

def add_checked_attribute(klass, attribute, &validation)

klass.class_eval do

define_method "#{attribute}=" do |value|

*

raise 'Invalid attribute' unless validation.call(value)

instance_variable_set("@#{attribute}", value)

end

define_method attribute do

instance_variable_get "@#{attribute}"

end

end

end

“Step 3 was quick,” Bill notes. “Let’s move on to step 4.”

Quiz: Checked Attributes (Step 4)

Where you pull a Class Macro (Class Macro) from your bag of tricks.

The fourth step in your development plan asks you to change the Kernel Method to a Class Macro (Class Macro) that’s available to all classes.

What this means is that instead of an add_checked_attribute method, you want an attr_checked method that the boss can use in a class definition. Also, instead of taking a class and an attribute name, this new method should take only the attribute name, because the class is already available asself.

Bill updates the test case:

ctwc/checked_attributes/macro.rb

require 'test/unit'

class Person

*

attr_checked :age do |v|

*

v >= 18

*

end

end

class TestCheckedAttributes < Test::Unit::TestCase

def setup

*

@bob = Person.new

end

def test_accepts_valid_values

@bob.age = 20

assert_equal 20, @bob.age

end

def test_refuses_invalid_values

assert_raises RuntimeError, 'Invalid attribute' do

@bob.age = 17

end

end

end

Can you write the attr_checked method and pass the tests?

Quiz Solution

Think back to the discussion of class definitions in Class Definitions Demystified. If you want to make attr_checked available to any class definition, you can simply make it an instance method of either Class or Module. Let’s go for the first option:

ctwc/checked_attributes/macro.rb

*

class Class

*

def attr_checked(attribute, &validation)

define_method "#{attribute}=" do |value|

raise 'Invalid attribute' unless validation.call(value)

instance_variable_set("@#{attribute}", value)

end

define_method attribute do

instance_variable_get "@#{attribute}"

end

*

end

*

end

This code doesn’t even need to call to class_eval, because when the method executes, the class is already taking the role of self.

“That’s great,” Bill says. “One more step, and we’ll be done.” For this last step, however, you need to learn about a feature that we haven’t talked about yet: Hook Methods.

Hook Methods

Where you get one of Bill’s thorough lessons in advanced coding.

The object model is an eventful place. Lots of things happen there as your code runs: classes are inherited, modules are mixed into classes, and methods are defined, undefined, and removed. Imagine if you could “catch” these events like you catch GUI mouse-click events. You’d be able to execute code whenever a class is inherited or whenever a class gains a new method.

Well, it turns out you can do all these things. This program prints a notification on the screen when a class inherits from String:

ctwc/hooks.rb

class String

def self.inherited(subclass)

puts "#{self} was inherited by #{subclass}"

end

end

class MyString < String; end

<=

String was inherited by MyString

The inherited method is an instance method of Class, and Ruby calls it when a class is inherited. By default, Class#inherited does nothing, but you can override it with your own code as in the earlier example. A method such as Class#inherited is called a Spell: Hook Method because you can use it to hook into a particular event.

More Hooks

Ruby provides a motley bunch of hooks that cover the most newsworthy events in the object model. Just as you override Class#inherited to plug into the lifecycle of classes, you can plug into the lifecycle of modules by overriding Module#included and (in Ruby 2.0) Module#prepended:

module M1

def self.included(othermod)

puts "M1 was included into #{othermod}"

end

end

module M2

def self.prepended(othermod)

puts "M2 was prepended to #{othermod}"

end

end

class C

include M1

prepend M2

end

<=

M1 was included into C

M2 was prepended to C

You can also execute code when a module extends an object by overriding Module#extended. Finally, you can execute method-related events by overriding Module#method_added, method_removed, or method_undefined.

module M

def self.method_added(method)

puts "New method: M##{method}"

end

def my_method; end

end

<=

New method: M#my_method

These hooks only work for regular instance methods, which live in the object’s class. They don’t work for Singleton Methods (Singleton Method), which live in the object’s singleton class. To catch Singleton Method events, you can use BasicObject#singleton_method_added,singleton_method_removed, and singleton_method_undefined.

Module#included is probably the most widely used hook, thanks to a common metaprogramming spell that’s worthy of an example of its own.

Plugging into Standard Methods

The notion of hooks extends beyond specialized methods like Class#inherited or Module#method_added. Because most operations in Ruby are just regular methods, you can easily twist them into improvised Hook Methods.

For example, in Hook Methods, you learned how to override Module#included to execute code when a module is included. But you can also plug into the same event, so to speak, from the other side: because you include a module with the include method, instead of overriding Module#included, you can override Module#include itself.

For example:

module M; end

class C

def self.include(*modules)

puts "Called: C.include(#{modules})"

super

end

include M

end

<=

Called: C.include(M)

There is an important difference between overriding Module#included and overriding Module#include. Module#included exists solely to be used as a Hook Method, and its default implementation is empty. But Module#include has some real work to do: it must actually include the module. That’s why our hook’s code also should call the base implementation of Module#include throughsuper. If you forget super, you’ll still catch the event, but you won’t include the module anymore.

As an alternative to overriding, you can turn a regular method into a Hook Method by using an Around Alias (Around Alias). You can find an example of this technique in The Thor Example.

The VCR Example

VCR is a gem that records and replays HTTP calls. The Request class in VCR includes a Normalizers::Body module:

module VCR

class Request #...

include Normalizers::Body

#...

The Body module defines methods that deal with an HTTP message body, such as body_from. After the include, those methods become class methods on Request. Yes, you read that right: Request is gaining new class methods by including Normalizers::Body. But a class usually gets instancemethods by including a module—not class methods. How can a mixin like Normalizers::Body bend the rules and define class methods on its includer?

Look for the answer in the definition of the Body module itself:

gems/vcr-2.5.0/lib/vcr/structs.rb

module VCR

module Normalizers

module Body

def self.included(klass)

klass.extend ClassMethods

end

module ClassMethods

def body_from(hash_or_string)

# ...

The code above pulls off a convoluted trick. Body has an inner module named ClassMethods that defines body_from and other methods as regular instance methods. Body also has an included Hook Method (Hook Method). When Request includes Body, it triggers a chain of events:

· Ruby calls the included hook on Body.

· The hook turns back to Request and extends it with the ClassMethods module.

· The extend method includes the methods from ClassMethods in the Request’s singleton class. (You might remember this last part of the trick from Quiz: Module Trouble.)

As a result, body_from and other instance methods get mixed into the singleton class of Request, effectively becoming class methods of Request. How’s that for a complicated code concoction?

This ClassMethods-plus-hook idiom used to be quite common, and it was used extensively by the Rails source code. As you’ll see in Chapter 10, Active Support’s Concern Module, Rails has since moved to an alternate mechanism—but you can still find examples of the idiom in VCR and other gems.

Quiz: Checked Attributes (Step 5)

Where you finally earn Bill’s respect and the title of Master of Metaprogramming.

The following is the code that we wrote in the previous development step:

class Class

def attr_checked(attribute, &validation)

define_method "#{attribute}=" do |value|

raise 'Invalid attribute' unless validation.call(value)

instance_variable_set("@#{attribute}", value)

end

define_method attribute do

instance_variable_get "@#{attribute}"

end

end

end

This code defines a Class Macro (Class Macro) named attr_checked. This Class Macro is an instance method of Class, so it’s available to all classes. The final step in your development plan asks you to restrict access to attr_checked: it should be available only to those classes that include a module named CheckedAttributes. The test suite for this step is pretty much the same one you used in step 4, with a single additional line:

ctwc/checked_attributes/module.rb

require 'test/unit'

class Person

*

include CheckedAttributes

attr_checked :age do |v|

v >= 18

end

end

class TestCheckedAttributes < Test::Unit::TestCase

def setup

@bob = Person.new

end

def test_accepts_valid_values

@bob.age = 18

assert_equal 18, @bob.age

end

def test_refuses_invalid_values

assert_raises RuntimeError, 'Invalid attribute' do

@bob.age = 17

end

end

end

Can you remove attr_checked from Class, write the CheckedAttributes module, and solve the boss’ challenge?

Quiz Solution

You can copy the trick that you learned in The VCR Example. CheckedAttributes defines attr_checked as a class method on its includers:

*

module CheckedAttributes

*

def self.included(base)

*

base.extend ClassMethods

*

end

*

*

module ClassMethods

def attr_checked(attribute, &validation)

define_method "#{attribute}=" do |value|

raise 'Invalid attribute' unless validation.call(value)

instance_variable_set("@#{attribute}", value)

end

define_method attribute do

instance_variable_get "@#{attribute}"

end

end

*

end

*

end

Your boss will be delighted. These are the same Class Macro and module that she challenged you to write this morning. If you can write code like this, you’re on your way to mastering the art of metaprogramming.

Wrap-Up

Today you solved a difficult metaprogramming problem, writing your own useful Class Macro (Class Macro). Along the way, you also learned about the powerful eval method, its issues, and how to work around them. Finally, you got introduced to Ruby’s Hook Methods (Hook Method), and you used them to your advantage.

“You learned a lot this week, my friend,” Bill says, smiling for what seems like the first time this week. “Now you know enough to walk the metaprogramming path on your own. Before we take off for the weekend, let me tell you one last story.”

“A master developer,” Bill begins, “sits on top of a mountain, meditating…”