Piotr Jaworski
Ruby on Rails

Ruby State Machine – AASM Tutorial with Sequel, SQLite, Rake and RSpec

   Back to list

Have you ever had a problem managing a record’s state change logic? For example, managing products or orders in a shop or invoices (changing their states – from new, to delivered etc.)? Here comes AASM – a state machine for Ruby. It works with plain Ruby and also with many libraries like ActiveRecord, Sequel, Dynamoid, Redis or Mongoid.

The greatest thing about AASM is that it’s really intuitive and easy to implement. It allows you painlessly add a fully working state machine in Ruby.

In this tutorial, I will show how to connect AASM with Sequel and how to create a micro Ruby application using Rake, DotEnv, SQLite and RSpec.

What is a state machine?

Formal definition: It’s a finite automaton or an abstract machine that can be in exactly one of a finite number of states at any given time.

Real world example: A security code lock system. It has a finite number of states and t changes from one state to another all the time.

Waiting -> Analyzing entered code -> Opening a door / returning an error that code is invalid -> Waiting -> ….

Our problem

Let’s imagine that we have a problem with invoices in our system. It’s hard to control their states and check which state change is allowed after another one. What’s more, we struggle with callbacks and logic after a single state change.

For instance, after one change we might want to add some logic by not change the state while after another one, we only want to change the state.

Logic and states

Here are our invoice logic and all available states:

ruby_state_machine_screenshot1

We have 5 states:

  • Draft
  • Unpaid
  • Sent
  • Paid
  • Archived

It’s allowed to change a status:

  • From Draft to Unpaid (confirm)
  • From Unpaid to Sent (sent)
  • From Sent to Paid (pay)
  • From Paid to Archived (archive)
  • From Unpaid to Draft (draft)
  • From Unpaid to Archived (archive)

Implementation

Logic seems easy, right? Yeah, that’s true, moreover, implementation is easy too!

Let’s do it – firstly prepare all needed files and install all gems.

Start by creating the main project folder and enter there:

Now let’s create folders for our files and Gemfile:

Add needed gems for now:

Now, please install everything using bundler:

Create the main file, which is responsible for wrapping all application settings:

This file includes bundler itself and all dependencies installed by it, by calling Bundler.require.

Also, includes all *.rb files in the /app folder.

Now let’s add a Rakefile, which allows us to run commands like rake console or rake db:setup etc.

By calling rake console, we run irb, which reads config from application.rb file. Why do we need it? Because we need to expose all installed modules and all needed files to irb.

Now let’s configure RSpec. Let’s add just a color output, we don’t need more for now.

And add:

Main class – invoice

Next let’s create our main file, which includes the invoice class.

First, we need to include AASM by adding: include AASM. Our class, for now, has only one field – name – defined in the attr_reader method.

How can we define the entire state machine logic? By defining everything in the AASN block. We list all available states, by writing:

States’ order isn’t important. If you want to define an initial state, add initial: true option.

To define a state change, define event :my_event_name and in a block, define transitions, from and to. The from and to parameters accept one and more states.

Pretty easy, right?! Ok, let’s add more logic to the invoice class. What if we want to call a method after a state is changed? Like sending an email or uploading a file to S3?

It’s easy, after an event name, define a method name under an after: key. Moreover, if you want to call a method after each state change, define it in after_all_transitions, like after_all_transitions :run_worker.

Please check the code above and add it:

Ok, now let’s test everything. Run rake console:

ruby_state_machine_screenshot2

ruby_state_machine_screenshot3

As you can see, when you want to change to unpermitted state, AASM throws an exception. When everything is fine, it changes a state. If you want to change exceptions behavior and return just true/false, add to the AASM block: aasm whiny_transitions: false

Testing – RSpec

Let’s write some specs to test our invoices. Add two files:

spec/invoice_spec.rb and spec/spec_helper.rb

We need to require our application config in the spec_helper.rb, in order to require all dependencies and AASM spec helpers:

After when we are ready with a basic configuration, let’s think about what we can test.

I think that checking an initial state and whether defined methods are run after a state change is a good idea. Let’s do it! Add the following code to the invoice_spec.rb:

To run added specs, run:

ruby_state_machine_screenshot4

Yeah, specs pass and everything is green 🙂

Database – Sequel

We now have a working version of our state machine and invoices but we can’t save them to a database. Have you ever tried to add a database system to a plain Ruby? If not, I’ll show you how to implement and connect everything together.

First of all, let’s update our Gemfile and add needed gems:

gem ‘sqlite3’
gem ‘sequel’
gem ‘dotenv

Now please install everything by running bundler:

As you probably noticed, I added dotenv gem, to manage with environment variables – development and test. Please add the environment.rb file under the config folder and add the following code:

As you can see, based on an environment, we load a different file and environment variables. Ok, now let’s add a database configuration and later we will add environment variables later.

Add the database.rb file under to config folder and add the following code:

As you can see, we define a database connection under the DB constant and add the timestamps module.

Ok, we need to load all added files when we run irb console, so let’s modify the application.rb file:

As stated previously, we need to add environment variables, let’s do it by adding these two files – .env and .env.test.

We’ve defined a database connection and an environment name.Next, we should add the .gitignore file, to exclude SQLite databases from git.

Almost everything is ready, a database connection is prepared but, we don’t have any tables for now. How can we create them? Let’s add a new namespace to the Rakefile and a task, which creates all needed tables.

Add to the Rakefile, the new namespace:

What does it do? It creates a table called invoices when it doesn’t exist. Otherwise the create_table? method allows us to skip this part.

Ok, let’s run it by:

Woohoo, we have full database setup. Now we need to define, that our invoice class, in fact, uses the database. Add the following modifications the the invoice.rb file and remember to remove the initialize method – it will override default Sequel’s method:

Since we added environment configuration, we need to define the environment name in the spec_helper.rb. Why there? It must be defined before reading the application.rb file.

Add the following line to the bottom of the file:

ENV[‘RACK_ENV’] = ‘test’

Yeah, everything is ready now! Let’s play with our application and create a few invoices.

Let’s check if our database is empty:

or

or

What is the difference? When we call DB[:invoices].all, we directly call the invoices table, so as a result, we get an array of hashes while running a query like Invoices.all, we get an array of Invoices. On a hash result we can’t run methods like: update, destroy.

Conclusion

In this Ruby State Machine tutorial, I showed how to connect plain Ruby with Sequel and used AASM to explain state machines. I hope that you liked it and it will be useful for you! Thank you for reading and the source code can be found here.