ruby-on-railsrubyinheritanceactivesupport-concern

Rails Concern class_macro with params from inherited class


This is probably a really easy question but I'd like to have a concern which adds a validation whose parameters depend on methods in the derived class. For instance, I'd like to have a concern SlugHelpers as follows

module SlugHelpers
  extend ActiveSupport::Concern


  included do
     validates :slug, uniqueness: { case_sensitive: false, message: "Slug must be unique", scope: slug_scope }, presence: true,

  end

  class_methods do
    def slug_scope
      []
    end
  end

end

But then have a model Post which overrides slug_scope, e.g.,

class Post < ApplicationRecord
  include SlugHelpers

  def self.slug_scope
     [:stream_id, :stream_type]
  end

I want the slug_scope defined in Post to override the slug_scope used in the included do (though I might be calling that wrong, maybe I need self.class.slug_scope) but as written I think that this won't work (isn't the included do executed before the derived class defines its methods)?

Can I do this somehow using prepended do? Or is the way I wrote this roughly correct? I know that included modules are entered into inheritance chain before the derived class but I'm obviously kinda confused about when/where code in an included do block gets executed.


Solution

  • As described by @mu_is_too_short you can just create a class method which adds the validations to the class when called:

    module SlugHelpers
      extend ActiveSupport::Concern
      class_methods do
        def has_slug(name = :slug, scope: :default_value)
            validates_uniqueness_of name, 
               case_sensitive: false, 
               message: "Slug must be unique", 
               scope: scope
            validates_presence_of name
        end
      end
    end
    

    This is the generally preferred way in Ruby to make the behavior provided by modules configurable.

    class Foo
      include SlugHelpers
      has_slug(scope: [:foo, :bar])
    end 
    
    class Bar
      include SlugHelpers
      has_slug(scope: [:baz, :bar])
    end 
    

    While you could actually call a method on the class when the module is included it would produce very strange results if you want to override the behavior in subclasses since it still would be evaluated just when the module was included.