[tests] Extracts schema validator to class

Created Fog::Schema::DataValidator to contain the matching logic for the
data based schemas used by the tests.

This is tested in isolation and removes dependency on Shindo. Also
exposes a #message call to get the last message set by the validator to
avoid coupling the test output to the internals.

Tests for the #data_matches_schema helper are added as well.
This commit is contained in:
Paul Thornthwaite 2013-01-23 11:27:14 +00:00
Родитель 0793a42a64
Коммит 5f63112640
4 изменённых файлов: 329 добавлений и 193 удалений

Просмотреть файл

@ -0,0 +1,153 @@
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"
# When being strict values not specified in the schema are fails
unless options[:allow_extra_keys]
if validator.respond_to?(:empty?) && 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 validator.respond_to?(:empty?) && value.respond_to?(:empty?)
# Validator has rules left but no more values
return false if value.empty? && !validator.empty?
end
end
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)
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

Просмотреть файл

@ -1,3 +1,5 @@
require "fog/schema/data_validator"
# format related hackery
# allows both true.is_a?(Fog::Boolean) and false.is_a?(Fog::Boolean)
# allows both nil.is_a?(Fog::Nullable::String) and ''.is_a?(Fog::Nullable::String)
@ -35,10 +37,10 @@ module Shindo
# Strict mode will fail if the data has additional keys. Setting
# +strict+ to +false+ will allow additional keys to appear.
#
# @param [Hash] schmea A Hash schema
# @param [Hash] schema A Hash schema
# @param [Hash] options Options to change validation rules
# @option options [Boolean] :allow_extra_keys
# If +true+ deoes not fail when keys are in the data that are
# If +true+ does not fail when keys are in the data that are
# not specified in the schema. This allows new values to
# appear in API output without breaking the check.
# @option options [Boolean] :allow_optional_rules
@ -47,13 +49,13 @@ module Shindo
# @yield Data to check with schema
#
# @example Using in a test
# Shindo.tests("data matches schema") do
# data = {:string => "Hello" }
# data_matches_schema(:string => String) { data }
# Shindo.tests("comparing welcome data against schema") do
# data = {:welcome => "Hello" }
# data_matches_schema(:welcome => String) { data }
# end
#
# data matches schema
# + has proper format
# comparing welcome data against schema
# + data matches schema
#
# @example Example schema
# {
@ -70,11 +72,14 @@ module Shindo
# @return [Boolean]
def data_matches_schema(schema, options = {})
test('data matches schema') do
confirm_data_matches_schema(yield, schema, options)
validator = Fog::Schema::DataValidator.new
valid = validator.validate(yield, schema, options)
@message = validator.message unless valid
valid
end
end
# @deprecation #formats is deprecated. Use #data_matches_schema instead
# @deprecated #formats is deprecated. Use #data_matches_schema instead
def formats(format, strict = true)
test('has proper format') do
if strict
@ -82,86 +87,10 @@ module Shindo
else
options = {:allow_extra_keys => true, :allow_optional_rules => true}
end
confirm_data_matches_schema(yield, format, options)
end
end
private
# 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. Allows
# @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 confirm_data_matches_schema(data, schema, options = {})
# Clear message passed to the Shindo tests
@message = nil
valid = validate_value(schema, data, options)
unless valid
@message = "#{data.inspect} does not match #{schema.inspect}"
end
valid
end
# 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"
# When being strict values not specified in the schema are fails
unless options[:allow_extra_keys]
if validator.respond_to?(:empty?) && 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 validator.respond_to?(:empty?) && value.respond_to?(:empty?)
# Validator has rules left but no more values
return false if value.empty? && !validator.empty?
end
end
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)
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
validator = Fog::Schema::DataValidator.new
valid = validator.validate(yield, format, options)
@message = validator.message unless valid
valid
end
end
end

Просмотреть файл

@ -1,5 +1,60 @@
Shindo.tests('test_helper', 'meta') do
tests('comparing welcome data against schema') do
data = {:welcome => "Hello" }
data_matches_schema(:welcome => String) { data }
end
tests('#data_matches_schema') do
tests('when value matches schema expectation') do
data_matches_schema({"key" => String}) { {"key" => "Value"} }
end
tests('when values within an array all match schema expectation') do
data_matches_schema({"key" => [Integer]}) { {"key" => [1, 2]} }
end
tests('when nested values match schema expectation') do
data_matches_schema({"key" => {:nested_key => String}}) { {"key" => {:nested_key => "Value"}} }
end
tests('when collection of values all match schema expectation') do
data_matches_schema([{"key" => String}]) { [{"key" => "Value"}, {"key" => "Value"}] }
end
tests('when collection is empty although schema covers optional members') do
data_matches_schema([{"key" => String}], {:allow_optional_rules => true}) { [] }
end
tests('when additional keys are passed and not strict') do
data_matches_schema({"key" => String}, {:allow_extra_keys => true}) { {"key" => "Value", :extra => "Bonus"} }
end
tests('when value is nil and schema expects NilClass') do
data_matches_schema({"key" => NilClass}) { {"key" => nil} }
end
tests('when value and schema match as hashes') do
data_matches_schema({}) { {} }
end
tests('when value and schema match as arrays') do
data_matches_schema([]) { [] }
end
tests('when value is a Time') do
data_matches_schema({"time" => Time}) { {"time" => Time.now} }
end
tests('when key is missing but value should be NilClass (#1477)') do
data_matches_schema({"key" => NilClass}, {:allow_optional_rules => true}) { {} }
end
tests('when key is missing but value is nullable (#1477)') do
data_matches_schema({"key" => Fog::Nullable::String}, {:allow_optional_rules => true}) { {} }
end
end
tests('#formats backwards compatible changes') do
tests('when value matches schema expectation') do
@ -52,109 +107,5 @@ Shindo.tests('test_helper', 'meta') do
end
tests('data matches schema') do
data = {:welcome => "Hello" }
data_matches_schema(:welcome => String) { data }
end
tests('#confirm_data_matches_schema') do
tests('returns true') do
returns(true, 'when value matches schema expectation') do
confirm_data_matches_schema({"key" => "Value"}, {"key" => String})
end
returns(true, 'when values within an array all match schema expectation') do
confirm_data_matches_schema({"key" => [1, 2]}, {"key" => [Integer]})
end
returns(true, 'when nested values match schema expectation') do
confirm_data_matches_schema({"key" => {:nested_key => "Value"}}, {"key" => {:nested_key => String}})
end
returns(true, 'when collection of values all match schema expectation') do
confirm_data_matches_schema([{"key" => "Value"}, {"key" => "Value"}], [{"key" => String}])
end
returns(true, 'when collection is empty although schema covers optional members') do
confirm_data_matches_schema([], [{"key" => String}], {:allow_optional_rules => true})
end
returns(true, 'when additional keys are passed and not strict') do
confirm_data_matches_schema({"key" => "Value", :extra => "Bonus"}, {"key" => String}, {:allow_extra_keys => true})
end
returns(true, 'when value is nil and schema expects NilClass') do
confirm_data_matches_schema({"key" => nil}, {"key" => NilClass})
end
returns(true, 'when value and schema match as hashes') do
confirm_data_matches_schema({}, {})
end
returns(true, 'when value and schema match as arrays') do
confirm_data_matches_schema([], [])
end
returns(true, 'when value is a Time') do
confirm_data_matches_schema({"time" => Time.now}, {"time" => Time})
end
returns(true, 'when key is missing but value should be NilClass (#1477)') do
confirm_data_matches_schema({}, {"key" => NilClass}, {:allow_optional_rules => true})
end
returns(true, 'when key is missing but value is nullable (#1477)') do
confirm_data_matches_schema({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => true})
end
end
tests('returns false') do
returns(false, 'when value does not match schema expectation') do
confirm_data_matches_schema({"key" => nil}, {"key" => String})
end
returns(false, 'when key formats do not match') do
confirm_data_matches_schema({"key" => "Value"}, {:key => String})
end
returns(false, 'when additional keys are passed and strict') do
confirm_data_matches_schema({"key" => "Missing"}, {})
end
returns(false, 'when some keys do not appear') do
confirm_data_matches_schema({}, {"key" => String})
end
returns(false, 'when collection contains a member that does not match schema') do
confirm_data_matches_schema([{"key" => "Value"}, {"key" => 5}], [{"key" => String}])
end
returns(false, 'when hash and array are compared') do
confirm_data_matches_schema({}, [])
end
returns(false, 'when array and hash are compared') do
confirm_data_matches_schema([], {})
end
returns(false, 'when a hash is expected but another data type is found') do
confirm_data_matches_schema({"key" => {:nested_key => []}}, {"key" => {:nested_key => {}}})
end
returns(false, 'when key is missing but value should be NilClass (#1477)') do
confirm_data_matches_schema({}, {"key" => NilClass}, {:allow_optional_rules => false})
end
returns(false, 'when key is missing but value is nullable (#1477)') do
confirm_data_matches_schema({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => false})
end
end
end
end

Просмотреть файл

@ -0,0 +1,103 @@
Shindo.tests('Fog::Schema::DataValidator', 'meta') do
validator = Fog::Schema::DataValidator.new
tests('#validate') do
tests('returns true') do
returns(true, 'when value matches schema expectation') do
validator.validate({"key" => "Value"}, {"key" => String})
end
returns(true, 'when values within an array all match schema expectation') do
validator.validate({"key" => [1, 2]}, {"key" => [Integer]})
end
returns(true, 'when nested values match schema expectation') do
validator.validate({"key" => {:nested_key => "Value"}}, {"key" => {:nested_key => String}})
end
returns(true, 'when collection of values all match schema expectation') do
validator.validate([{"key" => "Value"}, {"key" => "Value"}], [{"key" => String}])
end
returns(true, 'when collection is empty although schema covers optional members') do
validator.validate([], [{"key" => String}], {:allow_optional_rules => true})
end
returns(true, 'when additional keys are passed and not strict') do
validator.validate({"key" => "Value", :extra => "Bonus"}, {"key" => String}, {:allow_extra_keys => true})
end
returns(true, 'when value is nil and schema expects NilClass') do
validator.validate({"key" => nil}, {"key" => NilClass})
end
returns(true, 'when value and schema match as hashes') do
validator.validate({}, {})
end
returns(true, 'when value and schema match as arrays') do
validator.validate([], [])
end
returns(true, 'when value is a Time') do
validator.validate({"time" => Time.now}, {"time" => Time})
end
returns(true, 'when key is missing but value should be NilClass (#1477)') do
validator.validate({}, {"key" => NilClass}, {:allow_optional_rules => true})
end
returns(true, 'when key is missing but value is nullable (#1477)') do
validator.validate({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => true})
end
end
tests('returns false') do
returns(false, 'when value does not match schema expectation') do
validator.validate({"key" => nil}, {"key" => String})
end
returns(false, 'when key formats do not match') do
validator.validate({"key" => "Value"}, {:key => String})
end
returns(false, 'when additional keys are passed and strict') do
validator.validate({"key" => "Missing"}, {})
end
returns(false, 'when some keys do not appear') do
validator.validate({}, {"key" => String})
end
returns(false, 'when collection contains a member that does not match schema') do
validator.validate([{"key" => "Value"}, {"key" => 5}], [{"key" => String}])
end
returns(false, 'when hash and array are compared') do
validator.validate({}, [])
end
returns(false, 'when array and hash are compared') do
validator.validate([], {})
end
returns(false, 'when a hash is expected but another data type is found') do
validator.validate({"key" => {:nested_key => []}}, {"key" => {:nested_key => {}}})
end
returns(false, 'when key is missing but value should be NilClass (#1477)') do
validator.validate({}, {"key" => NilClass}, {:allow_optional_rules => false})
end
returns(false, 'when key is missing but value is nullable (#1477)') do
validator.validate({}, {"key" => Fog::Nullable::String}, {:allow_optional_rules => false})
end
end
end
end