scorpio 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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