Created
November 12, 2008 07:47
-
-
Save nakajima/24105 to your computer and use it in GitHub Desktop.
The "gist" of my talk from nyc.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| =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