Skip to content

Commit

Permalink
Merge branch 'instance_any_type'
Browse files Browse the repository at this point in the history
  • Loading branch information
notEthan committed Sep 5, 2019
2 parents 25a7b77 + 7bed254 commit 0e44225
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 37 deletions.
86 changes: 61 additions & 25 deletions lib/jsi/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ def initialize(instance, ancestor: nil)
@ancestor = ancestor || self
self.instance = instance

if @instance.is_a?(JSI::JSON::HashNode)
if @instance.respond_to?(:to_hash)
extend BaseHash
elsif @instance.is_a?(JSI::JSON::ArrayNode)
elsif @instance.respond_to?(:to_ary)
extend BaseArray
end
end
Expand All @@ -108,6 +108,7 @@ def each
#
# @return [Array<JSI::Base>]
def parents
check_can_get_parents!
parent = @ancestor
(@ancestor.instance.pointer.reference_tokens.size...self.instance.pointer.reference_tokens.size).map do |i|
parent.tap do
Expand All @@ -128,7 +129,7 @@ def parent
#
# @return [JSI::Base, self]
def deref
derefed = instance.deref
derefed = instance.respond_to?(:deref) ? instance.deref : instance
if derefed.object_id == instance.object_id
self
else
Expand All @@ -144,7 +145,7 @@ def deref
# in a (nondestructively) modified copy of this.
# @return [JSI::Base subclass the same as self] the modified copy of self
def modified_copy(&block)
modified_instance = instance.modified_copy(&block)
modified_instance = Typelike.modified_copy(instance, &block)
self.class.new(modified_instance, ancestor: @ancestor)
end

Expand Down Expand Up @@ -194,7 +195,7 @@ def pretty_print(q)

# @return [String] the instance's object_group_text
def object_group_text
instance.object_group_text
instance.respond_to?(:object_group_text) ? instance.object_group_text : instance.class.inspect
end

# @return [Object] a jsonifiable representation of the instance
Expand All @@ -217,13 +218,12 @@ def instance=(thing)
if instance_variable_defined?(:@instance)
raise(JSI::Bug, "overwriting instance is not supported")
end
if thing.is_a?(Base)
warn "assigning instance to a Base instance is incorrect. received: #{thing.pretty_inspect.chomp}"
@instance = thing.instance
elsif thing.is_a?(JSI::JSON::Node)
@instance = thing
if thing.is_a?(JSI::Base)
raise(TypeError, "assigning another JSI::Base instance to #{self.class.inspect} instance is incorrect. received: #{thing.pretty_inspect.chomp}")
elsif thing.is_a?(Schema)
raise(TypeError, "assigning a schema to #{self.class.inspect} instance is incorrect. received: #{thing.pretty_inspect.chomp}")
else
@instance = JSI::JSON::Node.new_doc(thing)
@instance = thing
end
end

Expand All @@ -239,6 +239,12 @@ def subscript_assign(subscript, value)
end
end

def check_can_get_parents!
unless @ancestor && @ancestor.instance.is_a?(JSI::JSON::Node)
raise(TypeError, "cannot get parents of JSI for instance that is not a JSI::JSON::Node. please wrap the root of the instance as a JSI::JSON::Node and instantiate from that in order to get parents. instance is: #{instance.pretty_inspect.chomp}")
end
end

# this is an instance method in order to allow subclasses of JSI classes to
# override it to point to other subclasses corresponding to other schemas.
def class_for_schema(schema)
Expand Down Expand Up @@ -349,7 +355,7 @@ module BaseHash
# @return [self, Enumerator]
def each
return to_enum(__method__) { instance.size } unless block_given?
instance.each_key { |k| yield(k, self[k]) }
instance_hash_pubsend(:each_key) { |k| yield(k, self[k]) }
self
end

Expand All @@ -361,9 +367,19 @@ def to_hash

include Hashlike

def instance_hash_pubsend(method_name, *a, &b)
if instance.respond_to?(method_name)
instance.public_send(method_name, *a, &b)
else
instance.to_hash.public_send(method_name, *a, &b)
end
end

# 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| instance.public_send(method_name, *a, &b) }
define_method(method_name) do |*a, &b|
instance_hash_pubsend(method_name, *a, &b)
end
end

# @return [JSI::Base, Object] the instance's subscript value at the given
Expand All @@ -374,20 +390,20 @@ def [](property_name_)
memoize(:[], property_name_) do |property_name|
begin
property_schema = schema.subschema_for_property(property_name)
property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
property_schema = property_schema && property_schema.match_to_instance(instance_sub(property_name))

if !instance.key?(property_name) && property_schema && property_schema.schema_object.key?('default')
if !instance_hash_pubsend(:key?, property_name) && property_schema && property_schema.schema_object.key?('default')
# use the default value
default = property_schema.schema_object['default']
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
class_for_schema(property_schema).new(default, ancestor: @ancestor)
else
default
end
elsif property_schema && (instance[property_name].respond_to?(:to_hash) || instance[property_name].respond_to?(:to_ary))
class_for_schema(property_schema).new(instance[property_name], ancestor: @ancestor)
elsif property_schema && (instance_sub(property_name).respond_to?(:to_hash) || instance_sub(property_name).respond_to?(:to_ary))
class_for_schema(property_schema).new(instance_sub(property_name), ancestor: @ancestor)
else
instance[property_name]
instance_sub(property_name)
end
end
end
Expand All @@ -402,6 +418,11 @@ def [](property_name_)
def []=(property_name, value)
subscript_assign(property_name, value)
end

private
def instance_sub(subscript)
instance_hash_pubsend(:[], subscript)
end
end

# module extending a {JSI::Base} object when its schema instance is Array-like (responds to #to_ary)
Expand All @@ -413,7 +434,7 @@ module BaseArray
# @return [self, Enumerator]
def each
return to_enum(__method__) { instance.size } unless block_given?
instance.each_index { |i| yield(self[i]) }
instance_ary_pubsend(:each_index) { |i| yield(self[i]) }
self
end

Expand All @@ -425,10 +446,20 @@ def to_ary

include Arraylike

def instance_ary_pubsend(method_name, *a, &b)
if instance.respond_to?(method_name)
instance.public_send(method_name, *a, &b)
else
instance.to_ary.public_send(method_name, *a, &b)
end
end

# 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| instance.public_send(method_name, *a, &b) }
define_method(method_name) do |*a, &b|
instance_ary_pubsend(method_name, *a, &b)
end
end

# @return [Object] returns the instance's subscript value at the given index
Expand All @@ -439,20 +470,20 @@ def [](i_)
memoize(:[], i_) do |i|
begin
index_schema = schema.subschema_for_index(i)
index_schema = index_schema && index_schema.match_to_instance(instance[i])
index_schema = index_schema && index_schema.match_to_instance(instance_sub(i))

if !instance.each_index.to_a.include?(i) && index_schema && index_schema.schema_object.key?('default')
if !instance_ary_pubsend(:each_index).to_a.include?(i) && index_schema && index_schema.schema_object.key?('default')
# use the default value
default = index_schema.schema_object['default']
if default.respond_to?(:to_hash) || default.respond_to?(:to_ary)
class_for_schema(index_schema).new(default, ancestor: @ancestor)
else
default
end
elsif index_schema && (instance[i].respond_to?(:to_hash) || instance[i].respond_to?(:to_ary))
class_for_schema(index_schema).new(instance[i], ancestor: @ancestor)
elsif index_schema && (instance_sub(i).respond_to?(:to_hash) || instance_sub(i).respond_to?(:to_ary))
class_for_schema(index_schema).new(instance_sub(i), ancestor: @ancestor)
else
instance[i]
instance_sub(i)
end
end
end
Expand All @@ -465,5 +496,10 @@ def [](i_)
def []=(i, value)
subscript_assign(i, value)
end

private
def instance_sub(subscript)
instance_ary_pubsend(:[], subscript)
end
end
end
6 changes: 5 additions & 1 deletion lib/jsi/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ def initialize(schema_object)
raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
elsif schema_object.is_a?(JSI::Base)
@schema_jsi = JSI.deep_stringify_symbol_keys(schema_object.deref)
@schema_node = @schema_jsi.instance
if @schema_jsi.instance.is_a?(JSI::JSON::HashNode)
@schema_node = @schema_jsi.instance
else
@schema_node = JSI::JSON::Node.new_doc(JSI.deep_stringify_symbol_keys(@schema_jsi.instance))
end
elsif schema_object.is_a?(JSI::JSON::HashNode)
@schema_jsi = nil
@schema_node = JSI.deep_stringify_symbol_keys(schema_object.deref)
Expand Down
11 changes: 11 additions & 0 deletions test/base_array_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@
it('#values_at') { assert_equal(['foo'], subject.values_at(0)) }
it('#zip') { assert_equal([['foo', 'foo'], [subject[1], subject[1]], [subject[2], subject[2]]], subject.zip(subject)) }
end
describe 'with an instance that has to_ary but not other ary instance methods' do
let(:instance) { SortOfArray.new(['foo', {'lamp' => SortOfArray.new([3])}, SortOfArray.new(['q', 'r'])]) }
describe 'delegating instance methods to #to_ary' do
it('#each_index') { assert_equal([0, 1, 2], subject.each_index.to_a) }
it('#size') { assert_equal(3, subject.size) }
it('#count') { assert_equal(1, subject.count('foo')) }
it('#slice') { assert_equal(['foo'], subject.slice(0, 1)) }
it('#[]') { assert_equal(SortOfArray.new(['q', 'r']), subject[2].instance) }
it('#as_json') { assert_equal(['foo', {'lamp' => [3]}, ['q', 'r']], subject.as_json) }
end
end
describe 'modified copy methods' do
it('#reject') { assert_equal(class_for_schema.new(JSI::JSON::ArrayNode.new(['foo'], pointer)), subject.reject { |e| e != 'foo' }) }
it('#reject block var') do
Expand Down
9 changes: 9 additions & 0 deletions test/base_hash_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@
it('#values') { assert_equal([subject['foo'], subject['bar'], true], subject.values) }
it('#values_at') { assert_equal([true], subject.values_at('baz')) }
end
describe 'with an instance that has to_hash but not other hash instance methods' do
let(:instance) { SortOfHash.new({'foo' => SortOfHash.new({'a' => 'b'})}) }
describe 'delegating instance methods to #to_hash' do
it('#each_key') { assert_equal(['foo'], subject.each_key.to_a) }
it('#each_pair') { assert_equal([['foo', subject['foo']]], subject.each_pair.to_a) }
it('#[]') { assert_equal(SortOfHash.new({'a' => 'b'}), subject['foo'].instance) }
it('#as_json') { assert_equal({'foo' => {'a' => 'b'}}, subject.as_json) }
end
end
describe 'modified copy methods' do
# I'm going to rely on the #merge test above to test the modified copy functionality and just do basic
# tests of all the modified copy methods here
Expand Down
38 changes: 28 additions & 10 deletions test/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,15 @@
describe 'nil' do
let(:instance) { nil }
it 'initializes with nil instance' do
assert_equal(JSI::JSON::Node.new_doc(nil), subject.instance)
assert_equal(nil, subject.instance)
assert(!subject.respond_to?(:to_ary))
assert(!subject.respond_to?(:to_hash))
end
end
describe 'arbitrary instance' do
let(:instance) { Object.new }
it 'initializes' do
assert_equal(JSI::JSON::Node.new_doc(instance), subject.instance)
assert_equal(instance, subject.instance)
assert(!subject.respond_to?(:to_ary))
assert(!subject.respond_to?(:to_hash))
end
Expand All @@ -125,7 +125,7 @@
let(:instance) { {'foo' => 'bar'} }
let(:schema_content) { {'type' => 'object'} }
it 'initializes' do
assert_equal(JSI::JSON::Node.new_doc({'foo' => 'bar'}), subject.instance)
assert_equal({'foo' => 'bar'}, subject.instance)
assert(!subject.respond_to?(:to_ary))
assert(subject.respond_to?(:to_hash))
end
Expand All @@ -143,7 +143,7 @@
let(:instance) { ['foo'] }
let(:schema_content) { {'type' => 'array'} }
it 'initializes' do
assert_equal(JSI::JSON::Node.new_doc(['foo']), subject.instance)
assert_equal(['foo'], subject.instance)
assert(subject.respond_to?(:to_ary))
assert(!subject.respond_to?(:to_hash))
end
Expand All @@ -157,14 +157,19 @@
assert(!subject.respond_to?(:to_hash))
end
end
describe 'another Base' do
describe 'another JSI::Base invalid' do
let(:schema_content) { {'type' => 'object'} }
let(:instance) { JSI.class_for_schema(schema).new({'foo' => 'bar'}) }
it 'initializes with a warning' do
assert_output(nil, /assigning instance to a Base instance is incorrect. received: #\{<JSI::SchemaClasses\["[^"]+#"\][^>]*>[^}]+}/) do
subject
end
assert_equal(JSI::JSON::HashNode.new_doc({'foo' => 'bar'}), subject.instance)
it 'initializes with an error' do
err = assert_raises(TypeError) { subject }
assert_match(%r(\Aassigning another JSI::Base instance to JSI::SchemaClasses\[\".*#\"\] instance is incorrect. received: #\{<JSI::SchemaClasses\[.*\] Hash>\s*"foo" => "bar"\s*\}\z)m, err.message)
end
end
describe 'Schema invalid' do
let(:instance) { JSI::Schema.new({}) }
it 'initializes with an error' do
err = assert_raises(TypeError) { subject }
assert_match(%r(\Aassigning a schema to JSI::SchemaClasses\[\".*#\"\] instance is incorrect. received: #<JSI::Schema schema_id=.*>\z)m, err.message)
end
end
end
Expand Down Expand Up @@ -198,6 +203,19 @@
end
end
describe '#modified_copy' do
describe 'with an instance that does not have #modified_copy' do
let(:instance) { Object.new }
it 'yields the instance to modify' do
new_instance = Object.new
modified = subject.modified_copy do |o|
assert_equal(instance, o)
new_instance
end
assert_equal(new_instance, modified.instance)
assert_equal(instance, subject.instance)
refute_equal(instance, modified)
end
end
describe 'with an instance that does have #modified_copy' do
it 'yields the instance to modify' do
modified = subject.modified_copy do |o|
Expand Down
2 changes: 1 addition & 1 deletion test/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
it 'initializes from a JSI' do
schema_jsi = SomeMetaschema.new('type' => 'object')
schema = JSI::Schema.new(schema_jsi)
assert_equal(schema_jsi.instance, schema.schema_node)
assert_equal(JSI::JSON::Node.new_doc(schema_jsi.instance), schema.schema_node)
assert_equal(schema_jsi, schema.schema_object)
end
it 'cannot instantiate from some unknown object' do
Expand Down

0 comments on commit 0e44225

Please sign in to comment.