Class: JSONSchemer::Schema

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Output
Defined in:
lib/json_schemer/schema.rb

Defined Under Namespace

Classes: Context

Constant Summary collapse

SCHEMA_KEYWORD_CLASS =
Draft202012::Vocab::Core::Schema
VOCABULARY_KEYWORD_CLASS =
Draft202012::Vocab::Core::Vocabulary
ID_KEYWORD_CLASS =
Draft202012::Vocab::Core::Id
UNKNOWN_KEYWORD_CLASS =
Draft202012::Vocab::Core::UnknownKeyword
NOT_KEYWORD_CLASS =
Draft202012::Vocab::Applicator::Not
PROPERTIES_KEYWORD_CLASS =
Draft202012::Vocab::Applicator::Properties
NET_HTTP_REF_RESOLVER =
proc { |uri| JSON.parse(Net::HTTP.get(uri)) }
RUBY_REGEXP_RESOLVER =
proc { |pattern| Regexp.new(pattern) }
ECMA_REGEXP_RESOLVER =
proc { |pattern| Regexp.new(EcmaRegexp.ruby_equivalent(pattern)) }
DEFAULT_PROPERTY_DEFAULT_RESOLVER =
proc do |instance, property, results_with_tree_validity|
  results_with_tree_validity = results_with_tree_validity.select(&:last) unless results_with_tree_validity.size == 1
  annotations = results_with_tree_validity.to_set { |result, _tree_valid| result.annotation }
  if annotations.size == 1
    instance[property] = annotations.first.clone
    true
  else
    false
  end
end
SYMBOL_PROPERTY_DEFAULT_RESOLVER =
proc do |instance, property, results_with_tree_validity|
  DEFAULT_PROPERTY_DEFAULT_RESOLVER.call(instance, property.to_sym, results_with_tree_validity)
end

Constants included from Output

Output::FRAGMENT_ENCODE_REGEX

Instance Attribute Summary collapse

Attributes included from Output

#keyword, #schema

Instance Method Summary collapse

Methods included from Output

#x_error

Constructor Details

#initialize(value, parent = nil, root = self, keyword = nil, configuration: JSONSchemer.configuration, base_uri: configuration.base_uri, meta_schema: configuration.meta_schema, vocabulary: configuration.vocabulary, format: configuration.format, formats: configuration.formats, content_encodings: configuration.content_encodings, content_media_types: configuration.content_media_types, keywords: configuration.keywords, before_property_validation: configuration.before_property_validation, after_property_validation: configuration.after_property_validation, insert_property_defaults: configuration.insert_property_defaults, property_default_resolver: configuration.property_default_resolver, ref_resolver: configuration.ref_resolver, regexp_resolver: configuration.regexp_resolver, output_format: configuration.output_format, resolve_enumerators: configuration.resolve_enumerators, access_mode: configuration.access_mode) ⇒ Schema

Returns a new instance of Schema.

[View source]

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/json_schemer/schema.rb', line 51

def initialize(
  value,
  parent = nil,
  root = self,
  keyword = nil,
  configuration: JSONSchemer.configuration,
  base_uri: configuration.base_uri,
  meta_schema: configuration.meta_schema,
  vocabulary: configuration.vocabulary,
  format: configuration.format,
  formats: configuration.formats,
  content_encodings: configuration.content_encodings,
  content_media_types: configuration.content_media_types,
  keywords: configuration.keywords,
  before_property_validation: configuration.before_property_validation,
  after_property_validation: configuration.after_property_validation,
  insert_property_defaults: configuration.insert_property_defaults,
  property_default_resolver: configuration.property_default_resolver,
  ref_resolver: configuration.ref_resolver,
  regexp_resolver: configuration.regexp_resolver,
  output_format: configuration.output_format,
  resolve_enumerators: configuration.resolve_enumerators,
  access_mode: configuration.access_mode
)
  @value = deep_stringify_keys(value)
  @parent = parent
  @root = root
  @keyword = keyword
  @schema = self
  @base_uri = base_uri
  @meta_schema = meta_schema
  @configuration = Configuration.new(
    :base_uri => base_uri,
    :meta_schema => meta_schema,
    :vocabulary => vocabulary,
    :format => format,
    :formats => formats,
    :content_encodings => content_encodings,
    :content_media_types => content_media_types,
    :keywords => keywords,
    :before_property_validation => Array(before_property_validation),
    :after_property_validation => Array(after_property_validation),
    :insert_property_defaults => insert_property_defaults,
    :property_default_resolver => property_default_resolver,
    :ref_resolver => ref_resolver,
    :regexp_resolver => regexp_resolver,
    :output_format => output_format,
    :resolve_enumerators => resolve_enumerators,
    :access_mode => access_mode
  )
  @parsed = parse
end

Instance Attribute Details

#base_uriObject

Returns the value of attribute base_uri.


46
47
48
# File 'lib/json_schemer/schema.rb', line 46

def base_uri
  @base_uri
end

#configurationObject (readonly)

Returns the value of attribute configuration.


47
48
49
# File 'lib/json_schemer/schema.rb', line 47

def configuration
  @configuration
end

#keyword_orderObject

Returns the value of attribute keyword_order.


46
47
48
# File 'lib/json_schemer/schema.rb', line 46

def keyword_order
  @keyword_order
end

#keywordsObject

Returns the value of attribute keywords.


46
47
48
# File 'lib/json_schemer/schema.rb', line 46

def keywords
  @keywords
end

#meta_schemaObject

Returns the value of attribute meta_schema.


46
47
48
# File 'lib/json_schemer/schema.rb', line 46

def meta_schema
  @meta_schema
end

#parentObject (readonly)

Returns the value of attribute parent.


47
48
49
# File 'lib/json_schemer/schema.rb', line 47

def parent
  @parent
end

#parsedObject (readonly)

Returns the value of attribute parsed.


47
48
49
# File 'lib/json_schemer/schema.rb', line 47

def parsed
  @parsed
end

#rootObject (readonly)

Returns the value of attribute root.


47
48
49
# File 'lib/json_schemer/schema.rb', line 47

def root
  @root
end

#valueObject (readonly)

Returns the value of attribute value.


47
48
49
# File 'lib/json_schemer/schema.rb', line 47

def value
  @value
end

Instance Method Details

#absolute_keyword_locationObject

[View source]

272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/json_schemer/schema.rb', line 272

def absolute_keyword_location
  # using `equal?` because `URI::Generic#==` is slow
  @absolute_keyword_location ||= if !parent || (!parent.schema.base_uri.equal?(base_uri) && (base_uri.fragment.nil? || base_uri.fragment.empty?))
    absolute_keyword_location_uri = base_uri.dup
    absolute_keyword_location_uri.fragment = ''
    absolute_keyword_location_uri.to_s
  elsif keyword
    "#{parent.absolute_keyword_location}/#{fragment_encode(escaped_keyword)}"
  else
    parent.absolute_keyword_location
  end
end

#bundleObject

[View source]

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/json_schemer/schema.rb', line 223

def bundle
  return value unless value.is_a?(Hash)

  id_keyword = meta_schema.id_keyword
  defs_keyword = meta_schema.defs_keyword

  compound_document = value.dup
  compound_document[id_keyword] = base_uri.to_s
  compound_document['$schema'] = meta_schema.base_uri.to_s
  embedded_resources = compound_document[defs_keyword] = (compound_document[defs_keyword]&.dup || {})

  if compound_document.key?('$ref') && meta_schema.keywords.fetch('$ref').exclusive?
    compound_document['allOf'] = (compound_document['allOf']&.dup || [])
    compound_document['allOf'] << { '$ref' => compound_document.delete('$ref') }
  end

  values = [self]
  while value = values.shift
    case value
    when Schema
      values << value.parsed
    when Keyword
      if value.respond_to?(:ref_uri) && value.respond_to?(:ref_schema)
        ref_uri = value.ref_uri.dup
        ref_uri.fragment = nil
        ref_id = ref_uri.to_s
        ref_schema = value.ref_schema.root

        next if ref_schema == root || embedded_resources.key?(ref_id)

        embedded_resource = ref_schema.value.dup
        embedded_resource[id_keyword] = ref_id
        embedded_resource['$schema'] = ref_schema.meta_schema.base_uri.to_s
        embedded_resources[ref_id] = embedded_resource

        values << ref_schema
      else
        values << value.parsed
      end
    when Hash
      values.concat(value.values)
    when Array
      values.concat(value)
    end
  end

  compound_document
end

#defs_keywordObject

[View source]

331
332
333
# File 'lib/json_schemer/schema.rb', line 331

def defs_keyword
  @defs_keyword ||= (keywords.key?('$defs') ? '$defs' : 'definitions')
end

#error(formatted_instance_location:, **options) ⇒ Object

[View source]

339
340
341
342
343
344
345
# File 'lib/json_schemer/schema.rb', line 339

def error(formatted_instance_location:, **options)
  if value == false && parent&.respond_to?(:false_schema_error)
    parent.false_schema_error(:formatted_instance_location => formatted_instance_location, **options)
  else
    "value at #{formatted_instance_location} does not match schema"
  end
end

#error_keyObject

[View source]

295
296
297
# File 'lib/json_schemer/schema.rb', line 295

def error_key
  '^'
end

#fetch(key) ⇒ Object

[View source]

299
300
301
# File 'lib/json_schemer/schema.rb', line 299

def fetch(key)
  parsed.fetch(key)
end

#fetch_content_encoding(content_encoding, *args, &block) ⇒ Object

[View source]

311
312
313
314
315
316
317
# File 'lib/json_schemer/schema.rb', line 311

def fetch_content_encoding(content_encoding, *args, &block)
  if meta_schema == self
    content_encodings.fetch(content_encoding, *args, &block)
  else
    content_encodings.fetch(content_encoding) { meta_schema.fetch_content_encoding(content_encoding, *args, &block) }
  end
end

#fetch_content_media_type(content_media_type, *args, &block) ⇒ Object

[View source]

319
320
321
322
323
324
325
# File 'lib/json_schemer/schema.rb', line 319

def fetch_content_media_type(content_media_type, *args, &block)
  if meta_schema == self
    content_media_types.fetch(content_media_type, *args, &block)
  else
    content_media_types.fetch(content_media_type) { meta_schema.fetch_content_media_type(content_media_type, *args, &block) }
  end
end

#fetch_format(format, *args, &block) ⇒ Object

[View source]

303
304
305
306
307
308
309
# File 'lib/json_schemer/schema.rb', line 303

def fetch_format(format, *args, &block)
  if meta_schema == self
    formats.fetch(format, *args, &block)
  else
    formats.fetch(format) { meta_schema.fetch_format(format, *args, &block) }
  end
end

#id_keywordObject

[View source]

327
328
329
# File 'lib/json_schemer/schema.rb', line 327

def id_keyword
  @id_keyword ||= (keywords.key?('$id') ? '$id' : 'id')
end

#inspectObject

[View source]

362
363
364
# File 'lib/json_schemer/schema.rb', line 362

def inspect
  "#<#{self.class.name} @value=#{@value.inspect} @parent=#{@parent.inspect} @keyword=#{@keyword.inspect}>"
end

#ref(value) ⇒ Object

[View source]

128
129
130
# File 'lib/json_schemer/schema.rb', line 128

def ref(value)
  root.resolve_ref(URI.join(base_uri, value))
end

#ref_resolverObject

[View source]

347
348
349
# File 'lib/json_schemer/schema.rb', line 347

def ref_resolver
  @ref_resolver ||= @configuration.ref_resolver == 'net/http' ? CachedResolver.new(&NET_HTTP_REF_RESOLVER) : @configuration.ref_resolver
end

#regexp_resolverObject

[View source]

351
352
353
354
355
356
357
358
359
360
# File 'lib/json_schemer/schema.rb', line 351

def regexp_resolver
  @regexp_resolver ||= case @configuration.regexp_resolver
  when 'ecma'
    CachedResolver.new(&ECMA_REGEXP_RESOLVER)
  when 'ruby'
    CachedResolver.new(&RUBY_REGEXP_RESOLVER)
  else
    @configuration.regexp_resolver
  end
end

#resolve_ref(uri) ⇒ Object

Raises:

[View source]

175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/json_schemer/schema.rb', line 175

def resolve_ref(uri)
  pointer = ''
  if Format.valid_json_pointer?(uri.fragment)
    pointer = URI.decode_www_form_component(uri.fragment)
    uri.fragment = nil
  end

  lexical_resources = resources.fetch(:lexical)
  schema = lexical_resources[uri]

  if !schema && uri.fragment.nil?
    empty_fragment_uri = uri.dup
    empty_fragment_uri.fragment = ''
    schema = lexical_resources[empty_fragment_uri]
  end

  unless schema
    location_independent_identifier = uri.fragment
    uri.fragment = nil
    remote_schema = JSONSchemer.schema(
      ref_resolver.call(uri) || raise(InvalidRefResolution, uri.to_s),
      :configuration => configuration,
      :base_uri => uri,
      :meta_schema => meta_schema,
      :ref_resolver => ref_resolver,
      :regexp_resolver => regexp_resolver
    )
    remote_uri = remote_schema.base_uri.dup
    remote_uri.fragment = location_independent_identifier if location_independent_identifier
    schema = remote_schema.resources.fetch(:lexical).fetch(remote_uri)
  end

  schema = Hana::Pointer.parse(pointer).reduce(schema) do |obj, token|
    obj.fetch(token)
  rescue IndexError
    raise InvalidRefPointer, pointer
  end

  schema = schema.parsed_schema if schema.is_a?(Keyword)
  raise InvalidRefPointer, pointer unless schema.is_a?(Schema)

  schema
end

#resolve_regexp(pattern) ⇒ Object

[View source]

219
220
221
# File 'lib/json_schemer/schema.rb', line 219

def resolve_regexp(pattern)
  regexp_resolver.call(pattern) || raise(InvalidRegexpResolution, pattern)
end

#resourcesObject

[View source]

335
336
337
# File 'lib/json_schemer/schema.rb', line 335

def resources
  @resources ||= { :lexical => Resources.new, :dynamic => Resources.new }
end

#schema_pointerObject

[View source]

285
286
287
288
289
290
291
292
293
# File 'lib/json_schemer/schema.rb', line 285

def schema_pointer
  @schema_pointer ||= if !parent
    ''
  elsif keyword
    "#{parent.schema_pointer}/#{escaped_keyword}"
  else
    parent.schema_pointer
  end
end

#valid?(instance, **options) ⇒ Boolean

Returns:

  • (Boolean)
[View source]

104
105
106
# File 'lib/json_schemer/schema.rb', line 104

def valid?(instance, **options)
  validate(instance, :output_format => 'flag', **options).fetch('valid')
end

#valid_schema?(**options) ⇒ Boolean

Returns:

  • (Boolean)
[View source]

120
121
122
# File 'lib/json_schemer/schema.rb', line 120

def valid_schema?(**options)
  meta_schema.valid?(value, **options)
end

#validate(instance, output_format: @configuration.output_format, resolve_enumerators: @configuration.resolve_enumerators, access_mode: @configuration.access_mode) ⇒ Object

[View source]

108
109
110
111
112
113
114
115
116
117
118
# File 'lib/json_schemer/schema.rb', line 108

def validate(instance, output_format: @configuration.output_format, resolve_enumerators: @configuration.resolve_enumerators, access_mode: @configuration.access_mode)
  instance_location = Location.root
  context = Context.new(instance, [], nil, (!insert_property_defaults && output_format == 'flag'), access_mode)
  result = validate_instance(deep_stringify_keys(instance), instance_location, root_keyword_location, context)
  if insert_property_defaults && result.insert_property_defaults(context, &property_default_resolver)
    result = validate_instance(deep_stringify_keys(instance), instance_location, root_keyword_location, context)
  end
  output = result.output(output_format)
  resolve_enumerators!(output) if resolve_enumerators
  output
end

#validate_instance(instance, instance_location, keyword_location, context) ⇒ Object

[View source]

132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/json_schemer/schema.rb', line 132

def validate_instance(instance, instance_location, keyword_location, context)
  context.dynamic_scope.push(self)
  original_adjacent_results = context.adjacent_results
  adjacent_results = context.adjacent_results = {}
  short_circuit = context.short_circuit

  begin
    return result(instance, instance_location, keyword_location, false) if value == false
    return result(instance, instance_location, keyword_location, true) if value == true || value.empty?

    valid = true
    nested = []

    parsed.each do |keyword, keyword_instance|
      next unless keyword_result = keyword_instance.validate(instance, instance_location, join_location(keyword_location, keyword), context)
      valid &&= keyword_result.valid
      return result(instance, instance_location, keyword_location, false) if short_circuit && !valid
      nested << keyword_result
      adjacent_results[keyword_instance.class] = keyword_result
    end

    if root.custom_keywords.any?
      resolved_instance_location = Location.resolve(instance_location)
      root.custom_keywords.each do |custom_keyword, callable|
        if value.key?(custom_keyword)
          [*callable.call(instance, value, resolved_instance_location)].each do |custom_keyword_result|
            custom_keyword_valid = custom_keyword_result == true
            valid &&= custom_keyword_valid
            type = custom_keyword_result.is_a?(String) ? custom_keyword_result : custom_keyword
            details = { 'keyword' => custom_keyword, 'result' => custom_keyword_result }
            nested << result(instance, instance_location, keyword_location, custom_keyword_valid, :type => type, :details => details)
          end
        end
      end
    end

    result(instance, instance_location, keyword_location, valid, nested)
  ensure
    context.dynamic_scope.pop
    context.adjacent_results = original_adjacent_results
  end
end

#validate_schema(**options) ⇒ Object

[View source]

124
125
126
# File 'lib/json_schemer/schema.rb', line 124

def validate_schema(**options)
  meta_schema.validate(value, **options)
end