Groovy FSM DSL (with closures)
2
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
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}"
}
}
/**
* 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}"
}
}






I've now a better understanding of DSL with Groovy.
Recently I've had to implement the State pattern in Java and C#, and it's clear that using a such DSL is more clear and concise !
In my C# application I've used an event (with the delegate notion) to notify some other part of my code when the current state moved from one state to another.
It would be interesting to do so with Groovy in order to execute some custom code when the FSM changes its current state.
Bertrand Goetzmann.
http://www.odelia-technologies.com/