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:
Write a Kernel Method () named add_checked_attribute using eval to add a super-simple validated attribute to a class.
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 (), add_checked_attribute is supposed to be a simple Kernel Method (). Second, attr_accessor is written in C, while add_checked_attribute should use plain Ruby (and a 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:
| 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?
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 . When you tell attr_accessor that you want an attribute named, say, :my_attr, it generates two Mimic Methods () like the following:
| def my_attr |
| @my_attr |
| end |
| |
| def my_attr=(value) |
| @my_attr = value |
| end |
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 () 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 (), 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.”