class Quby::Answers::Services::AnswerValidator

Attributes

answer[R]
questionnaire[R]

Public Class Methods

new(questionnaire, answer) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 14
def initialize(questionnaire, answer)
  @questionnaire = questionnaire
  @answer        = answer
end

Public Instance Methods

depends_on_key_answered(key) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 237
def depends_on_key_answered(key)
  answer.depends_on_lookup[key]
end
send_date_error(question, validation) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 124
def send_date_error(question, validation)
  answer.send(:add_error, question, :valid_date, validation[:message] || "Does not match expected pattern.")
end
validate() click to toggle source

rubocop:disable CyclomaticComplexity, Metrics/MethodLength

# File lib/quby/answers/services/answer_validator.rb, line 20
def validate
  questionnaire.questions.each do |question|
    next unless question
    next if question.hidden?

    if question.depends_on.present?
      next unless question.depends_on.any? { |key| depends_on_key_answered(key) }
    end

    value = answer.send(question.key)
    next if answer.skip_validation?(value, question)

    question.validations.each do |validation|
      begin
        case validation[:type]
        when :valid_integer
          validate_integer(question, validation, value)
        when :valid_float
          validate_float(question, validation, value)
        when :valid_date
          value = question.components.each_with_object({}) do |component, hash|
            key = question.send("#{component}_key")
            hash[component] = answer.send(key)
          end
          validate_date(question, validation, value)
        when :regexp
          validate_regexp(question, validation, value)
        when :requires_answer
          if question.type == :date
            value = question.answer_keys.each_with_object({}) do |key, hash|
              hash[key] = answer.send(key)
            end
          end
          validate_required(question, validation, value)
        when :minimum
          validate_minimum(question, validation, value)
        when :maximum
          validate_maximum(question, validation, value)
        when :too_many_checked
          validate_too_many_checked(question, validation, value)
        when :not_all_checked
          validate_not_all_checked(question, validation, value)
        when :maximum_checked_allowed
          validate_maximum_checked_allowed(question, validation, value)
        when :minimum_checked_required
          validate_minimum_checked_required(question, validation, value)
        when :answer_group_minimum
          validate_answer_group_minimum(question, validation, value)
        when :answer_group_maximum
          validate_answer_group_maximum(question, validation, value)
        end
      rescue InvalidValue
        answer.send(:add_error, question, validation[:type], validation[:message] || "Invalid value.")
      end
    end
  end
end
validate_answer_group_maximum(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 223
def validate_answer_group_maximum(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since it's possible to make a decision about which fields
  # to keep and which ones to blank. If you want to do anything at all
  # with this completion, you'll need to decide that at some point
  # anyway, so we think it's best to do that as early as possible.

  answered = answer.send(:calc_answered, answer.question_groups[validation[:group]])
  if answered > validation[:value]
    answer.send(:add_error, question, :answer_group_maximum,
                validation[:message] || "Needs at most #{validation[:value]} question(s) answered.")
  end
end
validate_answer_group_minimum(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 213
def validate_answer_group_minimum(question, validation, value)
  return if @answer.aborted

  answered = answer.send(:calc_answered, answer.question_groups[validation[:group]])
  if answered < validation[:value]
    answer.send(:add_error, question, :answer_group_minimum,
                validation[:message] || "Needs at least #{validation[:value]} question(s) answered.")
  end
end
validate_date(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 109
def validate_date(question, validation, value)
  # Skip this validation if all date parts are empty
  return if value.values.all?(&:blank?)

  # Check if there are required date parts missing
  required_values = value.fetch_values(*question.required_components)
  send_date_error(question, validation) if required_values.any?(&:blank?)

  begin
    convert_answer_value(question, value)
  rescue InvalidValue
    send_date_error(question, validation)
  end
end
validate_float(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 102
def validate_float(question, validation, value)
  return if value.blank?
  Float(value)
rescue ArgumentError
  answer.send(:add_error, question, :valid_float, validation[:message] || "Invalid float")
end
validate_integer(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 95
def validate_integer(question, validation, value)
  return if value.blank?
  Integer(value)
rescue ArgumentError
  answer.send(:add_error, question, :valid_integer, validation[:message] || "Invalid integer")
end
validate_maximum(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 154
def validate_maximum(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since whatever is using this data further on is likely not
  # built to take into account values outside the intended range. (e.g.
  # BMI calculation)

  return if value.blank? || (question.type == :date && value.values.all?(&:blank?))

  converted_answer_value = convert_answer_value(question, value)
  if converted_answer_value > validation[:value]
    answer.send(:add_error, question, validation[:type], validation[:message] || "Exceeds maximum")
  end
end
validate_maximum_checked_allowed(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 193
def validate_maximum_checked_allowed(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since it's possible to make a decision about which fields
  # to keep and which ones to blank. If you want to do anything at all
  # with this completion, you'll need to decide that at some point
  # anyway, so we think it's best to do that as early as possible.

  if value.values.reduce(:+) > question.maximum_checked_allowed.to_i
    answer.send(:add_error, question, :maximum_checked_allowed, validation[:message] || "Too many options checked.")
  end
end
validate_minimum(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 141
def validate_minimum(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since whatever is using this data further on is likely not
  # built to take into account values outside the intended range. (e.g.
  # BMI calculation)

  return if value.blank? || (question.type == :date && value.values.all?(&:empty?))
  converted_answer_value = convert_answer_value(question, value)
  if converted_answer_value < validation[:value]
    answer.send(:add_error, question, validation[:type], validation[:message] || "Smaller than minimum")
  end
end
validate_minimum_checked_required(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 205
def validate_minimum_checked_required(question, validation, value)
  return if @answer.aborted

  if value.values.reduce(:+) < question.minimum_checked_required.to_i
    answer.send(:add_error, question, :minimum_checked_required, validation[:message] || "Not enough options checked.")
  end
end
validate_not_all_checked(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 180
def validate_not_all_checked(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since it's possible to make a decision about which fields
  # to keep and which ones to blank. If you want to do anything at all
  # with this completion, you'll need to decide that at some point
  # anyway, so we think it's best to do that as early as possible.

  if answer.send(question.check_all_option) == 1 and
      value.values.reduce(:+) < value.length - (question.uncheck_all_option ? 1 : 0)
    answer.send(:add_error, question, :not_all_checked, validation[:message] || "Invalid combination of options.")
  end
end
validate_regexp(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 128
def validate_regexp(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since whatever is using this data further on is likely not
  # built to take into account values that do not conform to the given
  # format.

  return if value.blank?
  match = validation[:matcher].match(value)
  unless match && match[0] == value
    answer.send(:add_error, question, validation[:type], validation[:message] || "Does not match expected pattern.")
  end
end
validate_required(question, validation, value) click to toggle source

rubocop:enable CyclomaticComplexity, Metrics/MethodLength

# File lib/quby/answers/services/answer_validator.rb, line 79
def validate_required(question, validation, value)
  return if @answer.aborted
  valid = case question.type
          when :date
            required_keys = question.required_components.map do |key|
              question.send(key.to_s + "_key")
            end
            value.values_at(*required_keys).all?(&:present?)
          when :check_box
            value.values.reduce(:+) > 0
          else
            value.present?
          end
  answer.send(:add_error, question, validation[:type], validation[:message] || "Must be answered.") unless valid
end
validate_too_many_checked(question, validation, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 168
def validate_too_many_checked(question, validation, value)
  # We have decided not to allow bypassing this validation when
  # aborted, since it's possible to make a decision about which fields
  # to keep and which ones to blank. If you want to do anything at all
  # with this completion, you'll need to decide that at some point
  # anyway, so we think it's best to do that as early as possible.

  if answer.send(question.uncheck_all_option) == 1 and value.values.reduce(:+) > 1
    answer.send(:add_error, question, :too_many_checked, validation[:message] || "Invalid combination of options.")
  end
end

Private Instance Methods

convert_answer_value(question, value) click to toggle source
# File lib/quby/answers/services/answer_validator.rb, line 243
def convert_answer_value(question, value)
  case question.type
  when :float
    Float(value)
  when :integer
    Integer(value)
  when :date
    non_empty_values = value.transform_values(&:presence).compact
    day    = non_empty_values[:day] || 1
    month  = non_empty_values[:month] || 1
    year   = non_empty_values[:year] || 2000
    hour   = non_empty_values[:hour] || '00'
    minute = non_empty_values[:minute] || '00'
    DateTime.strptime("#{day}-#{month}-#{year} #{hour}:#{minute}", "%d-%m-%Y %H:%M")
  else
    value
  end
rescue ArgumentError => e
  raise InvalidValue, e.message
rescue TypeError => e
  fail InvalidValue, e.message
end