module JSONAPI::Exceptions::DocumentExceptions

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

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

Constants

Top level links objects MAY contain the following members

Each member of a links object is a link. A link MUST be represented as either

Pagination member names in a links object

RELATIONSHIP_KEYS

A relationships object MUST contain one of the following:

REQUIRED_TOP_LEVEL_KEYS

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

RESOURCE_IDENTIFIER_KEYS

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

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.

A relationship that is to-one or to-many must conatin at least one of the following.

A to-many relationship can also contain the addition 'pagination' key.

Public Class Methods

any_additional_includes?(possible_includes, actual_includes) click to toggle source

@param possible_includes [Hash] The collection of possible includes @param actual_includes [Hash] The included top level object

# 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) click to toggle source

@param attributes [Hash] The attributes for resource @raise (see check_compliance)

# 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 = {}) click to toggle source

Checks a request document against the JSON:API spec to see if it complies @param document [String | Hash] The jsonapi document included with the http request @param opts [Hash] Includes path, http_method, sparse_fieldsets @raise InvalidDocument if any part of the spec is not observed

# 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) click to toggle source

@param data [Hash | Array<Hash>] A resource or array or resources @param (see check_compliance) @param (see check_compliance) @raise (see check_compliance)

# 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) click to toggle source

@param error [Hash] The individual error object @raise (see check_compliance)

# 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) click to toggle source

@param errors [Array] The array of errors contained in the jsonapi document @raise (see check_compliance)

# 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) click to toggle source

Checks the essentials of a jsonapi document. It is

used by #check_compliance and JSONAPI::Document's #initialize method

@param (see check_compliance)

# 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) click to toggle source

Raises a 409 error if the endpoint type does not match the data type on a post request @param document (see check_compliance) @param http_method [String] The request request method @param path [String] The request path

# 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) click to toggle source

Checking if document is fully linked @param document [Hash] The jsonapi document @param http_method (see check_for_matching_types)

# 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) click to toggle source

@param included [Array] The array of included resources @raise (see check_compliance)

# 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) click to toggle source

Check each included resource for compliance and make sure each type/id pair is unique @param (see check_included) @raise (see check_compliance)

# 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) click to toggle source

Checks individual members of the jsonapi document for errors @param (see check_compliance) @raise (see check_complaince)

# 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) click to toggle source

@param jsonapi [Hash] The top level jsonapi object @raise (see check_compliance)

# 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
check_member_names(obj) click to toggle source

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

@param obj The entire request document or part of the request document. @raise (see check_compliance)

# 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) click to toggle source

Checks if any errors exist in the jsonapi document members @param http_method [String] The http verb @param sparse_fieldsets [TrueClass | FalseClass | Nilclass] @raise (see check_compliance)

# 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) click to toggle source

@param meta [Hash] The meta object @raise (see check_compliance)

# 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) click to toggle source

@param name The invidual member's name that is being checked @raise (see check_compliance)

# 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) click to toggle source

Raise 409 unless path resource type and id == endpoint resource type and id @param path_type [String] The resource type taken from the request path @param res_type [String] The resource type taken from the request body @param path_id [String] The resource id taken from the path @param res_id [String] The resource id taken from the request body @raise [JSONAPI::Exceptions::DocumentExceptions::InvalidDocument]

# 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) click to toggle source

Raise 409 unless post resource type == endpoint resource type @param path_type [String] The resource type taken from the request path @param res_type [String] The resource type taken from the request body @raise [JSONAPI::Exceptions::DocumentExceptions::InvalidDocument]

# 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) click to toggle source

@param rel [Hash] A relationship object @raise (see check_compliance)

# 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) click to toggle source

@param data [Hash] A resources relationships relationship data @raise (see check_compliance)

# 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
check_relationships(rels) click to toggle source

@param rels [Hash] The relationships obj for resource @raise (see check_compliance)

# 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) click to toggle source

@param resource [Hash] The jsonapi resource object @param (see check_compliance) @raise (see check_compliance)

# 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) click to toggle source

@param res_id [Hash] A resource identifier object

# 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) click to toggle source

Checks whether the resource members conform to the spec @param (see check_resource) @raise (see check_compliance)

# 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) click to toggle source

Checks if there are any errors in the top level hash @param (see *check_compliance) @raise (see check_compliance)

# 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
contains_type_or_id_member?(hash) click to toggle source

@param hash [Hash] The hash to check

# 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) click to toggle source

Helper function to raise InvalidDocument errors @param condition The condition to evaluate @param error_message [String] The message to raise InvalidDocument with @raise InvalidDocument

# 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) click to toggle source

@param document [Hash] The jsonapi document hash

# 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) click to toggle source

Get a collection of all possible includes

Need to check relationships on primary resource(s) and also
relationships on the included resource(s)

@param (see full_linkage?) @return [Hash] Collection of possible includes

# 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) click to toggle source

Checks to see if two hashes share any key members names @param arr1 [Array<Symbol>] The first hash key array @param arr2 [Array<Symbol>] The second hash key array

# 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) click to toggle source

@param possible_includes (see any_additional_includes?) @param include_arr [Array<Hash>] The array of includes

# 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) click to toggle source

@param possible_includes (see any_additional_includes?) @param primary_data [Hash] The primary data of a document

# 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) click to toggle source

@param possible_includes (see any_additional_includes?) @param resource [Hash] The resource to check

# 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
res_id_to_sym(type, id) click to toggle source

Creates a hash key using type and id @param type [String] the resource type @param id [String] the resource id

# 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) click to toggle source

Checks whether a resource's fields share a common namespace @param attributes [Hash] A resource's attributes @param relationships [Hash] A resource's relationships

# 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) click to toggle source

@param set [Hash] Set of unique pairs so far @param res [Hash] The resource to inspect @return [TrueClass | FalseClass] Whether the resource has a unique

type and id pair
# 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