module JSON::Api::Vanilla

Constants

VERSION

Public Class Methods

add_accessor(klass, name) click to toggle source
# File lib/json-api-vanilla/parser.rb, line 158
def self.add_accessor(klass, name)
  ruby_name = ruby_ident_name(name)
  if !klass.method_defined?(ruby_name)
    klass.send(:attr_accessor, ruby_name)
  end
end
build(hash) click to toggle source

Convert a ruby hash JSON API representation to vanilla Ruby objects. Similar to .parse but takes hash as a parameter.

Example:

>> hash = { errors: [{ source: { pointer: "" }, detail:  "Missing `data` Member at document's top level." }]}
>> doc = JSON::Api::Vanilla.build(hash)
>> doc.errors.first["detail"]
=> "Missing `data` Member at document's top level."

@param hash [Hash] parsed JSON API payload. @return [JSON::Api::Vanilla::Document] a wrapper for the objects.

# File lib/json-api-vanilla/parser.rb, line 34
def self.build(hash)
  naive_validate(hash)
  # Object storage.
  container = Module.new
  superclass = Class.new

  data_hash = hash['data']
  data_hash_array = if data_hash.is_a?(Array)
    data_hash
  else
    [data_hash].compact
  end
  obj_hashes = (hash['included'] || []) + data_hash_array
  errors = hash['errors']

  # Create all the objects.
  # Store them in the `objects` hash from [type, id] to the object.
  objects = {}
  links = {}  # Object links.
  rel_links = {}  # Relationship links.
  meta = {}  # Meta information.
  # Map from objects to map from keys to values, for use when two keys are
  # converted to the same ruby method identifier.
  original_keys = {}

  obj_hashes.each do |o_hash|
    klass = prepare_class(o_hash, superclass, container)
    obj = prepare_object(o_hash, klass, original_keys)

    if o_hash['links']
      links[obj] = o_hash['links']
    end

    objects[[obj.type, obj.id]] = obj
  end

  # Now that all objects have been created, we can link everything together.
  obj_hashes.each do |o_hash|
    obj = objects[[o_hash['type'], o_hash['id']]]
    if o_hash['relationships']
      o_hash['relationships'].each do |key, value|
        if value['data']
          data = value['data']
          if data.is_a?(Array)
            # One-to-many relationship.
            ref = data.map do |ref_hash|
              _ref = objects[[ref_hash['type'], ref_hash['id']]]

              if _ref.nil?
                klass = prepare_class(ref_hash, superclass, container)
                _ref = prepare_object(ref_hash, klass)
              end

              _ref
            end
          else
            ref = objects[[data['type'], data['id']]]

            if ref.nil?
              klass = prepare_class(data, superclass, container)
              ref = prepare_object(data, klass)
            end
          end
        end

        ref = ref || Object.new
        set_key(obj, key, ref, original_keys)

        rel_links[ref] = value['links']
        meta[ref] = value['meta']
      end
    end
  end

  # Create the main object.
  data = if data_hash.is_a?(Array)
    data_hash.map do |o_hash|
      objects[[o_hash['type'], o_hash['id']]]
    end
  elsif data_hash
    objects[[data_hash['type'], data_hash['id']]]
  end
  links[data] = hash['links']
  meta[data] = hash['meta']
  Document.new(data, links: links, rel_links: rel_links, meta: meta,
               objects: objects, keys: original_keys, errors: errors,
               container: container, superclass: superclass)
end
generate_object(ruby_name, superclass, container) click to toggle source
# File lib/json-api-vanilla/parser.rb, line 152
def self.generate_object(ruby_name, superclass, container)
  klass = Class.new(superclass)
  container.const_set(ruby_name, klass)
  klass
end
naive_validate(hash) click to toggle source

Naïvely validate the top level document structure @param hash [Hash] json:api document as a hash @raise [InvalidRootStructure] raised if the document doesn't have data, errors nor meta objects at its root.

# File lib/json-api-vanilla/parser.rb, line 193
def self.naive_validate(hash)
  root_keys = %i(data errors meta)
  present_structures = root_keys & hash.keys.map(&:to_sym)
  if present_structures.empty?
    raise InvalidRootStructure.new("JSON:API document must contain at least one of these objects: #{root_keys.join(', ')}")
  end
end
parse(json) click to toggle source

Convert a String JSON API payload to vanilla Ruby objects.

Example:

>> json = IO.read("articles.json")  # From http://jsonapi.org
>> doc = JSON::Api::Vanilla.parse(json)
>> doc.data[0].comments[1].author.last_name
=> "Gebhardt"

@param json [String] the JSON API payload. @return [JSON::Api::Vanilla::Document] a wrapper for the objects.

# File lib/json-api-vanilla/parser.rb, line 18
def self.parse(json)
  hash = JSON.parse(json)
  build(hash)
end
prepare_class(hash, superclass, container) click to toggle source
# File lib/json-api-vanilla/parser.rb, line 123
def self.prepare_class(hash, superclass, container)
  name = ruby_class_name(hash['type']).to_sym
  if container.constants.include?(name)
    klass = container.const_get(name)
  else
    klass = generate_object(name, superclass, container)
  end
  add_accessor(klass, 'id')
  add_accessor(klass, 'type')
  attr_keys = hash['attributes'] ? hash['attributes'].keys : []
  rel_keys = hash['relationships'] ? hash['relationships'].keys : []
  (attr_keys + rel_keys).each do |key|
    add_accessor(klass, key)
  end
  klass
end
prepare_object(hash, klass, original_keys = {}) click to toggle source
# File lib/json-api-vanilla/parser.rb, line 140
def self.prepare_object(hash, klass, original_keys = {})
  (klass.new).tap do |obj|
    obj.type = hash['type']
    obj.id = hash['id']
    if hash['attributes']
      hash['attributes'].each do |key, value|
        set_key(obj, key, value, original_keys)
      end
    end
  end
end
ruby_class_name(name) click to toggle source

Convert a name String to a String that is a valid Ruby class name.

# File lib/json-api-vanilla/parser.rb, line 176
def self.ruby_class_name(name)
  name.scan(/[a-zA-Z_][a-zA-Z_0-9]+/).map(&:capitalize).join
end
ruby_ident_name(name) click to toggle source

Convert a name String to a String that is a valid snake-case Ruby identifier.

# File lib/json-api-vanilla/parser.rb, line 182
def self.ruby_ident_name(name)
  name.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
     .gsub(/([a-z\d])([A-Z])/,'\1_\2')
     .tr("-", "_")
     .downcase
end
set_key(obj, key, value, original_keys) click to toggle source

Set a value to an object's key through its setter. original_keys is a map from objects to a map from String keys to their values.

# File lib/json-api-vanilla/parser.rb, line 168
def self.set_key(obj, key, value, original_keys)
  ruby_key = ruby_ident_name(key)
  obj.send("#{ruby_key}=", value)
  original_keys[obj] ||= {}
  original_keys[obj][key] = value
end