lib/jsonschema.rb in jsonschema-1.0.0 vs lib/jsonschema.rb in jsonschema-2.0.0

- old
+ new

@@ -1,381 +1,265 @@ # vim: fileencoding=utf-8 module JSON class Schema - VERSION = '1.0.0' + VERSION = '2.0.0' class ValueError < Exception;end + class Undefined;end TypesMap = { "string" => String, - "integer" => Integer, - "number" => [Integer, Float], + "integer" => [Integer, Fixnum], + "number" => [Integer, Float, Fixnum, Numeric], "boolean" => [TrueClass, FalseClass], "object" => Hash, "array" => Array, "null" => NilClass, "any" => nil } - TypesList = [String, Integer, Float, TrueClass, FalseClass, Hash, Array, NilClass] - DefaultSchema = { - "id" => nil, - "type" => nil, - "properties" => nil, - "items" => nil, - "optional" => false, - "additionalProperties" => nil, - "requires" => nil, - "identity" => nil, - "minimum" => nil, - "maximum" => nil, - "minItems" => nil, - "maxItems" => nil, - "pattern" => nil, - "maxLength" => nil, - "minLength" => nil, - "enum" => nil, - "options" => nil, - "readonly" => nil, - "title" => nil, - "description" => nil, - "format" => nil, - "default" => nil, - "transient" => nil, - "maxDecimal" => nil, - "hidden" => nil, - "disallow" => nil, - "extends" => nil - } - def initialize interactive=true + TypesList = [String, Integer, Float, Fixnum, Numeric, TrueClass, FalseClass, Hash, Array, NilClass] + def initialize interactive @interactive = interactive @refmap = {} end - def validate data, schema - @refmap = { - '$' => schema - } - _validate(data, schema) - end + def check_property value, schema, key, parent + if schema +# if @interactive && schema['readonly'] +# raise ValueError, "#{key} is a readonly field , it can not be changed" +# end - private - def validate_id x, fieldname, schema, id=nil - unless id.nil? - if id == '$' - raise ValueError, "Reference id for field '#{fieldname}' cannot equal '$'" + if schema['id'] + @refmap[schema['id']] = schema end - @refmap[id] = schema - end - return x - end - def validate_type x, fieldname, schema, fieldtype=nil - converted_fieldtype = convert_type(fieldtype) - fieldexists = true - begin - val = x.fetch(fieldname) - rescue IndexError - fieldexists = false - ensure - val = x[fieldname] - end - if converted_fieldtype && fieldexists - if converted_fieldtype.kind_of? Array - datavalid = false - converted_fieldtype.each do |type| - begin - validate_type(x, fieldname, type, type) - datavalid = true - break - rescue ValueError - next + if schema['extends'] + check_property(value, schema['extends'], key, parent) + end + + if value == Undefined + unless schema['optional'] + raise ValueError, "#{key} is missing and it is not optional" + end + + # default + if @interactive && !parent.include?(key) && !schema['default'].nil? + unless schema["readonly"] + parent[key] = schema['default'] end end - unless datavalid - raise ValueError, "Value #{val} for field '#{fieldname}' is not of type #{fieldtype}" - end - elsif converted_fieldtype.kind_of? Hash - begin - __validate(fieldname, x, converted_fieldtype) - rescue ValueError => e - raise e - end else - unless val.kind_of? converted_fieldtype - raise ValueError, "Value #{val} for field '#{fieldname}' is not of type #{fieldtype}" + + # type + if schema['type'] + check_type(value, schema['type'], key, parent) end - end - end - return x - end - def validate_properties x, fieldname, schema, properties=nil - if !properties.nil? && x[fieldname] - value = x[fieldname] - if value - if value.kind_of? Hash - if properties.kind_of? Hash - properties.each do |key, val| - __validate(key, value, val) - end - else - raise ValueError, "Properties definition of field '#{fieldname}' is not an object" + # disallow + if schema['disallow'] + flag = true + begin + check_type(value, schema['disallow'], key, parent) + rescue ValueError + flag = false end + raise ValueError, "disallowed value was matched" if flag end - end - end - return x - end - def validate_items x, fieldname, schema, items=nil - if !items.nil? && x[fieldname] - value = x[fieldname] - unless value.nil? - if value.kind_of? Array - if items.kind_of? Array - if items.size == value.size - items.each_with_index do |item, index| - begin - validate(value[index], item) - rescue ValueError => e - raise ValueError, "Failed to validate field '#{fieldname}' list schema: #{e.message}" + unless value.nil? + if value.instance_of? Array + if schema['items'] + if schema['items'].instance_of?(Array) + schema['items'].each_with_index {|val, index| + check_property(undefined_check(value, index), schema['items'][index], index, value) + } + if schema.include?('additionalProperties') + additional = schema['additionalProperties'] + if additional.instance_of?(FalseClass) + if schema['items'].size < value.size + raise ValueError, "There are more values in the array than are allowed by the items and additionalProperties restrictions." + end + else + value.each_with_index {|val, index| + check_property(undefined_check(value, index), schema['additionalProperties'], index, value) + } + end end + else + value.each_with_index {|val, index| + check_property(undefined_check(value, index), schema['items'], index, value) + } end - else - raise ValueError, "Length of list #{value} for field '#{fieldname}' is not equal to length of schema list" end - elsif items.kind_of? Hash - value.each do |val| - begin - _validate(val, items) - rescue ValueError => e - raise ValueError, "Failed to validate field '#{fieldname}' list schema: #{e.message}" + if schema['minItems'] && value.size < schema['minItems'] + raise ValueError, "There must be a minimum of #{schema['minItems']} in the array" + end + if schema['maxItems'] && value.size > schema['maxItems'] + raise ValueError, "There must be a maximum of #{schema['maxItems']} in the array" + end + elsif schema['properties'] + check_object(value, schema['properties'], schema['additionalProperties']) + elsif schema.include?('additionalProperties') + additional = schema['additionalProperties'] + unless additional.instance_of?(TrueClass) + if additional.instance_of?(Hash) || additional.instance_of?(FalseClass) + properties = {} + value.each {|k, val| + if additional.instance_of?(FalseClass) + raise ValueError, "Additional properties not defined by 'properties' are not allowed in field '#{k}'" + else + check_property(val, schema['additionalProperties'], k, value) + end + } + else + raise ValueError, "additionalProperties schema definition for field '#{}' is not an object" end end - else - raise ValueError, "Properties definition of field '#{fieldname}' is not a list or an object" end - end - end - end - return x - end - def validate_optional x, fieldname, schema, optional=false - if !x.include?(fieldname) && !optional - raise ValueError, "Required field '#{fieldname}' is missing" - end - return x - end + if value.instance_of?(String) + # pattern + if schema['pattern'] && !(value =~ Regexp.new(schema['pattern'])) + raise ValueError, "does not match the regex pattern #{schema['pattern']}" + end - def validate_additionalProperties x, fieldname, schema, additional_properties=nil - unless additional_properties.nil? - if additional_properties.kind_of? TrueClass - return x - end - value = x[fieldname] - if additional_properties.kind_of?(Hash) || additional_properties.kind_of?(FalseClass) - properties = schema["properties"] - unless properties - properties = {} - end - value.keys.each do |key| - unless properties.include? key - if additional_properties.kind_of? FalseClass - raise ValueError, "Additional properties not defined by 'properties' are not allowed in field '#{fieldname}'" - else - __validate(key, value, additional_properties) + strlen = value.split(//).size + # maxLength + if schema['maxLength'] && strlen > schema['maxLength'] + raise ValueError, "may only be #{schema['maxLength']} characters long" end + + # minLength + if schema['minLength'] && strlen < schema['minLength'] + raise ValueError, "must be at least #{schema['minLength']} characters long" + end end - end - else - raise ValueError, "additionalProperties schema definition for field '#{fieldname}' is not an object" - end - end - return x - end - def validate_requires x, fieldname, schema, requires=nil - if x[fieldname] && !requires.nil? - unless x[requires] - raise ValueError, "Field '#{requires}' is required by field '#{fieldname}'" - end - end - return x - end + if value.kind_of?(Numeric) - def validate_identity x, fieldname, schema, unique=false - return x - end + # minimum + minimumCanEqual + if schema['minimum'] + minimumCanEqual = schema.fetch('minimumCanEqual', Undefined) + if minimumCanEqual == Undefined || minimumCanEqual + if value < schema['minimum'] + raise ValueError, "must have a minimum value of #{schema['minimum']}" + end + else + if value <= schema['minimum'] + raise ValueError, "must have a minimum value of #{schema['minimum']}" + end + end + end - def validate_minimum x, fieldname, schema, minimum=nil - if !minimum.nil? && x[fieldname] - value = x[fieldname] - if value - if (value.kind_of?(Integer) || value.kind_of?(Float)) && value < minimum - raise ValueError, "Value #{value} for field '#{fieldname}' is less than minimum value: #{minimum}" - elsif value.kind_of?(Array) && value.size < minimum - raise ValueError, "Value #{value} for field '#{fieldname}' has fewer values than the minimum: #{minimum}" - end - end - end - return x - end + # maximum + maximumCanEqual + if schema['maximum'] + maximumCanEqual = schema.fetch('maximumCanEqual', Undefined) + if maximumCanEqual == Undefined || maximumCanEqual + if value > schema['maximum'] + raise ValueError, "must have a maximum value of #{schema['maximum']}" + end + else + if value >= schema['maximum'] + raise ValueError, "must have a maximum value of #{schema['maximum']}" + end + end + end - def validate_maximum x, fieldname, schema, maximum=nil - if !maximum.nil? && x[fieldname] - value = x[fieldname] - if value - if (value.kind_of?(Integer) || value.kind_of?(Float)) && value > maximum - raise ValueError, "Value #{value} for field '#{fieldname}' is greater than maximum value: #{maximum}" - elsif value.kind_of?(Array) && value.size > maximum - raise ValueError, "Value #{value} for field '#{fieldname}' has more values than the maximum: #{maximum}" - end - end - end - return x - end + # maxDecimal + if schema['maxDecimal'] && schema['maxDecimal'].kind_of?(Numeric) + if value.to_s =~ /\.\d{#{schema['maxDecimal']+1},}/ + raise ValueError, "may only have #{schema['maxDecimal']} digits of decimal places" + end + end - def validate_minItems x, fieldname, schema, minitems=nil - if !minitems.nil? && x[fieldname] - value = x[fieldname] - if value - if value.kind_of?(Array) && value.size < minitems - raise ValueError, "Value #{value} for field '#{fieldname}' must have a minimum of #{minitems} items" - end - end - end - return x - end + end - def validate_maxItems x, fieldname, schema, maxitems=nil - if !maxitems.nil? && x[fieldname] - value = x[fieldname] - if value - if value.kind_of?(Array) && value.size > maxitems - raise ValueError, "Value #{value} for field '#{fieldname}' must have a maximum of #{maxitems} items" + # enum + if schema['enum'] + unless(schema['enum'].detect{|enum| enum == value }) + raise ValueError, "does not have a value in the enumeration #{schema['enum'].join(", ")}" + end + end + + # description + if schema['description'] && !schema['description'].instance_of?(String) + raise ValueError, "The description for field '#{value}' must be a string" + end + + # title + if schema['title'] && !schema['title'].instance_of?(String) + raise ValueError, "The title for field '#{value}' must be a string" + end + + # format + if schema['format'] + end + end end end - return x end - def validate_pattern x, fieldname, schema, pattern=nil - value = x[fieldname] - if !pattern.nil? && value && value.kind_of?(String) - p = Regexp.new(pattern) - if !p.match(value) - raise ValueError, "Value #{value} for field '#{fieldname}' does not match regular expression '#{pattern}'" + def check_object value, object_type_def, additional + if object_type_def.instance_of? Hash + if !value.instance_of?(Hash) || value.instance_of?(Array) + raise ValueError, "an object is required" end - end - return x - end - def validate_maxLength x, fieldname, schema, length=nil - value = x[fieldname] - if !length.nil? && value && value.kind_of?(String) - # string length => 正規表現で分割して計測 - if value.split(//).size > length - raise ValueError, "Length of value #{value} for field '#{fieldname}' must be less than or equal to #{length}" - end + object_type_def.each {|key, odef| + if key.index('__') != 0 + check_property(undefined_check(value, key), odef, key, value) + end + } end - return x - end - - def validate_minLength x, fieldname, schema, length=nil - value = x[fieldname] - if !length.nil? && value && value.kind_of?(String) - if value.split(//).size < length - raise ValueError, "Length of value #{value} for field '#{fieldname}' must be more than or equal to #{length}" + value.each {|key, val| + if key.index('__') != 0 && object_type_def && !object_type_def[key] && additional == false + raise ValueError, "#{value.class} The property #{key} is not defined in the schema and the schema does not allow additional properties" end - end - return x - end - - def validate_enum x, fieldname, schema, options=nil - value = x[fieldname] - if !options.nil? && value - unless options.kind_of? Array - raise ValueError, "Enumeration #{options} for field '#{fieldname}' is not a list type" + requires = object_type_def && object_type_def[key] && object_type_def[key]['requires'] + if requires && !value.include?(requires) + raise ValueError, "the presence of the property #{key} requires that #{requires} also be present" end - unless options.include? value - raise ValueError, "Value #{value} for field '#{fieldname}' is not in the enumeration: #{options}" + if object_type_def && object_type_def.instance_of?(Hash) && !object_type_def.include?(key) + check_property(val, additional, key, value) end - end - return x - end - - def validate_options x, fieldname, schema, options=nil - return x - end - - def validate_readonly x, fieldname, schema, readonly=false - return x - end - - def validate_title x, fieldname, schema, title=nil - if !title.nil? && !title.kind_of?(String) - raise ValueError, "The title for field '#{fieldname}' must be a string" - end - return x - end - - def validate_description x, fieldname, schema, description=nil - if !description.nil? && !description.kind_of?(String) - raise ValueError, "The description for field '#{fieldname}' must be a string" - end - return x - end - - def validate_format x, fieldname, schema, format=nil - return x - end - - def validate_default x, fieldname, schema, default=nil - if @interactive && !x.include?(fieldname) && !default.nil? - unless schema["readonly"] - x[fieldname] = default + if !@interactive && val && val['$schema'] + check_property(val, val['$schema'], key, value) end - end - return x + } end - def validate_transient x, fieldname, schema, transient=false - return x - end - - def validate_maxDecimal x, fieldname, schema, maxdecimal=nil - value = x[fieldname] - if !maxdecimal.nil? && value - maxdecstring = value.to_s - index = maxdecstring.index('.') - if index && maxdecstring[(index+1)...maxdecstring.size].split(//u).size > maxdecimal - raise ValueError, "Value #{value} for field '#{fieldname}' must not have more than #{maxdecimal} decimal places" + def check_type value, type, key, parent + converted_fieldtype = convert_type(type) + if converted_fieldtype + if converted_fieldtype.instance_of? Array + datavalid = false + converted_fieldtype.each do |t| + begin + check_type(value, t, key, parent) + datavalid = true + break + rescue ValueError + next + end + end + unless datavalid + raise ValueError, "#{value.class} value found, but a #{type} is required" + end + elsif converted_fieldtype.instance_of? Hash + check_property(value, type, key, parent) + else + unless value.instance_of? converted_fieldtype + raise ValueError, "#{value.class} value found, but a #{type} is required" + end end end - return x end - def validate_hidden x, fieldname, schema, hidden=false - return x + def undefined_check value, key + value.fetch(key, Undefined) end - def validate_disallow x, fieldname, schema, disallow=nil - if !disallow.nil? - begin - validate_type(x, fieldname, schema, disallow) - rescue ValueError - return x - end - raise ValueError, "Value #{x[fieldname]} of type #{disallow} is disallowed for field '#{fieldname}'" - end - return x - end - - def validate_extends x, fieldname, schema, extends=nil - return x - end - def convert_type fieldtype if TypesList.include?(fieldtype) || fieldtype.kind_of?(Hash) return fieldtype elsif fieldtype.kind_of? Array converted_fields = [] @@ -393,37 +277,24 @@ raise ValueError, "Field type '#{fieldtype}' is not supported." end end end - def __validate fieldname, data, schema + def validate instance, schema + @tree = { + 'self' => instance + } if schema - if !schema.kind_of?(Hash) - raise ValueError, "Schema structure is invalid" - end - # copy - new_schema = Marshal.load(Marshal.dump(schema)) - DefaultSchema.each do |key, val| - new_schema[key] = val unless new_schema.include?(key) - end - new_schema.each do |key ,val| - validatorname = "validate_"+key - begin - __send__(validatorname, data, fieldname, schema, new_schema[key]) - rescue NoMethodError => e - raise ValueError, "Schema property '#{e.message}' is not supported" - end - end + check_property(instance, schema, 'self', @tree) + elsif instance && instance['$schema'] + # self definition schema + check_property(instance, instance['$schema'], 'self', @tree) end - return data + return @tree['self'] end - def _validate data, schema - __validate("_data", {"_data" => data}, schema) - end - class << self - def validate data, schema, interactive=true + def validate data, schema=nil, interactive=true validator = JSON::Schema.new(interactive) validator.validate(data, schema) end end end