Introducing ActionState for Rails
One of my favourite features in Rails is the ability to quickly define named filters for ActiveRecord models using a simple query language.
If you have an Article model, for example, where records can be published, you could define a scope called published
, that allows you to filter just the published articles.
class Article
scope :published, -> { where(published: true) }
end
Calling Article.published
will return all the articles that have the published
attribute set to true
.
Let’s say you want to be able to check if a specific article is itself published using the same rules you defined in the scope. You could write a predicate method like this:
def published?
published == true
end
Now calling the published?
predicate method on an article will return true
if it’s published and false
if not.
The problem is we now have two different definitions of what it means to be published.
Let’s say in the future, you decide to make a scheduling feature so articles can be published on a specific date in the future. You add a published_at
field to the Article model and change your published
scope to this:
scope :published, -> { where(published_at: ..Time.current) }
Now articles are only considered published if the published_at
attribute is a date / time before the current time. But unless you remember to update the predicate method, the logic is out-of-sync.
You could write a test to ensure your scope and predicate stay in sync so you never forget to update one or the other, but in the best case you’ve got the same logic defined in two places.
That’s where ActionState comes in. ActionState provides a state
method on ActiveRecord objects that defines both a scope and a predicate at the same time using the same logic.
That means this:
state(:published) { where(published_at: ..Time.current }
Is equivalent to this:
scope :published, -> { where(published_at: ..Time.current) }
def published?
(..Time.current).cover?(published_at)
end
ActionState supports a small subset of the ActiveRecord query language but I think it’s enough to cover most use-cases it’s designed for. Supported query methods include: where
, where.not
and excluding
. It also supports states with arguments and state composition.
You can find further documentation and installation instructions on GitHub.
I’d love to know if you find it useful or if you have any questions.
— Joel