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

DEFAULT_SCHEMA =
Draft202012::BASE_URI.to_s.freeze
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
DEFAULT_BASE_URI =
URI('json-schemer://schema').freeze
DEFAULT_FORMATS =
{}.freeze
DEFAULT_CONTENT_ENCODINGS =
{}.freeze
DEFAULT_CONTENT_MEDIA_TYPES =
{}.freeze
DEFAULT_KEYWORDS =
{}.freeze
DEFAULT_BEFORE_PROPERTY_VALIDATION =
[].freeze
DEFAULT_AFTER_PROPERTY_VALIDATION =
[].freeze
DEFAULT_REF_RESOLVER =
proc { |uri| raise UnknownRef, uri.to_s }
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

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, base_uri: DEFAULT_BASE_URI, meta_schema: nil, vocabulary: nil, format: true, formats: DEFAULT_FORMATS, content_encodings: DEFAULT_CONTENT_ENCODINGS, content_media_types: DEFAULT_CONTENT_MEDIA_TYPES, keywords: DEFAULT_KEYWORDS, before_property_validation: DEFAULT_BEFORE_PROPERTY_VALIDATION, after_property_validation: DEFAULT_AFTER_PROPERTY_VALIDATION, insert_property_defaults: false, property_default_resolver: DEFAULT_PROPERTY_DEFAULT_RESOLVER, ref_resolver: DEFAULT_REF_RESOLVER, regexp_resolver: 'ruby', output_format: 'classic', resolve_enumerators: false, access_mode: nil) ⇒ Schema

Returns a new instance of Schema.



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

def initialize(
  value,
  parent = nil,
  root = self,
  keyword = nil,
  base_uri: DEFAULT_BASE_URI,
  meta_schema: nil,
  vocabulary: nil,
  format: true,
  formats: DEFAULT_FORMATS,
  content_encodings: DEFAULT_CONTENT_ENCODINGS,
  content_media_types: DEFAULT_CONTENT_MEDIA_TYPES,
  keywords: DEFAULT_KEYWORDS,
  before_property_validation: DEFAULT_BEFORE_PROPERTY_VALIDATION,
  after_property_validation: DEFAULT_AFTER_PROPERTY_VALIDATION,
  insert_property_defaults: false,
  property_default_resolver: DEFAULT_PROPERTY_DEFAULT_RESOLVER,
  ref_resolver: DEFAULT_REF_RESOLVER,
  regexp_resolver: 'ruby',
  output_format: 'classic',
  resolve_enumerators: false,
  access_mode: nil
)
  @value = deep_stringify_keys(value)
  @parent = parent
  @root = root
  @keyword = keyword
  @schema = self
  @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.



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

def after_property_validation
  @after_property_validation
end

#base_uriObject

Returns the value of attribute base_uri.



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

def base_uri
  @base_uri
end

#before_property_validationObject (readonly)

Returns the value of attribute before_property_validation.



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

def before_property_validation
  @before_property_validation
end

#content_encodingsObject (readonly)

Returns the value of attribute content_encodings.



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

def content_encodings
  @content_encodings
end

#content_media_typesObject (readonly)

Returns the value of attribute content_media_types.



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

def content_media_types
  @content_media_types
end

#custom_keywordsObject (readonly)

Returns the value of attribute custom_keywords.



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

def custom_keywords
  @custom_keywords
end

#formatObject (readonly)

Returns the value of attribute format.



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

def format
  @format
end

#formatsObject (readonly)

Returns the value of attribute formats.



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

def formats
  @formats
end

#insert_property_defaultsObject (readonly)

Returns the value of attribute insert_property_defaults.



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

def insert_property_defaults
  @insert_property_defaults
end

#keyword_orderObject

Returns the value of attribute keyword_order.



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

def keyword_order
  @keyword_order
end

#keywordsObject

Returns the value of attribute keywords.



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

def keywords
  @keywords
end

#meta_schemaObject

Returns the value of attribute meta_schema.



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

def meta_schema
  @meta_schema
end

#parentObject (readonly)

Returns the value of attribute parent.



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

def parent
  @parent
end

#parsedObject (readonly)

Returns the value of attribute parsed.



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

def parsed
  @parsed
end

#property_default_resolverObject (readonly)

Returns the value of attribute property_default_resolver.



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

def property_default_resolver
  @property_default_resolver
end

#rootObject (readonly)

Returns the value of attribute root.



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

def root
  @root
end

#valueObject (readonly)

Returns the value of attribute value.



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

def value
  @value
end

#vocabularyObject (readonly)

Returns the value of attribute vocabulary.



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

def vocabulary
  @vocabulary
end

Instance Method Details

#absolute_keyword_locationObject



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

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



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
266
267
268
# File 'lib/json_schemer/schema.rb', line 221

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



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

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

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



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

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



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

def error_key
  '^'
end

#fetch(key) ⇒ Object



297
298
299
# File 'lib/json_schemer/schema.rb', line 297

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

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



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

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



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

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



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

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



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

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

#inspectObject



345
346
347
# File 'lib/json_schemer/schema.rb', line 345

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

#ref(value) ⇒ Object



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

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

#resolve_ref(uri) ⇒ Object

Raises:



166
167
168
169
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
213
214
215
# File 'lib/json_schemer/schema.rb', line 166

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),
      :base_uri => uri,
      :meta_schema => meta_schema,
      :format => format,
      :formats => formats,
      :content_encodings => content_encodings,
      :content_media_types => content_media_types,
      :keywords => custom_keywords,
      :before_property_validation => before_property_validation,
      :after_property_validation => after_property_validation,
      :property_default_resolver => property_default_resolver,
      :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



217
218
219
# File 'lib/json_schemer/schema.rb', line 217

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

#resourcesObject



333
334
335
# File 'lib/json_schemer/schema.rb', line 333

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

#schema_pointerObject



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

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)


96
97
98
# File 'lib/json_schemer/schema.rb', line 96

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

#valid_schema?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'lib/json_schemer/schema.rb', line 112

def valid_schema?
  meta_schema.valid?(value)
end

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



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/json_schemer/schema.rb', line 100

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



124
125
126
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
# File 'lib/json_schemer/schema.rb', line 124

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 custom_keywords.any?
      custom_keywords.each do |custom_keyword, callable|
        if value.key?(custom_keyword)
          [*callable.call(instance, value, 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_schemaObject



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

def validate_schema
  meta_schema.validate(value)
end