Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Created June 17, 2009 19:13
Show Gist options
  • Save tenderlove/131431 to your computer and use it in GitHub Desktop.
Save tenderlove/131431 to your computer and use it in GitHub Desktop.
require 'visitor'
##
# This Visitor knows how to turn any object in to an Array
class AsArray < Visitor
visitor_for Array do |array|
array
end
visitor_for Object do |o|
[o]
end
end
require 'visitor'
###
# This Visitor knows how to do stuff with arguments based on class type.
class AsFieldsForFormObject < Visitor
def initialize args
@args = args
end
##
# If the recipient is a String or Symbol, we just want to return the first
# from @arg
visitor_for String, Symbol do |thing|
@args.first
end
###
# If it is an object, we want to get it as a form object
visitor_for Object do |thing|
form_for(thing).as_form_object
end
end
require 'visitor'
##
# This class knows how to turn any object in to a form object.
class AsFormObject < Visitor
##
# If it is a String, Symbol, Fixnum, or Object just return the thing
visitor_for String, Symbol, Fixnum do |thing|
thing
end
##
# If it's an Array, we just want the last bit of the array, and we'll convert
# it to a form object as well
visitor_for Array do |list|
list.last.accept self
end
end
require 'visitable'
require 'visitor'
require 'as_form_object'
require 'as_array'
require 'as_fields_for_form_object'
###
# This class sets up a proxy to +thing+. The purpose of this class is to let
# us convert an object to something else without knowing what visitor is
# required to do that conversion:
#
# p form_for([1, :two, [3]]).as_form_object
#
class ConversionProxy < Struct.new(:thing)
def as_form_object
thing.accept AsFormObject.new
end
def as_array
thing.accept AsArray.new
end
def as_fields_for_form_object args
thing.accept AsFieldsForFormObject.new(args)
end
end
def form_for thing
ConversionProxy.new(thing)
end
p form_for(:two).as_form_object
p form_for(:two).as_array
p form_for([:two]).as_array
p form_for(:two).as_fields_for_form_object [:foo]
p form_for(['hello world']).as_fields_for_form_object [:foo]
##
# The visitable module makes a class "visitable". It follows the double dispatch
# pattern and simply calls "visit" on the argument with itself.
module Visitable
def accept visitor
visitor.visit self
end
end
##
# For this example, we want to make *all* objects visitable, so we'll mix the
# Visitable module in to Object.
class Object
include Visitable
end
##
# The Visitor class is our base class for all visitors. It implements the
# "visit" method which Object#accept will call.
class Visitor
###
# Dynamically create a visitor method for each class in +klasses+
def self.visitor_for *klasses, &block
klasses.each do |klass|
define_method(:"visit_#{klass.name}", block)
end
end
##
# This method will examine the class and ancestors of +thing+. For each
# class in the "ancestors" list, it will check to see if the visitor knows
# how to handle that particular class. If it can't find a handler for the
# +thing+ it will raise an exception.
def visit thing
thing.class.ancestors.each do |ancestor|
method_name = :"visit_#{ancestor.name}"
next unless respond_to? method_name
return send method_name, thing
end
raise "Can't handle #{thing.class}"
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment