Search code examples
ruby-on-railsrubyruby-on-rails-5

Skip validation from include element


I am having an issue, I have the next class

class Question < ApplicationRecord
  include Mappable

 ...

end

so my problem is at the time to create a Question, I need to keep the Question validations but skip the ones that are incoming from Mappable

because by now I am using question.save(validate: false) and I have to update that to something like question.save(mappable_validate: false), just to skip the validations from Mappable

EDIT Mappable:

module Mappable
  extend ActiveSupport::Concern

  included do
    attr_accessor :skip_map

    has_one :map_location, dependent: :destroy
    accepts_nested_attributes_for :map_location, allow_destroy: true, reject_if: :all_blank

    validate :map_must_be_valid, on: :create, if: :feature_maps?

    def map_must_be_valid
      return true if skip_map?

      unless map_location.try(:available?)
        skip_map_error = "Map error"
        errors.add(:skip_map, skip_map_error)
      end
    end

    def feature_maps?
      Setting["feature.map"].present?
    end

    def skip_map?
      skip_map == "1"
    end

  end

end


Solution

  • There are quite a few ways to solve this. But no reliable ones that don't involve modifying the module.

    One would be simply to use composition and move the validations to its own module:

    module Mappable
      module Validations
        extend ActiveSupport::Concern
        included do
          validate :map_must_be_valid, on: :create, if: :feature_maps?
        end
      end
    end
    
    class Question < ApplicationRecord
      include Mappable
    end
    
    class Foo < ApplicationRecord
      include Mappable
      include Mappable::Validations
    end
    

    Another very common way to make the behavior provided by a module customizeable is to not just cram all your code into the Module#included hook which doesn't let you pass options.

    Instead create a class method:

    module Mappable
      extend ActiveSupport::Concern
       
      def map_must_be_valid
        return true if skip_map?
    
        unless map_location.try(:available?)
          skip_map_error = "Map error"
          errors.add(:skip_map, skip_map_error)
        end
      end
    
      def feature_maps?
        Setting["feature.map"].present?
      end
    
      def skip_map?
        skip_map == "1"
      end
    
      module ClassMethods
        def make_mappable(validate: true)
          attr_accessor :skip_map
          has_one :map_location, dependent: :destroy
          accepts_nested_attributes_for :map_location, 
             allow_destroy: true, reject_if: :all_blank
          if validate
            validate :map_must_be_valid, on: :create, if: :feature_maps?
          end
        end
      end
    end
    

    And then just call the class method in the class you want to modify.

    class Question < ApplicationRecord
      include Mappable
      make_mappable(validate: false)
    end
    

    This pattern can be found everywhere in Rails and Ruby in general and lets you make the functionality you're providing much flexible.

    I understand that this might not seem to be immediately helpful as the code is coming from a gem. But it can help you understand what to do to fix the gem or evaluate if its actually worthwhile/needed.