lib/qyu/workers/concerns/payload_validator.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

module Qyu
  module Workers
    module Concerns
      # Qyu::Workers::Concerns::PayloadValidator
      #
      # Adds ability to workers to perform validations on
      # params for the task to be processed
      #
      # Usage:
      #
      # Qyu::Worker.new do
      #   validates :user_id,    presence: true, type: :integer, unless: :no_user
      #   validates :name,       presence: true, type: :string
      #   validates :hotels,     presence: true, type: :array
      #   validates :account_id, absence: true,  if: :customer_id
      #   validates :account_id, presence: true, unless: :customer_id
      # end
      #
      module PayloadValidator
        # Adds a validation option to worker
        #
        #   validates :user_id,    presence: true, type: :integer, unless: :no_user
        #   validates :name,       presence: true, type: :string
        #   validates :account_id, absence: true,  if: :customer_id
        #   validates :account_id, presence: true, unless: :customer_id
        #
        # @param [Symbol] payload key to validate
        # @param [Hash] options for validation
        # @return [Hash] registereds services
        def validates(parameter, opts = {})
          @_validations ||= {}
          @_validations[parameter.to_s] = Qyu::Utils.stringify_hash_keys(opts)
        end

        # Validates payload in processing task and raises a Qyu::Errors::PayloadValidationError
        # if any of the conditions do not hold up
        #
        # @param [Task] task with payload
        # @return [nil]
        # @raise [Qyu::Errors::PayloadValidationError]
        def validate_payload!(model)
          return unless @_validations
          payload = Qyu::Utils.stringify_hash_keys(model.payload || {})
          validation_errors = {}
          @_validations.each do |attribute, opts|
            # example: attribute :name
            # example opts { presence: true, type: integer }
            next unless if_validation(payload, opts['if'])
            next unless unless_validation(payload, opts['unless'])
            opts.map do |option, value|
              error = run_validation(option, payload[attribute.to_s], value)
              # next if error is nil
              next unless error
              validation_errors["#{attribute}.#{option}"] = error
            end
          end

          if validation_errors.size.positive?
            fail Qyu::Errors::PayloadValidationError, validation_errors
          end
          nil
        end

        private

        # :nodoc:
        def run_validation(option, param, value)
          # Skip if and unless conditionals (return nil)
          return if option.eql?('if')
          return if option.eql?('unless')
          # supported options are presence, absence and type
          __send__(option, param, value)
        rescue NoMethodError
          raise Qyu::Errors::UnknownValidationOption, option
        end

        def presence(param, value)
          return unless value
          return unless param.nil?
          { expected: 'present' }
        end

        def absence(param, value)
          return unless value
          return if param.nil?
          { expected: 'absent' }
        end

        def type(param, data_type)
          __send__("#{data_type}_type_validator", param)
        end

        # DataType validators
        def integer_type_validator(param)
          type_validator('integer', [Integer], param.class)
        end

        def string_type_validator(param)
          type_validator('string', [String, Symbol], param.class)
        end

        def number_type_validator(param)
          type_validator('number', [Integer, Float], param.class)
        end

        def boolean_type_validator(param)
          type_validator('boolean', [TrueClass, FalseClass], param.class)
        end

        def hash_type_validator(param)
          type_validator('hash', [Hash], param.class)
        end

        def array_type_validator(param)
          type_validator('array', [Array], param.class)
        end

        def type_validator(type_name, data_types, param_class)
          return if data_types.include?(param_class)
          { expected: type_name, got: param_class.name.downcase }
        end

        # Conditonal validation
        def if_validation(payload, key)
          # TODO: support block passing "return yield if block_given?"
          return true if key.nil?
          return true if payload[key.to_s]
          false
        end

        def unless_validation(payload, key)
          # TODO: support block passing "return yield if block_given?"
          return true if key.nil?
          return true unless payload[key.to_s]
          false
        end
      end
    end
  end
end