Module: JSON::Api::Vanilla

Defined in:
lib/json-api-vanilla/version.rb,
lib/json-api-vanilla/parser.rb

Defined Under Namespace

Classes: Document, InvalidRootStructure

Constant Summary collapse

VERSION =
'1.0.1'

Class Method Summary collapse

Class Method Details

.add_accessor(klass, name) ⇒ Object



140
141
142
143
144
145
# File 'lib/json-api-vanilla/parser.rb', line 140

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) ⇒ JSON::Api::Vanilla::Document

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

Parameters:

  • hash (Hash)

    parsed JSON API payload.

Returns:



34
35
36
37
38
39
40
41
42
43
44
45
46
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# 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 = klass.new
    obj.type = o_hash['type']
    obj.id = o_hash['id']
    if o_hash['attributes']
      o_hash['attributes'].each do |key, value|
        set_key(obj, key, value, original_keys)
      end
    end
    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|
    klass = container.const_get(ruby_class_name(o_hash['type']).to_sym)
    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|
              objects[[ref_hash['type'], ref_hash['id']]]
            end
          else
            ref = objects[[data['type'], data['id']]]
          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) ⇒ Object



134
135
136
137
138
# File 'lib/json-api-vanilla/parser.rb', line 134

def self.generate_object(ruby_name, superclass, container)
  klass = Class.new(superclass)
  container.const_set(ruby_name, klass)
  klass
end

.naive_validate(hash) ⇒ Object

Naïvely validate the top level document structure data, errors nor meta objects at its root.

Parameters:

  • hash (Hash)

    json:api document as a hash

Raises:



175
176
177
178
179
180
181
# File 'lib/json-api-vanilla/parser.rb', line 175

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) ⇒ JSON::Api::Vanilla::Document

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"

Parameters:

  • json (String)

    the JSON API payload.

Returns:



18
19
20
21
# 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) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/json-api-vanilla/parser.rb', line 117

def self.prepare_class(hash, superclass, container)
  name = ruby_class_name(hash['type']).to_sym
  if container.const_defined?(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

.ruby_class_name(name) ⇒ Object

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



158
159
160
# File 'lib/json-api-vanilla/parser.rb', line 158

def self.ruby_class_name(name)
  name.scan(/[a-zA-Z_][a-zA-Z_0-9]+/).map(&:capitalize).join
end

.ruby_ident_name(name) ⇒ Object

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



164
165
166
167
168
169
# File 'lib/json-api-vanilla/parser.rb', line 164

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) ⇒ Object

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.



150
151
152
153
154
155
# File 'lib/json-api-vanilla/parser.rb', line 150

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