Skip to content

Instantly share code, notes, and snippets.

@nakajima
Created November 12, 2008 07:47
Show Gist options
  • Select an option

  • Save nakajima/24105 to your computer and use it in GitHub Desktop.

Select an option

Save nakajima/24105 to your computer and use it in GitHub Desktop.
The "gist" of my talk from nyc.rb
=begin
nyc.rb talk examples
These examples are centered around a DSL for generating
HTML for giving a talk. The basic syntax for it looks
like this:
Talk "Pat's Projects" do
slide "controller-validations"
slide "Why?"
slide "Because Rails doesn't have them."
slide "Just kidding guys."
slide "alias_alias_alias"
slide "alias_maybe_chain"
slide "alias_monster_chain"
end
contact: github.com/nakajima
=end
%w(rubygems spec open-uri nokogiri).each { |lib| require lib }
# Couldn't really fit method director code in a nice clean way,
# so we're downloading it from Gist #23362 and evaling it.
puts "loading context_eval code from Gist #23362" unless defined?(Spec::Mate)
eval(open('http://gist.github.com/23362.txt').read)
describe "making sure that the download and eval from Gist worked" do
it "should have context_eval" do
Object.new.should respond_to(:context_eval)
end
end
# The Nakajima::Talk class is the sandbox where all of our DSL
# logic lives. It uses a plain instance_eval to handle the block
# passed on instantiation. I've pulled that behavior into the
# handle_block method, since we'll be overriding it in subclasses
# in order to illustrate different techniques.
module Nakajima
class Talk
attr_reader :name, :slides
def initialize(name, &block)
@name = name
@slides = []
handle_block(&block)
end
def handle_block(&block)
instance_eval(&block)
end
def slide(name)
slides.push(name)
end
def to_html
buffer = []
buffer << "<h1>#{name}</h1>"
buffer << "<ul>"
slides.each { |s| buffer << "<li>#{s}</li>" }
buffer << "</ul>"
buffer.join("\n")
end
end
end
# Allow for Hpricot-eque class/method arity-based meaning.
def Talk(*args, &block)
args.empty? ?
Nakajima::Talk :
Nakajima::Talk.new(*args, &block)
end
describe Nakajima::Talk, "basic behaviors" do
attr_reader :html, :talk
before do
@talk = Talk("Example") { slide "A slide" }
end
describe "slides" do
it "are added by #slide method" do
proc {
talk.slide "Another"
}.should change(talk.slides, :length).by(1)
end
end
describe "generates HTML" do
before { @html = Nokogiri(talk.to_html) }
it "generates HTML for header" do
html.at('h1').text.should == "Example"
end
it "generates HTML for slides" do
html.search('li').should have(1).node
end
end
end
# A speaker can create a speak. While doing so, a
# Talk is created, using the speaker's prepared slides.
#
# This is where we get into trouble. See the following
# spec example groups to see how.
class Speaker
attr_reader :name
def initialize(name, talk_type=Nakajima::Talk)
@name = name
@talk_type = talk_type
end
def speak
@talk_type.new("A talk about slides") do
prepared_slides.each { |name| slide name }
end
end
def prepared_slides
["My name is #{name}.", "I am prepared."]
end
end
describe Speaker, "basic behaviors" do
attr_reader :speaker
before do
@speaker = Speaker.new("Pat")
end
it "has a name" do
speaker.name.should == "Pat"
end
it "has prepared slides" do
speaker.should have(2).prepared_slides
end
end
=begin
-------------- LET THE GREAT EXPERIMENT BEGIN! --------------
=end
describe "the trouble with instance_eval" do
attr_reader :speaker
before { @speaker = Speaker.new("Pat") }
it "does not allow a Speaker to use prepared_slides" do
proc {
speaker.speak
}.should raise_error(/undefined local variable or method `prepared_slides' for #<Nakajima::Talk:[a-z0-9]+>/)
end
end
describe "solutions to instance_eval trouble: " do
describe "skipping instance_eval all-together" do
attr_reader :ugly_talk, :ugly_speaker
before(:each) do
# This implementation of Talk does not use instance_eval. Instead,
# it just calls the block, passing itself as an argument on
# which methods can be called.
#
# It's quite safe, but it requires a different implementation on
# the part of the Speaker class.
@ugly_talk = Class.new(Nakajima::Talk) do
def handle_block(&block)
block.call(self)
end
end
# This implementation of speaker uses the ugly_talk class to
# generate its talk. In addition, it uses a block argument to
# call the #slide method
@ugly_speaker = Class.new(Speaker) {
def speak
@talk_type.new("A talk about slides") do |talk|
prepared_slides.each { |name| talk.slide name }
end
end
}.new("Pat", ugly_talk)
end
it "can speak without raising an error" do
proc {
ugly_speaker.speak
}.should_not raise_error
end
it "has two slides" do
ugly_speaker.speak.should have(2).slides
end
end
describe "storing a reference to self and using in the block" do
attr_reader :annoyed_speaker
before(:each) do
# This implementation of Speaker handles its talk creation in a
# way that doesn't require the Nakajima::Talk class to be
# implemented any differently.
#
# This technique is often your only option for working with
# some libraries that use instance_eval.
@annoyed_speaker = Class.new(Speaker) {
def speak
this = self # storing a local reference to self
@talk_type.new("A talk about slides") do
this.prepared_slides.each { |name| slide name }
end
end
}.new("Pat")
end
it "can speak without raising an error" do
proc {
annoyed_speaker.speak
}.should_not raise_error
end
it "has two slides" do
annoyed_speaker.speak.should have(2).slides
end
end
describe "instance_eval with delegation" do
attr_reader :good_talk, :happy_speaker
before(:each) do
# This subclass of the Nakajima::Talk class handles the block
# in a way that doesn't require the Speaker class to be implemented
# any differently
@good_talk = Class.new(Nakajima::Talk) do
def handle_block(&block)
@context = eval('self', block.binding)
instance_eval(&block)
end
def method_missing(sym, *args, &block)
@context.respond_to?(sym) ? @context.send(sym, *args, &block) : super
end
end
@happy_speaker = Speaker.new("Pat", good_talk)
end
it "can speak without raising an error" do
proc {
happy_speaker.speak
}.should_not raise_error
end
it "has two slides" do
happy_speaker.speak.should have(2).slides
end
end
describe "method director, using context_eval" do
attr_reader :overkill_talk, :happy_speaker
before(:each) do
# This subclass of Nakajima::Talk handles the block using context_eval,
# which is just a wrapper around the Method Director pattern. Unfortunately
# our example isn't quite complex enough for it to make sense for use to use
# this pattern, but again, there are few cases in the wild that are either.
#
# The upside to this approach is that you don't need to change speaker, but you
# get that with instance_eval with delegation too. Buyer beware.
@overkill_talk = Class.new(Nakajima::Talk) do
def handle_block(&block)
context_eval(self, &block)
end
end
@happy_speaker = Speaker.new("Pat", overkill_talk)
end
it "can speak without raising an error" do
proc {
happy_speaker.speak
}.should_not raise_error
end
it "has two slides" do
happy_speaker.speak.should have(2).slides
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment