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