jsi 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/jsi/util.rb ADDED
@@ -0,0 +1,103 @@
1
+ module JSI
2
+ module Util
3
+ def stringify_symbol_keys(hash)
4
+ unless hash.respond_to?(:to_hash)
5
+ raise(ArgumentError, "expected argument to be a hash; got #{hash.class.inspect}: #{hash.pretty_inspect.chomp}")
6
+ end
7
+ JSI::Typelike.modified_copy(hash) do |hash_|
8
+ changed = false
9
+ out = {}
10
+ hash_.each do |k, v|
11
+ if k.is_a?(Symbol)
12
+ changed = true
13
+ k = k.to_s
14
+ end
15
+ out[k] = v
16
+ end
17
+ changed ? out : hash_
18
+ end
19
+ end
20
+
21
+ def deep_stringify_symbol_keys(object)
22
+ if object.respond_to?(:to_hash)
23
+ JSI::Typelike.modified_copy(object) do |hash|
24
+ changed = false
25
+ out = {}
26
+ hash.each do |k, v|
27
+ if k.is_a?(Symbol)
28
+ changed = true
29
+ k = k.to_s
30
+ end
31
+ out_k = deep_stringify_symbol_keys(k)
32
+ out_v = deep_stringify_symbol_keys(v)
33
+ changed = true if out_k.object_id != k.object_id
34
+ changed = true if out_v.object_id != v.object_id
35
+ out[out_k] = out_v
36
+ end
37
+ changed ? out : hash
38
+ end
39
+ elsif object.respond_to?(:to_ary)
40
+ JSI::Typelike.modified_copy(object) do |ary|
41
+ changed = false
42
+ out = ary.map do |e|
43
+ out_e = deep_stringify_symbol_keys(e)
44
+ changed = true if out_e.object_id != e.object_id
45
+ out_e
46
+ end
47
+ changed ? out : ary
48
+ end
49
+ else
50
+ object
51
+ end
52
+ end
53
+ end
54
+ extend Util
55
+
56
+ module FingerprintHash
57
+ def ==(other)
58
+ object_id == other.object_id || (other.respond_to?(:fingerprint) && other.fingerprint == self.fingerprint)
59
+ end
60
+
61
+ alias_method :eql?, :==
62
+
63
+ def hash
64
+ fingerprint.hash
65
+ end
66
+ end
67
+
68
+ module Memoize
69
+ def memoize(key, *args_)
70
+ @memos ||= {}
71
+ @memos[key] ||= Hash.new do |h, args|
72
+ h[args] = yield(*args)
73
+ end
74
+ @memos[key][args_]
75
+ end
76
+
77
+ def clear_memo(key, *args)
78
+ @memos ||= {}
79
+ if @memos[key]
80
+ if args.empty?
81
+ @memos[key].clear
82
+ else
83
+ @memos[key].delete(args)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ extend Memoize
89
+
90
+ # this is the Y-combinator, which allows anonymous recursive functions. for a simple example,
91
+ # to define a recursive function to return the length of an array:
92
+ #
93
+ # length = ycomb do |len|
94
+ # proc{|list| list == [] ? 0 : 1 + len.call(list[1..-1]) }
95
+ # end
96
+ #
97
+ # see https://secure.wikimedia.org/wikipedia/en/wiki/Fixed_point_combinator#Y_combinator
98
+ # and chapter 9 of the little schemer, available as the sample chapter at http://www.ccs.neu.edu/home/matthias/BTLS/
99
+ def ycomb
100
+ proc { |f| f.call(f) }.call(proc { |f| yield proc{|*x| f.call(f).call(*x) } })
101
+ end
102
+ module_function :ycomb
103
+ end
@@ -0,0 +1,3 @@
1
+ module JSI
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,142 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe JSI::BaseArray do
4
+ let(:document) do
5
+ ['foo', {'lamp' => [3]}, ['q', 'r']]
6
+ end
7
+ let(:path) { [] }
8
+ let(:instance) { JSI::JSON::Node.new_by_type(document, path) }
9
+ let(:schema_content) do
10
+ {
11
+ 'type' => 'array',
12
+ 'items' => [
13
+ {'type' => 'string'},
14
+ {'type' => 'object', 'items' => {}},
15
+ {'type' => 'array'},
16
+ ],
17
+ }
18
+ end
19
+ let(:schema) { JSI::Schema.new(schema_content) }
20
+ let(:class_for_schema) { JSI.class_for_schema(schema) }
21
+ let(:subject) { class_for_schema.new(instance) }
22
+
23
+ describe 'arraylike []=' do
24
+ it 'sets an index' do
25
+ orig_2 = subject[2]
26
+
27
+ subject[2] = {'y' => 'z'}
28
+
29
+ assert_equal({'y' => 'z'}, subject[2].as_json)
30
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['items'][2]), orig_2)
31
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['items'][2]), subject[2])
32
+ end
33
+ it 'modifies the instance, visible to other references to the same instance' do
34
+ orig_instance = subject.instance
35
+
36
+ subject[2] = {'y' => 'z'}
37
+
38
+ assert_equal(orig_instance, subject.instance)
39
+ assert_equal({'y' => 'z'}, orig_instance[2].as_json)
40
+ assert_equal({'y' => 'z'}, subject.instance[2].as_json)
41
+ assert_equal(orig_instance.class, subject.instance.class)
42
+ end
43
+ describe 'when the instance is not arraylike' do
44
+ let(:instance) { nil }
45
+ it 'errors' do
46
+ err = assert_raises(NoMethodError) { subject[2] = 0 }
47
+ assert_match(%r(\Aundefined method `\[\]=' for #<JSI::SchemaClasses::X.*>\z), err.message)
48
+ end
49
+ end
50
+ end
51
+ # these methods just delegate to Array so not going to test excessively
52
+ describe 'index only methods' do
53
+ it('#each_index') { assert_equal([0, 1, 2], subject.each_index.to_a) }
54
+ it('#empty?') { assert_equal(false, subject.empty?) }
55
+ it('#length') { assert_equal(3, subject.length) }
56
+ it('#size') { assert_equal(3, subject.size) }
57
+ end
58
+ describe 'index + element methods' do
59
+ it('#|') { assert_equal(['foo', subject[1], subject[2], 0], subject | [0]) }
60
+ it('#&') { assert_equal(['foo'], subject & ['foo']) }
61
+ it('#*') { assert_equal(subject.to_a, subject * 1) }
62
+ it('#+') { assert_equal(subject.to_a, subject + []) }
63
+ it('#-') { assert_equal([subject[1], subject[2]], subject - ['foo']) }
64
+ it('#<=>') { assert_equal(1, subject <=> []) }
65
+ it('#<=>') { assert_equal(-1, [] <=> subject) }
66
+ require 'abbrev'
67
+ it('#abbrev') { assert_equal({'a' => 'a'}, class_for_schema.new(['a']).abbrev) }
68
+ it('#assoc') { assert_equal(['q', 'r'], subject.instance.assoc('q')) }
69
+ it('#at') { assert_equal('foo', subject.at(0)) }
70
+ it('#bsearch') { assert_equal(nil, subject.bsearch { false }) }
71
+ it('#bsearch_index') { assert_equal(nil, subject.bsearch_index { false }) } if [].respond_to?(:bsearch_index)
72
+ it('#combination') { assert_equal([['foo'], [subject[1]], [subject[2]]], subject.combination(1).to_a) }
73
+ it('#count') { assert_equal(1, subject.count('foo')) }
74
+ it('#cycle') { assert_equal(subject.to_a, subject.cycle(1).to_a) }
75
+ it('#dig') { assert_equal(3, subject.dig(1, 'lamp', 0)) } if [].respond_to?(:dig)
76
+ it('#drop') { assert_equal([subject[2]], subject.drop(2)) }
77
+ it('#drop_while') { assert_equal([subject[1], subject[2]], subject.drop_while { |e| e == 'foo' }) }
78
+ it('#fetch') { assert_equal('foo', subject.fetch(0)) }
79
+ it('#find_index') { assert_equal(0, subject.find_index { true }) }
80
+ it('#first') { assert_equal('foo', subject.first) }
81
+ it('#include?') { assert_equal(true, subject.include?('foo')) }
82
+ it('#index') { assert_equal(0, subject.index('foo')) }
83
+ it('#join') { assert_equal('a b', class_for_schema.new(['a', 'b']).join(' ')) }
84
+ it('#last') { assert_equal(subject[2], subject.last) }
85
+ it('#pack') { assert_equal(' ', class_for_schema.new([32]).pack('c')) }
86
+ it('#permutation') { assert_equal([['foo'], [subject[1]], [subject[2]]], subject.permutation(1).to_a) }
87
+ it('#product') { assert_equal([], subject.product([])) }
88
+ # due to differences in implementation between #assoc and #rassoc, the reason for which
89
+ # I cannot begin to fathom, assoc works but rassoc does not because rassoc has different
90
+ # type checking than assoc for the array(like) array elements.
91
+ # compare:
92
+ # assoc: https://github.com/ruby/ruby/blob/v2_5_0/array.c#L3780-L3813
93
+ # rassoc: https://github.com/ruby/ruby/blob/v2_5_0/array.c#L3815-L3847
94
+ # for this reason, rassoc is NOT defined on Arraylike and #content must be called.
95
+ it('#rassoc') { assert_equal(['q', 'r'], subject.instance.content.rassoc('r')) }
96
+ it('#repeated_combination') { assert_equal([[]], subject.repeated_combination(0).to_a) }
97
+ it('#repeated_permutation') { assert_equal([[]], subject.repeated_permutation(0).to_a) }
98
+ it('#reverse') { assert_equal([subject[2], subject[1], 'foo'], subject.reverse) }
99
+ it('#reverse_each') { assert_equal([subject[2], subject[1], 'foo'], subject.reverse_each.to_a) }
100
+ it('#rindex') { assert_equal(0, subject.rindex('foo')) }
101
+ it('#rotate') { assert_equal([subject[1], subject[2], 'foo'], subject.rotate) }
102
+ it('#sample') { assert_equal('a', class_for_schema.new(['a']).sample) }
103
+ it('#shelljoin') { assert_equal('a', class_for_schema.new(['a']).shelljoin) } if [].respond_to?(:shelljoin)
104
+ it('#shuffle') { assert_equal(3, subject.shuffle.size) }
105
+ it('#slice') { assert_equal(['foo'], subject.slice(0, 1)) }
106
+ it('#sort') { assert_equal(['a'], class_for_schema.new(['a']).sort) }
107
+ it('#take') { assert_equal(['foo'], subject.take(1)) }
108
+ it('#take_while') { assert_equal([], subject.take_while { false }) }
109
+ it('#transpose') { assert_equal([], class_for_schema.new([]).transpose) }
110
+ it('#uniq') { assert_equal(subject.to_a, subject.uniq) }
111
+ it('#values_at') { assert_equal(['foo'], subject.values_at(0)) }
112
+ it('#zip') { assert_equal([['foo', 'foo'], [subject[1], subject[1]], [subject[2], subject[2]]], subject.zip(subject)) }
113
+ end
114
+ describe 'modified copy methods' do
115
+ it('#reject') { assert_equal(class_for_schema.new(JSI::JSON::ArrayNode.new(['foo'], [])), subject.reject { |e| e != 'foo' }) }
116
+ it('#reject block var') do
117
+ subj_a = subject.to_a
118
+ subject.reject { |e| assert_equal(e, subj_a.shift) }
119
+ end
120
+ it('#select') { assert_equal(class_for_schema.new(JSI::JSON::ArrayNode.new(['foo'], [])), subject.select { |e| e == 'foo' }) }
121
+ it('#select block var') do
122
+ subj_a = subject.to_a
123
+ subject.select { |e| assert_equal(e, subj_a.shift) }
124
+ end
125
+ it('#compact') { assert_equal(subject, subject.compact) }
126
+ describe 'at a depth' do
127
+ let(:document) { [['b', 'q'], {'c' => ['d', 'e']}] }
128
+ let(:path) { ['1', 'c'] }
129
+ it('#select') do
130
+ selected = subject.select { |e| e == 'd' }
131
+ equivalent_node = JSI::JSON::ArrayNode.new([['b', 'q'], {'c' => ['d']}], path)
132
+ equivalent = class_for_schema.new(equivalent_node)
133
+ assert_equal(equivalent, selected)
134
+ end
135
+ end
136
+ end
137
+ JSI::Arraylike::DESTRUCTIVE_METHODS.each do |destructive_method_name|
138
+ it("does not respond to destructive method #{destructive_method_name}") do
139
+ assert(!subject.respond_to?(destructive_method_name))
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,135 @@
1
+ require_relative 'test_helper'
2
+
3
+ describe JSI::BaseHash do
4
+ let(:document) do
5
+ {'foo' => {'x' => 'y'}, 'bar' => [9], 'baz' => true}
6
+ end
7
+ let(:path) { [] }
8
+ let(:instance) { JSI::JSON::Node.new_by_type(document, path) }
9
+ let(:schema_content) do
10
+ {
11
+ 'type' => 'object',
12
+ 'properties' => {
13
+ 'foo' => {'type' => 'object'},
14
+ 'bar' => {},
15
+ },
16
+ }
17
+ end
18
+ let(:schema) { JSI::Schema.new(schema_content) }
19
+ let(:class_for_schema) { JSI.class_for_schema(schema) }
20
+ let(:subject) { class_for_schema.new(instance) }
21
+
22
+ describe 'hashlike []=' do
23
+ it 'sets a property' do
24
+ orig_foo = subject['foo']
25
+
26
+ subject['foo'] = {'y' => 'z'}
27
+
28
+ assert_equal({'y' => 'z'}, subject['foo'].as_json)
29
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), orig_foo)
30
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), subject['foo'])
31
+ end
32
+ it 'sets a property to a schema instance with a different schema' do
33
+ orig_foo = subject['foo']
34
+
35
+ subject['foo'] = subject['bar']
36
+
37
+ # the content of the subscripts' instances is the same but the subscripts' classes are different
38
+ assert_equal([9], subject['foo'].as_json)
39
+ assert_equal([9], subject['bar'].as_json)
40
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), subject['foo'])
41
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['bar']), subject['bar'])
42
+ end
43
+ it 'sets a property to a schema instance with the same schema' do
44
+ other_subject = class_for_schema.new(JSI::JSON::Node.new_by_type({'foo' => {'x' => 'y'}, 'bar' => [9], 'baz' => true}, []))
45
+ # Given
46
+ assert_equal(other_subject, subject)
47
+
48
+ # When:
49
+ subject['foo'] = other_subject['foo']
50
+
51
+ # Then:
52
+ # still equal
53
+ assert_equal(other_subject, subject)
54
+ # but different instances
55
+ refute_equal(other_subject['foo'].object_id, subject['foo'].object_id)
56
+ end
57
+ it 'modifies the instance, visible to other references to the same instance' do
58
+ orig_instance = subject.instance
59
+
60
+ subject['foo'] = {'y' => 'z'}
61
+
62
+ assert_equal(orig_instance, subject.instance)
63
+ assert_equal({'y' => 'z'}, orig_instance['foo'].as_json)
64
+ assert_equal({'y' => 'z'}, subject.instance['foo'].as_json)
65
+ assert_equal(orig_instance.class, subject.instance.class)
66
+ end
67
+ describe 'when the instance is not hashlike' do
68
+ let(:instance) { nil }
69
+ it 'errors' do
70
+ err = assert_raises(NoMethodError) { subject['foo'] = 0 }
71
+ assert_match(%r(\Aundefined method `\[\]=' for #<JSI::SchemaClasses::.*>\z), err.message)
72
+ end
73
+ end
74
+ end
75
+ # these methods just delegate to Hash so not going to test excessively
76
+ describe 'key only methods' do
77
+ it('#each_key') { assert_equal(['foo', 'bar', 'baz'], subject.each_key.to_a) }
78
+ it('#empty?') { assert_equal(false, subject.empty?) }
79
+ it('#has_key?') { assert_equal(true, subject.has_key?('bar')) }
80
+ it('#include?') { assert_equal(false, subject.include?('q')) }
81
+ it('#key?') { assert_equal(true, subject.key?('baz')) }
82
+ it('#keys') { assert_equal(['foo', 'bar', 'baz'], subject.keys) }
83
+ it('#length') { assert_equal(3, subject.length) }
84
+ it('#member?') { assert_equal(false, subject.member?(0)) }
85
+ it('#size') { assert_equal(3, subject.size) }
86
+ end
87
+ describe 'key + value methods' do
88
+ it('#<') { assert_equal(true, subject < {'foo' => subject['foo'], 'bar' => subject['bar'], 'baz' => true, 'x' => 'y'}) } if {}.respond_to?(:<)
89
+ it('#<=') { assert_equal(true, subject <= subject) } if {}.respond_to?(:<=)
90
+ it('#>') { assert_equal(true, subject > {}) } if {}.respond_to?(:>)
91
+ it('#>=') { assert_equal(false, subject >= {'foo' => 'bar'}) } if {}.respond_to?(:>=)
92
+ it('#any?') { assert_equal(false, subject.any? { |k, v| v == 3 }) }
93
+ it('#assoc') { assert_equal(['foo', subject['foo']], subject.assoc('foo')) }
94
+ it('#dig') { assert_equal(9, subject.dig('bar', 0)) } if {}.respond_to?(:dig)
95
+ it('#each_pair') { assert_equal([['foo', subject['foo']], ['bar', subject['bar']], ['baz', true]], subject.each_pair.to_a) }
96
+ it('#each_value') { assert_equal([subject['foo'], subject['bar'], true], subject.each_value.to_a) }
97
+ it('#fetch') { assert_equal(true, subject.fetch('baz')) }
98
+ it('#fetch_values') { assert_equal([true], subject.fetch_values('baz')) } if {}.respond_to?(:fetch_values)
99
+ it('#has_value?') { assert_equal(true, subject.has_value?(true)) }
100
+ it('#invert') { assert_equal({subject['foo'] => 'foo', subject['bar'] => 'bar', true => 'baz'}, subject.invert) }
101
+ it('#key') { assert_equal('baz', subject.key(true)) }
102
+ it('#rassoc') { assert_equal(['baz', true], subject.rassoc(true)) }
103
+ it('#to_h') { assert_equal({'foo' => subject['foo'], 'bar' => subject['bar'], 'baz' => true}, subject.to_h) }
104
+ it('#to_proc') { assert_equal(true, subject.to_proc.call('baz')) } if {}.respond_to?(:to_proc)
105
+ if {}.respond_to?(:transform_values)
106
+ it('#transform_values') { assert_equal({'foo' => nil, 'bar' => nil, 'baz' => nil}, subject.transform_values { |_| nil}) }
107
+ end
108
+ it('#value?') { assert_equal(false, subject.value?('0')) }
109
+ it('#values') { assert_equal([subject['foo'], subject['bar'], true], subject.values) }
110
+ it('#values_at') { assert_equal([true], subject.values_at('baz')) }
111
+ end
112
+ describe 'modified copy methods' do
113
+ # I'm going to rely on the #merge test above to test the modified copy functionality and just do basic
114
+ # tests of all the modified copy methods here
115
+ it('#merge') { assert_equal(subject, subject.merge({})) }
116
+ it('#reject') { assert_equal(class_for_schema.new(JSI::JSON::HashNode.new({}, [])), subject.reject { true }) }
117
+ it('#select') { assert_equal(class_for_schema.new(JSI::JSON::HashNode.new({}, [])), subject.select { false }) }
118
+ describe '#select' do
119
+ it 'yields properly too' do
120
+ subject.select do |k, v|
121
+ assert_equal(subject[k], v)
122
+ end
123
+ end
124
+ end
125
+ # Hash#compact only available as of ruby 2.5.0
126
+ if {}.respond_to?(:compact)
127
+ it('#compact') { assert_equal(subject, subject.compact) }
128
+ end
129
+ end
130
+ JSI::Hashlike::DESTRUCTIVE_METHODS.each do |destructive_method_name|
131
+ it("does not respond to destructive method #{destructive_method_name}") do
132
+ assert(!subject.respond_to?(destructive_method_name))
133
+ end
134
+ end
135
+ end
data/test/base_test.rb ADDED
@@ -0,0 +1,395 @@
1
+ require_relative 'test_helper'
2
+
3
+ NamedSchemaInstance = JSI.class_for_schema({id: 'https://schemas.jsi.unth.net/test/base/named_schema'})
4
+
5
+ describe JSI::Base do
6
+ let(:document) { {} }
7
+ let(:path) { [] }
8
+ let(:instance) { JSI::JSON::Node.new_by_type(document, path) }
9
+ let(:schema_content) { {} }
10
+ let(:schema) { JSI::Schema.new(schema_content) }
11
+ let(:subject) { JSI.class_for_schema(schema).new(instance) }
12
+ describe 'class .inspect + .to_s' do
13
+ it 'is the same as Class#inspect on the base' do
14
+ assert_equal('JSI::Base', JSI::Base.inspect)
15
+ assert_equal('JSI::Base', JSI::Base.to_s)
16
+ end
17
+ it 'is SchemaClasses[] for generated subclass without id' do
18
+ assert_match(%r(\AJSI::SchemaClasses\["[a-f0-9\-]+#"\]\z), subject.class.inspect)
19
+ assert_match(%r(\AJSI::SchemaClasses\["[a-f0-9\-]+#"\]\z), subject.class.to_s)
20
+ end
21
+ describe 'with schema id' do
22
+ let(:schema_content) { {'id' => 'https://jsi/foo'} }
23
+ it 'is SchemaClasses[] for generated subclass with id' do
24
+ assert_equal(%q(JSI::SchemaClasses["https://jsi/foo#"]), subject.class.inspect)
25
+ assert_equal(%q(JSI::SchemaClasses["https://jsi/foo#"]), subject.class.to_s)
26
+ end
27
+ end
28
+ it 'is the constant name (plus id for .inspect) for a class assigned to a constant' do
29
+ assert_equal(%q(NamedSchemaInstance (https://schemas.jsi.unth.net/test/base/named_schema#)), NamedSchemaInstance.inspect)
30
+ assert_equal(%q(NamedSchemaInstance), NamedSchemaInstance.to_s)
31
+ end
32
+ end
33
+ describe 'class name' do
34
+ let(:schema_content) { {'id' => 'https://jsi/BaseTest'} }
35
+ it 'generates a class name from schema_id' do
36
+ assert_equal('JSI::SchemaClasses::Https___jsi_BaseTest_', subject.class.name)
37
+ end
38
+ it 'uses an existing name' do
39
+ assert_equal('NamedSchemaInstance', NamedSchemaInstance.name)
40
+ end
41
+ end
42
+ describe 'class for schema .schema' do
43
+ it '.schema' do
44
+ assert_equal(schema, JSI.class_for_schema(schema).schema)
45
+ end
46
+ end
47
+ describe 'class for schema .schema_id' do
48
+ it '.schema_id' do
49
+ assert_equal(schema.schema_id, JSI.class_for_schema(schema).schema_id)
50
+ end
51
+ end
52
+ describe 'module for schema .inspect' do
53
+ it '.inspect' do
54
+ assert_match(%r(\A#<Module for Schema: .+#>\z), JSI.module_for_schema(schema).inspect)
55
+ end
56
+ end
57
+ describe 'module for schema .schema' do
58
+ it '.schema' do
59
+ assert_equal(schema, JSI.module_for_schema(schema).schema)
60
+ end
61
+ end
62
+ describe 'SchemaClasses[]' do
63
+ it 'stores the class for the schema' do
64
+ assert_equal(JSI.class_for_schema(schema), JSI::SchemaClasses[schema.schema_id])
65
+ end
66
+ end
67
+ describe '.class_for_schema' do
68
+ it 'returns a class from a schema' do
69
+ class_for_schema = JSI.class_for_schema(schema)
70
+ # same class every time
71
+ assert_equal(JSI.class_for_schema(schema), class_for_schema)
72
+ assert_operator(class_for_schema, :<, JSI::Base)
73
+ end
74
+ it 'returns a class from a hash' do
75
+ assert_equal(JSI.class_for_schema(schema), JSI.class_for_schema(schema.schema_node.content))
76
+ end
77
+ it 'returns a class from a schema node' do
78
+ assert_equal(JSI.class_for_schema(schema), JSI.class_for_schema(schema.schema_node))
79
+ end
80
+ it 'returns a class from a Base' do
81
+ assert_equal(JSI.class_for_schema(schema), JSI.class_for_schema(JSI.class_for_schema({}).new(schema.schema_node)))
82
+ end
83
+ end
84
+ describe '.module_for_schema' do
85
+ it 'returns a module from a schema' do
86
+ module_for_schema = JSI.module_for_schema(schema)
87
+ # same module every time
88
+ assert_equal(JSI.module_for_schema(schema), module_for_schema)
89
+ end
90
+ it 'returns a module from a hash' do
91
+ assert_equal(JSI.module_for_schema(schema), JSI.module_for_schema(schema.schema_node.content))
92
+ end
93
+ it 'returns a module from a schema node' do
94
+ assert_equal(JSI.module_for_schema(schema), JSI.module_for_schema(schema.schema_node))
95
+ end
96
+ it 'returns a module from a Base' do
97
+ assert_equal(JSI.module_for_schema(schema), JSI.module_for_schema(JSI.class_for_schema({}).new(schema.schema_node)))
98
+ end
99
+ end
100
+ describe 'initialization' do
101
+ describe 'on Base' do
102
+ it 'errors' do
103
+ err = assert_raises(TypeError) { JSI::Base.new({}) }
104
+ assert_equal('cannot instantiate JSI::Base which has no method #schema. please use JSI.class_for_schema', err.message)
105
+ end
106
+ end
107
+ describe 'nil' do
108
+ let(:instance) { nil }
109
+ it 'initializes with nil instance' do
110
+ assert_equal(JSI::JSON::Node.new_by_type(nil, []), subject.instance)
111
+ assert(!subject.respond_to?(:to_ary))
112
+ assert(!subject.respond_to?(:to_hash))
113
+ end
114
+ end
115
+ describe 'arbitrary instance' do
116
+ let(:instance) { Object.new }
117
+ it 'initializes' do
118
+ assert_equal(JSI::JSON::Node.new_by_type(instance, []), subject.instance)
119
+ assert(!subject.respond_to?(:to_ary))
120
+ assert(!subject.respond_to?(:to_hash))
121
+ end
122
+ end
123
+ describe 'hash' do
124
+ let(:instance) { {'foo' => 'bar'} }
125
+ let(:schema_content) { {'type' => 'object'} }
126
+ it 'initializes' do
127
+ assert_equal(JSI::JSON::Node.new_by_type({'foo' => 'bar'}, []), subject.instance)
128
+ assert(!subject.respond_to?(:to_ary))
129
+ assert(subject.respond_to?(:to_hash))
130
+ end
131
+ end
132
+ describe 'JSI::JSON::Hashnode' do
133
+ let(:document) { {'foo' => 'bar'} }
134
+ let(:schema_content) { {'type' => 'object'} }
135
+ it 'initializes' do
136
+ assert_equal(JSI::JSON::HashNode.new({'foo' => 'bar'}, []), subject.instance)
137
+ assert(!subject.respond_to?(:to_ary))
138
+ assert(subject.respond_to?(:to_hash))
139
+ end
140
+ end
141
+ describe 'array' do
142
+ let(:instance) { ['foo'] }
143
+ let(:schema_content) { {'type' => 'array'} }
144
+ it 'initializes' do
145
+ assert_equal(JSI::JSON::Node.new_by_type(['foo'], []), subject.instance)
146
+ assert(subject.respond_to?(:to_ary))
147
+ assert(!subject.respond_to?(:to_hash))
148
+ end
149
+ end
150
+ describe 'JSI::JSON::Arraynode' do
151
+ let(:document) { ['foo'] }
152
+ let(:schema_content) { {'type' => 'array'} }
153
+ it 'initializes' do
154
+ assert_equal(JSI::JSON::ArrayNode.new(['foo'], []), subject.instance)
155
+ assert(subject.respond_to?(:to_ary))
156
+ assert(!subject.respond_to?(:to_hash))
157
+ end
158
+ end
159
+ describe 'another Base' do
160
+ let(:schema_content) { {'type' => 'object'} }
161
+ let(:instance) { JSI.class_for_schema(schema).new({'foo' => 'bar'}) }
162
+ it 'initializes with a warning' do
163
+ assert_output(nil, /assigning instance to a Base instance is incorrect. received: #\{<JSI::SchemaClasses\["[^"]+#"\][^>]*>[^}]+}/) do
164
+ subject
165
+ end
166
+ assert_equal(JSI::JSON::HashNode.new({'foo' => 'bar'}, []), subject.instance)
167
+ end
168
+ end
169
+ end
170
+ describe '#parents, #parent' do
171
+ let(:schema_content) { {properties: {foo: {properties: {bar: {properties: {baz: {}}}}}}} }
172
+ let(:document) { {foo: {bar: {baz: {}}}} }
173
+ describe 'no parents' do
174
+ it 'has none' do
175
+ assert_equal([], subject.parents)
176
+ assert_equal(nil, subject.parent)
177
+ end
178
+ end
179
+ describe 'one parent' do
180
+ it 'has one' do
181
+ assert_equal([subject], subject.foo.parents)
182
+ assert_equal(subject, subject.foo.parent)
183
+ end
184
+ end
185
+ describe 'more parents' do
186
+ it 'has more' do
187
+ assert_equal([subject.foo.bar, subject.foo, subject], subject.foo.bar.baz.parents)
188
+ assert_equal(subject.foo.bar, subject.foo.bar.baz.parent)
189
+ end
190
+ end
191
+ end
192
+ describe '#each, Enumerable methods' do
193
+ let(:document) { 'a string' }
194
+ it "raises NoMethodError calling each or Enumerable methods" do
195
+ assert_raises(NoMethodError) { subject.each { nil } }
196
+ assert_raises(NoMethodError) { subject.map { nil } }
197
+ end
198
+ end
199
+ describe '#modified_copy' do
200
+ describe 'with an instance that does have #modified_copy' do
201
+ it 'yields the instance to modify' do
202
+ modified = subject.modified_copy do |o|
203
+ assert_equal({}, o)
204
+ {'a' => 'b'}
205
+ end
206
+ assert_equal({'a' => 'b'}, modified.instance.content)
207
+ assert_equal({}, subject.instance.content)
208
+ refute_equal(instance, modified)
209
+ end
210
+ end
211
+ describe 'no modification' do
212
+ it 'yields the instance to modify' do
213
+ modified = subject.modified_copy { |o| o }
214
+ # this doesn't really need to be tested but ... whatever
215
+ assert_equal(subject.instance.content.object_id, modified.instance.content.object_id)
216
+ assert_equal(subject, modified)
217
+ refute_equal(subject.object_id, modified.object_id)
218
+ end
219
+ end
220
+ describe 'resulting in a different type' do
221
+ let(:schema_content) { {'type' => 'object'} }
222
+ it 'works' do
223
+ # I'm not really sure the best thing to do here, but this is how it is for now. this is subject to change.
224
+ modified = subject.modified_copy do |o|
225
+ o.to_s
226
+ end
227
+ assert_equal('{}', modified.instance.content)
228
+ assert_equal({}, subject.instance.content)
229
+ refute_equal(instance, modified)
230
+ # interesting side effect
231
+ assert(subject.respond_to?(:to_hash))
232
+ assert(!modified.respond_to?(:to_hash))
233
+ assert_equal(JSI::JSON::HashNode, subject.instance.class)
234
+ assert_equal(JSI::JSON::Node, modified.instance.class)
235
+ end
236
+ end
237
+ end
238
+ it('#fragment') { assert_equal('#', subject.fragment) }
239
+ describe 'validation' do
240
+ describe 'without errors' do
241
+ it '#fully_validate' do
242
+ assert_equal([], subject.fully_validate)
243
+ end
244
+ it '#validate' do
245
+ assert_equal(true, subject.validate)
246
+ end
247
+ it '#validate!' do
248
+ assert_equal(true, subject.validate!)
249
+ end
250
+ end
251
+ end
252
+ describe 'property accessors' do
253
+ let(:schema_content) do
254
+ {
255
+ 'type' => 'object',
256
+ 'properties' => {
257
+ 'foo' => {'type' => 'object'},
258
+ 'bar' => {'type' => 'array'},
259
+ 'baz' => {},
260
+ },
261
+ }
262
+ end
263
+ let(:document) do
264
+ {'foo' => {'x' => 'y'}, 'bar' => [3.14159], 'baz' => true, 'qux' => []}
265
+ end
266
+ describe 'readers' do
267
+ it 'reads attributes described as properties' do
268
+ assert_equal({'x' => 'y'}, subject.foo.as_json)
269
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), subject.foo)
270
+ assert_respond_to(subject.foo, :to_hash)
271
+ refute_respond_to(subject.foo, :to_ary)
272
+ assert_equal([3.14159], subject.bar.as_json)
273
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['bar']), subject.bar)
274
+ refute_respond_to(subject.bar, :to_hash)
275
+ assert_respond_to(subject.bar, :to_ary)
276
+ assert_equal(true, subject.baz)
277
+ refute_respond_to(subject.baz, :to_hash)
278
+ refute_respond_to(subject.baz, :to_ary)
279
+ refute_respond_to(subject, :qux)
280
+ end
281
+ describe 'when the instance is not hashlike' do
282
+ let(:instance) { nil }
283
+ it 'errors' do
284
+ err = assert_raises(NoMethodError) { subject.foo }
285
+ assert_match(%r(\Ainstance does not respond to \[\]; cannot call reader `foo' for: #<JSI::SchemaClasses\["[^"]+#"\].*nil.*>\z)m, err.message)
286
+ end
287
+ end
288
+ describe 'properties with the same names as instance methods' do
289
+ let(:schema_content) do
290
+ {
291
+ 'type' => 'object',
292
+ 'properties' => {
293
+ 'foo' => {}, # not an instance method
294
+ 'initialize' => {}, # Base
295
+ 'inspect' => {}, # Base
296
+ 'pretty_inspect' => {}, # Kernel
297
+ 'as_json' => {}, # Base::OverrideFromExtensions, extended on initialization
298
+ 'each' => {}, # BaseHash / BaseArray
299
+ 'instance_exec' => {}, # BasicObject
300
+ 'instance' => {}, # Base
301
+ 'schema' => {}, # module_for_schema singleton definition
302
+ },
303
+ }
304
+ end
305
+ let(:document) do
306
+ {
307
+ 'foo' => 'bar',
308
+ 'initialize' => 'hi',
309
+ 'inspect' => 'hi',
310
+ 'pretty_inspect' => 'hi',
311
+ 'as_json' => 'hi',
312
+ 'each' => 'hi',
313
+ 'instance_exec' => 'hi',
314
+ 'instance' => 'hi',
315
+ 'schema' => 'hi',
316
+ }
317
+ end
318
+ it 'does not define readers' do
319
+ assert_equal('bar', subject.foo)
320
+ assert_equal(JSI.module_for_schema(subject.schema), subject.method(:foo).owner)
321
+
322
+ assert_equal(JSI::Base, subject.method(:initialize).owner)
323
+ assert_equal('hi', subject['initialize'])
324
+ assert_match(%r(\A#\{<JSI::SchemaClasses\[".*#"\].*}\z)m, subject.inspect)
325
+ assert_equal('hi', subject['inspect'])
326
+ assert_match(%r(\A#\{<JSI::SchemaClasses\[".*#"\].*}\Z)m, subject.pretty_inspect)
327
+ assert_equal(document, subject.as_json)
328
+ assert_equal(subject, subject.each { })
329
+ assert_equal(2, subject.instance_exec { 2 })
330
+ assert_equal(instance, subject.instance)
331
+ assert_equal(schema, subject.schema)
332
+ end
333
+ end
334
+ end
335
+ describe 'writers' do
336
+ it 'writes attributes described as properties' do
337
+ orig_foo = subject.foo
338
+
339
+ subject.foo = {'y' => 'z'}
340
+
341
+ assert_equal({'y' => 'z'}, subject.foo.as_json)
342
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), orig_foo)
343
+ assert_instance_of(JSI.class_for_schema(schema.schema_node['properties']['foo']), subject.foo)
344
+ end
345
+ it 'modifies the instance, visible to other references to the same instance' do
346
+ orig_instance = subject.instance
347
+
348
+ subject.foo = {'y' => 'z'}
349
+
350
+ assert_equal(orig_instance, subject.instance)
351
+ assert_equal({'y' => 'z'}, orig_instance['foo'].as_json)
352
+ assert_equal({'y' => 'z'}, subject.instance['foo'].as_json)
353
+ assert_equal(orig_instance.class, subject.instance.class)
354
+ end
355
+ describe 'when the instance is not hashlike' do
356
+ let(:instance) { nil }
357
+ it 'errors' do
358
+ err = assert_raises(NoMethodError) { subject.foo = 0 }
359
+ assert_match(%r(\Ainstance does not respond to \[\]=; cannot call writer `foo=' for: #<JSI::SchemaClasses\["[^"]+#"\].*nil.*>\z)m, err.message)
360
+ end
361
+ end
362
+ end
363
+ end
364
+ describe '#inspect' do
365
+ # if the instance is hash-like, #inspect gets overridden
366
+ let(:document) { Object.new }
367
+ it 'inspects' do
368
+ assert_match(%r(\A#<JSI::SchemaClasses\["[^"]+#"\] #<JSI::JSON::Node fragment="#" #<Object:[^<>]*>>>\z), subject.inspect)
369
+ end
370
+ end
371
+ describe '#pretty_print' do
372
+ # if the instance is hash-like, #pretty_print gets overridden
373
+ let(:document) { Object.new }
374
+ it 'pretty_prints' do
375
+ assert_match(%r(\A#<JSI::SchemaClasses\["[^"]+#"\]\n #<JSI::JSON::Node fragment="#" #<Object:[^<>]*>>\n>\z), subject.pretty_inspect.chomp)
376
+ end
377
+ end
378
+ describe '#as_json' do
379
+ it '#as_json' do
380
+ assert_equal({'a' => 'b'}, JSI.class_for_schema({}).new(JSI::JSON::Node.new_by_type({'a' => 'b'}, [])).as_json)
381
+ assert_equal({'a' => 'b'}, JSI.class_for_schema({'type' => 'object'}).new(JSI::JSON::Node.new_by_type({'a' => 'b'}, [])).as_json)
382
+ assert_equal(['a', 'b'], JSI.class_for_schema({'type' => 'array'}).new(JSI::JSON::Node.new_by_type(['a', 'b'], [])).as_json)
383
+ assert_equal(['a'], JSI::class_for_schema({}).new(['a']).as_json(some_option: true))
384
+ end
385
+ end
386
+ describe 'overwrite schema instance with instance=' do
387
+ # this error message indicates an internal bug (hence Bug class), so there isn't an intended way to
388
+ # trigger it using JSI::Base properly. we use it improperly just to test that code path. this
389
+ # is definitely not defined behavior.
390
+ it 'errors' do
391
+ err = assert_raises(JSI::Bug) { subject.send(:instance=, {'foo' => 'bar'}) }
392
+ assert_match(%r(\Aoverwriting instance is not supported\z), err.message)
393
+ end
394
+ end
395
+ end