Skip to content

Commit

Permalink
Merge 15ad8de into 1b5f459
Browse files Browse the repository at this point in the history
  • Loading branch information
notEthan committed Sep 19, 2018
2 parents 1b5f459 + 15ad8de commit 03bb5f6
Show file tree
Hide file tree
Showing 6 changed files with 341 additions and 264 deletions.
11 changes: 6 additions & 5 deletions lib/jsi/json-schema-fragments.rb
Expand Up @@ -84,24 +84,25 @@ def initialize(type, representation)
# pointed to by this pointer.
def evaluate(document)
reference_tokens.inject(document) do |value, token|
if value.is_a?(Array)
if value.respond_to?(:to_ary)
if token.is_a?(String) && token =~ /\A\d|[1-9]\d+\z/
token = token.to_i
end
unless token.is_a?(Integer)
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not an integer and cannot be resolved in array #{value.inspect}")
end
unless (0...value.size).include?(token)
unless (0...(value.respond_to?(:size) ? value : value.to_ary).size).include?(token)
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid index of #{value.inspect}")
end
elsif value.is_a?(Hash)
unless value.key?(token)
(value.respond_to?(:[]) ? value : value.to_ary)[token]
elsif value.respond_to?(:to_hash)
unless (value.respond_to?(:key?) ? value : value.to_hash).key?(token)
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} is not a valid key of #{value.inspect}")
end
(value.respond_to?(:[]) ? value : value.to_hash)[token]
else
raise(ReferenceError, "Invalid resolution for #{to_s}: #{token.inspect} cannot be resolved in #{value.inspect}")
end
value[token]
end
end

Expand Down
82 changes: 48 additions & 34 deletions lib/jsi/json/node.rb
Expand Up @@ -2,16 +2,16 @@ module JSI
module JSON
# JSI::JSON::Node is an abstraction of a node within a JSON document.
# it aims to act like the underlying data type of the node's content
# (Hash or Array, generally) in most cases.
# (generally Hash or Array-like) in most cases.
#
# the main advantage offered by using a Node over the underlying data
# is in dereferencing. if a Node consists of a hash with a $ref property
# pointing within the same document, then the Node will transparently
# follow the ref and return the referenced data.
#
# in most other respects, a Node aims to act like a Hash when the content
# is a Hash, an Array when the content is an array. methods of Hash and
# Array are defined and delegated to the node's content.
# is Hash-like, an Array when the content is Array-like. methods of Hash
# and Array are defined and delegated to the node's content.
#
# however, destructive methods are for the most part not implemented.
# at the moment only #[]= is implemented. since Node thinly wraps the
Expand All @@ -26,16 +26,16 @@ def self.new_doc(document)
new_by_type(document, [])
end

# if the content of the document at the given path is a Hash, returns
# a HashNode; if an Array, returns ArrayNode. otherwise returns a
# regular Node, though, for the most part this will be called with Hash
# or Array content.
# if the content of the document at the given path is Hash-like, returns
# a HashNode; if Array-like, returns ArrayNode. otherwise returns a
# regular Node, although Nodes are for the most part instantiated from
# Hash or Array-like content.
def self.new_by_type(document, path)
node = Node.new(document, path)
content = node.content
if content.is_a?(Hash)
if content.respond_to?(:to_hash)
HashNode.new(document, path)
elsif content.is_a?(Array)
elsif content.respond_to?(:to_ary)
ArrayNode.new(document, path)
else
node
Expand Down Expand Up @@ -66,18 +66,27 @@ def content
#
# if the content cannot be subscripted, raises TypeError.
#
# if the subcontent is a hash, it is wrapped as a JSI::JSON::HashNode before being returned.
# if the subcontent is an array, it is wrapped as a JSI::JSON::ArrayNode before being returned.
# if the subcontent is Hash-like, it is wrapped as a JSI::JSON::HashNode before being returned.
# if the subcontent is Array-like, it is wrapped as a JSI::JSON::ArrayNode before being returned.
#
# if this node's content is a $ref - that is, a hash with a $ref attribute - and the subscript is
# not a key of the hash, then the $ref is followed before returning the subcontent.
def [](subscript)
node = self
content = node.content
if content.is_a?(Hash) && !content.key?(subscript)
if content.respond_to?(:to_hash) && !(content.respond_to?(:key?) ? content : content.to_hash).key?(subscript)
node = node.deref
content = node.content
end
unless content.respond_to?(:[])
if content.respond_to?(:to_hash)
content = content.to_hash
elsif content.respond_to?(:to_ary)
content = content.to_ary
else
raise(NoMethodError, "undefined method `[]`\nsubscripting with #{subscript.pretty_inspect.chomp} (#{subscript.class}) from #{content.class.inspect}. content is: #{content.pretty_inspect.chomp}")
end
end
begin
subcontent = content[subscript]
rescue TypeError => e
Expand All @@ -101,31 +110,34 @@ def []=(subscript, value)
end
end

# returns a Node, dereferencing a $ref attribute if possible. if this node is not a hash,
# returns a Node, dereferencing a $ref attribute if possible. if this node is not hash-like,
# does not have a $ref, or if what its $ref cannot be found, this node is returned.
#
# currently only $refs pointing within the same document are followed.
def deref
content = self.content

return self unless content.is_a?(Hash) && content['$ref'].is_a?(String)
if content.respond_to?(:to_hash)
ref = (content.respond_to?(:[]) ? content : content.to_hash)['$ref']
end
return self unless ref.is_a?(String)

if content['$ref'][/\A#/]
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(content['$ref'])).deref
if ref[/\A#/]
return self.class.new_by_type(document, ::JSON::Schema::Pointer.parse_fragment(ref)).deref
end

# HAX for how google does refs and ids
if document_node['schemas'].respond_to?(:to_hash)
if document_node['schemas'][content['$ref']]
return document_node['schemas'][content['$ref']]
if document_node['schemas'][ref]
return document_node['schemas'][ref]
end
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == content['$ref'] }
_, deref_by_id = document_node['schemas'].detect { |_k, schema| schema['id'] == ref }
if deref_by_id
return deref_by_id
end
end

#raise(NotImplementedError, "cannot dereference #{content['$ref']}") # TODO
#raise(NotImplementedError, "cannot dereference #{ref}") # TODO
return self
end

Expand Down Expand Up @@ -177,11 +189,12 @@ def modified_copy
car = subpath[0]
cdr = subpath[1..-1]
if subdocument.respond_to?(:to_hash)
car_object = rec.call(subdocument[car], cdr)
if car_object.object_id == subdocument[car].object_id
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_hash)[car]
car_object = rec.call(subdocument_car, cdr)
if car_object.object_id == subdocument_car.object_id
subdocument
else
subdocument.merge({car => car_object})
(subdocument.respond_to?(:merge) ? subdocument : subdocument.to_hash).merge({car => car_object})
end
elsif subdocument.respond_to?(:to_ary)
if car.is_a?(String) && car =~ /\A\d+\z/
Expand All @@ -190,11 +203,12 @@ def modified_copy
unless car.is_a?(Integer)
raise(TypeError, "bad subscript #{car.pretty_inspect.chomp} with remaining subpath: #{cdr.inspect} for array: #{subdocument.pretty_inspect.chomp}")
end
car_object = rec.call(subdocument[car], cdr)
if car_object.object_id == subdocument[car].object_id
subdocument_car = (subdocument.respond_to?(:[]) ? subdocument : subdocument.to_ary)[car]
car_object = rec.call(subdocument_car, cdr)
if car_object.object_id == subdocument_car.object_id
subdocument
else
subdocument.dup.tap do |arr|
(subdocument.respond_to?(:[]=) ? subdocument : subdocument.to_ary).dup.tap do |arr|
arr[car] = car_object
end
end
Expand All @@ -209,7 +223,7 @@ def modified_copy

# meta-information about the object, outside the content. used by #inspect / #pretty_print
def object_group_text
"fragment=#{fragment.inspect}"
"fragment=#{fragment.inspect}" + (content.respond_to?(:object_group_text) ? ' ' + content.object_group_text : '')
end

# a string representing this node
Expand Down Expand Up @@ -247,8 +261,8 @@ def fingerprint
class ArrayNode < Node
# iterates over each element in the same manner as Array#each
def each
return to_enum(__method__) { content.size } unless block_given?
content.each_index { |i| yield self[i] }
return to_enum(__method__) { (content.respond_to?(:size) ? content : content.to_ary).size } unless block_given?
(content.respond_to?(:each_index) ? content : content.to_ary).each_index { |i| yield self[i] }
self
end

Expand All @@ -268,7 +282,7 @@ def as_json(*opt) # needs redefined after including Enumerable
# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
# we override these methods from Arraylike
SAFE_INDEX_ONLY_METHODS.each do |method_name|
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_ary).public_send(method_name, *a, &b) }
end
end

Expand All @@ -277,11 +291,11 @@ def as_json(*opt) # needs redefined after including Enumerable
class HashNode < Node
# iterates over each element in the same manner as Array#each
def each(&block)
return to_enum(__method__) { content.size } unless block_given?
return to_enum(__method__) { content.respond_to?(:size) ? content.size : content.to_ary.size } unless block_given?
if block.arity > 1
content.each_key { |k| yield k, self[k] }
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield k, self[k] }
else
content.each_key { |k| yield [k, self[k]] }
(content.respond_to?(:each_key) ? content : content.to_hash).each_key { |k| yield [k, self[k]] }
end
self
end
Expand All @@ -301,7 +315,7 @@ def as_json(*opt) # needs redefined after including Enumerable

# methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
SAFE_KEY_ONLY_METHODS.each do |method_name|
define_method(method_name) { |*a, &b| content.public_send(method_name, *a, &b) }
define_method(method_name) { |*a, &b| (content.respond_to?(method_name) ? content : content.to_hash).public_send(method_name, *a, &b) }
end
end
end
Expand Down
16 changes: 10 additions & 6 deletions lib/jsi/typelike_modules.rb
Expand Up @@ -37,14 +37,14 @@ def self.modified_copy(object, &block)
# array of object) cannot be expressed as json
def self.as_json(object, *opt)
if object.respond_to?(:to_hash)
object.map do |k, v|
(object.respond_to?(:map) ? object : object.to_hash).map do |k, v|
unless k.is_a?(Symbol) || k.respond_to?(:to_str)
raise(TypeError, "json object (hash) cannot be keyed with: #{k.pretty_inspect.chomp}")
end
{k.to_s => as_json(v, *opt)}
end.inject({}, &:update)
elsif object.respond_to?(:to_ary)
object.map { |e| as_json(e, *opt) }
(object.respond_to?(:map) ? object : object.to_ary).map { |e| as_json(e, *opt) }
elsif [String, TrueClass, FalseClass, NilClass, Numeric].any? { |c| object.is_a?(c) }
object
elsif object.is_a?(Symbol)
Expand Down Expand Up @@ -83,14 +83,16 @@ module Hashlike
safe_modified_copy_methods.each do |method_name|
define_method(method_name) do |*a, &b|
JSI::Typelike.modified_copy(self) do |object_to_modify|
object_to_modify.public_send(method_name, *a, &b)
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
responsive_object.public_send(method_name, *a, &b)
end
end
end
safe_kv_block_modified_copy_methods.each do |method_name|
define_method(method_name) do |*a, &b|
JSI::Typelike.modified_copy(self) do |object_to_modify|
object_to_modify.public_send(method_name, *a) do |k, _v|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
responsive_object.public_send(method_name, *a) do |k, _v|
b.call(k, self[k])
end
end
Expand Down Expand Up @@ -161,15 +163,17 @@ module Arraylike
safe_modified_copy_methods.each do |method_name|
define_method(method_name) do |*a, &b|
JSI::Typelike.modified_copy(self) do |object_to_modify|
object_to_modify.public_send(method_name, *a, &b)
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
responsive_object.public_send(method_name, *a, &b)
end
end
end
safe_el_block_methods.each do |method_name|
define_method(method_name) do |*a, &b|
JSI::Typelike.modified_copy(self) do |object_to_modify|
i = 0
object_to_modify.public_send(method_name, *a) do |_e|
responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
responsive_object.public_send(method_name, *a) do |_e|
b.call(self[i]).tap { i += 1 }
end
end
Expand Down

0 comments on commit 03bb5f6

Please sign in to comment.