open.a.table.today.at 2The users of the system requested some documentation to help them in building their business rules. Obviously we could have written or generated some documentation, but I thought displaying valid options within the system was a better choice.
The solution involves executing your DSL code in the syntax checking context and having key methods return Stunt Double instances. In the above code
open
is clearly a DSL keyword. In the syntax checking context open
would be defined as:def open
OpenStuntDouble.new(@messenger)
end
OpenStuntDouble
is a class that inherits from StuntDouble and responds to all public instance methods of Open.class OpenStuntDoubleAny method that is executed on OpenStuntDouble will return a new instance of AStuntDouble. However, any method call that results in method_missing being executed will be reported as an invalid method call. Each invalid method call will also report the full list of available methods. Using this error you can provide your users with feedback on valid syntax.
def initialize(block)
super("open", &block)
end
stand_in_for(ThoughtWorks::Open) { AStuntDouble.new(@messenger) }
end
Of course, the majority of the magic is in the StuntDouble class.
class StuntDoubleOf course, no code is complete without tests.
alias __methods__ methods
alias __class__ class
def initialize(name, &block)
raise "a messenger proc is required for stunt double" unless block_given?
@messenger = block
@name = name
end
def self.stand_in(*array)
block = block_given? ? Proc.new : Proc.new {}
array.each do |element|
define_method(element.to_sym, &block)
end
end
def self.stand_in_for(mod)
block = block_given? ? Proc.new : Proc.new {}
stand_in(*mod.public_instance_methods, &block)
end
def method_missing(sym, *args)
@notify_block.call "#{does_not_support(sym)} #{does_support}"
end
private
attr_reader :name
def does_support
return "Supported: #{valid_methods}." if valid_methods != ''
"Nothing is supported."
end
def does_not_support(sym)
"#{name} does not support #{sym.to_s}."
end
def valid_methods
result = self.__class__.public_instance_methods.sort.select do |operation|
!@@excluded.include? operation
end
result.join(', ')
end
@@excluded = public_instance_methods.select { |method| method =~ /^__.+/ }
@@excluded += %w(to_s inspect method_missing instance_eval)
instance_methods.each { |m| undef_method m unless @@excluded.include? m }
end
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")The end result is user friendly errors that can be displayed directly to your users.
class StuntDoubleTest < Test::Unit::TestCase
class Inner < BlankSlate
def included_from_elsewhere
end
end
class FooStuntDouble < StuntDouble
end
class BarStuntDouble < StuntDouble
end
def setup_foo
FooStuntDouble.stand_in(:bar) { }
FooStuntDouble.stand_in_for(Inner)
end
def test_name_is_returned_from_message
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
assert_equal 'foo does not support zeb. Supported: bar, included_from_elsewhere.', error
return
end
stunt.zeb
fail 'assertion skipped'
end
def test_name_is_returned_from_message_but_nothing_supported
stunt = BarStuntDouble.new("bar") do |error|
assert_equal 'bar does not support zeb. Nothing is supported.', error
return
end
stunt.zeb
fail 'assertion skipped'
end
def test_methods_are_added_from_array
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
fail "error"
end
stunt.bar
end
def test_methods_are_added_from_class
setup_foo
stunt = FooStuntDouble.new("foo") do |error|
fail "error"
end
stunt.included_from_elsewhere
end
end
open does not support the. Supported: a.An advantage to this form of syntax help is that as the system evolves so will the help without any additional effort.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.