I'm trying to build a Bookable
model concern that adds an enum to the including model, that's used for tracking the stage of the booking:
module Bookable
extend ActiveSupport::Concern
STAGES = {
confirmed: 0,
completed: 1,
cancelled: 2,
issue_raised: 3
}.freeze
included do
enum stage: STAGES.merge(self.extra_stages)
belongs_to :customer
belongs_to :provider
validates :stage, presence: true
def self.extra_stages
{}
end
end
end
The STAGES
constant defines available stages, however I want including models to be able to add stages that are specific to them, by overriding the self.extra_stages
method. For example, in a Mission
model:
class Mission < ApplicationRecord
include Bookable
def self.extra_stages
{
awaiting_estimate: 4,
awaiting_payment: 5,
awaiting_report: 6,
report_sent: 7
}
end
end
However, this code fails:
$ bundle exec rails c
Loading development environment (Rails 6.0.2.2)
[1] pry(main)> Mission.stages
NoMethodError: undefined method `extra_stages' for Mission (call 'Mission.connection' to establish a connection):Class
Did you mean? extract_associated
from /home/gueorgui/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.2.2/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
Any clue as to what I might be doing wrong? Thank you in advance!
There are cleaner ways to do what I'm about to present, but it will accomplish what you're trying to do. Mind the brevity (I removed your associations to do a quick spot-check on my machine):
module Bookable
extend ActiveSupport::Concern
included do
STAGES = {
confirmed: 0,
completed: 1,
cancelled: 2,
issue_raised: 3
}.freeze
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def self.acts_as_bookable_with(extra_stages = {})
include Bookable
enum stage: self::STAGES.merge(extra_stages)
end
end
class Mission < ApplicationRecord
acts_as_bookable_with({
awaiting_estimate: 4,
awaiting_payment: 5,
awaiting_report: 6,
report_sent: 7
})
end
If you want to define these on the class, it would look like this:
module Bookable
extend ActiveSupport::Concern
included do
STAGES = {
confirmed: 0,
completed: 1,
cancelled: 2,
issue_raised: 3
}.freeze
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def self.acts_as_bookable_with(extra_stages)
include Bookable
if extra_stages.is_a?(Symbol)
extra_stages = self.send(extra_stages)
elsif extra_stages.is_a?(Hash)
# do nothing
else
raise TypeError, "can't find extra_stages from #{extra_stages.inspect}"
end
stages = self::STAGES.merge(extra_stages)
enum stage: stages
end
end
class Comment < ApplicationRecord
def self.extra_stages
{
awaiting_estimate: 4,
awaiting_payment: 5,
awaiting_report: 6,
report_sent: 7
}
end
acts_as_bookable_with(:extra_stages)
end
Note that we're calling acts_as_bookable_with
after we define our class method. Otherwise, we'll get undefined method error.
There isn't a whole lot of "bad" in having this in ApplicationRecord. It's not the most ideal way of doing it, but most of these acts_as_*
modules follow this exact pattern anyway and inject into ActiveRecord::Base
.