scorpio 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,33 +1,94 @@
1
+ require 'scorpio/json/node'
2
+
1
3
  module Scorpio
2
4
  class Schema
3
- def initialize(schema_node)
4
- @schema_node = schema_node
5
+ include Memoize
6
+
7
+ def initialize(schema_object)
8
+ if schema_object.is_a?(Scorpio::Schema)
9
+ raise(TypeError, "will not instantiate Schema from another Schema: #{schema_object.pretty_inspect.chomp}")
10
+ elsif schema_object.is_a?(Scorpio::SchemaInstanceBase)
11
+ @schema_object = Scorpio.deep_stringify_symbol_keys(schema_object.deref)
12
+ @schema_node = @schema_object.instance
13
+ elsif schema_object.is_a?(Scorpio::JSON::HashNode)
14
+ @schema_object = nil
15
+ @schema_node = Scorpio.deep_stringify_symbol_keys(schema_object.deref)
16
+ elsif schema_object.respond_to?(:to_hash)
17
+ @schema_object = nil
18
+ @schema_node = Scorpio::JSON::Node.new_by_type(Scorpio.deep_stringify_symbol_keys(schema_object), [])
19
+ else
20
+ raise(TypeError, "cannot instantiate Schema from: #{schema_object.pretty_inspect.chomp}")
21
+ end
22
+ if @schema_object
23
+ define_singleton_method(:instance) { schema_node } # aka schema_object.instance
24
+ define_singleton_method(:schema) { schema_object.schema }
25
+ extend SchemaInstanceBaseHash
26
+ else
27
+ define_singleton_method(:[]) { |*a, &b| schema_node.public_send(:[], *a, &b) }
28
+ end
5
29
  end
6
30
  attr_reader :schema_node
31
+ def schema_object
32
+ @schema_object || @schema_node
33
+ end
7
34
 
8
- def subschema_for_property(property_name)
9
- if schema_node['properties'].respond_to?(:to_hash) && schema_node['properties'][property_name].respond_to?(:to_hash)
10
- self.class.new(schema_node['properties'][property_name].deref)
11
- else
12
- if schema_node['patternProperties'].respond_to?(:to_hash)
13
- _, pattern_schema_node = schema_node['patternProperties'].detect do |pattern, _|
14
- property_name =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
35
+ def schema_id
36
+ @schema_id ||= begin
37
+ # start from schema_node and ascend parents looking for an 'id' property.
38
+ # append a fragment to that id (appending to an existing fragment if there
39
+ # is one) consisting of the path from that parent to our schema_node.
40
+ node_for_id = schema_node
41
+ path_from_id_node = []
42
+ done = false
43
+
44
+ while !done
45
+ # TODO: track what parents are schemas. somehow.
46
+ # look at 'id' if node_for_id is a schema, or the document root.
47
+ # decide whether to look at '$id' for all parent nodes or also just schemas.
48
+ if node_for_id.respond_to?(:to_hash)
49
+ if node_for_id.path.empty? || node_for_id.object_id == schema_node.object_id
50
+ # I'm only looking at 'id' for the document root and the schema node
51
+ # until I track what parents are schemas.
52
+ parent_id = node_for_id['$id'] || node_for_id['id']
53
+ else
54
+ # will look at '$id' everywhere since it is less likely to show up outside schemas than
55
+ # 'id', but it will be better to only look at parents that are schemas for this too.
56
+ parent_id = node_for_id['$id']
57
+ end
15
58
  end
16
- end
17
- if pattern_schema_node
18
- self.class.new(pattern_schema_node.deref)
19
- else
20
- if schema_node['additionalProperties'].is_a?(Scorpio::JSON::Node)
21
- self.class.new(schema_node['additionalProperties'].deref)
59
+
60
+ if parent_id || node_for_id.path.empty?
61
+ done = true
22
62
  else
23
- nil
63
+ path_from_id_node.unshift(node_for_id.path.last)
64
+ node_for_id = node_for_id.parent_node
24
65
  end
25
66
  end
67
+ if parent_id
68
+ parent_auri = Addressable::URI.parse(parent_id)
69
+ else
70
+ node_for_id = schema_node.document_node
71
+ validator = ::JSON::Validator.new(node_for_id.content, nil)
72
+ # TODO not good instance_exec'ing into another library's ivars
73
+ parent_auri = validator.instance_exec { @base_schema }.uri
74
+ end
75
+ if parent_auri.fragment
76
+ # add onto the fragment
77
+ parent_id_path = ::JSON::Schema::Pointer.new(:fragment, '#' + parent_auri.fragment).reference_tokens
78
+ path_from_id_node = parent_id_path + path_from_id_node
79
+ parent_auri.fragment = nil
80
+ #else: no fragment so parent_id good as is
81
+ end
82
+
83
+ fragment = ::JSON::Schema::Pointer.new(:reference_tokens, path_from_id_node).fragment
84
+ schema_id = parent_auri.to_s + fragment
85
+
86
+ schema_id
26
87
  end
27
88
  end
28
89
 
29
- def match_to_object(object)
30
- # matching oneOf is good here. one schema for one object.
90
+ def match_to_instance(instance)
91
+ # matching oneOf is good here. one schema for one instance.
31
92
  # matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
32
93
  # than one is a match, the problems of allOf occur.
33
94
  # matching allOf is questionable. all of the schemas must be matched but we just return the first match.
@@ -36,88 +97,139 @@ module Scorpio
36
97
  %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
37
98
  schema_node[someof_key].map(&:deref).map do |someof_node|
38
99
  someof_schema = self.class.new(someof_node)
39
- if someof_schema.validate(object)
40
- return someof_schema.match_to_object(object)
100
+ if someof_schema.validate(instance)
101
+ return someof_schema.match_to_instance(instance)
41
102
  end
42
103
  end
43
104
  end
44
105
  return self
45
106
  end
46
107
 
47
- def subschema_for_index(index)
48
- if schema_node['items'].is_a?(Scorpio::JSON::ArrayNode)
49
- if index < schema_node['items'].size
50
- self.class.new(schema_node['items'][index].deref)
51
- elsif schema_node['additionalItems'].is_a?(Node)
52
- self.class.new(schema_node['additionalItems'].deref)
108
+ def subschema_for_property(property_name_)
109
+ memoize(:subschema_for_property, property_name_) do |property_name|
110
+ if schema_object['properties'].respond_to?(:to_hash) && schema_object['properties'][property_name].respond_to?(:to_hash)
111
+ self.class.new(schema_object['properties'][property_name])
112
+ else
113
+ if schema_object['patternProperties'].respond_to?(:to_hash)
114
+ _, pattern_schema_object = schema_object['patternProperties'].detect do |pattern, _|
115
+ property_name.to_s =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
116
+ end
117
+ end
118
+ if pattern_schema_object
119
+ self.class.new(pattern_schema_object)
120
+ else
121
+ if schema_object['additionalProperties'].respond_to?(:to_hash)
122
+ self.class.new(schema_object['additionalProperties'])
123
+ else
124
+ nil
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ def subschema_for_index(index_)
132
+ memoize(:subschema_for_index, index_) do |index|
133
+ if schema_object['items'].respond_to?(:to_ary)
134
+ if index < schema_object['items'].size
135
+ self.class.new(schema_object['items'][index])
136
+ elsif schema_object['additionalItems'].respond_to?(:to_hash)
137
+ self.class.new(schema_object['additionalItems'])
138
+ end
139
+ elsif schema_object['items'].respond_to?(:to_hash)
140
+ self.class.new(schema_object['items'])
141
+ else
142
+ nil
53
143
  end
54
- elsif schema_node['items'].is_a?(Scorpio::JSON::Node)
55
- self.class.new(schema_node['items'].deref)
56
- else
57
- nil
58
144
  end
59
145
  end
60
146
 
61
147
  def describes_array?
62
- schema_node['type'] == 'array' ||
63
- schema_node['items'] ||
64
- schema_node['additionalItems'] ||
65
- schema_node['default'].respond_to?(:to_ary) || # TODO make sure this is right
66
- (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_ary) }) ||
67
- schema_node['maxItems'] ||
68
- schema_node['minItems'] ||
69
- schema_node.key?('uniqueItems') ||
70
- schema_node['oneOf'].respond_to?(:to_ary) &&
71
- schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
72
- schema_node['allOf'].respond_to?(:to_ary) &&
73
- schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
74
- schema_node['anyOf'].respond_to?(:to_ary) &&
75
- schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
148
+ memoize(:describes_array?) do
149
+ schema_node['type'] == 'array' ||
150
+ schema_node['items'] ||
151
+ schema_node['additionalItems'] ||
152
+ schema_node['default'].respond_to?(:to_ary) || # TODO make sure this is right
153
+ (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_ary) }) ||
154
+ schema_node['maxItems'] ||
155
+ schema_node['minItems'] ||
156
+ schema_node.key?('uniqueItems') ||
157
+ schema_node['oneOf'].respond_to?(:to_ary) &&
158
+ schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
159
+ schema_node['allOf'].respond_to?(:to_ary) &&
160
+ schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
161
+ schema_node['anyOf'].respond_to?(:to_ary) &&
162
+ schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
163
+ end
76
164
  end
77
165
  def describes_hash?
78
- schema_node['type'] == 'object' ||
79
- schema_node['required'].respond_to?(:to_ary) ||
80
- schema_node['properties'].respond_to?(:to_hash) ||
81
- schema_node['additionalProperties'] ||
82
- schema_node['patternProperties'] ||
83
- schema_node['default'].respond_to?(:to_hash) ||
84
- (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
85
- schema_node['oneOf'].respond_to?(:to_ary) &&
86
- schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
87
- schema_node['allOf'].respond_to?(:to_ary) &&
88
- schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
89
- schema_node['anyOf'].respond_to?(:to_ary) &&
90
- schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
166
+ memoize(:describes_hash?) do
167
+ schema_node['type'] == 'object' ||
168
+ schema_node['required'].respond_to?(:to_ary) ||
169
+ schema_node['properties'].respond_to?(:to_hash) ||
170
+ schema_node['additionalProperties'] ||
171
+ schema_node['patternProperties'] ||
172
+ schema_node['default'].respond_to?(:to_hash) ||
173
+ (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
174
+ schema_node['oneOf'].respond_to?(:to_ary) &&
175
+ schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
176
+ schema_node['allOf'].respond_to?(:to_ary) &&
177
+ schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
178
+ schema_node['anyOf'].respond_to?(:to_ary) &&
179
+ schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
180
+ end
91
181
  end
92
182
 
93
183
  def described_hash_property_names
94
- Set.new.tap do |property_names|
95
- if schema_node['properties'].respond_to?(:to_hash)
96
- property_names.merge(schema_node['properties'].keys)
97
- end
98
- if schema_node['required'].respond_to?(:to_ary)
99
- property_names.merge(schema_node['required'].to_ary)
100
- end
101
- # we _could_ look at the properties of 'default' and each 'enum' but ... nah.
102
- # we should look at dependencies (TODO).
103
- %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
104
- schema_node[schemas_key].map(&:deref).map do |someof_node|
105
- property_names.merge(self.class.new(someof_node).described_hash_property_names)
184
+ memoize(:described_hash_property_names) do
185
+ Set.new.tap do |property_names|
186
+ if schema_node['properties'].respond_to?(:to_hash)
187
+ property_names.merge(schema_node['properties'].keys)
188
+ end
189
+ if schema_node['required'].respond_to?(:to_ary)
190
+ property_names.merge(schema_node['required'].to_ary)
191
+ end
192
+ # we _could_ look at the properties of 'default' and each 'enum' but ... nah.
193
+ # we should look at dependencies (TODO).
194
+ %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
195
+ schema_node[schemas_key].map(&:deref).map do |someof_node|
196
+ property_names.merge(self.class.new(someof_node).described_hash_property_names)
197
+ end
106
198
  end
107
199
  end
108
200
  end
109
201
  end
110
202
 
111
- def fully_validate(object)
112
- ::JSON::Validator.fully_validate(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
203
+ def fully_validate(instance)
204
+ ::JSON::Validator.fully_validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
113
205
  end
114
- def validate(object)
115
- ::JSON::Validator.validate(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
206
+ def validate(instance)
207
+ ::JSON::Validator.validate(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
116
208
  end
117
- def validate!(object)
118
- ::JSON::Validator.validate!(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
209
+ def validate!(instance)
210
+ ::JSON::Validator.validate!(schema_node.document, object_to_content(instance), fragment: schema_node.fragment)
119
211
  end
120
212
 
213
+ def object_group_text
214
+ "schema_id=#{schema_id}"
215
+ end
216
+ def inspect
217
+ "\#<#{self.class.inspect} #{object_group_text} #{schema_object.inspect}>"
218
+ end
219
+ alias_method :to_s, :inspect
220
+ def pretty_print(q)
221
+ q.instance_exec(self) do |obj|
222
+ text "\#<#{obj.class.inspect} #{obj.object_group_text}"
223
+ group_sub {
224
+ nest(2) {
225
+ breakable ' '
226
+ pp obj.schema_object
227
+ }
228
+ }
229
+ breakable ''
230
+ text '>'
231
+ end
232
+ end
121
233
  def fingerprint
122
234
  {class: self.class, schema_node: schema_node}
123
235
  end
@@ -125,7 +237,7 @@ module Scorpio
125
237
 
126
238
  private
127
239
  def object_to_content(object)
128
- object = object.object if object.is_a?(Scorpio::SchemaObjectBase)
240
+ object = object.instance if object.is_a?(Scorpio::SchemaInstanceBase)
129
241
  object = object.content if object.is_a?(Scorpio::JSON::Node)
130
242
  object
131
243
  end
@@ -0,0 +1,309 @@
1
+ require 'json'
2
+ require 'scorpio/typelike_modules'
3
+
4
+ module Scorpio
5
+ # base class for representing an instance of an instance described by a schema
6
+ class SchemaInstanceBase
7
+ include Memoize
8
+
9
+ class << self
10
+ def schema_id
11
+ schema.schema_id
12
+ end
13
+
14
+ def inspect
15
+ if !respond_to?(:schema)
16
+ super
17
+ elsif !name || name =~ /\AScorpio::SchemaClasses::/
18
+ %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
19
+ else
20
+ %Q(#{name} (#{schema_id}))
21
+ end
22
+ end
23
+ def to_s
24
+ if !respond_to?(:schema)
25
+ super
26
+ elsif !name || name =~ /\AScorpio::SchemaClasses::/
27
+ %Q(#{SchemaClasses.inspect}[#{schema_id.inspect}])
28
+ else
29
+ name
30
+ end
31
+ end
32
+
33
+ def schema_classes_const_name
34
+ name = schema.schema_id.gsub(/[^\w]/, '_')
35
+ name = 'X' + name unless name[/\A[a-zA-Z_]/]
36
+ name = name[0].upcase + name[1..-1]
37
+ name
38
+ end
39
+
40
+ def name
41
+ unless super
42
+ SchemaClasses.const_set(schema_classes_const_name, self)
43
+ end
44
+ super
45
+ end
46
+ end
47
+
48
+ def initialize(instance)
49
+ unless respond_to?(:schema)
50
+ raise(TypeError, "cannot instantiate #{self.class.inspect} which has no method #schema. please use Scorpio.class_for_schema")
51
+ end
52
+
53
+ self.instance = instance
54
+
55
+ if @instance.is_a?(Scorpio::JSON::HashNode)
56
+ extend SchemaInstanceBaseHash
57
+ elsif @instance.is_a?(Scorpio::JSON::ArrayNode)
58
+ extend SchemaInstanceBaseArray
59
+ end
60
+ # certain methods need to be redefined after we are extended by Enumerable
61
+ extend OverrideFromExtensions
62
+ end
63
+
64
+ module OverrideFromExtensions
65
+ def as_json
66
+ Typelike.as_json(instance)
67
+ end
68
+ end
69
+
70
+ attr_reader :instance
71
+
72
+ def deref
73
+ derefed = instance.deref
74
+ if derefed.object_id == instance.object_id
75
+ self
76
+ else
77
+ self.class.new(derefed)
78
+ end
79
+ end
80
+
81
+ def modified_copy(&block)
82
+ modified_instance = instance.modified_copy(&block)
83
+ self.class.new(modified_instance)
84
+ end
85
+
86
+ def fragment
87
+ instance.fragment
88
+ end
89
+
90
+ def fully_validate
91
+ schema.fully_validate(instance)
92
+ end
93
+ def validate
94
+ schema.validate(instance)
95
+ end
96
+ def validate!
97
+ schema.validate!(instance)
98
+ end
99
+ def inspect
100
+ "\#<#{self.class.to_s} #{instance.inspect}>"
101
+ end
102
+ def pretty_print(q)
103
+ q.instance_exec(self) do |obj|
104
+ text "\#<#{obj.class.to_s}"
105
+ group_sub {
106
+ nest(2) {
107
+ breakable ' '
108
+ pp obj.instance
109
+ }
110
+ }
111
+ breakable ''
112
+ text '>'
113
+ end
114
+ end
115
+
116
+ def object_group_text
117
+ instance.object_group_text
118
+ end
119
+
120
+ def fingerprint
121
+ {class: self.class, instance: instance}
122
+ end
123
+ include FingerprintHash
124
+
125
+ private
126
+ def instance=(thing)
127
+ clear_memo(:[])
128
+ if instance_variable_defined?(:@instance)
129
+ if @instance.class != thing.class
130
+ raise(Scorpio::Bug, "will not accept instance of different class #{thing.class} to current instance class #{@instance.class} on #{self.class.inspect}")
131
+ end
132
+ end
133
+ if thing.is_a?(SchemaInstanceBase)
134
+ warn "assigning instance to a SchemaInstanceBase instance is incorrect. received: #{thing.pretty_inspect.chomp}"
135
+ @instance = Scorpio.deep_stringify_symbol_keys(thing.instance)
136
+ elsif thing.is_a?(Scorpio::JSON::Node)
137
+ @instance = Scorpio.deep_stringify_symbol_keys(thing)
138
+ else
139
+ @instance = Scorpio::JSON::Node.new_by_type(Scorpio.deep_stringify_symbol_keys(thing), [])
140
+ end
141
+ end
142
+ end
143
+
144
+ # this module is just a namespace for schema classes.
145
+ module SchemaClasses
146
+ def self.[](schema_id)
147
+ @classes_by_id[schema_id]
148
+ end
149
+ @classes_by_id = {}
150
+ end
151
+
152
+ def self.class_for_schema(schema_object)
153
+ if schema_object.is_a?(Scorpio::Schema)
154
+ schema__ = schema_object
155
+ else
156
+ schema__ = Scorpio::Schema.new(schema_object)
157
+ end
158
+
159
+ memoize(:class_for_schema, schema__) do |schema_|
160
+ begin
161
+ begin
162
+ Class.new(SchemaInstanceBase).instance_exec(schema_) do |schema|
163
+ begin
164
+ include(Scorpio.module_for_schema(schema))
165
+
166
+ SchemaClasses.instance_exec(self) { |klass| @classes_by_id[klass.schema_id] = klass }
167
+
168
+ self
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def self.module_for_schema(schema_object)
177
+ if schema_object.is_a?(Scorpio::Schema)
178
+ schema__ = schema_object
179
+ else
180
+ schema__ = Scorpio::Schema.new(schema_object)
181
+ end
182
+
183
+ memoize(:module_for_schema, schema__) do |schema_|
184
+ Module.new.tap do |m|
185
+ m.instance_exec(schema_) do |schema|
186
+ define_method(:schema) { schema }
187
+ define_singleton_method(:schema) { schema }
188
+ define_singleton_method(:included) do |includer|
189
+ includer.send(:define_singleton_method, :schema) { schema }
190
+ end
191
+
192
+ define_singleton_method(:schema_id) do
193
+ schema.schema_id
194
+ end
195
+ define_singleton_method(:inspect) do
196
+ %Q(#<Module for Schema: #{schema_id}>)
197
+ end
198
+
199
+ if schema.describes_hash?
200
+ instance_method_modules = [m, SchemaInstanceBase, SchemaInstanceBaseArray, SchemaInstanceBaseHash, SchemaInstanceBase::OverrideFromExtensions]
201
+ instance_methods = instance_method_modules.map do |mod|
202
+ mod.instance_methods + mod.private_instance_methods
203
+ end.inject(Set.new, &:|)
204
+ accessors_to_define = schema.described_hash_property_names.map(&:to_s) - instance_methods.map(&:to_s)
205
+ accessors_to_define.each do |property_name|
206
+ define_method(property_name) do
207
+ if respond_to?(:[])
208
+ self[property_name]
209
+ else
210
+ raise(NoMethodError, "instance does not respond to []; cannot call reader `#{property_name}' for: #{pretty_inspect.chomp}")
211
+ end
212
+ end
213
+ define_method("#{property_name}=") do |value|
214
+ if respond_to?(:[]=)
215
+ self[property_name] = value
216
+ else
217
+ raise(NoMethodError, "instance does not respond to []=; cannot call writer `#{property_name}=' for: #{pretty_inspect.chomp}")
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ module SchemaInstanceBaseHash
228
+ # Hash methods
229
+ def each
230
+ return to_enum(__method__) { instance.size } unless block_given?
231
+ instance.each_key { |k| yield(k, self[k]) }
232
+ self
233
+ end
234
+
235
+ def to_hash
236
+ inject({}) { |h, (k, v)| h[k] = v; h }
237
+ end
238
+
239
+ include Hashlike
240
+
241
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_hash)
242
+ SAFE_KEY_ONLY_METHODS.each do |method_name|
243
+ define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
244
+ end
245
+
246
+ def [](property_name_)
247
+ memoize(:[], property_name_) do |property_name|
248
+ begin
249
+ property_schema = schema.subschema_for_property(property_name)
250
+ property_schema = property_schema && property_schema.match_to_instance(instance[property_name])
251
+
252
+ if property_schema && instance[property_name].is_a?(JSON::Node)
253
+ Scorpio.class_for_schema(property_schema).new(instance[property_name])
254
+ else
255
+ instance[property_name]
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ def []=(property_name, value)
262
+ self.instance = instance.modified_copy do |hash|
263
+ hash.merge(property_name => value)
264
+ end
265
+ end
266
+ end
267
+
268
+ module SchemaInstanceBaseArray
269
+ def each
270
+ return to_enum(__method__) { instance.size } unless block_given?
271
+ instance.each_index { |i| yield(self[i]) }
272
+ self
273
+ end
274
+
275
+ def to_ary
276
+ to_a
277
+ end
278
+
279
+ include Arraylike
280
+
281
+ # methods that don't look at the value; can skip the overhead of #[] (invoked by #to_a).
282
+ # we override these methods from Arraylike
283
+ SAFE_INDEX_ONLY_METHODS.each do |method_name|
284
+ define_method(method_name) { |*a, &b| instance.public_send(method_name, *a, &b) }
285
+ end
286
+
287
+ def [](i_)
288
+ memoize(:[], i_) do |i|
289
+ begin
290
+ index_schema = schema.subschema_for_index(i)
291
+ index_schema = index_schema && index_schema.match_to_instance(instance[i])
292
+
293
+ if index_schema && instance[i].is_a?(JSON::Node)
294
+ Scorpio.class_for_schema(index_schema).new(instance[i])
295
+ else
296
+ instance[i]
297
+ end
298
+ end
299
+ end
300
+ end
301
+ def []=(i, value)
302
+ self.instance = instance.modified_copy do |ary|
303
+ ary.each_with_index.map do |el, ary_i|
304
+ ary_i == i ? value : el
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end