Class: JSONSchemer::Schema

Inherits:
Object
  • Object
show all
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.



49
50
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
# File 'lib/json_schemer/schema.rb', line 49

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
  @configuration = configuration
  @base_uri = base_uri
  @meta_schema = meta_schema
  @vocabulary = vocabulary
  @format = format
  @formats = formats
  @content_encodings = content_encodings
  @content_media_types = content_media_types
  @custom_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
  @original_ref_resolver = ref_resolver
  @original_regexp_resolver = regexp_resolver
  @output_format = output_format
  @resolve_enumerators = resolve_enumerators
  @access_mode = access_mode
  @parsed = parse
end

Instance Attribute Details

#after_property_validationObject (readonly)

Returns the value of attribute after_property_validation.



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

def after_property_validation
  @after_property_validation
end

#base_uriObject

Returns the value of attribute base_uri.



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

def base_uri
  @base_uri
end

#before_property_validationObject (readonly)

Returns the value of attribute before_property_validation.



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

def before_property_validation
  @before_property_validation
end

#configurationObject (readonly)

Returns the value of attribute configuration.



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

def configuration
  @configuration
end

#content_encodingsObject (readonly)

Returns the value of attribute content_encodings.



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

def content_encodings
  @content_encodings
end

#content_media_typesObject (readonly)

Returns the value of attribute content_media_types.



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

def content_media_types
  @content_media_types
end

#custom_keywordsObject (readonly)

Returns the value of attribute custom_keywords.



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

def custom_keywords
  @custom_keywords
end

#formatObject (readonly)

Returns the value of attribute format.



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

def format
  @format
end

#formatsObject (readonly)

Returns the value of attribute formats.



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

def formats
  @formats
end

#insert_property_defaultsObject (readonly)

Returns the value of attribute insert_property_defaults.



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

def insert_property_defaults
  @insert_property_defaults
end

#keyword_orderObject

Returns the value of attribute keyword_order.



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

def keyword_order
  @keyword_order
end

#keywordsObject

Returns the value of attribute keywords.



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

def keywords
  @keywords
end

#meta_schemaObject

Returns the value of attribute meta_schema.



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

def meta_schema
  @meta_schema
end

#parentObject (readonly)

Returns the value of attribute parent.



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

def parent
  @parent
end

#parsedObject (readonly)

Returns the value of attribute parsed.



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

def parsed
  @parsed
end

#rootObject (readonly)

Returns the value of attribute root.



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

def root
  @root
end

#valueObject (readonly)

Returns the value of attribute value.



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

def value
  @value
end

#vocabularyObject (readonly)

Returns the value of attribute vocabulary.



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

def vocabulary
  @vocabulary
end

Instance Method Details

#absolute_keyword_locationObject



267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/json_schemer/schema.rb', line 267

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



218
219
220
221
222
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
# File 'lib/json_schemer/schema.rb', line 218

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



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

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

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



334
335
336
337
338
339
340
# File 'lib/json_schemer/schema.rb', line 334

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



290
291
292
# File 'lib/json_schemer/schema.rb', line 290

def error_key
  '^'
end

#fetch(key) ⇒ Object



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

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

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



306
307
308
309
310
311
312
# File 'lib/json_schemer/schema.rb', line 306

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



314
315
316
317
318
319
320
# File 'lib/json_schemer/schema.rb', line 314

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



298
299
300
301
302
303
304
# File 'lib/json_schemer/schema.rb', line 298

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



322
323
324
# File 'lib/json_schemer/schema.rb', line 322

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

#inspectObject



357
358
359
# File 'lib/json_schemer/schema.rb', line 357

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

#ref(value) ⇒ Object



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

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

#ref_resolverObject



342
343
344
# File 'lib/json_schemer/schema.rb', line 342

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

#regexp_resolverObject



346
347
348
349
350
351
352
353
354
355
# File 'lib/json_schemer/schema.rb', line 346

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

#resolve_ref(uri) ⇒ Object

Raises:



170
171
172
173
174
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
# File 'lib/json_schemer/schema.rb', line 170

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



214
215
216
# File 'lib/json_schemer/schema.rb', line 214

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

#resourcesObject



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

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

#schema_pointerObject



280
281
282
283
284
285
286
287
288
# File 'lib/json_schemer/schema.rb', line 280

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)


99
100
101
# File 'lib/json_schemer/schema.rb', line 99

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

#valid_schema?(**options) ⇒ Boolean

Returns:

  • (Boolean)


115
116
117
# File 'lib/json_schemer/schema.rb', line 115

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

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



103
104
105
106
107
108
109
110
111
112
113
# File 'lib/json_schemer/schema.rb', line 103

def validate(instance, output_format: @output_format, resolve_enumerators: @resolve_enumerators, access_mode: @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



127
128
129
130
131
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
# File 'lib/json_schemer/schema.rb', line 127

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



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

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