lib/autoparse/instance.rb in autoparse-0.1.0 vs lib/autoparse/instance.rb in autoparse-0.2.0
- old
+ new
@@ -19,17 +19,53 @@
require 'addressable/uri'
module AutoParse
class Instance
def self.uri
- return @uri ||= nil
+ return (@uri ||=
+ (@schema_data ? Addressable::URI.parse(@schema_data['id']) : nil)
+ )
end
+ def self.dereference
+ if @schema_data['$ref']
+ # Dereference the schema if necessary.
+ schema_uri =
+ self.uri + Addressable::URI.parse(@schema_data['$ref'])
+ schema_class = AutoParse.schemas[schema_uri]
+ if schema_class == nil
+ raise ArgumentError,
+ "Could not find schema: #{@schema_data['$ref']}. " +
+ "Referenced schema must be parsed first."
+ else
+ return schema_class
+ end
+ else
+ return self
+ end
+ end
+
def self.properties
- return @properties ||= {}
+ return @properties ||= (
+ if self.superclass.ancestors.include?(::AutoParse::Instance)
+ self.superclass.properties.dup
+ else
+ {}
+ end
+ )
end
+ def self.keys
+ return @keys ||= (
+ if self.superclass.ancestors.include?(::AutoParse::Instance)
+ self.superclass.keys.dup
+ else
+ {}
+ end
+ )
+ end
+
def self.additional_properties_schema
return EMPTY_SCHEMA
end
def self.property_dependencies
@@ -52,104 +88,16 @@
# TODO: implement more than type-checking
return true
end
end
- def self.define_string_property(property_name, key, schema_data)
- define_method(property_name) do
- value = self[key] || schema_data['default']
- if value != nil
- if schema_data['format'] == 'byte'
- Base64.decode64(value)
- elsif schema_data['format'] == 'date-time'
- Time.parse(value)
- elsif schema_data['format'] == 'url'
- Addressable::URI.parse(value)
- elsif schema_data['format'] =~ /^u?int(32|64)$/
- value.to_i
- else
- value
- end
- else
- nil
- end
- end
- define_method(property_name + '=') do |value|
- if schema_data['format'] == 'byte'
- self[key] = Base64.encode64(value)
- elsif schema_data['format'] == 'date-time'
- if value.respond_to?(:to_str)
- value = Time.parse(value.to_str)
- elsif !value.respond_to?(:xmlschema)
- raise TypeError,
- "Could not obtain RFC 3339 timestamp from #{value.class}."
- end
- self[key] = value.xmlschema
- elsif schema_data['format'] == 'url'
- # This effectively does limited URI validation.
- self[key] = Addressable::URI.parse(value).to_str
- elsif schema_data['format'] =~ /^u?int(32|64)$/
- self[key] = value.to_s
- elsif value.respond_to?(:to_str)
- self[key] = value.to_str
- elsif value.kind_of?(Symbol)
- self[key] = value.to_s
- else
- raise TypeError,
- "Expected String or Symbol, got #{value.class}."
- end
- end
- end
-
- def self.define_boolean_property(property_name, key, schema_data)
- define_method(property_name) do
- value = self[key] || schema_data['default']
- case value.to_s.downcase
- when 'true', 'yes', 'y', 'on', '1'
- true
- when 'false', 'no', 'n', 'off', '0'
- false
- when 'nil', 'null'
- nil
- else
- raise TypeError,
- "Expected boolean, got #{value.class}."
- end
- end
- define_method(property_name + '=') do |value|
- case value.to_s.downcase
- when 'true', 'yes', 'y', 'on', '1'
- self[key] = true
- when 'false', 'no', 'n', 'off', '0'
- self[key] = false
- when 'nil', 'null'
- self[key] = nil
- else
- raise TypeError, "Expected boolean, got #{value.class}."
- end
- end
- end
-
- def self.validate_number_property(property_value, schema_data)
- return false if !property_value.kind_of?(Numeric)
+ def self.validate_boolean_property(property_value, schema_data)
+ return false if property_value != true && property_value != false
# TODO: implement more than type-checking
return true
end
- def self.define_number_property(property_name, key, schema_data)
- define_method(property_name) do
- Float(self[key] || schema_data['default'])
- end
- define_method(property_name + '=') do |value|
- if value == nil
- self[key] = value
- else
- self[key] = Float(value)
- end
- end
- end
-
def self.validate_integer_property(property_value, schema_data)
return false if !property_value.kind_of?(Integer)
if schema_data['minimum'] && schema_data['exclusiveMinimum']
return false if property_value <= schema_data['minimum']
elsif schema_data['minimum']
@@ -161,21 +109,14 @@
return false if property_value > schema_data['maximum']
end
return true
end
- def self.define_integer_property(property_name, key, schema_data)
- define_method(property_name) do
- Integer(self[key] || schema_data['default'])
- end
- define_method(property_name + '=') do |value|
- if value == nil
- self[key] = value
- else
- self[key] = Integer(value)
- end
- end
+ def self.validate_number_property(property_value, schema_data)
+ return false if !property_value.kind_of?(Numeric)
+ # TODO: implement more than type-checking
+ return true
end
def self.validate_array_property(property_value, schema_data)
if property_value.respond_to?(:to_ary)
property_value = property_value.to_ary
@@ -188,79 +129,71 @@
end
end
return true
end
- def self.define_array_property(property_name, key, schema_data)
- define_method(property_name) do
- # The default value of an empty Array obviates a mutator method.
- value = self[key] || []
- array = if value != nil && !value.respond_to?(:to_ary)
- raise TypeError,
- "Expected Array, got #{value.class}."
+ def self.validate_object_property(property_value, schema_data)
+ if property_value.kind_of?(Instance)
+ return property_value.valid?
+ else
+ # This is highly ineffecient, but currently hard to avoid given the
+ # schema is anonymous, making lookups very difficult.
+ if schema_data.has_key?('id')
+ schema = AutoParse.generate(schema_data)
else
- value.to_ary
+ # If the schema has no ID, it inherits the ID from the parent schema,
+ # which should be `self`.
+ schema = AutoParse.generate(schema_data, self.uri)
end
- if schema_data['items'] && schema_data['items']['$ref']
- schema_name = schema_data['items']['$ref']
- # FIXME: Vestigial bits need to be replaced with a more viable
- # lookup system.
- if AutoParse.schemas[schema_name]
- schema_class = AutoParse.schemas[schema_name]
- array.map! do |item|
- schema_class.new(item)
- end
- else
- raise ArgumentError,
- "Could not find schema: #{schema_uri}."
- end
+ begin
+ return schema.new(property_value).valid?
+ rescue TypeError, ArgumentError, ::JSON::ParserError
+ return false
end
- array
end
end
- def self.validate_object_property(property_value, schema_data, schema=nil)
- if property_value.kind_of?(Instance)
- return property_value.valid?
- elsif schema != nil && schema.kind_of?(Class)
- return schema.new(property_value).valid?
- else
- # This is highly ineffecient, but hard to avoid given the schema is
- # anonymous.
- schema = AutoParse.generate(schema_data)
- return schema.new(property_value).valid?
- end
- end
-
- def self.define_object_property(property_name, key, schema_data)
- # TODO finish this up...
- if schema_data['$ref']
- schema_uri = self.uri + Addressable::URI.parse(schema_data['$ref'])
- schema = AutoParse.schemas[schema_uri]
- if schema == nil
- raise ArgumentError,
- "Could not find schema: #{schema_data['$ref']} " +
- "Referenced schema must be parsed first."
+ def self.validate_union_property(property_value, schema_data)
+ union = schema_data['type']
+ possible_types = [union].flatten.compact
+ for type in possible_types
+ case type
+ when 'string'
+ return true if self.validate_string_property(
+ property_value, schema_data
+ )
+ when 'boolean'
+ return true if self.validate_boolean_property(
+ property_value, schema_data
+ )
+ when 'integer'
+ return true if self.validate_integer_property(
+ property_value, schema_data
+ )
+ when 'number'
+ return true if self.validate_number_property(
+ property_value, schema_data
+ )
+ when 'array'
+ return true if self.validate_array_property(
+ property_value, schema_data
+ )
+ when 'object'
+ return true if self.validate_object_property(
+ property_value, schema_data
+ )
+ when 'null'
+ return true if property_value.nil?
+ when 'any'
+ return true
end
- else
- # Anonymous schema
- schema = AutoParse.generate(schema_data)
end
- define_method(property_name) do
- schema.new(self[key] || schema_data['default'])
- end
+ # None of the union types validated.
+ # An empty union will fail to validate anything.
+ return false
end
- def self.define_any_property(property_name, key, schema_data)
- define_method(property_name) do
- self[key] || schema_data['default']
- end
- define_method(property_name + '=') do |value|
- self[key] = value
- end
- end
-
##
# @api private
def self.validate_property_value(property_value, schema_data)
if property_value == nil && schema_data['required'] == true
return false
@@ -273,11 +206,11 @@
if schema_data['$ref']
schema_uri = self.uri + Addressable::URI.parse(schema_data['$ref'])
schema = AutoParse.schemas[schema_uri]
if schema == nil
raise ArgumentError,
- "Could not find schema: #{schema_data['$ref']} " +
+ "Could not find schema: #{schema_data['$ref']}. " +
"Referenced schema must be parsed first."
end
schema_data = schema.data
end
case schema_data['type']
@@ -287,53 +220,175 @@
)
when 'boolean'
return false unless self.validate_boolean_property(
property_value, schema_data
)
- when 'number'
- return false unless self.validate_number_property(
- property_value, schema_data
- )
when 'integer'
return false unless self.validate_integer_property(
property_value, schema_data
)
+ when 'number'
+ return false unless self.validate_number_property(
+ property_value, schema_data
+ )
when 'array'
return false unless self.validate_array_property(
property_value, schema_data
)
when 'object'
return false unless self.validate_object_property(
property_value, schema_data
)
+ when 'null'
+ return false unless property_value.nil?
+ when Array
+ return false unless self.validate_union_property(
+ property_value, schema_data
+ )
else
# Either type 'any' or we don't know what this is,
# default to anything goes. Validation of an 'any' property always
# succeeds.
end
return true
end
- def initialize(data)
- if self.class.data &&
- self.class.data['type'] &&
- self.class.data['type'] != 'object'
- raise TypeError,
- "Only schemas of type 'object' are instantiable."
+ def initialize(data={})
+ if (self.class.data || {})['type'] == nil
+ # Type is omitted, default value is any.
+ else
+ type_set = [(self.class.data || {})['type']].flatten.compact
+ if !type_set.include?('object')
+ raise TypeError,
+ "Only schemas of type 'object' are instantiable:\n" +
+ "#{self.class.data.inspect}"
+ end
end
if data.respond_to?(:to_hash)
data = data.to_hash
elsif data.respond_to?(:to_json)
data = JSON.parse(data.to_json)
else
raise TypeError,
'Unable to parse. ' +
'Expected data to respond to either :to_hash or :to_json.'
end
+ if data['$ref']
+ raise TypeError,
+ "Cannot instantiate a reference schema. Must be dereferenced first."
+ end
@data = data
end
+ def method_missing(method, *params, &block)
+ schema_data = self.class.data
+ unless schema_data['additionalProperties']
+ # Do nothing special if additionalProperties is not set.
+ super
+ else
+ # We can't modify the method in-place because this affects the call
+ # to super.
+ property_name = method.to_s
+ assignment = false
+ # Property names simply identify the property and thus don't
+ # include the assignment operator.
+ if property_name[-1..-1] == '='
+ assignment = true
+ property_name[-1..-1] = ''
+ end
+ property_key = self.class.keys[property_name]
+ property_schema = self.class.properties[property_key]
+ # TODO: Properly support additionalProperties.
+ if property_key == nil || property_schema == nil
+ # Method not found.
+ return super
+ end
+ # If additionalProperties is simply set to true, no parsing takes
+ # place and all values are treated as 'any'.
+ if assignment
+ new_value = params[0]
+ __set__(property_name, new_value)
+ else
+ __get__(property_name)
+ end
+ end
+ end
+
+ def __get__(property_name)
+ property_key = self.class.keys[property_name]
+
+ schema_class = self.class.properties[property_key]
+ if !schema_class
+ raise TypeError,
+ "Missing property schema for '#{property_key}'."
+ end
+ if schema_class.data['$ref']
+ # Dereference the schema if necessary.
+ schema_class = schema_class.dereference
+ # Avoid this dereference in the future.
+ self.class.properties[property_key] = schema_class
+ end
+
+ value = self[property_key] || schema_class.data['default']
+
+ case schema_class.data['type']
+ when 'string'
+ AutoParse.import_string(value, schema_class)
+ when 'boolean'
+ AutoParse.import_boolean(value, schema_class)
+ when 'integer'
+ AutoParse.import_integer(value, schema_class)
+ when 'number'
+ AutoParse.import_number(value, schema_class)
+ when 'array'
+ AutoParse.import_array(value, schema_class)
+ when 'object'
+ AutoParse.import_object(value, schema_class)
+ when 'null'
+ nil
+ when Array
+ AutoParse.import_union(value, schema_class)
+ else
+ AutoParse.import_any(value, schema_class)
+ end
+ end
+ protected :__get__
+
+ def __set__(property_name, value)
+ property_key = self.class.keys[property_name]
+
+ schema_class = self.class.properties[property_key]
+ if schema_class.data['$ref']
+ # Dereference the schema if necessary.
+ schema_class = schema_class.dereference
+ # Avoid this dereference in the future.
+ self.class.properties[property_key] = schema_class
+ end
+
+ case schema_class.data['type']
+ when 'string'
+ self[property_key] = AutoParse.export_string(value, schema_class)
+ when 'boolean'
+ self[property_key] = AutoParse.export_boolean(value, schema_class)
+ when 'integer'
+ self[property_key] = AutoParse.export_integer(value, schema_class)
+ when 'number'
+ self[property_key] = AutoParse.export_number(value, schema_class)
+ when 'array'
+ self[property_key] = AutoParse.export_array(value, schema_class)
+ when 'object'
+ self[property_key] = AutoParse.export_object(value, schema_class)
+ when 'null'
+ self[property_key] = nil
+ when Array
+ self[property_key] = AutoParse.export_union(value, schema_class)
+ else
+ self[property_key] = AutoParse.export_any(value, schema_class)
+ end
+ end
+ protected :__set__
+
def [](key)
return @data[key]
end
def []=(key, value)
@@ -342,16 +397,17 @@
##
# Validates the parsed data against the schema.
def valid?
unvalidated_fields = @data.keys.dup
- for property_key, property_schema in self.class.properties
+ for property_key, schema_class in self.class.properties
property_value = self[property_key]
- if !self.class.validate_property_value(property_value, property_schema)
+ if !self.class.validate_property_value(
+ property_value, schema_class.data)
return false
end
- if property_value == nil && property_schema['required'] != true
+ if property_value == nil && schema_class.data['required'] != true
# Value was omitted, but not required. Still valid. Skip dependency
# checks.
next
end
@@ -395,10 +451,10 @@
def to_hash
return @data
end
def to_json
- return JSON.generate(self.to_hash)
+ return ::JSON.generate(self.to_hash)
end
##
# Returns a <code>String</code> representation of the schema instance.
#