# frozen_string_literal: true require 'active_support/hash_with_indifferent_access' require 'rspec_rails_api' module RSpec module Rails module Api # Set of method to validate data and data structures class Validator class << self ## # Validates an object keys and values types # # @param actual [*] Value to compare # @param expected [Hash, NilClass] Definition # # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors # otherwise def validate_object(actual, expected) return 'is not a hash' unless actual.is_a? Hash # Don't validate without a definition return unless expected keys_errors = validate_object_keys actual, expected return keys_errors unless keys_errors.nil? attributes_errors = validate_object_attributes(actual, expected) attributes_errors unless attributes_errors.keys.empty? end ## # Validates each entry of an array # # @param array [*] The array to check # @param expected [Symbol, Hash, NilClass] Attributes configuration # # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors # otherwise def validate_array(array, expected) return 'is not an array' unless array.is_a? Array # Arrays without an expected entry type return unless expected errors = {} array.each_with_index do |array_entry, index| value_error = validate_array_entry array_entry, expected errors["##{index}"] = value_error if value_error end errors unless errors.keys.empty? end ## # Returns a human-readable string from matcher errors # # @param errors [String,Hash] Validation errors # @param values [String] JSON string representing the value # # @return [String] def format_failure_message(errors, values) if errors.is_a? Hash errors = errors.deep_stringify_keys.to_yaml.split("\n") errors.shift errors.map! do |line| " #{line.sub(/^(\s+)"(#\d+)":(.*)$/, '\1\2:\3')}" end errors = errors.join("\n") end <<~TXT expected object structure not to have these errors: #{errors} As a notice, here is the JSON object: #{values} TXT end ## # Checks if a given type is in the supported types list # # @param type [Symbol] Type to check # @param except [[Symbol]] List of types to ignore # # @return [Boolean] def valid_type?(type, except: []) keys = PARAM_TYPES.keys.reject { |key| except.include? key } keys.include?(type) end ## # Checks if a value is of the given type # # @param value [*] Value to test # @param type [Symbol] Type to compare to # # @return [String,NilClass] True when the value corresponds to the given type def validate_type(value, type) if type == :boolean return nil if value.is_a?(TrueClass) || value.is_a?(FalseClass) return 'is not a "boolean"' end raise "Unknown type #{type}" unless PARAM_TYPES.key? type return nil if value.is_a? PARAM_TYPES[type][:class] "is not a \"#{type}\"" end private # Checks if a key should be skipped, whether it's missing and optional or nil # # @param key [Symbol] Key to check # @param value [*] Associated value # @param definition [Hash] Entity definitions # # @return [Boolean] def skip_key_check?(key, value, definition) # Ignore missing optional keys return true unless value.key?(key.to_s) || definition[key][:required] # Ignore null optional keys return true if !definition[key][:required] && value[key.to_s].nil? false end # Validates the keys of a hash # # @param actual [Hash] The hash to check # @param definition [Hash] The object definition # # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors # otherwise def validate_object_keys(actual, definition) # Hashes without an expected attributes type return unless definition errors = {} actual.each_key do |key| errors[key] = 'is not defined' unless definition.key?(key.to_sym) end errors unless errors.keys.empty? end ## # Validates the attributes of a Hash # # @param actual [Hash] Value to compare # @param expected [Hash] Definition # # @return [String, Hash, NilClass] Nil when no error, string when not an object and dictionary of errors # otherwise def validate_object_attributes(actual, expected) errors = {} expected.each_key do |key| next if skip_key_check? key, actual, expected value_error = validate_object_attribute key, actual, expected[key][:type], expected[key][:attributes] errors[key] = value_error unless value_error.nil? end errors unless errors.keys.nil? end # Checks the value of an entry in a Hash # # @param key [Symbol] Key to check # @param actual [Hash] Hash to check # @param expected_type [Symbol] Expected type # @param definition [Symbol,Hash] Attribute definition # # @return [String,Hash,NilClass] Nil when no error is met, string when not a primitive and dictionary of # errors otherwise def validate_object_attribute(key, actual, expected_type, definition) return 'is missing' unless actual.key? key.to_s case expected_type when :object validate_object actual[key.to_s], definition when :array validate_array actual[key.to_s], definition else validate_type actual[key.to_s], expected_type end end # Checks the validity of an array entry against a definition # # @param entry [*] Entry to check # @param definition [Hash] Fields definition # # @return [String,Hash,NilClass] Nil when no error is met, string when not a primitive and dictionary of # errors otherwise def validate_array_entry(entry, definition) if definition[:type].is_a? Symbol # Array of "simple" values validate_type entry, definition[:type] else # Objects validate_object entry, definition end end end end end end end