Is it possible to have ActiveRecord automagically create boolean methods for DateTime
columns in your models?
Let’s say you have a model House
which has a few timestamp columns such as construction_started_at
, construction_finished_at
, purchased_at
, inspection_started_at
, moved_in_at
, and sold_at
. It’s nice that your model stores this data, but you want to be able to ask questions like “Has construction on this house started? (House#construction_started?
)”, or “Have the buyers moved in? (House#moved_in?
)” etc. You can of course define the methods individually:
class House < ApplicationRecord
def construction_started?
construction_started_at.present?
end
def construction_finished?
construction_finished_at.present?
end
# and so on...
end
This may work for you if all you have is one model with a few fields, but if you foresee yourself writing these methods all over your codebase because you have multiple models (like OfficeBuilding
, Factory
, ShoppingMall
, and ApartmentBuilding
for example), you may start to wonder if there is an approach that will save you time, both now and in the future if you have to change anything about how these methods work. Is there a way to DRY this up?
In Rails, when you have some logic that needs to be shared across multiple models, consider creating a “concern”. A concern, in Rails, is a Ruby module which extend
s ActiveSupport::Concern
. ActiveSupport::Concern
gives you, the module author, a couple of convenience methods that make it easy to two do things:
included
method gives you the ability to call class methods like validates
, define_method
and others when the concern is included into your model.class_methods
method gives you the ability to define class level methods on the model that is including your concern.Before we start, it’d be nice to figure out what we want the “user experience” of this to look like. For our example problem, I think I’d want something that works like this:
class House < ApplicationRecord
include DateTimeToBoolean
date_time_to_boolean :construction_started_at, :construction_finished_at,
:purchased_at, :inspection_started_at
end
# this should "automagically" give me the ability to do things like house.construction_started?, house.purchased? etc
Now it is a bit clearer what we need to do make this happen.
module DateTimeToBoolean
extend ActiveSupport::Concern
class_methods do
def date_time_to_boolean(*columns)
columns.each do |column|
# remove the _at suffix, assuming that your time columns end with _at. If they don't you'll
# either want to modify the next line, or add a parameter to your method to specify the
# method name
column = column.to_s.gsub(/_at$/, '').to_sym
define_method("#{column}?") do
public_send(column).present?
end
end
end
end
end
We’re defining a class method in the “includer” (House
, in our example) called date_time_to_boolean
, that takes in a list of arguments corresponding to the DateTime
fields we want to generate boolean methods for. date_time_to_boolean
loops over these fields, and defines a boolean method for each of them which answers the question of if the given field is “truthy”.
Now, we can include this concern in any model that needs to have boolean methods automatically created for its DateTime
fields.
Using Ruby meta-programming (define_method
and send
) and the ActiveSupport::Concern
, we’ve seen how it is possible to dynamically create convenience methods for DateTime
columns in our models. Hopefully, it should not be too much of a stretch to imagine how this might be done for other types of columns and methods.
If you got value out of this article, consider subscribing to my email newsletter to get notified whenever I publish a new one.