Module: JSONAPI::Exceptions::DocumentExceptions

Defined in:
lib/easy/jsonapi/exceptions.rb,
lib/easy/jsonapi/exceptions/document_exceptions.rb

Overview

Validates that the request or response document complies with the JSONAPI specification

Defined Under Namespace

Classes: InvalidDocument

Constant Summary collapse

REQUIRED_TOP_LEVEL_KEYS =

A jsonapi document MUST contain at least one of the following top-level members

i[data errors meta].freeze
i[self related first prev next last].freeze
i[first prev next last].freeze
i[href meta].freeze
RESOURCE_KEYS =

A resource object MUST contain at least id and type (unless a post resource) In addition, a resource object MAY contain these top-level members.

i[type id attributes relationships links meta].freeze
RELATIONSHIP_KEYS =

A relationships object MUST contain one of the following:

i[data links meta].freeze
i[self related].freeze
RESOURCE_IDENTIFIER_KEYS =

Every resource object MUST contain an id member and a type member.

i[type id].freeze

Class Method Summary collapse

Class Method Details

.any_additional_includes?(possible_includes, actual_includes) ⇒ Boolean



568
569
570
571
572
573
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 568

def any_additional_includes?(possible_includes, actual_includes)
  actual_includes.each do |res|
    return false unless possible_includes.key? res_id_to_sym(res[:type], res[:id])
  end
  true
end

.check_attributes(attributes) ⇒ Object



199
200
201
202
203
204
205
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 199

def check_attributes(attributes)
  ensure!(attributes.is_a?(Hash),
          'The value of the attributes key MUST be an object')
  # Attribute members can contain any json value (verified using OJ JSON parser), but
  #   must not contain any attribute or links member -- see #check_full_linkage for this check
  # Member names checked separately.
end

.check_compliance(document, config_manager = nil, opts = {}) ⇒ Object

Checks a request document against the JSON:API spec to see if it complies

Raises:

  • InvalidDocument if any part of the spec is not observed



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 60

def self.check_compliance(document, config_manager = nil, opts = {})
  document = JSONAPI::Parser::JSONParser.parse(document) if document.is_a? String
  ensure!(!document.nil?, 'A document cannot be nil')
  
  check_essentials(document, opts[:http_method])
  check_members(document, opts[:http_method], opts[:path], opts[:sparse_fieldsets])
  check_for_matching_types(document, opts[:http_method], opts[:path])
  check_member_names(document)
  
  usr_opts = { http_method: opts[:http_method], path: opts[:path] }
  err = JSONAPI::Exceptions::UserDefinedExceptions.check_user_document_requirements(document, config_manager, usr_opts)
  raise err unless err.nil?
  
  nil
end

.check_data(data, http_method, path) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 145

def check_data(data, http_method, path)
  ensure!(data.is_a?(Hash) || http_method.nil? || clearing_relationship_link?(data, http_method, path),
          'The request MUST include a single resource object as primary data, ' \
          'unless it is a PATCH request clearing a relationship using a relationship link')
  case data
  when Hash
    check_resource(data, http_method)
  when Array
    data.each { |res| check_resource(res, http_method) }
  else
    ensure!(data.nil?,
            'Primary data must be either nil, an object or an array')
  end
end

.check_error(error) ⇒ Object

Raises:

  • InvalidDocument if any part of the spec is not observed



385
386
387
388
389
390
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 385

def check_error(error)
  ensure!(error.is_a?(Hash),
          'Error objects MUST be objects')
  check_links(error[:links]) if error.key? :links
  check_links(error[:meta]) if error.key? :meta
end

.check_errors(errors) ⇒ Object



377
378
379
380
381
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 377

def check_errors(errors)
  ensure!(errors.is_a?(Array),
          'Top level errors member MUST be an array')
  errors.each { |error| check_error(error) }
end

.check_essentials(document, http_method) ⇒ Object

Checks the essentials of a jsonapi document. It is used by #check_compliance and JSONAPI::Document's #initialize method



82
83
84
85
86
87
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 82

def check_essentials(document, http_method)
  ensure!(document.is_a?(Hash),
          'A JSON object MUST be at the root of every JSON API request ' \
          'and response containing data')  
  check_top_level(document, http_method)
end

.check_for_matching_types(document, http_method, path) ⇒ Object

Raises a 409 error if the endpoint type does not match the data type on a post request



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 442

def check_for_matching_types(document, http_method, path)
  return unless http_method
  return unless path
  
  return unless JSONAPI::Utility.all_hash_path?(document, i[data type])
  
  res_type = document[:data][:type]
  case http_method
  when 'POST'
    path_type = path.split('/')[-1]
    check_post_type(path_type, res_type)
  when 'PATCH'
    temp = path.split('/')
    path_type = temp[-2]
    path_id = temp[-1]
    res_id = document.dig(:data, :id)
    check_patch_type(path_type, res_type, path_id, res_id)
  end
end

.check_full_linkage(document, http_method) ⇒ Object

Checking if document is fully linked



397
398
399
400
401
402
403
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 397

def check_full_linkage(document, http_method)
  return if http_method
  
  ensure!(full_linkage?(document),
          'Compound documents require “full linkage”, meaning that every included resource MUST be ' \
          'identified by at least one resource identifier object in the same document.')
end

.check_included(included) ⇒ Object



272
273
274
275
276
277
278
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 272

def check_included(included)
  ensure!(included.is_a?(Array),
          'The top level included member MUST be represented as an array of resource objects')
  
  check_included_resources(included)
  # Full linkage check is in #check_members
end

.check_included_resources(included) ⇒ Object

Check each included resource for compliance and make sure each type/id pair is unique



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 283

def check_included_resources(included)
  no_duplicate_type_and_id_pairs = true
  set = {}
  included.each do |res|
    check_resource(res)
    unless unique_pair?(set, res)
      no_duplicate_type_and_id_pairs = false
      break
    end
  end
  ensure!(no_duplicate_type_and_id_pairs,
          'A compound document MUST NOT include more ' \
          'than one resource object for each type and id pair.')
end

.check_individual_members(document, http_method, path) ⇒ Object

Checks individual members of the jsonapi document for errors



130
131
132
133
134
135
136
137
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 130

def check_individual_members(document, http_method, path)
  check_data(document[:data], http_method, path) if document.key? :data
  check_included(document[:included]) if document.key? :included
  check_meta(document[:meta]) if document.key? :meta
  check_errors(document[:errors]) if document.key? :errors
  check_jsonapi(document[:jsonapi]) if document.key? :jsonapi
  check_links(document[:links]) if document.key? :links
end

.check_jsonapi(jsonapi) ⇒ Object

Raises:

  • InvalidDocument if any part of the spec is not observed



364
365
366
367
368
369
370
371
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 364

def check_jsonapi(jsonapi)
  ensure!(jsonapi.is_a?(Hash), 'A JSONAPI object MUST be an object')
  if jsonapi.key?(:version)
    ensure!(jsonapi[:version].is_a?(String),
            "The value of JSONAPI's version member MUST be a string")
  end
  check_meta(jsonapi[:meta]) if jsonapi.key?(:meta)
end

Raises:

  • InvalidDocument if any part of the spec is not observed



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 342

def check_link(link)
  # A link MUST be either a string URL or an object with href / meta
  case link
  when String
    # Do nothing
  when Hash
    ensure!((link.keys - LINK_KEYS).empty?,
            'If the link is an object, it can contain the members href or meta')
    ensure!(link[:href].nil? || link[:href].instance_of?(String),
            'The member href MUST be a string')
    ensure!(link[:meta].nil? || link[:meta].instance_of?(Hash),
            'The value of each meta member MUST be an object')
  else
    ensure!(false,
            'A link MUST be represented as either a string or an object')
  end
end

Raises:

  • InvalidDocument if any part of the spec is not observed



334
335
336
337
338
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 334

def check_links(links)
  ensure!(links.is_a?(Hash), 'A links object MUST be an object')
  links.each_value { |link| check_link(link) }
  nil
end

.check_member_names(obj) ⇒ Object

Checks all the member names in a document recursively and raises an error saying which member did not observe the jsonapi member name rules and which rule



413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 413

def check_member_names(obj)
  case obj
  when Hash
    obj.each do |k, v| 
      check_name(k)
      check_member_names(v)
    end
  when Array
    obj.each { |hsh| check_member_names(hsh) }
  end
  nil
end

.check_members(document, http_method, path, sparse_fieldsets) ⇒ Object

Checks if any errors exist in the jsonapi document members



122
123
124
125
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 122

def check_members(document, http_method, path, sparse_fieldsets)
  check_individual_members(document, http_method, path)
  check_full_linkage(document, http_method) unless sparse_fieldsets && http_method.nil?
end

.check_meta(meta) ⇒ Object

Raises:

  • InvalidDocument if any part of the spec is not observed



315
316
317
318
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 315

def check_meta(meta)
  ensure!(meta.is_a?(Hash), 'A meta object MUST be an object')
  # Any members may be specified in a meta obj (all members will be valid json bc string is parsed by oj)
end

.check_name(name) ⇒ Object

Raises:

  • InvalidDocument if any part of the spec is not observed



428
429
430
431
432
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 428

def check_name(name)
  msg = JSONAPI::Exceptions::NamingExceptions.check_member_constraints(name)
  return if msg.nil?
  raise InvalidDocument, "The member named '#{name}' raised: #{msg}"
end

.check_patch_type(path_type, res_type, path_id, res_id) ⇒ Object

Raise 409 unless path resource type and id == endpoint resource type and id

Raises:



479
480
481
482
483
484
485
486
487
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 479

def check_patch_type(path_type, res_type, path_id, res_id)
  check = 
    path_type.to_s.downcase.gsub(/-/, '_') == res_type.to_s.downcase.gsub(/-/, '_') &&
    path_id.to_s.downcase.gsub(/-/, '_') == res_id.to_s.downcase.gsub(/-/, '_')
  ensure!(check,
          "When processing a PATCH request, the resource object's type and id MUST " \
          "match the server's endpoint",
          status_code: 409)
end

.check_post_type(path_type, res_type) ⇒ Object

Raise 409 unless post resource type == endpoint resource type

Raises:



466
467
468
469
470
471
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 466

def check_post_type(path_type, res_type)
  ensure!(path_type.to_s.downcase.gsub(/-/, '_') == res_type.to_s.downcase.gsub(/-/, '_'),
          "When processing a POST request, the resource object's type MUST " \
          'be amoung the type(s) that constitute the collection represented by the endpoint',
          status_code: 409)
end

.check_relationship(rel) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 217

def check_relationship(rel)
  ensure!(rel.is_a?(Hash), 'Each relationships member MUST be a object')
  ensure!(!(rel.keys & RELATIONSHIP_KEYS).empty?,
          'A relationship object MUST contain at least one of ' \
          "#{RELATIONSHIP_KEYS}")
  
  # If relationship is a To-Many relationship, the links member may also have pagination links
  #   that traverse the pagination data
  check_relationship_links(rel[:links]) if rel.key? :links
  check_relationship_data(rel[:data]) if rel.key? :data
  check_meta(rel[:meta]) if rel.key? :meta
end

.check_relationship_data(data) ⇒ Object



243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 243

def check_relationship_data(data)
  case data
  when Hash
    check_resource_identifier(data)
  when Array
    data.each { |res_id| check_resource_identifier(res_id) }
  when nil
    # Do nothing
  else
    ensure!(false, 'Resource linkage (relationship data) MUST be either nil, an object or an array')
  end
end

Raise if links don't contain at least one of the TO_ONE_RELATIONSHIP_LINK_KEYS TODO: If a pagination links are present, they MUST paginate the relationships not the related resource data



234
235
236
237
238
239
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 234

def check_relationship_links(links)
  ensure!(!(links.keys & TO_ONE_RELATIONSHIP_LINK_KEYS).empty?,
          'A relationship link MUST contain at least one of '\
          "#{TO_ONE_RELATIONSHIP_LINK_KEYS}")
  check_links(links)
end

.check_relationships(rels) ⇒ Object



209
210
211
212
213
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 209

def check_relationships(rels)
  ensure!(rels.is_a?(Hash),
          'The value of the relationships key MUST be an object')
  rels.each_value { |rel| check_relationship(rel) }
end

.check_resource(resource, http_method = nil) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 163

def check_resource(resource, http_method = nil)
  if http_method == 'POST'
    ensure!(resource[:type],
            'The resource object (for a post request) MUST contain at least a type member')
  else
    ensure!((resource[:type] && resource[:id]),
            'Every resource object MUST contain an id member and a type member')
  end
  ensure!(resource[:type].instance_of?(String),
          'The value of the resource type member MUST be string')
  if resource[:id]
    ensure!(resource[:id].instance_of?(String),
            'The value of the resource id member MUST be string')
  end
  # Check for sharing a common namespace is in #check_resource_members
  ensure!(JSONAPI::Exceptions::NamingExceptions.check_member_constraints(resource[:type]).nil?,
          'The values of type members MUST adhere to the same constraints as member names')
  
  check_resource_members(resource)      
end

.check_resource_identifier(res_id) ⇒ Object



257
258
259
260
261
262
263
264
265
266
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 257

def check_resource_identifier(res_id)
  ensure!(res_id.is_a?(Hash),
          'A resource identifier object MUST be an object')
  ensure!((res_id.keys & RESOURCE_IDENTIFIER_KEYS) == RESOURCE_IDENTIFIER_KEYS,
          'A resource identifier object MUST contain ' \
          "#{RESOURCE_IDENTIFIER_KEYS} members")
  ensure!(res_id[:id].is_a?(String), 'The resource identifier id member must be a string')
  ensure!(res_id[:type].is_a?(String), 'The resource identifier type member must be a string')
  check_meta(res_id[:meta]) if res_id.key? :meta
end

.check_resource_members(resource) ⇒ Object

Checks whether the resource members conform to the spec



187
188
189
190
191
192
193
194
195
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 187

def check_resource_members(resource)
  check_attributes(resource[:attributes]) if resource.key? :attributes
  check_relationships(resource[:relationships]) if resource.key? :relationships
  check_meta(resource[:meta]) if resource.key? :meta
  check_links(resource[:links]) if resource.key? :links
  ensure!(shares_common_namespace?(resource[:attributes], resource[:relationships]),
          'Fields for a resource object MUST share a common namespace with each ' \
          'other and with type and id')
end

.check_top_level(document, http_method) ⇒ Object

Checks if there are any errors in the top level hash

Raises:

  • InvalidDocument if any part of the spec is not observed



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 96

def check_top_level(document, http_method)
  ensure!(!(document.keys & REQUIRED_TOP_LEVEL_KEYS).empty?, 
          'A document MUST contain at least one of the following ' \
          "top-level members: #{REQUIRED_TOP_LEVEL_KEYS}")

  if document.key? :data
    ensure!(!document.key?(:errors),
            'The members data and errors MUST NOT coexist in the same document')
  else
    ensure!(!document.key?(:included),
            'If a document does not contain a top-level data key, the included ' \
            'member MUST NOT be present either')
    ensure!(http_method.nil?,
            'The request MUST include a single resource object as primary data, ' \
            'unless it is a PATCH request clearing a relationship using a relationship link')
  end
end

.clearing_relationship_link?(data, http_method, path) ⇒ Boolean

TODO: Write tests for clearing_relationship_link



504
505
506
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 504

def clearing_relationship_link?(data, http_method, path)
  http_method == 'PATCH' && data == [] && relationship_link?(path)
end

.contains_type_or_id_member?(hash) ⇒ Boolean



528
529
530
531
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 528

def contains_type_or_id_member?(hash)
  return false unless hash
  hash.key?(:id) || hash.key?(:type)
end

.ensure!(condition, error_message, status_code: 400) ⇒ Object

Helper function to raise InvalidDocument errors

Raises:

  • InvalidDocument



497
498
499
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 497

def ensure!(condition, error_message, status_code: 400)
  raise InvalidDocument.new(status_code), error_message unless condition
end

.full_linkage?(document) ⇒ Boolean



544
545
546
547
548
549
550
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 544

def full_linkage?(document)
  return true unless document[:included] 
  # ^ Checked earlier to make sure included only exists w data
  
  possible_includes = get_possible_includes(document)
  any_additional_includes?(possible_includes, document[:included])
end

.get_possible_includes(document) ⇒ Hash

Get a collection of all possible includes Need to check relationships on primary resource(s) and also relationships on the included resource(s)



557
558
559
560
561
562
563
564
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 557

def get_possible_includes(document)
  possible_includes = {}
  primary_data = document[:data]
  include_arr = document[:included]
  populate_w_primary_data(possible_includes, primary_data)
  populate_w_include_mem(possible_includes, include_arr)
  possible_includes
end

.keys_intersection_empty?(arr1, arr2) ⇒ Boolean

Checks to see if two hashes share any key members names



536
537
538
539
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 536

def keys_intersection_empty?(arr1, arr2)
  return true unless arr1 && arr2
  arr1.keys & arr2.keys == []
end

.populate_w_include_mem(possible_includes, include_arr) ⇒ Object



589
590
591
592
593
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 589

def populate_w_include_mem(possible_includes, include_arr)
  include_arr.each do |res|
    populate_w_res_rels(possible_includes, res)
  end
end

.populate_w_primary_data(possible_includes, primary_data) ⇒ Object



577
578
579
580
581
582
583
584
585
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 577

def populate_w_primary_data(possible_includes, primary_data)
  if primary_data.is_a? Array
    primary_data.each do |res|
      populate_w_res_rels(possible_includes, res)
    end
  else
    populate_w_res_rels(possible_includes, primary_data)
  end
end

.populate_w_res_rels(possible_includes, resource) ⇒ Object



597
598
599
600
601
602
603
604
605
606
607
608
609
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 597

def populate_w_res_rels(possible_includes, resource)
  return unless resource[:relationships]
  resource[:relationships].each_value do |rel|
    res_id = rel[:data]
    next unless res_id

    if res_id.is_a? Array
      res_id.each { |id| possible_includes[res_id_to_sym(id[:type], id[:id])] = true }
    else
      possible_includes[res_id_to_sym(res_id[:type], res_id[:id])] = true
    end
  end
end

.relationship_link?(path) ⇒ Boolean

Does the path length and values indicate that it is a relationsip link



510
511
512
513
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 510

def relationship_link?(path)
  path_arr = path.split('/')
  path_arr[-2] == 'relationships' && path_arr.length >= 4
end

.res_id_to_sym(type, id) ⇒ Object

Creates a hash key using type and id



614
615
616
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 614

def res_id_to_sym(type, id)
  "#{type}|#{id}".to_sym
end

.shares_common_namespace?(attributes, relationships) ⇒ Boolean

Checks whether a resource's fields share a common namespace



520
521
522
523
524
525
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 520

def shares_common_namespace?(attributes, relationships)
  true && \
    !contains_type_or_id_member?(attributes) && \
    !contains_type_or_id_member?(relationships) && \
    keys_intersection_empty?(attributes, relationships)
end

.unique_pair?(set, res) ⇒ TrueClass | FalseClass



302
303
304
305
306
307
308
309
# File 'lib/easy/jsonapi/exceptions/document_exceptions.rb', line 302

def unique_pair?(set, res)
  pair = "#{res[:type]}|#{res[:id]}"
  if set.key?(pair)
    return false
  end
  set[pair] = true
  true
end