module Fog module Schema # This validates a data object against a Ruby based schema to see # if they match. # # * An object matches the schema if +==+ or +===+ returns +true+ # * Hashes match if all the key's values match the classes given # in the schema as well. This can be configured in the options # * Arrays match when every element in the data matches the case # given in the schema. # # The schema and validation are very simple and probably not # suitable for some cases. # # The following classes can be used to check for special behaviour # # * Fog::Boolean - value may be +true+ or +false+ # * Fog::Nullable::Boolean - value may be +true+, +false+ or +nil+ # * Fog::Nullable::Integer - value may be an Integer or +nil+ # * Fog::Nullable::String # * Fog::Nullable::Time # * Fog::Nullable::Float # * Fog::Nullable::Hash # * Fog::Nullable::Array # # All the "nullable" objects will pass if the value is of the class # or if it is +nil+. This allows you to match APIs that may include # keys when the value is not available in some cases but will # always be a String. Such as an password that is only displayed # on the reset action. # # The keys for "nullable" resources should always be present but # original matcher had a bug that allowed these to also appear to # work as optional keys/values. # # If you need the original behaviour, data with a missing key is # still valid, then you may pass the +:allow_optional_rules+ # option to the #validate method. # # That is not recommended because you are describing a schema # with optional keys in a format that does not support it. # # Setting +:allow_extra_keys+ as +true+ allows the data to # contain keys not declared by the schema and still pass. This # is useful if new attributes appear in the API in a backwards # compatible manner and can be ignored. # # This is the behaviour you would have seen with +strict+ being # +false+ in the original test helper. # # @example Schema example # { # "id" => String, # "ram" => Integer, # "disks" => [ # "size" => Float # ], # "dns_name" => Fog::Nullable::String, # "active" => Fog::Boolean, # "created" => DateTime # } # class DataValidator def initialize @message = nil end # Checks if the data structure matches the schema passed in and # returns +true+ if it fits. # # @param [Object] data Hash or Array to check # @param [Object] schema Schema pattern to check against # @param [Boolean] options # @option options [Boolean] :allow_extra_keys # If +true+ does not fail if extra keys are in the data # that are not in the schema. # @option options [Boolean] :allow_optional_rules # If +true+ does not fail if extra keys are in the schema # that do not match the data. Not recommended! # # @return [Boolean] Did the data fit the schema? def validate(data, schema, options = {}) valid = validate_value(schema, data, options) unless valid @message = "#{data.inspect} does not match #{schema.inspect}" end valid end # This returns the last message set by the validator # # @return [String] def message @message end private # This contains a slightly modified version of the Hashidator gem # but unfortunately the gem does not cope with Array schemas. # # @see https://github.com/vangberg/hashidator/blob/master/lib/hashidator.rb # def validate_value(validator, value, options) Fog::Logger.write :debug, "[yellow][DEBUG] #{value.inspect} against #{validator.inspect}[/]\n" case validator when Array return false if value.is_a?(Hash) value.respond_to?(:all?) && value.all? {|x| validate_value(validator[0], x, options)} when Symbol value.respond_to? validator when Hash return false if value.is_a?(Array) # When being strict values not specified in the schema are fails unless options[:allow_extra_keys] if value.respond_to?(:empty?) # Validator is empty but values are not return false if !value.empty? && validator.empty? end end unless options[:allow_optional_rules] if value.respond_to?(:empty?) # Validator has rules left but no more values return false if value.empty? && !validator.empty? end end validator.all? do |key, sub_validator| Fog::Logger.write :debug, "[blue][DEBUG] #{key.inspect} against #{sub_validator.inspect}[/]\n" validate_value(sub_validator, value[key], options) end else result = validator == value result = validator === value unless result # Repeat unless we have a Boolean already unless (result.is_a?(TrueClass) || result.is_a?(FalseClass)) result = validate_value(result, value, options) end if result Fog::Logger.write :debug, "[green][DEBUG] Validation passed: #{value.inspect} against #{validator.inspect}[/]\n" else Fog::Logger.write :debug, "[red][DEBUG] Validation failed: #{value.inspect} against #{validator.inspect}[/]\n" end result end end end end end