Class: JSONSchemer::Schema

Inherits:
Object
  • Object
show all
Includes:
Format::JSONPointer, 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_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 Format::JSONPointer

Format::JSONPointer::JSON_POINTER_REGEX, Format::JSONPointer::JSON_POINTER_REGEX_STRING, Format::JSONPointer::RELATIVE_JSON_POINTER_REGEX

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 Format::JSONPointer

#valid_json_pointer?, #valid_relative_json_pointer?

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, 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.



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

def initialize(
  value,
  parent = nil,
  root = self,
  keyword = nil,
  base_uri: DEFAULT_BASE_URI,
  meta_schema: nil,
  vocabulary: nil,
  format: true,
  formats: DEFAULT_FORMATS,
  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
  @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.



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

def after_property_validation
  @after_property_validation
end

#base_uriObject

Returns the value of attribute base_uri.



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

def base_uri
  @base_uri
end

#before_property_validationObject (readonly)

Returns the value of attribute before_property_validation.



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

def before_property_validation
  @before_property_validation
end

#custom_keywordsObject (readonly)

Returns the value of attribute custom_keywords.



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

def custom_keywords
  @custom_keywords
end

#formatObject (readonly)

Returns the value of attribute format.



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

def format
  @format
end

#formatsObject (readonly)

Returns the value of attribute formats.



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

def formats
  @formats
end

#insert_property_defaultsObject (readonly)

Returns the value of attribute insert_property_defaults.



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

def insert_property_defaults
  @insert_property_defaults
end

#keyword_orderObject

Returns the value of attribute keyword_order.



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

def keyword_order
  @keyword_order
end

#keywordsObject

Returns the value of attribute keywords.



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

def keywords
  @keywords
end

#meta_schemaObject

Returns the value of attribute meta_schema.



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

def meta_schema
  @meta_schema
end

#parentObject (readonly)

Returns the value of attribute parent.



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

def parent
  @parent
end

#parsedObject (readonly)

Returns the value of attribute parsed.



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

def parsed
  @parsed
end

#property_default_resolverObject (readonly)

Returns the value of attribute property_default_resolver.



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

def property_default_resolver
  @property_default_resolver
end

#rootObject (readonly)

Returns the value of attribute root.



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

def root
  @root
end

#valueObject (readonly)

Returns the value of attribute value.



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

def value
  @value
end

#vocabularyObject (readonly)

Returns the value of attribute vocabulary.



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

def vocabulary
  @vocabulary
end

Instance Method Details

#absolute_keyword_locationObject



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

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



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

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



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

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

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



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

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

#id_keywordObject



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

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

#inspectObject



311
312
313
# File 'lib/json_schemer/schema.rb', line 311

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

#ref(value) ⇒ Object



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

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

#resolve_ref(uri) ⇒ Object



161
162
163
164
165
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
# File 'lib/json_schemer/schema.rb', line 161

def resolve_ref(uri)
  pointer = ''
  if 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,
      :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|
    if obj.is_a?(UNKNOWN_KEYWORD_CLASS)
      obj.fetch_unknown!(token)
    elsif obj.parsed.is_a?(Array)
      obj.parsed.fetch(token.to_i)
    else
      obj.parsed.fetch(token)
    end
  rescue IndexError
    raise InvalidRefPointer, pointer
  end

  schema = schema.unknown_schema! unless schema.is_a?(Schema)

  schema
end

#resolve_regexp(pattern) ⇒ Object



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

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

#resourcesObject



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

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

#schema_pointerObject



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

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)


91
92
93
# File 'lib/json_schemer/schema.rb', line 91

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

#valid_schema?Boolean

Returns:

  • (Boolean)


107
108
109
# File 'lib/json_schemer/schema.rb', line 107

def valid_schema?
  meta_schema.valid?(value)
end

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



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/json_schemer/schema.rb', line 95

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



119
120
121
122
123
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
# File 'lib/json_schemer/schema.rb', line 119

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



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

def validate_schema
  meta_schema.validate(value)
end