Groovy FSM DSL (with closures)





3
Date Submitted Thu. Apr. 12th, 2007 1:42 PM
Revision 1 of 1
Helper esumerfd
Tags dsl | FSM | groovy | Machines | State
Comments 1 comments
Try to create a DSL for a finite state machine.

Inspired by Chris vanBuskirk version on this site and the Ruby article at

http://www-128.ibm.com/developerworks/java/library/j-cb03137/index.html
package simulation.finitestatemachine;
/**
 * GOAL: Try to create a DSL for a finite state machine.
 * Inspired by Chris vanBuskirk version on this site and the Ruby article at
 *       http://www-128.ibm.com/developerworks/java/library/j-cb03137/index.html,
 */


class FiniteStateMachineTest extends GroovyTestCase
{
    void testNormalProcess()
    {
        def fsm = FiniteStateMachine.newInstance('submitted')

        def recorder = fsm.record()

        recorder.on('accept') {
            from('processing').to('accepted')
            from('nonprofit_reviewing').to('accepted')
        }

        recorder.on('receive').from('submitted').to('processing')

        recorder.on('send_for_review') {
            from('processing').to('nonprofit_reviewing')
            from('nonprofit_reviewing').to('processing')
            from('accepted').to('nonprofit_reviewing')
        }

        recorder.on('revise').from('accepted').to('processing')

        assert 'processing' == fsm.fire('receive')
        assert 'accepted' == fsm.fire('accept')
    }

    void testReset()
    {
        def fsm = FiniteStateMachine.newInstance('1')
        def recorder = fsm.record()
        recorder.on('a') {
            from('1').to('2')
            from('2').to('3')
        }

        assert '2' == fsm.fire('a')

        fsm.reset()

        assert '2' == fsm.fire('a')
    }
   
    void testNoTransitionForEvent()
    {
        def fsm = FiniteStateMachine.newInstance('1')
        def recorder = fsm.record()
        recorder.on('a').from('1').to('2')
       
        fsm.fire('a')

        shouldFail {
            fsm.fire('a')
        }
    }

    void testNoInitialState()
    {
        shouldFail {
            FiniteStateMachine.newInstance()
        }
    }

    void testInvalidInitialState()
    {
        def fsm = FiniteStateMachine.newInstance('INVALID')
        fsm.record().on('1').from('a').to('b')

        shouldFail {
            fsm.fire('1')
        }
    }

    void testNoStateForEvent()
    {
        def fsm = FiniteStateMachine.newInstance('a')
        fsm.record().on('1').from('a').to('b')
        fsm.record().on('2').from('X').to('Y')
        fsm.fire('1')

        shouldFail {
            fsm.fire('2')
        }
    }

    void testDupFromState()
    {
        def fsm = FiniteStateMachine.newInstance('a')
        def recorder = fsm.record()
        recorder.on('SAME').from('DUPLICATESTATE').to('a')

        shouldFail {
            recorder.on('SAME').from('DUPLICATESTATE').to('b')
        }
    }
   
    void testInvalidToState()
    {
        def fsm = FiniteStateMachine.newInstance('a')
        def recorder = fsm.record()

        shouldFail {
            recorder.on('1').from('DUPLICATESTATE').to(null)
        }
    }
   
    void testIncompleteTransition()
    {
        def fsm = FiniteStateMachine.newInstance('a')
        def recorder = fsm.record()

        shouldFail {
            recorder.on('1').to('a')
        }
    }
   
    void testIncompleteTransitionWithClosure()
    {
        def fsm = FiniteStateMachine.newInstance('a')
        def recorder = fsm.record()

        shouldFail {
            recorder.on('1') {
                to('a')
            }
        }
    }
}

class FiniteStateMachine
{
    /*
     * A map or maps to store events and from states of each
     * transition.
     */

    def transitions = [:]
   
    def initialState
    def currentState
   
    FiniteStateMachine(a_initialState)
    {
        assert a_initialState, "You need to provide an initial state"

        initialState = a_initialState
        currentState = a_initialState
    }
   
    def record()
    {
        Grammer.newInstance(this);
    }
   
    def registerTransition(a_grammer)
    {
        assert a_grammer.isValid(), "Invalid transition (${a_grammer})"

        def transition

        def event = a_grammer.event
        def fromState = a_grammer.fromState
        def toState = a_grammer.toState
       
        if (!transitions[event])
        {
            transitions[event] = [:]
        }

        transition = transitions[event]
        assert !transition[fromState], "Duplicate fromState ${fromState} for transition ${a_grammer}"

        transition[fromState] = toState
    }

    def reset()
    {
        currentState = initialState
    }

    def fire(a_event)
    {
        assert currentState, "Invalid current state '${currentState}': pass into constructor"
        assert transitions.containsKey(a_event), "Invalid event '${a_event}', should be one of ${transitions.keySet()}"
       
        def transition = transitions[a_event]
        def nextState = transition[currentState]
       
        assert nextState, "There is no transition from '${currentState}' to any other state"
           currentState = nextState
           
           currentState
    }

    def isState(a_state)
    {
        currentState == a_state
    }
}

class Grammer
{
    def fsm

    def event
    def fromState
    def toState

    Grammer(a_fsm)
    {
        fsm = a_fsm
    }

    def on(a_event)
    {
        event = a_event
        this
    }

    def on(a_event, a_transitioner)
    {
        on(a_event)
       
        a_transitioner.delegate = this
        a_transitioner.call()

        this
    }
   
    def from(a_fromState)
    {
        fromState = a_fromState
        this
    }

    def to(a_toState)
    {
        assert a_toState, "Invalid toState: ${a_toState}"

        toState = a_toState
        fsm.registerTransition(this)
        this
    }
   
    def isValid()
    {
        event && fromState && toState
    }
   
    public String toString()
    {
        "${event}:${fromState}=>${toState}"
    }
}
 

Edward Sumerfield

Comments

Comments Unstanding DSL with Groov
Mon. May. 21st, 2007 3:59 PM    Newbie bgoetzmann

Voting