- Notifications
You must be signed in to change notification settings - Fork209
Ruby finite-state-machine-inspired API for modeling workflow
License
geekq/workflow
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Note: you can find documentation for specific workflow rubygem versionsathttp://rubygems.org/gems/workflow : select a version (optional,default is latest release), click "Documentation" link. When reading ongithub.com, the README refers to the upcoming release.
Workflow is a finite-state-machine-inspired API for modeling andinteracting with what we tend to refer to as 'workflow'.
A lot of business modeling tends to involve workflow-like concepts, andthe aim of this library is to make the expression of these concepts asclear as possible, using similar terminology as found in state machinetheory.
So, a workflow has a state. It can only be in one state at a time. Whena workflow changes state, we call that a transition. Transitions occuron an event, so events cause transitions to occur. Additionally, when anevent fires, other arbitrary code can be executed, we call those actions.So any given state has a bunch of events, any event in a state causes atransition to another state and potentially causes code to be executed(an action). We can hook into states when they are entered, and exitedfrom, and we can cause transitions to fail (guards), and we can hook into every transition that occurs ever for whatever reason we can come upwith.
Now, all that’s a mouthful, but we’ll demonstrate the API bit by bitwith a real-ish world example.
Let’s say we’re modeling article submission from journalists. An articleis written, then submitted. When it’s submitted, it’s awaiting review.Someone reviews the article, and then either accepts or rejects it.Here is the expression of this workflow using the API:
classArticleincludeWorkflowworkflowdostate:newdoevent:submit,:transitions_to=>:awaiting_reviewendstate:awaiting_reviewdoevent:review,:transitions_to=>:being_reviewedendstate:being_revieweddoevent:accept,:transitions_to=>:acceptedevent:reject,:transitions_to=>:rejectedendstate:acceptedstate:rejectedendend
Nice, isn’t it!
Note: the first state in the definition (:new in the example, but youcan name it as you wish) is used as the initial state - newly createdobjects start their life cycle in that state.
Let’s create an article instance and check in which state it is:
article=Article.newarticle.accepted?# => falsearticle.new?# => true
You can also access the wholecurrent_state object including the listof possible events and other meta information:
article.current_state=> #<Workflow::State:0x7f1e3d6731f0 @events={ :submit=>#<Workflow::Event:0x7f1e3d6730d8 @action=nil, @transitions_to=:awaiting_review, @name=:submit, @meta={}>}, name:new, meta{}You can also check, whether a state comes before or after another state (by theorder they were defined):
article.current_state# => being_reviewedarticle.current_state <:accepted# => truearticle.current_state >=:accepted# => falsearticle.current_state.between?:awaiting_review,:rejected# => true
Now we can call the submit event, which transitions to the:awaiting_review state:
article.submit!article.awaiting_review?# => true
Events are actually instance methods on a workflow, and depending on thestate you’re in, you’ll have a different set of events used totransition to other states.
It is also easy to check, if a certain transition is possible from thecurrent state .article.can_submit? checks if there is a:submitevent (transition) defined for the current state.
gem install workflow
Important: If you’re interested in graphing your workflow state machine, you will also need toinstall theactivesupport andruby-graphviz gems.
Versions up to and including 1.0.0 are also available as a single file download -lib/workflow.rb file.
After installation or downloading the library you can easily try outall the example code from this README in irb.
$ irbrequire 'rubygems'require 'workflow'
Now just copy and paste the source code from the beginning of this READMEfile snippet by snippet and observe the output.
The best way is to use convention over configuration and to define amethod with the same name as the event. Then it is automatically invokedwhen event is raised. For the Article workflow defined earlier it wouldbe:
classArticledefrejectputs'sending email to the author explaining the reason...'endend
article.review!; article.reject! will cause state transition tobeing_reviewed state, persist the new state (if integrated withActiveRecord), invoke this user definedreject method and finallypersist therejected state.
Note: on successful transition from one state to another the workflowgem immediately persists the new workflow state withupdate_column(),bypassing any ActiveRecord callbacks includingupdated_at update.This way it is possible to deal with the validation and to save thepending changes to a record at some later point instead of the momentwhen transition occurs.
You can also define event handler accepting/requiring additionalarguments:
classArticledefreview(reviewer='')puts"[#{reviewer}] is now reviewing the article"endendarticle2=Article.newarticle2.submit!article2.review!('Homer Simpson')# => [Homer Simpson] is now reviewing the article
Alternative way is to use a block (only recommended for short eventimplementation without further code nesting):
event:review,:transitions_to=>:being_revieweddo |reviewer|# store the reviewerend
We’ve noticed, that mixing the list of events and states with the blocksinvoked for particular transitions leads to a bumpy and poorly readable codedue to a deep nesting. We tried (and dismissed) lambdas for this. Eventuallywe decided to invoke an optional user defined callback method with the samename as the event (convention over configuration) as explained before.
Note: Workflow 2.0 is a major refactoring for theworklow library.If your application suddenly breaks after the workflow 2.0 release, you’veprobably got your Gemfile wrong ;-). workflow usessemantic versioning.For highest compatibility please reference the desired major+minor version.
Note on ActiveRecord/Rails 4.*, 5.\* Support:
Since integration with ActiveRecord makes over 90% of the issues andmaintenance effort, and also to allow for an independent (faster) release cyclefor Rails support, starting with workflowversion 2.0 in January 2019 thesupport for ActiveRecord (4.*, 5.\* and newer) has been extracted into a separategem. Read atworkflow-activerecord, how toinclude the right gem.
To use legacy built-in ActiveRecord 2.3 - 4.* support, reference Workflow 1.2 inyour Gemfile:
gem 'workflow', '~> 1.2'
If you do not use a relational database and ActiveRecord, you can stillintegrate the workflow very easily. To implement persistence you justneed to overrideload_workflow_state andpersist_workflow_state(new_value) methods. Next section contains an example forusing CouchDB, a document oriented database.
Tim Lossen implemented supportforremodel /rediskey-value store.
We are using the compactcouchtiny libraryhere. But the implementation would look similar for the popularcouchrest library.
require'couchtiny'require'couchtiny/document'require'workflow'classUser <CouchTiny::DocumentincludeWorkflowworkflowdostate:submitteddoevent:activate_via_link,:transitions_to=>:proved_emailendstate:proved_emailenddefload_workflow_stateself[:workflow_state]enddefpersist_workflow_state(new_value)self[:workflow_state]=new_valuesave!endend
Please also have a look atthe full source code.
I get a lot of requests to integrate persistence support for differentdatabases, object-relational adapters, column stores, documentdatabases.
To enable highest possible quality, avoid too many dependencies and toavoid unneeded maintenance burden on theworkflow core it is best toimplement such support as a separate gem.
Only support for the ActiveRecord will remain for the foreseeablefuture. So Rails beginners can expectworkflow to work with Rails outof the box. Other already included adapters stay for a while but shouldbe extracted to separate gems.
If you want to implement support for your favorite ORM mapper or yourfavorite NoSQL database, you just need to implement a module whichoverrides the persistence methodsload_workflow_state andpersist_workflow_state. Example:
moduleWorkflowmoduleSuperCoolDbmoduleInstanceMethodsdefload_workflow_state# Load and return the workflow_state from some storage.# You can use self.class.workflow_column configuration.enddefpersist_workflow_state(new_value)# save the new_value workflow stateendendmoduleClassMethods# class methods of your adapter go hereenddefself.included(klass)klass.send:include,InstanceMethodsklass.extendClassMethodsendendend
The user of the adapter can use it then as:
classArticleincludeWorkflowincludeWorkflow:SuperCoolDbworkflowdostate:submitted# ...endend
I can then link to your implementation from this README. Please let mealso know, if you need any interface beyondload_workflow_state andpersist_workflow_state methods to implement an adapter for yourfavorite database.
Conditions can be a "method name symbol" with a corresponding instance method, aproc orlambda which are added to events, like so:
state:offevent:turn_on,:transition_to=>:on,:if=>:sufficient_battery_level?event:turn_on,:transition_to=>:low_battery,:if=>proc{ |device|device.battery_level >0}end# corresponding instance methoddefsufficient_battery_level?battery_level >10end
When calling adevice.can_<fire_event>? check, or attempting adevice.<event>!, each event is checked in turn:
With no
:ifcheck, proceed as usual.If an
:ifcheck is present, proceed if it evaluates to true, or drop to the next event.If you’ve run out of events to check (eg.
battery_level == 0), then the transition isn’t possible.
You can also pass additional arguments, which can be evaluated by :if methods or procs. See examples inconditionals_test.rb
We already had a look at the declaring callbacks for particular workflowevents. If you would like to react to all transitions to/from the same statein the same way you can use the on_entry/on_exit hooks. You can either define itwith a block inside the workflow definition or through namingconvention, e.g. for the state :pending just define the methodon_pending_exit(new_state, event, *args) somewhere in your class.
If you want to be informed about everything happening everywhere, e.g. forlogging then you can use the universalon_transition hook:
workflowdostate:onedoevent:increment,:transitions_to=>:twoendstate:twoon_transitiondo |from,to,triggering_event, *event_args|Log.info"#{from} ->#{to}"endend
If you want to do custom exception handling internal to workflow, you can define anon_error hook in your workflow.For example:
workflowdostate:firstdoevent:forward,:transitions_to=>:secondendstate:secondon_errordo |error,from,to,event, *args|Log.info"Exception(#{error.class}) on#{from} ->#{to}"endend
If forward! results in an exception,on_error is invoked and the workflow stays in a 'first' state. This capabilityis particularly useful if your errors are transient and you want to queue up a job to retry in the future withoutaffecting the existing workflow state.
If you want to halt the transition conditionally, you can just raise anexception in your [transition event handler](#transition_event_handler).There is a helper calledhalt!, which raises theWorkflow::TransitionHalted exception. You can provide an additionalhalted_because parameter.
defreject(reason)halt!'We do not reject articles unless the reason is important' \unlessreason =~/important/iend
The traditionalhalt (without the exclamation mark) is still supportedtoo. This just prevents the state change without raising anexception.
You can checkhalted? andhalted_because values later.
The whole event sequence is as follows:
before_transition
event specific action
on_transition (if action did not halt)
on_exit
PERSIST WORKFLOW STATE (i.e. transition) or on_error
on_entry
after_transition
You can easily reflect on workflow specification programmatically - forthe whole class or for the current object. Examples:
article2.current_state.events# lists possible events from herearticle2.current_state.events[:reject].transitions_to# => :rejectedArticle.workflow_spec.states.keys#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]Article.workflow_spec.state_names#=> [:rejected, :awaiting_review, :being_reviewed, :accepted, :new]# list all events for all statesArticle.workflow_spec.states.values.collect &:events
You can also store and later retrieve additional meta data for everystate and every event:
classMyProcessincludeWorkflowworkflowdostate:main,:meta=>{:importance=>8}state:supplemental,:meta=>{:importance=>1}endendputsMyProcess.workflow_spec.states[:supplemental].meta[:importance]# => 1
The workflow library itself uses this feature to tweak the graphicalrepresentation of the workflow. See below.
For an advance example please seeworkflow_from_json_test.rb.
In case you have very extensive workflow definition or would like to reuseworkflow definition for different classes, you can include parts like intheincluding a child workflow definition example.
You can generate a graphical representation of the workflow fora particular class for documentation purposes.UseWorkflow::create_workflow_diagram(class) in your rake task like:
namespace:docdodesc"Generate a workflow graph for a model passed e.g. as 'MODEL=Order'."task:workflow=>:environmentdorequire'workflow/draw'Workflow::Draw::workflow_diagram(ENV['MODEL'].constantize)endend
sudo apt-get install graphviz# Linuxbrew install graphviz# Mac OScd workflowgem install bundlerbundle install# run all the testsbundleexec raketest
❏ unit tests for the new behavior provided: new tests fail without you change, all tests succeed with your change
❏ documentation update included
ActiveAdmin-Workflow - is anintegration withActiveAdmin.
Author: Vladimir Dobriakov,https://infrastructure-as-code.de
Copyright (c) 2010-2024 Vladimir Dobriakov and Contributors
Copyright (c) 2008-2009 Vodafone
Copyright (c) 2007-2008 Ryan Allen, FlashDen Pty Ltd
Based on the work of Ryan Allen and Scott Barron
Licensed under MIT license, see the MIT-LICENSE file.
About
Ruby finite-state-machine-inspired API for modeling workflow
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Uh oh!
There was an error while loading.Please reload this page.
Contributors7
Uh oh!
There was an error while loading.Please reload this page.