h1. Impresario
A light-weight workflow (state machine) library in Clojure.
h1. Overview
Impresario is a workflow system - mostly like a finite state machine, with the exception that an Impresario flow may transition through multiple nodes if the predicates defined in the flow allow for it.
Impresario flows are implemented as a map that defines the states, transitions, predicates that determine if those transitions should be followed and call back handlers which may be attached to specific events.
Workflow state is maintained in a map: the context. The context is availalbe to all of the predicates and triggers. Just as with many of the Clojure data strucutres, the context is immutable.
Predicates may access the context, but are unable to modify it. Triggers must return the context - which allows for modification by triggers.
Workflows once defined must be registered with Impresario in order to be used. When a workflow is registered, it is validated to ensure that: each state configured as the target of a transition exists; and each state in the flow is reachable (a state marked as a start state does not need to be reachable).
An Impresario flow must have a start state (marked with a @:start true@). The @on-enter!@ trigger for the :start state can be used to initialize the workflow's context.
h2. DSL
To make the process of defining the flows simpler, a DSL has been created. This is an example (from the unit tests) of a workflow that simulates a door. The door may be open or closed, and if the door is closed it may be locked. If the door is locked, it can not be opened.
(ns impresario.test.door-workflow (:use impresario.dsl impresario.core)) (on-enter! :start (assoc *context* :transitions [])) (defpredicate :start :closed true) (defpredicate :open :closed (get *context* :close-door)) (defpredicate :closed :locked (get *context* :lock-door)) (on-enter! :locked (assoc *context* :locked? true)) (defpredicate :closed :open (and (not (get *context* :locked?)) (not (get *context* :lock-door)))) (defpredicate :locked :closed (get *context* :locked?)) (on-transition! :locked :closed (dissoc *context* :locked?)) (on-transition-any! (update-in (dissoc *context* :close-door :lock-door :unlock-door) [:transitions] conj [*current-state* :=> *next-state*])) (defmachine :door-workflow (state :start {:start true :transitions [:closed]}) (state :closed {:transitions [:locked :open]}) (state :locked {:transitions [:closed]}) (state :open {:transitions [:closed]})) (register-workflow :door-workflow *door-workflow*)
h3. Bindings
Impresario makes use of several bindings to allow handlers to not have to take a long list of parameters. The available bindings within a predicate or handler are:
- workflow - the definition of the workflow
- current-state - the current state
- next-state - the subsequen state (if transitioning)
- context - the context
h3. @defmachine@
This is used to define the workflow. It must be given a keyword which names the workflow. States are defined within the @defmachine@ form.
States are declared with the @state@ form. They must have a name and a map of properties. The properties may contain: @:start@, @:transitions@ or @:stop@. @:start@ indicates that the state is a start state, where the workflow may be entered. @:stop@ indicates that the state is terminal, and if reached the workflow should no longer transition.
NB: @defmachine@ will create a package level var with the name of your worklfow. In the example above it creates @door-workflow@. This var contains the definition of your workflow.
To support interactive development, if you type in a @defmachine@ and attempt to evalutate it without defining the required predicates, Impresario will include a simple template for the predicate as part of the error message so that you may cut and paste it into your editor as a baseline.
h3. @register-workflow@
This funtion adds your workflow into the Impresario registry. It takes the name of the workflow (as a keyword) and the workflow definition.
h3. @defpredicate@
This is used to define a function in your package that will act as a transition predicate between two states.
h3. @on-enter-any!@
This declares a trigger that is fired any time a state is entered. Note that the body of this handler must return the context.
h3. @on-enter!@
Given a state name and a body, this will be the handler called when the workflow transitions into the given state.
h3. @on-exit!@
Given a state name and a body, this will be the handler called when the workflow transitions out of the given state.
h3. @on-transition-any!@
The body of this form will be called any time any transition is made. @*current-state@ and @next-state@ will be set to the names of the states involved in the transition.
h3. @on-transition!@
Given a from state, a to state and a body, will invoke the body whenever the specified edge is followed in the workflow.
h1. Installation
h3. "Leiningen":https://github.com/technomancy/leiningen
pre. [com.github.kyleburton/impresario "1.0.12"]
h3. Maven
pre. com.github.kyleburton impresario 1.0.12
h1. Future Direction
h3. Workflow Versioning
Introduce a second form of register-workflow that accepts a version. Make the default (when unspecified) "1.0.0".
h3. global exception state
A declared state, with possible transition's out, but no explicit edges leading in. If any predicate or trigger throws an exception, this state is entered. This gives the exception state the ability to handle errors and transition to one of the other states - or abort the workflow completely (by re-raising the exception).
h3. Timeouts
Each node should have the ability to declare a particular edge to be followed after some amount of time has elapsed.
Workflows should support a 'global' timeout. If the workflow has not transitioned (or been woken up) in the declared time span, it should cause an exception within the workflow. This allows workflow instances to be expired, discarded, etc.