# frozen-string-literal: true

module Sequel
  module Plugins
    # The validates_associated plugin allows you to validate associated
    # objects.  It also offers the ability to delay the validation of
    # associated objects until the current object is validated.
    # If the associated object is invalid, validation error messages
    # from the associated object will be added to the current object's
    # validation errors.
    #
    # Usage:
    #
    #   # Make all model subclass support validating associated objects
    #   Sequel::Model.plugin :validate_associated
    #
    #   # Make the Album class support validating associated objects
    #   Album.plugin :validate_associated
    #
    #   class Album
    #     many_to_one :artist
    #     many_to_many :tags
    #
    #     # Always validate associated artist when saving the album
    #     def validate
    #       super
    #       if artist
    #         validate_associated_object(model.association_reflection(:artist), artist)
    #       end
    #     end
    #
    #     # When saving after calling this method, validate the given tag as well.
    #     def check_tag!(tag)
    #       delay_validate_associated_object(model.association_reflection(:tags), tag)
    #     end
    #   end
    module ValidateAssociated
      # Depend on the instance_hooks plugin.
      def self.apply(mod)
        mod.plugin :instance_hooks
      end

      module InstanceMethods
        private

        # Delay validating the associated object until validating the current object.
        def delay_validate_associated_object(reflection, obj)
          after_validation_hook{validate_associated_object(reflection, obj)}
        end

        # Validate the given associated object, adding any validation error messages from the
        # given object to the parent object.
        def validate_associated_object(reflection, obj)
          return if reflection[:validate] == false
          association = reflection[:name]
          if (reflection[:type] == :one_to_many || reflection[:type] == :one_to_one) && (key = reflection[:key]).is_a?(Symbol) && !(pk_val = obj.values[key])
            # There could be a presence validation on the foreign key in the associated model,
            # which will fail if we validate before saving the current object.  If there is
            # no value for the foreign key, set it to the current primary key value, or a dummy
            # value of 0 if we haven't saved the current object.
            p_key = pk unless pk.is_a?(Array)
            obj.values[key] = p_key || 0
            key = nil if p_key
          end
          obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
          if key && !pk_val
            # If we used a dummy value of 0, remove it so it doesn't accidently remain.
            obj.values.delete(key)
          end
        end
      end
    end
  end
end