Skip to content

Repository files navigation

FlowMachine

Build finite state machines in a backend-agnostic, class-centric way.

The basic features will work with any PORO, and more features and callbacks are available when used with an ORM like ActiveRecord and/or ActiveModel::Dirty.

Circle CI Status:Circle CI

Raison d'être

After exploring several of the existing Ruby state machine options, they all seem too tightly coupled to an ORM models and tend to pollute the object model's code far too much. The goal of this gem is to provide a clean, testable interface for working with a state machine that decouples as much as possible from the model object itself.

Upgrading

CHANGELOG.md will contain changes that need to be made at each version

Requirements

Ruby 2.0+

Simple Usage

classBlogPostattr_accessor:state,:title,:body,:authordefinitializeself.state=:draftendendclassPublishingWorkflowincludeFlowMachine::WorkflowstateDraftStatestatePublishedStateendclassDraftState < FlowMachine::WorkflowStateevent:publishdotransitionto: :publishedendendclassPublishedState < FlowMachine::WorkflowStateon_enter:notify_email_authoron_exit:clear_published_atdefnotify_email_author# Send an emailenddefclear_published_atobject.published_at=nilendend
blog_post=BlogPost.newblog_post.author="author@example.org"workflow=PublishingWorkflow.new(blog_post)workflow.publish# notify_email_author is called (returns true if successful)workflow.published?# => trueblog_post.state# => :published

transition!

If you are using the workflow around an ORM model like ActiveRecord, calling the bang version of the transition will perform the transition and call save on the object. This method will return the value returned by save (true/false for ActiveRecord) or false if any of the guards fail.

E.g. workflow.publish! will transition the object to published and call save on the object.

Guards

Guards are used to allow or prevent events from being called. :guard accepts a single symbol or an array of symbols representing methods. The method may be on the state, the workflow, or the object itself, and the method will be searched for in that order.

Best practice Use predicate methods that return a simple true/false. All guard methods are called, so avoid side affects in these methods.

Calling the transition with a failing guard will result in the object not being transitioned and returning false. If using the bang version, save will not be called.

may_xxx?

workflow.may_publish? will call all the guard methods and return false if any of the guard methods return false. It will also return false if you are not in a state that has a defined event (e.g. published_workflow.may_publish? will always return false)

guard_errors

After calling may_xxx?, the workflow will have an array of the guard methods that failed. To avoid additional dependencies, the developer is responsible for converting these to human readable messages (using I18n or the like). This may include :invalid_event in the case where a transition from the current state is not defined.

classDraftState < FlowMachine::WorkflowStateevent:publish,guard: [:content_present?,:can_publish?]transitionto: :publishedenddefcan_publish?falseendendclassBlogPostdefcontent_present?content.present?# assuming you have ActiveSupport loadedendend
workflow.may_publish?# => falseworkfow.guard_errors# => [:can_publish?]workflow.publish# => false

Callbacks

State and Workflow callbacks accept if and unless options. They may be a symbol or array of symbols (looking for the method in the state, workflow, and object in that order) or a Proc.

State callbacks

Declared in the WorkflowState class.

  • on_exit Called after the object has transitioned out of the state.
  • on_enter Called after the object has transitioned into the state. Triggered after the previous state's on_exit.

The following are available when Workflow#save is used (workflow.save or workflow.transition!) Not called if you call save directly on the decorated model.

  • after_enter Called when the object has transitioned into the state and the object has been saved either workflow.save or workflow.transition! has been called.
  • before_change Useful when watching for changes to a model, but only when in a certain state. Will be called if anything exists in the object#changes hash (if it exists), often provided by ActiveModel#dirty.
  • after_change Useful when watching for changes to a model in a certain state, but you only want to trigger when the save is successful (e.g. the model is valid?)

Workflow callbacks

Declared in the Workflow class.

  • after_transition Called anytime a transition takes place

The following are available when Workflow#save is used:

  • before_save Called when Workflow#save is called, but before object#save is called
  • after_save Called after object#save has returned true

Transition callbacks

Declared as an option to the transition method inside an event block.

  • after Will be called after the transition has happened successfully including persistance (if applicable). Useful when you only want something to trigger when moving from a specific state to another.

transition to: :published, after: :send_mailing_list_email

FlowMachine::Workflow.for

You can easily access the workflow for your particular object, class, or collection of objects.

Examples:

blog=BlogPost.newFlowMachine::Workflow.for(blog)# => BlogPostWorkflowFlowMachine::Workflow.for(BlogPost)# => BlogPostWorkflow

You can also create an collection of workflow objects via:

blog_posts=BlogPost.allFlowMachine::Workflow.collection_for(blog_posts)# => [BlogPostWorkfow.new(blog_post[0]), ..., BlogPostWorkflow.new(blog_post[n])

Scopes and Predicate methods

If you want scopes and predicate methods defined on your model, use the following:

PublishingWorkflow.create_scopes_on(self) within the model.

Assuming BlogPost is an ActiveRecord model, this will create BlogPost.draft and BlogPost.published scopes as well as the BlogPost#draft? and BlogPost#published? methods.

Other useful features

Use a different attribute for the state

If you don't want to use state as your field for storing state, simply declare state_attribute :status in the Workflow class.

List of state names

PublishingWorkflow.state_names` # => ['draft', 'published']

Especially useful in an ActiveModel validation:

validates:state,presence: true,inclusion: {in: PublishingWorkflow.state_names}

Options Hash

You can pass an options hash into the workflow which is available at any time while using the workflow. A prime example is tracking the user who performed an action.

classPublishedState < FlowMachine::WorkflowStateon_enter:update_published_bydefupdate_published_byobject.published_by=options[:current_user]endendworkflow=PublishingWorkflow.new(blog_post,current_user: User.find(123))workflow.publishblog_post.published_by# => User #123

About

Build finite state machines in a backend-agnostic, class-centric way.

Resources

Stars

9 stars

Watchers

31 watching

Forks

Releases

Packages

Contributors

Languages