class Quby::Compiler::Services::DefinitionValidator

Constants

KEY_PREFIX
MAX_KEY_LENGTH

Attributes

definition[R]
questionnaire[R]

Public Class Methods

check_duplicate_headers(seed) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 203
def self.check_duplicate_headers(seed)
  return
  # TODO
  # column_headers = DataExport::QuestionnaireHeaders.new(questionnaire).headers
  # duplicate_header_names = column_headers.find_all { |e| column_headers.rindex(e) != column_headers.index(e) }.uniq
  # raise "key clashes for: #{duplicate_header_names}" if duplicate_header_names.present?
end
check_score_keys_consistency(seed) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 187
        def self.check_score_keys_consistency(seed)
          score_keys = seed["properties"][:score_keys]
          most_keys = score_keys.map { |score| keys_for_score(score).join }.max_by(&:length)

          faulty_scores = score_keys.reject do |score|
            most_keys.starts_with? keys_for_score(score).join
          end

          if faulty_scores.present?
            raise "scores mismatch other scores, check if this was intentional: #{faulty_scores}

If this was intentional, rerun quby proxy with the flag '--skip_score_keys_consistency_check' *and* manually add \
scores_schema tables to the resulting seed."
          end
        end

Public Instance Methods

subquestions_cant_have_default_invisible(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 181
def subquestions_cant_have_default_invisible(question)
  if question.subquestion? && question.default_invisible
    fail "Question #{question.key} is a subquestion with default_invisible."
  end
end
to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 163
def to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:)
  return if option.hides_questions.blank?
  msg_base += " hides_questions"
  option.hides_questions.each do |key|
    validate_question_key_exists?(questionnaire, key, msg_base: msg_base)
    validate_not_subquestion(questionnaire, key, msg_base: msg_base)
  end
end
to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 172
def to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base:)
  return if option.shows_questions.blank?
  msg_base += " shows_questions"
  option.shows_questions.each do |key|
    validate_question_key_exists?(questionnaire, key, msg_base: msg_base)
    validate_not_subquestion(questionnaire, key, msg_base: msg_base)
  end
end
validate(definition) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 16
def validate(definition)
  questionnaire = DSL.build_from_definition(definition)
  validate_fields(questionnaire)
  validate_title(questionnaire)
  validate_questions(questionnaire)
  validate_scores(questionnaire)
  validate_table_edgecases(questionnaire)
  validate_flags(questionnaire)
  validate_respondent_types(questionnaire)
  validate_outcome_tables(questionnaire)
  validate_markdown_fields(questionnaire) if questionnaire.validate_html
  validate_raw_content_items(questionnaire) if questionnaire.validate_html
# Some compilation errors are Exceptions (pure syntax errors) and some StandardErrors (NameErrors)
rescue Exception => exception # rubocop:disable Lint/RescueException
  definition.errors.add(:sourcecode, {message: "Questionnaire error: #{definition.key}\n" \
                                               "#{exception.message}",
                                      backtrace: exception.backtrace[0..20]})
end
validate_fields(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 35
def validate_fields(questionnaire)
  questionnaire.fields.input_keys
                      .find { |k| !k.is_a?(Symbol) }
                     &.tap { |k| fail "Input key #{k} is not a symbol" }
  questionnaire.fields.answer_keys
                      .find { |k| !k.is_a?(Symbol) }
                     &.tap { |k| fail "Answer key #{k} is not a symbol" }
end
validate_flag_depends_on(questionnaire, flag) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 139
def validate_flag_depends_on(questionnaire, flag)
  return if (missing = flag.depends_on - questionnaire.flags.keys).blank?

  fail ArgumentError, "Flag #{flag.key} depends_on nonexistent flag '#{missing.to_sentence}'"
end
validate_flag_hides(questionnaire, flag) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 132
def validate_flag_hides(questionnaire, flag)
  unknown_questions = flag.hides_questions.select { |key| !questionnaire.key_in_use?(key) }
  return if unknown_questions.blank?

  fail ArgumentError, "Flag '#{key}' has unknown hides_questions keys #{unknown_questions}"
end
validate_flag_shows(questionnaire, flag) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 125
def validate_flag_shows(questionnaire, flag)
  unknown_questions = flag.shows_questions.select { |key| !questionnaire.key_in_use?(key) }
  return if unknown_questions.blank?

  fail ArgumentError, "Flag '#{key}' has unknown shows_questions keys #{unknown_questions}"
end
validate_flags(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 117
def validate_flags(questionnaire)
  questionnaire.flags.each_value do |flag|
    validate_flag_shows(questionnaire, flag)
    validate_flag_hides(questionnaire, flag)
    validate_flag_depends_on(questionnaire, flag)
  end
end
validate_outcome_tables(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 156
def validate_outcome_tables(questionnaire)
  questionnaire.outcome_tables.each do |table|
    next if table.valid?
    fail "Outcome table #{table.errors.full_messages}"
  end
end
validate_presence_of_titles(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 100
def validate_presence_of_titles(question)
  return if question.allow_blank_titles
  if !question.subquestion? && question.title.blank? && question.context_free_title.blank?
    fail "Question #{question.key} must define either `:title` or `:context_free_title`."
  end
end
validate_question(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 68
def validate_question(question)
  unless question.valid?
    fail "Question #{question.key} is invalid: #{question.errors.full_messages.join(', ')}"
  end
end
validate_question_options(questionnaire, question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 92
def validate_question_options(questionnaire, question)
  question.options.each do |option|
    msg_base = "Question #{option.question.key} option #{option.key}"
    to_be_hidden_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
    to_be_shown_questions_exist_and_not_subquestion?(questionnaire, option, msg_base: msg_base)
  end
end
validate_questions(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 50
def validate_questions(questionnaire)
  questionnaire.answer_keys.each do |key|
    validate_key_format(key)
  end

  questionnaire.question_hash.each_value do |question|
    validate_question(question)
    subquestions_cant_have_default_invisible question
    validate_subquestion_absence_in_select question
    validate_placeholder_options_nil_values question
    validate_values_unique question

    validate_question_options(questionnaire, question)
    validate_presence_of_titles question
    validate_no_spaces_before_question_nr_in_title question
  end
end
validate_respondent_types(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 145
def validate_respondent_types(questionnaire)
  valid_respondent_types = Entities::Questionnaire::RESPONDENT_TYPES

  invalid_types = questionnaire.respondent_types - valid_respondent_types

  if invalid_types.present?
    fail "Invalid respondent types: :#{invalid_types.join(', :')}\n"\
         "Choose one or more from: :#{valid_respondent_types.join(', :')}"
  end
end
validate_scores(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 74
def validate_scores(questionnaire)
  questionnaire.scores.each do |score|
    validate_score_key_length(score)
    validate_score_label_present(score)

    score_schema = questionnaire.score_schemas[score.key]
    fail "Score #{score.key} does not have a score schema" unless score_schema
    fail "Score label langer dan 100 tekens (geeft problemen oru accare)\n #{score_schema.label}" if score_schema.label&.length > 100
  end

  export_keys = questionnaire.score_schemas.flat_map { |_key, score_schema|
    score_schema.subscore_schemas.map(&:export_key)
  }

  duplicate_export_keys = export_keys.tally.select { |key, count| count > 1 }.keys
  fail "Score export keys not unique, duplicates: #{duplicate_export_keys}" if duplicate_export_keys.present?
end
validate_table_edgecases(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 107
def validate_table_edgecases(questionnaire)
  questionnaire.panels.each do |panel|
    tables = panel.items.select { |item| item.is_a?(Entities::Table) }
    tables.each do |table|
      questions = table.items.select { |item| item.is_a?(Entities::Question) }
      questions.each { |question| validate_table_question(question) }
    end
  end
end
validate_title(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 44
def validate_title(questionnaire)
  if questionnaire.title.blank?
    fail "Questionnaire title is missing."
  end
end

Private Instance Methods

validate_html(html, key = nil) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 325
def validate_html(html, key = nil)
  fragment = Nokogiri::HTML5.fragment(html, max_errors: 3)
  return unless fragment.errors.present?

  fail "#{key || html} contains invalid html: #{fragment.errors.map(&:to_s).join(', ')}."
end
validate_key_format(key) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 233
def validate_key_format(key)
  if key.to_s.length > MAX_KEY_LENGTH
    fail "Key '#{key}' should contain at most #{MAX_KEY_LENGTH} characters."
  end
  unless key.to_s.start_with?(KEY_PREFIX)
    fail "Key '#{key}' should start with '#{KEY_PREFIX}'."
  end
end
validate_markdown(markdown, key) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 321
def validate_markdown(markdown, key)
  validate_html(MarkdownParser.new(markdown).to_html, key)
end
validate_markdown_fields(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 293
def validate_markdown_fields(questionnaire)
  questionnaire.panels.each do |panel|
    panel.items.select { |item| item.is_a?(Entities::Text) }.each do |text_item|
      validate_markdown(text_item.str, text_item.str)
    end
  end
  questionnaire.questions.each do |question|
    Entities::Question::MARKDOWN_ATTRIBUTES.each do |attr|
      validate_markdown(question.send(attr), "#{question.key}.#{attr}")
      question.options.each do |option|
        Entities::QuestionOption::MARKDOWN_ATTRIBUTES.each do |option_attr|
          validate_markdown(option.send(option_attr), "#{question.key}:#{option.key}.#{option_attr}")
        end
      end
    end
  end
end
validate_no_spaces_before_question_nr_in_title(question) click to toggle source

Don't write question numbers as “ 1. Title”, but as “1\. Title”.

# File lib/quby/compiler/services/definition_validator.rb, line 215
def validate_no_spaces_before_question_nr_in_title(question)
  if question.title && question.title.match(/^\s{2,}\d+\\\./)
    fail "Question with number does not need leading spaces."
  end
end
validate_not_subquestion(questionnaire, key, msg_base:) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 227
def validate_not_subquestion(questionnaire, key, msg_base:)
  if questionnaire.question_hash[key].subquestion?
    fail msg_base + " references subquestion #{key}"
  end
end
validate_placeholder_options_nil_values(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 261
def validate_placeholder_options_nil_values(question)
  question.options.each do |question_option|
    if question_option.placeholder && question_option.value.present?
      fail "#{question.key}:#{question_option.key}: Placeholder options should not have values defined."
    end
  end
end
validate_question_key_exists?(questionnaire, key, msg_base:) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 221
def validate_question_key_exists?(questionnaire, key, msg_base:)
  unless questionnaire.question_hash[key]
    fail msg_base + " references nonexistent question #{key}"
  end
end
validate_raw_content_items(questionnaire) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 311
def validate_raw_content_items(questionnaire)
  questionnaire.panels.each do |panel|
    panel.items.each do |item|
      next if item.raw_content.blank?

      validate_html(item.raw_content)
    end
  end
end
validate_score_key_length(score) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 242
def validate_score_key_length(score)
  if score.key.to_s.length > MAX_KEY_LENGTH
    fail "Score key `#{score.key}` should contain at most #{MAX_KEY_LENGTH} characters."
  end
end
validate_score_label_present(score) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 248
def validate_score_label_present(score)
  fail "Score #{score.key} label must be passed in as an option." unless score.label.present?
end
validate_subquestion_absence_in_select(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 252
def validate_subquestion_absence_in_select(question)
  return unless question.type == :select
  question.options.each do |option|
    unless option.questions.empty?
      fail "Question '#{question.key}' of type ':select' may not include other questions."
    end
  end
end
validate_table_question(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 284
def validate_table_question(question)
  question.subquestions.each do |subquestion|
    if subquestion.presentation != :next_to_title
      fail "Question #{question.key} is inside a table, but has a subquestion #{subquestion.key}, " \
            "which is not allowed."
    end
  end
end
validate_values_unique(question) click to toggle source
# File lib/quby/compiler/services/definition_validator.rb, line 269
def validate_values_unique(question)
  return if question.type == :check_box || question.allow_duplicate_option_values

  question.options.each_with_object([]) do |question_option, seen_values|
    next if question_option.placeholder || question_option.inner_title

    fail "#{question.key}:#{question_option.key}: Has no option value defined." if question_option.value.blank?
    if seen_values.include?(question_option.value)
      fail "#{question.key}:#{question_option.key}: " \
              "Another option with value #{question_option.value} is already defined."
    end
    seen_values << question_option.value
  end
end