dato_json_schema 0.20.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,364 @@
1
+ require "set"
2
+
3
+ module JsonSchema
4
+ class ReferenceExpander
5
+ attr_accessor :errors
6
+ attr_accessor :store
7
+
8
+ def expand(schema, options = {})
9
+ @errors = []
10
+ @local_store = DocumentStore.new
11
+ @schema = schema
12
+ @schema_paths = {}
13
+ @store = options[:store] || DocumentStore.new
14
+
15
+ # If the given JSON schema is _just_ a JSON reference and nothing else,
16
+ # short circuit the whole expansion process and return the result.
17
+ if schema.reference && !schema.expanded?
18
+ return dereference(schema, [])
19
+ end
20
+
21
+ @uri = URI.parse(schema.uri)
22
+
23
+ @store.each do |uri, store_schema|
24
+ build_schema_paths(uri, store_schema)
25
+ end
26
+
27
+ # we run #to_s on lookup for URIs; the #to_s of nil is ""
28
+ build_schema_paths("", schema)
29
+
30
+ traverse_schema(schema)
31
+
32
+ refs = unresolved_refs(schema).sort
33
+ if refs.count > 0
34
+ message = %{Couldn't resolve references: #{refs.to_a.join(", ")}.}
35
+ @errors << SchemaError.new(schema, message, :unresolved_references)
36
+ end
37
+
38
+ @errors.count == 0
39
+ end
40
+
41
+ def expand!(schema, options = {})
42
+ if !expand(schema, options)
43
+ raise AggregateError.new(@errors)
44
+ end
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ def add_reference(schema)
51
+ uri = URI.parse(schema.uri)
52
+
53
+ # In case we've already added a schema for the same reference, don't
54
+ # re-add it unless the new schema's pointer path is shorter than the one
55
+ # we've already stored.
56
+ stored_schema = lookup_reference(uri)
57
+ if stored_schema && stored_schema.pointer.length < schema.pointer.length
58
+ return
59
+ end
60
+
61
+ if uri.absolute?
62
+ @store.add_schema(schema)
63
+ else
64
+ @local_store.add_schema(schema)
65
+ end
66
+ end
67
+
68
+ def build_schema_paths(uri, schema)
69
+ return if schema.reference
70
+
71
+ paths = @schema_paths[uri] ||= {}
72
+ paths[schema.pointer] = schema
73
+
74
+ schema_children(schema) do |subschema|
75
+ build_schema_paths(uri, subschema)
76
+ end
77
+
78
+ # Also insert alternate tree for schema's custom URI. O(crazy).
79
+ if schema.uri != uri
80
+ fragment, parent = schema.fragment, schema.parent
81
+ schema.fragment, schema.parent = "#", nil
82
+ build_schema_paths(schema.uri, schema)
83
+ schema.fragment, schema.parent = fragment, parent
84
+ end
85
+ end
86
+
87
+ def dereference(ref_schema, ref_stack, parent_ref: nil)
88
+ ref = ref_schema.reference
89
+
90
+ # Some schemas don't have a reference, but do
91
+ # have children. If that's the case, we need to
92
+ # dereference the subschemas.
93
+ if !ref
94
+ schema_children(ref_schema) do |subschema|
95
+ next unless subschema.reference
96
+ next if ref_schema.uri == parent_ref.uri.to_s
97
+
98
+ if !subschema.reference.uri && parent_ref
99
+ subschema.reference = JsonReference::Reference.new("#{parent_ref.uri}#{subschema.reference.pointer}")
100
+ end
101
+
102
+ dereference(subschema, ref_stack)
103
+ end
104
+ return true
105
+ end
106
+
107
+ # detects a reference cycle
108
+ if ref_stack.include?(ref)
109
+ message = %{Reference loop detected: #{ref_stack.sort.join(", ")}.}
110
+ @errors << SchemaError.new(ref_schema, message, :loop_detected)
111
+ return false
112
+ end
113
+
114
+ new_schema = resolve_reference(ref_schema)
115
+ return false unless new_schema
116
+
117
+ # if the reference resolved to a new reference we need to continue
118
+ # dereferencing until we either hit a non-reference schema, or a
119
+ # reference which is already resolved
120
+ if new_schema.reference && !new_schema.expanded?
121
+ success = dereference(new_schema, ref_stack + [ref])
122
+ return false unless success
123
+ end
124
+
125
+ # If the reference schema is a global reference
126
+ # then we'll need to manually expand any nested
127
+ # references.
128
+ if ref.uri
129
+ schema_children(new_schema) do |subschema|
130
+ # Don't bother if the subschema points to the same
131
+ # schema as the reference schema.
132
+ next if ref_schema == subschema
133
+
134
+ if subschema.reference
135
+ # If the subschema has a reference, then
136
+ # we don't need to recurse if the schema is
137
+ # already expanded.
138
+ next if subschema.expanded?
139
+
140
+ if !subschema.reference.uri
141
+ # the subschema's ref is local to the file that the
142
+ # subschema is in; however since there's no URI
143
+ # the 'resolve_pointer' method would try to look it up
144
+ # within @schema. So: manually reconstruct the reference to
145
+ # use the URI of the parent ref.
146
+ subschema.reference = JsonReference::Reference.new("#{ref.uri}#{subschema.reference.pointer}")
147
+ end
148
+ end
149
+
150
+ if subschema.items && subschema.items.reference
151
+ next if subschema.expanded?
152
+
153
+ if !subschema.items.reference.uri
154
+ # The subschema's ref is local to the file that the
155
+ # subschema is in. Manually reconstruct the reference
156
+ # so we can resolve it.
157
+ subschema.items.reference = JsonReference::Reference.new("#{ref.uri}#{subschema.items.reference.pointer}")
158
+ end
159
+ end
160
+
161
+ # If we're recursing into a schema via a global reference, then if
162
+ # the current subschema doesn't have a reference, we have no way of
163
+ # figuring out what schema we're in. The resolve_pointer method will
164
+ # default to looking it up in the initial schema. Instead, we're
165
+ # passing the parent ref here, so we can grab the URI
166
+ # later if needed.
167
+ dereference(subschema, ref_stack, parent_ref: ref)
168
+ end
169
+ end
170
+
171
+ # copy new schema into existing one while preserving parent, fragment,
172
+ # and reference
173
+ parent = ref_schema.parent
174
+ ref_schema.copy_from(new_schema)
175
+ ref_schema.parent = parent
176
+
177
+ # correct all parent references to point back to ref_schema instead of
178
+ # new_schema
179
+ if ref_schema.original?
180
+ schema_children(ref_schema) do |schema|
181
+ schema.parent = ref_schema
182
+ end
183
+ end
184
+
185
+ true
186
+ end
187
+
188
+ def lookup_pointer(uri, pointer)
189
+ paths = @schema_paths[uri.to_s] ||= {}
190
+ paths[pointer]
191
+ end
192
+
193
+ def lookup_reference(uri)
194
+ if uri.absolute?
195
+ @store.lookup_schema(uri.to_s)
196
+ else
197
+ @local_store.lookup_schema(uri.to_s)
198
+ end
199
+ end
200
+
201
+ def resolve_pointer(ref_schema, resolved_schema)
202
+ ref = ref_schema.reference
203
+
204
+ if !(new_schema = lookup_pointer(ref.uri, ref.pointer))
205
+ new_schema = JsonPointer.evaluate(resolved_schema, ref.pointer)
206
+
207
+ # couldn't resolve pointer within known schema; that's an error
208
+ if new_schema.nil?
209
+ message = %{Couldn't resolve pointer "#{ref.pointer}".}
210
+ @errors << SchemaError.new(resolved_schema, message, :unresolved_pointer)
211
+ return
212
+ end
213
+
214
+ # Try to aggressively detect a circular dependency in case of another
215
+ # reference. See:
216
+ #
217
+ # https://github.com/brandur/json_schema/issues/50
218
+ #
219
+ if new_schema.reference &&
220
+ new_new_schema = lookup_pointer(ref.uri, new_schema.reference.pointer)
221
+ new_new_schema.clones << ref_schema
222
+ else
223
+ # Parse a new schema and use the same parent node. Basically this is
224
+ # exclusively for the case of a reference that needs to be
225
+ # de-referenced again to be resolved.
226
+ build_schema_paths(ref.uri, resolved_schema)
227
+ end
228
+ else
229
+ # insert a clone record so that the expander knows to expand it when
230
+ # the schema traversal is finished
231
+ new_schema.clones << ref_schema
232
+ end
233
+ new_schema
234
+ end
235
+
236
+ def resolve_reference(ref_schema)
237
+ ref = ref_schema.reference
238
+ uri = ref.uri
239
+
240
+ if uri && uri.host
241
+ scheme = uri.scheme || "http"
242
+ # allow resolution if something we've already parsed has claimed the
243
+ # full URL
244
+ if @store.lookup_schema(uri.to_s)
245
+ resolve_uri(ref_schema, uri)
246
+ else
247
+ message =
248
+ %{Reference resolution over #{scheme} is not currently supported (URI: #{uri}).}
249
+ @errors << SchemaError.new(ref_schema, message, :scheme_not_supported)
250
+ nil
251
+ end
252
+ # absolute
253
+ elsif uri && uri.path[0] == "/"
254
+ resolve_uri(ref_schema, uri)
255
+ # relative
256
+ elsif uri
257
+ # Build an absolute path using the URI of the current schema.
258
+ #
259
+ # Note that this code path will never currently be hit because the
260
+ # incoming reference schema will never have a URI.
261
+ if ref_schema.uri
262
+ schema_uri = ref_schema.uri.chomp("/")
263
+ resolve_uri(ref_schema, URI.parse(schema_uri + "/" + uri.path))
264
+ else
265
+ nil
266
+ end
267
+
268
+ # just a JSON Pointer -- resolve against schema root
269
+ else
270
+ resolve_pointer(ref_schema, @schema)
271
+ end
272
+ end
273
+
274
+ def resolve_uri(ref_schema, uri)
275
+ if schema = lookup_reference(uri)
276
+ resolve_pointer(ref_schema, schema)
277
+ else
278
+ message = %{Couldn't resolve URI: #{uri.to_s}.}
279
+ @errors << SchemaError.new(ref_schema, message, :unresolved_pointer)
280
+ nil
281
+ end
282
+ end
283
+
284
+ def schema_children(schema)
285
+ schema.all_of.each { |s| yield s }
286
+ schema.any_of.each { |s| yield s }
287
+ schema.one_of.each { |s| yield s }
288
+ schema.definitions.each { |_, s| yield s }
289
+ schema.pattern_properties.each { |_, s| yield s }
290
+ schema.properties.each { |_, s| yield s }
291
+
292
+ if additional = schema.additional_properties
293
+ if additional.is_a?(Schema)
294
+ yield additional
295
+ end
296
+ end
297
+
298
+ if schema.not
299
+ yield schema.not
300
+ end
301
+
302
+ # can either be a single schema (list validation) or multiple (tuple
303
+ # validation)
304
+ if items = schema.items
305
+ if items.is_a?(Array)
306
+ items.each { |s| yield s }
307
+ else
308
+ yield items
309
+ end
310
+ end
311
+
312
+ # dependencies can either be simple or "schema"; only replace the
313
+ # latter
314
+ schema.dependencies.
315
+ each_value { |s| yield s if s.is_a?(Schema) }
316
+
317
+ # schemas contained inside hyper-schema links objects
318
+ if schema.links
319
+ schema.links.each { |l|
320
+ yield l.schema if l.schema
321
+ yield l.target_schema if l.target_schema
322
+ }
323
+ end
324
+ end
325
+
326
+ def unresolved_refs(schema)
327
+ # prevent endless recursion
328
+ return [] unless schema.original?
329
+
330
+ arr = []
331
+ schema_children(schema) do |subschema|
332
+ if !subschema.expanded?
333
+ arr += [subschema.reference]
334
+ else
335
+ arr += unresolved_refs(subschema)
336
+ end
337
+ end
338
+ arr
339
+ end
340
+
341
+ def traverse_schema(schema)
342
+ add_reference(schema)
343
+
344
+ schema_children(schema) do |subschema|
345
+ if subschema.reference && !subschema.expanded?
346
+ dereference(subschema, [])
347
+ end
348
+
349
+ if !subschema.reference
350
+ traverse_schema(subschema)
351
+ end
352
+ end
353
+
354
+ # after finishing a schema traversal, find all clones and re-hydrate them
355
+ if schema.original?
356
+ schema.clones.each do |clone_schema|
357
+ parent = clone_schema.parent
358
+ clone_schema.copy_from(schema)
359
+ clone_schema.parent = parent
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
@@ -0,0 +1,295 @@
1
+ require "json"
2
+
3
+ module JsonSchema
4
+ class Schema
5
+ TYPE_MAP = {
6
+ "array" => Array,
7
+ "boolean" => [FalseClass, TrueClass],
8
+ "integer" => Integer,
9
+ "number" => [Integer, Float],
10
+ "null" => NilClass,
11
+ "object" => Hash,
12
+ "string" => String,
13
+ }
14
+
15
+ include Attributes
16
+
17
+ def initialize
18
+ # nil out all our fields so that it's possible to instantiate a schema
19
+ # instance without going through the parser and validate against it
20
+ # without Ruby throwing warnings about uninitialized instance variables.
21
+ initialize_attrs
22
+
23
+ # Don't put this in as an attribute default. We require that this precise
24
+ # pointer gets copied between all clones of any given schema so that they
25
+ # all share exactly the same set.
26
+ @clones = Set.new
27
+ end
28
+
29
+ # Fragment of a JSON Pointer that can help us build a pointer back to this
30
+ # schema for debugging.
31
+ attr_accessor :fragment
32
+
33
+ # Rather than a normal schema, the node may be a JSON Reference. In this
34
+ # case, no other attributes will be filled in except for #parent.
35
+ attr_accessor :reference
36
+
37
+ attr_copyable :expanded
38
+
39
+ # A reference to the data which the Schema was initialized from. Used for
40
+ # resolving JSON Pointer references.
41
+ #
42
+ # Type: Hash
43
+ attr_copyable :data
44
+
45
+ #
46
+ # Relations
47
+ #
48
+
49
+ # Parent Schema object. Child may come from any of `definitions`,
50
+ # `properties`, `anyOf`, etc.
51
+ #
52
+ # Type: Schema
53
+ attr_copyable :parent
54
+
55
+ # Collection of clones of this schema object, meaning all Schemas that were
56
+ # initialized after the original. Used for JSON Reference expansion. The
57
+ # only copy not present in this set is the original Schema object.
58
+ #
59
+ # Note that this doesn't have a default option because we rely on the fact
60
+ # that the set is the *same object* between all clones of any given schema.
61
+ #
62
+ # Type: Set[Schema]
63
+ attr_copyable :clones
64
+
65
+ # The normalized URI of this schema. Note that child schemas inherit a URI
66
+ # from their parent unless they have one explicitly defined, so this is
67
+ # likely not a unique value in any given schema hierarchy.
68
+ #
69
+ # Type: String
70
+ attr_copyable :uri
71
+
72
+ #
73
+ # Metadata
74
+ #
75
+
76
+ # Alters resolution scope. This value is used along with the parent scope's
77
+ # URI to build a new address for this schema. Relative ID's will append to
78
+ # the parent, and absolute URI's will replace it.
79
+ #
80
+ # Type: String
81
+ attr_schema :id
82
+
83
+ # Short title of the schema (or the hyper-schema link if this is one).
84
+ #
85
+ # Type: String
86
+ attr_schema :title
87
+
88
+ # More detailed description of the schema (or the hyper-schema link if this
89
+ # is one).
90
+ #
91
+ # Type: String
92
+ attr_schema :description
93
+
94
+ # Default JSON value for this particular schema
95
+ #
96
+ # Type: [any]
97
+ attr_schema :default
98
+
99
+ #
100
+ # Validation: Any
101
+ #
102
+
103
+ # A collection of subschemas of which data must validate against the full
104
+ # set of to be valid.
105
+ #
106
+ # Type: Array[Schema]
107
+ attr_schema :all_of, :default => [], :schema_name => :allOf
108
+
109
+ # A collection of subschemas of which data must validate against any schema
110
+ # in the set to be be valid.
111
+ #
112
+ # Type: Array[Schema]
113
+ attr_schema :any_of, :default => [], :schema_name => :anyOf
114
+
115
+ # A collection of inlined subschemas. Standard convention is to subschemas
116
+ # here and reference them from elsewhere.
117
+ #
118
+ # Type: Hash[String => Schema]
119
+ attr_schema :definitions, :default => {}
120
+
121
+ # A collection of objects that must include the data for it to be valid.
122
+ #
123
+ # Type: Array
124
+ attr_schema :enum
125
+
126
+ # A collection of subschemas of which data must validate against exactly
127
+ # one of to be valid.
128
+ #
129
+ # Type: Array[Schema]
130
+ attr_schema :one_of, :default => [], :schema_name => :oneOf
131
+
132
+ # A subschema which data must not validate against to be valid.
133
+ #
134
+ # Type: Schema
135
+ attr_schema :not
136
+
137
+ # An array of types that data is allowed to be. The spec allows this to be
138
+ # a string as well, but the parser will always normalize this to an array
139
+ # of strings.
140
+ #
141
+ # Type: Array[String]
142
+ attr_schema :type, :default => [], :clear_cache => :@type_parsed
143
+
144
+ # validation: array
145
+ attr_schema :additional_items, :default => true, :schema_name => :additionalItems
146
+ attr_schema :items
147
+ attr_schema :max_items, :schema_name => :maxItems
148
+ attr_schema :min_items, :schema_name => :minItems
149
+ attr_schema :unique_items, :schema_name => :uniqueItems
150
+
151
+ # validation: number/integer
152
+ attr_schema :max, :schema_name => :maximum
153
+ attr_schema :max_exclusive, :default => false, :schema_name => :exclusiveMaximum
154
+ attr_schema :min, :schema_name => :minimum
155
+ attr_schema :min_exclusive, :default => false, :schema_name => :exclusiveMinimum
156
+ attr_schema :multiple_of, :schema_name => :multipleOf
157
+
158
+ # validation: object
159
+ attr_schema :additional_properties, :default => true, :schema_name => :additionalProperties
160
+ attr_schema :dependencies, :default => {}
161
+ attr_schema :max_properties, :schema_name => :maxProperties
162
+ attr_schema :min_properties, :schema_name => :minProperties
163
+ attr_schema :pattern_properties, :default => {}, :schema_name => :patternProperties
164
+ attr_schema :properties, :default => {}
165
+ attr_schema :required
166
+ # warning: strictProperties is technically V5 spec (but I needed it now)
167
+ attr_schema :strict_properties, :default => false, :schema_name => :strictProperties
168
+
169
+ # validation: string
170
+ attr_schema :format
171
+ attr_schema :max_length, :schema_name => :maxLength
172
+ attr_schema :min_length, :schema_name => :minLength
173
+ attr_schema :pattern
174
+
175
+ # hyperschema
176
+ attr_schema :links, :default => []
177
+ attr_schema :media
178
+ attr_schema :path_start, :schema_name => :pathStart
179
+ attr_schema :read_only, :schema_name => :readOnly
180
+
181
+ # hyperschema link attributes
182
+ attr_schema :enc_type, :schema_name => :encType, :default => "application/json"
183
+ attr_schema :href
184
+ attr_schema :media_type, :schema_name => :mediaType, :default => "application/json"
185
+ attr_schema :method
186
+ attr_schema :rel
187
+ attr_schema :schema
188
+ attr_schema :target_schema, :schema_name => :targetSchema
189
+ attr_schema :job_schema, :schema_name => :jobSchema
190
+
191
+ # allow booleans to be access with question mark
192
+ alias :additional_items? :additional_items
193
+ alias :expanded? :expanded
194
+ alias :max_exclusive? :max_exclusive
195
+ alias :min_exclusive? :min_exclusive
196
+ alias :read_only? :read_only
197
+ alias :unique_items? :unique_items
198
+
199
+ def expand_references(options = {})
200
+ expander = ReferenceExpander.new
201
+ if expander.expand(self, options)
202
+ [true, nil]
203
+ else
204
+ [false, expander.errors]
205
+ end
206
+ end
207
+
208
+ def expand_references!(options = {})
209
+ ReferenceExpander.new.expand!(self, options)
210
+ true
211
+ end
212
+
213
+ # An array of Ruby classes that are equivalent to the types defined in the
214
+ # schema.
215
+ #
216
+ # Type: Array[Class]
217
+ def type_parsed
218
+ @type_parsed ||= type.flat_map { |t| TYPE_MAP[t] }.compact
219
+ end
220
+
221
+ def inspect
222
+ "\#<JsonSchema::Schema pointer=#{pointer}>"
223
+ end
224
+
225
+ def inspect_schema
226
+ if reference
227
+ str = reference.to_s
228
+ str += expanded? ? " [EXPANDED]" : " [COLLAPSED]"
229
+ str += original? ? " [ORIGINAL]" : " [CLONE]"
230
+ str
231
+ else
232
+ hash = {}
233
+ self.class.copyable_attrs.each do |copyable, _|
234
+ next if [:@clones, :@data, :@parent, :@uri].include?(copyable)
235
+ if value = instance_variable_get(copyable)
236
+ if value.is_a?(Array)
237
+ if !value.empty?
238
+ hash[copyable] = value.map { |v| inspect_value(v) }
239
+ end
240
+ elsif value.is_a?(Hash)
241
+ if !value.empty?
242
+ hash[copyable] =
243
+ Hash[*value.map { |k, v| [k, inspect_value(v)] }.flatten]
244
+ end
245
+ else
246
+ hash[copyable] = inspect_value(value)
247
+ end
248
+ end
249
+ end
250
+ hash
251
+ end
252
+ end
253
+
254
+ def inspect_value(value)
255
+ if value.is_a?(Schema)
256
+ value.inspect_schema
257
+ else
258
+ value.inspect
259
+ end
260
+ end
261
+
262
+ def original?
263
+ !clones.include?(self)
264
+ end
265
+
266
+ def pointer
267
+ if parent
268
+ (parent.pointer + "/".freeze + fragment).freeze
269
+ else
270
+ fragment
271
+ end
272
+ end
273
+
274
+ def validate(data, fail_fast: false)
275
+ validator = Validator.new(self)
276
+ valid = validator.validate(data, fail_fast: fail_fast)
277
+ [valid, validator.errors]
278
+ end
279
+
280
+ def validate!(data, fail_fast: false)
281
+ Validator.new(self).validate!(data, fail_fast: fail_fast)
282
+ end
283
+
284
+ # Link subobject for a hyperschema.
285
+ class Link < Schema
286
+ inherit_attrs
287
+ end
288
+
289
+ # Media type subobject for a hyperschema.
290
+ class Media
291
+ attr_accessor :binary_encoding
292
+ attr_accessor :type
293
+ end
294
+ end
295
+ end