# # Copyright (c) 2008 Marcus Westin Design # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # module SequenceContract class Contractor # Create a sequence contract. The contractor gets creater by SequenceContract#sequence_contract # and should never be called directly def initialize contractee_class @contractee_class = contractee_class @transitions = Hash.new(false) @monitored_methods = [:initialize] @current_state = Hash.new # Indexed by object instances @accept_states = [] @define_state = false @start_state = false @frozen = false return self end attr_reader :monitored_methods def freeze @frozen = true end # Add a contractee instance to the set of contractees. # This method automatically gets called by SequenceContract upon extension, # and so should never be called directly def add_contractee contractee_instance @current_state[contractee_instance.object_id] = @start_state end # Declare the starting state and its transition # # Example: # # start :start # # Example # start :beginning def start first_state=:start, &block @start_state = first_state state first_state, &block end alias_method :start_state, :start # Declare a state and it's legal #transitions. # # Example: # # start :start # accept :done # # state :start do # transition 'greet' => :chat # transition 'ignore' => :done # end # # state :chat do # transition 'ask_qestion' => :chat # transition 'talk' => :chat # transition 'give_answer' => :chat # transition 'say_goodbye' => :done # end def state state, &block @define_state = state throw :DoubleStateDefinition if @transitions[@define_state] @transitions[@define_state] = Hash.new instance_eval &block @define_state = false end # Declare a transition from the current state # # Example: # # state :chat do # transition 'ask_qestion' => :chat # transition 'talk' => :chat # transition 'give_answer' => :chat # transition 'say_goodbye' => :done # end def transition tr raise NoStateDefined unless @define_state tr.each do |method, state| method = method.to_sym @monitored_methods << method @transitions[@define_state][method] = state SequenceContract.extend_method @contractee_class, method, SequenceContract.extended_method_actions(method) end end # Transition from the current state with the given method. # This method gets called from a redefined version of the # give method, and should never have to be called directly. # do_transition raises IllegalSequence if the current state # does not have the given method as a legal transition. def do_transition method, contractee_instance legal_transitions = @transitions[@current_state[contractee_instance.object_id]] raise(IllegalSequence, "Method #{method} called for #{contractee_instance} while in state without legal transitions") unless legal_transitions raise(IllegalSequence, "Method #{method} called for #{contractee_instance} while in state #{@current_state[contractee_instance.object_id]}") unless legal_transitions.has_key? method @current_state[contractee_instance.object_id] = legal_transitions[method] end end # Exceptions raised by a SequenceContract class ContractException < Exception end # Raised by SequenceContract#transition when called outside of a #state block class NoStateDefined < ContractException end # Raised when a method call is made that is not a legal transition class IllegalSequence < ContractException end # Extend a class with a method, and have it perform the string of actions passed in. # Any given class will only be allowed to be extended once using this method. def SequenceContract.extend_method extend_class, method, actions if extend_class.sequence_contractor.monitored_methods.include? method and (@currently_defining != method) @currently_defining = method method_renamed = "__sequence__#{method}".to_sym if not extend_class.method_defined? method_renamed actions = %{ alias_method #{method_renamed.inspect}, #{method.inspect} def #{method}(*args) #{actions} return #{method_renamed}(*args) end } #puts actions extend_class.class_eval actions end end @currently_defining = nil if method == :initialize end def SequenceContract.re_extend_method extend_class, method, actions if extend_class.sequence_contractor.monitored_methods.include? method and (@currently_defining != method) @currently_defining = method method_renamed = "__sequence__#{method}".to_sym actions = %{ alias_method #{method_renamed.inspect}, #{method.inspect} def #{method}(*args) #{actions} return #{method_renamed}(*args) end } extend_class.class_eval actions end @currently_defining = nil if method == :initialize end # Get a string with the appropriate actions to be performed by the # wrapped/extended/decorated method def SequenceContract.extended_method_actions method return 'self.class.sequence_contractor.add_contractee self' if method == :initialize return "self.class.sequence_contractor.do_transition(#{method.inspect}, self)" end def SequenceContract.listen_for_method_redefinitions includor def includor.method_added method SequenceContract.re_extend_method self, method, SequenceContract.extended_method_actions(method) end end def SequenceContract.included includor includor.class_eval "@sequence_contractor = Contractor.new self" def includor.sequence_contractor @sequence_contractor end # Pass the sequence contract on to all subclasses def includor.inherited includor_subclass # Make all subclasses have the same sequence contract as the includor class def includor_subclass.sequence_contractor return superclass.sequence_contractor end # Listen for initialize being added SequenceContract.listen_for_method_redefinitions includor_subclass end SequenceContract.extend_method includor, :initialize, extended_method_actions(:initialize) SequenceContract.listen_for_method_redefinitions includor class << includor # Write a sequence contract for this class. The sequence contract block # gets executed within the scope of a SequenceContract::Contractor. # # Example: # # # class Greeter # def greet # puts "Hello!" # end # # def ignore # puts "Pss!" # end # # def ask_question # puts "How are you?" # end # # def talk # puts "Blah blah blah" # end # # def give_answer # puts "Good thank you!" # end # # def say_goodbye # puts "Bye!" # end # # def leave # puts "Walking away..." # end # # extend SequenceContract # sequence_contract do # start :start # accept :done # state :start do # transition 'greet' => :chat # transition 'ignore' => :walk_away # end # state :chat do # transition 'ask_question' => :chat # transition 'talk' => :chat # transition 'give_answer' => :chat # transition 'say_goodbye' => :walk_away # end # state :walk_away do # transition 'leave' => :done # end # end # end # # # Should not raise an IllegalSequence # g = Greeter.new # g.greet # g.ask_question # g.say_goodbye # g.leave # # # Should raise an IllegalSequence # g = Greeter.new # g.greet # g.ask_question # # Must say_goodbye before you leave! # g.leave def sequence_contract &block # Use the contractor to declare a lawful sequence. sequence_contractor.instance_eval(&block) # Don't allow #sequence_contractor.freeze end end end end if $0 == __FILE__ require 'test/unit' class TestSequenceContract < Test::Unit::TestCase class Greeter def greet puts "Hello!" end def ignore puts "Pss!" end def ask_question puts "How are you?" end include SequenceContract def initialize end def talk puts "Blah blah blah" end def give_answer puts "Good thank you!" end def say_goodbye puts "Bye!" end def leave puts "Walking away..." end # Declare a sequential contract sequence_contract do start_state :start do transition 'greet' => :chat transition 'ignore' => :walk_away end state :chat do transition 'ask_question' => :chat transition 'talk' => :chat transition 'give_answer' => :chat transition 'say_goodbye' => :walk_away end state :walk_away do transition 'leave' => :done end end end class InheritedGreeter < Greeter puts "InhreitedGreeter: I'm redefining ask_question!" def ask_question puts "Inherited How are you?" end puts "InhreitedGreeter: I'm redefining talk!" def talk puts "Inherited Blah blah blah" end puts "InhreitedGreeter: I'm redefining leave!" def leave puts "Inherited Walking away..." end puts "InhreitedGreeter: I'm redefining another_function!" def another_function puts "I should be able to be called whenever" end end def illegal_sequence g assert_nothing_raised do g.greet g.ask_question end assert_raises SequenceContract::IllegalSequence do g.leave end end def no_transition g assert_nothing_raised do g.ignore g.leave end assert_raises SequenceContract::IllegalSequence do g.leave end end def legal_sequence g assert_nothing_raised do g.greet g.ask_question g.say_goodbye g.leave end end def test_illegal_sequence g = Greeter.new assert_nothing_raised do g.greet g.ask_question end assert_raises SequenceContract::IllegalSequence do g.leave end end def test_no_transition g = Greeter.new no_transition g end def test_legal_sequence g = Greeter.new legal_sequence g end def test_multiple_instances g = Greeter.new assert_nothing_raised do g.greet g.ask_question g.say_goodbye end g = Greeter.new assert_raises SequenceContract::IllegalSequence do g.leave end assert_nothing_raised do g.greet g.ask_question g.say_goodbye g.leave end end def test_multiple_instances_inherited g = InheritedGreeter.new assert_nothing_raised do g.greet g.ask_question g.say_goodbye end g = InheritedGreeter.new assert_raises SequenceContract::IllegalSequence do g.leave end assert_nothing_raised do g.greet g.ask_question g.say_goodbye g.leave end end def test_multiple_instances_mixed g = InheritedGreeter.new assert_nothing_raised do g.greet g.ask_question g.say_goodbye end g = Greeter.new assert_raises SequenceContract::IllegalSequence do g.leave end assert_nothing_raised do g.greet g.ask_question g.say_goodbye g.leave end g = InheritedGreeter.new assert_raises SequenceContract::IllegalSequence do g.leave end assert_nothing_raised do g.greet g.ask_question g.say_goodbye g.leave end g = Greeter.new assert_nothing_raised do g.greet g.ask_question g.say_goodbye end end def test_illegal_sequence_inherited g = InheritedGreeter.new illegal_sequence g end def test_no_transition_inherited g = InheritedGreeter.new no_transition g end def test_legal_sequence_inherited g = InheritedGreeter.new legal_sequence g end end # TestSequenceContract end # if testing