如何在 Rails 6 个模型问题中构建动态枚举?

How to build dynamic enums in Rails 6 model concerns?

我正在尝试构建一个 Bookable 模型关注点,将枚举添加到包含模型,用于跟踪预订阶段:

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

STAGES 常量定义了可用的阶段,但是我希望包括模型能够通过覆盖 self.extra_stages 方法来添加特定于它们的阶段。例如,在 Mission 模型中:

class Mission < ApplicationRecord
  include Bookable

  def self.extra_stages
    {
      awaiting_estimate: 4,
      awaiting_payment: 5,
      awaiting_report: 6,
      report_sent: 7
    }
  end
end

然而,这段代码失败了:

$ 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'

关于我可能做错了什么的任何线索?提前致谢!

感谢 yzalavin 给我指明了正确的方向(事实上 included 是一个在 include Bookable 之后求值的钩子),我找到了这个解决方案,它的工作方式与我很满意:

module Bookable
  extend ActiveSupport::Concern

  STAGES = {
    confirmed: 0,
    completed: 1,
    cancelled: 2,
    issue_raised: 3
  }.freeze

  included do |base|
    enum stage: STAGES.merge(base.extra_stages)

    belongs_to :customer
    belongs_to :provider
    validates :stage, presence: true
  end

  class_methods do
    def extra_stages
      return {} unless defined? self::EXTRA_STAGES

      self::EXTRA_STAGES
    end
  end
end

并且在包含的模型中:

class Mission < ApplicationRecord
  EXTRA_STAGES = {
    awaiting_estimate: 4,
    awaiting_payment: 5,
    awaiting_report: 6,
    report_sent: 7
  }.freeze

  include Bookable

  # (...)
end

我欢迎对此解决方案进行任何改进,因为它仍然感觉可以简化。

有更简洁的方法来完成我即将介绍的内容,但它会完成您想要做的事情。注意简洁(我删除了您的关联以在我的机器上进行快速抽查):

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

如果你想在 class 上定义这些,它看起来像这样:

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

请注意,我们在定义 class 方法后调用 acts_as_bookable_with。否则,我们将得到未定义的方法错误。

在 ApplicationRecord 中没有很多 "bad"。这不是最理想的方式,但这些 acts_as_* 模块中的大多数无论如何都遵循这个确切的模式并注入 ActiveRecord::Base.