jsonapi-object-mapper 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ef9bc7d28430b0e48b01bcc3ec63e1298209ad9d1e6daaed479ca9710d4677e
4
- data.tar.gz: d02434eb5eef6e593c6d98d39f64892d61d37c76314c7bf728d9e7a597a99c22
3
+ metadata.gz: c55b670a2ffe1d7d8d64a77103e3652694ad29076072cda48899f65228fb8232
4
+ data.tar.gz: 5e597a8fe9650ef363ce6bf6902d3b5de7b3b316a49b50107e57018191c93e57
5
5
  SHA512:
6
- metadata.gz: f7ef9678bc19aa674fb35ae2033fd3c43c1f1a7c9ea174d1fa460795728468ae558c946bfafbb8f7788705b8a77caeb1dc86a13075d33309f06eaf9b3123c0fc
7
- data.tar.gz: 800ea410fee0c3452aee0a5767805b48f1bc90b77a3abd8fa74049461f104cdf4480717cf6e54c19f5388a4fecb3ccaf033aa56a084d392816bc182e47ac581f
6
+ metadata.gz: 7b037952245ea17ff6baed1cde1fe3d17620c29d268be9df0230ece3d48f51ac1b92ca9d99b9f21ae563a3e1e5d6cde23825e8b9a7294d21f8844bc7fb3ddad6
7
+ data.tar.gz: 187087f89ba2fd1d2fa18faaca2ea9f2f2312875466b24fe4593ff933d964ccd36b5eefdb8e114f60b3afe04636730cb2f7642ec62845936cc950189ccf7a2ab
@@ -59,6 +59,9 @@ Style/ClassAndModuleChildren:
59
59
  Style/Documentation:
60
60
  Enabled: false
61
61
 
62
+ Naming/PredicateName:
63
+ Enabled: false
64
+
62
65
  Style/Lambda:
63
66
  Enabled: false
64
67
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jsonapi-object-mapper (0.6.5)
4
+ jsonapi-object-mapper (0.7.0)
5
5
  oj (~> 3.0)
6
6
 
7
7
  GEM
@@ -13,9 +13,9 @@ GEM
13
13
  diff-lcs (1.3)
14
14
  jaro_winkler (1.5.1)
15
15
  method_source (0.9.0)
16
- oj (3.6.3)
16
+ oj (3.6.4)
17
17
  parallel (1.12.1)
18
- parser (2.5.1.0)
18
+ parser (2.5.1.2)
19
19
  ast (~> 2.4.0)
20
20
  powerpack (0.1.2)
21
21
  pry (0.11.3)
@@ -39,10 +39,10 @@ GEM
39
39
  diff-lcs (>= 1.2.0, < 2.0)
40
40
  rspec-support (~> 3.7.0)
41
41
  rspec-support (3.7.1)
42
- rubocop (0.57.2)
42
+ rubocop (0.58.1)
43
43
  jaro_winkler (~> 1.5.1)
44
44
  parallel (~> 1.10)
45
- parser (>= 2.5)
45
+ parser (>= 2.5, != 2.5.1.1)
46
46
  powerpack (~> 0.1)
47
47
  rainbow (>= 2.2.2, < 4.0)
48
48
  ruby-progressbar (~> 1.7)
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Gem Version](https://badge.fury.io/rb/jsonapi-object-mapper.svg)](https://badge.fury.io/rb/jsonapi-object-mapper) [![Build Status](https://travis-ci.com/GeorgeKaraszi/jsonapi-object-mapper.svg?branch=master)](https://travis-ci.com/GeorgeKaraszi/jsonapi-object-mapper) [![Maintainability](https://api.codeclimate.com/v1/badges/6eef293ed23cf92a4c1a/maintainability)](https://codeclimate.com/github/GeorgeKaraszi/jsonapi-object-mapper/maintainability)
2
+
1
3
  # JsonapiObjectMapper
2
4
 
3
5
  Deserialize's raw or pre-hashed JsonAPI objects into plan ruby objects as well as embeds any included relational resources.
@@ -11,8 +11,8 @@ module JsonAPIObjectMapper
11
11
  attr_accessor :collection_data
12
12
 
13
13
  def initialize(parser, klass:)
14
- raise InvalidResource, "Must provide a valid resource klass" unless klass.is_a?(Class)
15
- raise InvalidParser, "Must provide a parsed document" unless parser.is_a?(JsonAPIObjectMapper::Parser::Document)
14
+ raise InvalidResource unless klass.is_a?(Class)
15
+ raise InvalidParser unless parser.is_a?(JsonAPIObjectMapper::Parser::Document)
16
16
  @errors = parser.errors
17
17
  @collection_data =
18
18
  if document_invalid?
@@ -26,6 +26,14 @@ module JsonAPIObjectMapper
26
26
  freeze
27
27
  end
28
28
 
29
+ def to_hash
30
+ @collection_data.map(&:to_hash)
31
+ end
32
+
33
+ def [](index)
34
+ @collection_data[index]
35
+ end
36
+
29
37
  def each
30
38
  @collection_data.each do |data|
31
39
  yield data
@@ -3,7 +3,8 @@
3
3
  module JsonAPIObjectMapper
4
4
  module Deserialize
5
5
  module DSL
6
- DEFAULT_BLOCK = proc { |value| value }
6
+ DEFAULT_BLOCK = proc { |value| value }
7
+ HAS_MANY_BLOCK = proc { |values| values.is_a?(Collection) ? values : Array(values) }
7
8
 
8
9
  def self.extended(klass)
9
10
  klass.include ClassMethods
@@ -28,14 +29,39 @@ module JsonAPIObjectMapper
28
29
  attributes_names.each(&method(:attribute))
29
30
  end
30
31
 
31
- def has_one(relationship_name, embed_with: nil, &block) # rubocop:disable Naming/PredicateName
32
- rel_blocks[relationship_name.to_s] = block || DEFAULT_BLOCK
33
- rel_options[relationship_name.to_s] = embed_with unless embed_with.nil?
32
+ def has_one(relationship_name, **options, &block)
33
+ rel_options_process!(relationship_name, options)
34
+ rel_has_one_blocks[relationship_name.to_s] = block || DEFAULT_BLOCK
34
35
  define_method(relationship_name.to_sym) { fetch_relationship(relationship_name) }
35
36
  end
36
- alias has_many has_one
37
37
  alias belongs_to has_one
38
38
 
39
+ def has_many(relationship_name, **options, &block)
40
+ rel_options_process!(relationship_name, options)
41
+ rel_has_many_blocks[relationship_name.to_s] = block || HAS_MANY_BLOCK
42
+ define_method(relationship_name.to_sym) { fetch_relationship(relationship_name) }
43
+ end
44
+
45
+ def kind_of_resource?(klass)
46
+ !klass.nil? && klass < Resource
47
+ end
48
+
49
+ private
50
+
51
+ def rel_options_process!(relationship_name, **options)
52
+ embed_klass = options.delete(:embed_with)
53
+ return if embed_klass.nil?
54
+
55
+ embed_klass = embed_klass.is_a?(String) ? Kernel.const_get(embed_klass) : embed_klass
56
+ if kind_of_resource?(embed_klass)
57
+ rel_options[relationship_name.to_s] = { embed_with: embed_klass }
58
+ else
59
+ raise InvalidEmbedKlass
60
+ end
61
+ rescue NameError # Rescue from `Kernel.const_get/1`
62
+ raise InvalidEmbedKlass
63
+ end
64
+
39
65
  module ClassMethods
40
66
  def initialize(*args)
41
67
  @_class_attributes = {}
@@ -66,29 +92,45 @@ module JsonAPIObjectMapper
66
92
  end
67
93
 
68
94
  def assign_attribute(key, value)
69
- block = self.class.attr_blocks[key.to_s] || DEFAULT_BLOCK
95
+ block = self.class.attr_blocks.fetch(key.to_s, DEFAULT_BLOCK)
70
96
  @_class_attributes[key.to_s] = block.call(value)
71
97
  end
72
98
 
73
- def assign_relationship(key, value)
74
- block = self.class.rel_blocks[key.to_s] || DEFAULT_BLOCK
75
- rel_embed_class = self.class.rel_options[key.to_s]
76
- rel_value = @includes.fetch(value)
77
-
78
- @_class_relationships[key.to_s] =
79
- if rel_value != value && rel_embed_class.respond_to?(:embed!)
80
- block.call(rel_embed_class.embed!(rel_value))
81
- else
82
- block.call(rel_value)
83
- end
99
+ def assign_has_one_relationship(key, value)
100
+ key = key.to_s
101
+ block = self.class.rel_has_one_blocks.fetch(key, DEFAULT_BLOCK)
102
+ rel_embed_class = self.class.rel_options.dig(key, :embed_with)
103
+ rel_value = embed!(rel_embed_class, @includes.fetch(value))
104
+ @_class_relationships[key] = block.call(rel_value)
105
+ end
106
+
107
+ def assign_has_many_relationship(key, values)
108
+ key = key.to_s
109
+ block = self.class.rel_has_many_blocks.fetch(key, HAS_MANY_BLOCK)
110
+ rel_embed_class = self.class.rel_options.dig(key, :embed_with)
111
+ rel_values = values.map { |value| @includes.fetch(value) }
112
+ @_class_relationships[key] = block.call(embed!(rel_embed_class, rel_values))
84
113
  end
85
114
 
86
- def include_attribute?(attribute_name)
115
+ def attribute_defined?(attribute_name)
87
116
  self.class.attr_blocks.key?(attribute_name)
88
117
  end
89
118
 
90
- def include_relationship?(rel_name)
91
- self.class.rel_blocks.key?(rel_name)
119
+ def has_one_defined?(rel_name)
120
+ self.class.rel_has_one_blocks.key?(rel_name)
121
+ end
122
+
123
+ def has_many_defined?(rel_name)
124
+ self.class.rel_has_many_blocks.key?(rel_name)
125
+ end
126
+
127
+ def kind_of_resource?(rel_embed_class)
128
+ self.class.kind_of_resource?(rel_embed_class)
129
+ end
130
+
131
+ def embed!(rel_embed_class, attributes)
132
+ return attributes unless self.class.kind_of_resource?(rel_embed_class)
133
+ rel_embed_class.load("data" => attributes)
92
134
  end
93
135
  end
94
136
  end
@@ -11,22 +11,28 @@ module JsonAPIObjectMapper
11
11
  extend DSL
12
12
 
13
13
  class << self
14
- attr_accessor :rel_blocks, :rel_options, :attr_blocks, :id_block, :type_block
14
+ attr_accessor :rel_has_one_blocks, :rel_has_many_blocks, :rel_options, :attr_blocks, :id_block, :type_block
15
15
  end
16
16
  instance_variable_set("@attr_blocks", {})
17
- instance_variable_set("@rel_blocks", {})
17
+ instance_variable_set("@rel_has_one_blocks", {})
18
+ instance_variable_set("@rel_has_many_blocks", {})
18
19
  instance_variable_set("@rel_options", {})
19
20
 
20
21
  def self.inherited(klass)
21
22
  super
22
23
  klass.instance_variable_set("@attr_blocks", attr_blocks.dup)
23
- klass.instance_variable_set("@rel_blocks", rel_blocks.dup)
24
+ klass.instance_variable_set("@rel_has_one_blocks", rel_has_one_blocks.dup)
25
+ klass.instance_variable_set("@rel_has_many_blocks", rel_has_many_blocks.dup)
24
26
  klass.instance_variable_set("@rel_options", rel_options.dup)
25
27
  klass.instance_variable_set("@id_block", id_block)
26
28
  klass.instance_variable_set("@type_block", type_block)
27
29
  end
28
30
 
29
31
  def self.call(document)
32
+ load(document)
33
+ end
34
+
35
+ def self.load(document)
30
36
  parser = JsonAPIObjectMapper::Parser::Document.new(document)
31
37
  if parser.document["data"].is_a?(Array) || parser.invalid?
32
38
  Collection.new(parser, klass: self)
@@ -35,14 +41,9 @@ module JsonAPIObjectMapper
35
41
  end
36
42
  end
37
43
 
38
- def self.embed!(attributes)
39
- parser = JsonAPIObjectMapper::Parser::Document.new("attributes" => attributes)
40
- new(parser)
41
- end
42
-
43
44
  def initialize(parser, document: nil)
44
45
  super()
45
- raise InvalidParser, "Must provide a parsed document" unless parser.is_a?(JsonAPIObjectMapper::Parser::Document)
46
+ raise InvalidParser unless parser.is_a?(JsonAPIObjectMapper::Parser::Document)
46
47
  @errors = parser.errors
47
48
 
48
49
  if document_valid?
@@ -75,28 +76,35 @@ module JsonAPIObjectMapper
75
76
  end
76
77
 
77
78
  def deserialize_id_type!
78
- assign_attribute("id", self.class.id_block.call(@id)) if self.class.id_block
79
- assign_attribute("type", self.class.type_block.call(@type)) if self.class.type_block
79
+ # Initialize ID and Type attribute blocks if one does not exist
80
+ self.class.id unless self.class.id_block
81
+ self.class.type unless self.class.type_block
82
+
83
+ assign_attribute("id", self.class.id_block.call(@id))
84
+ assign_attribute("type", self.class.type_block.call(@type))
80
85
  end
81
86
 
82
87
  def deserialize_attributes!
83
88
  return if @attributes.empty?
84
- @attributes.each_pair(&method(:initialize_attribute))
89
+ @attributes.each_pair(&method(:new_attribute))
85
90
  end
86
91
 
87
92
  def deserialize_relationships!
88
93
  return if @relationships.empty?
89
- @relationships.each_pair(&method(:initialize_relationship))
94
+ @relationships.each_pair(&method(:new_relationship))
90
95
  end
91
96
 
92
- def initialize_attribute(attr_name, attr_value)
93
- return unless include_attribute?(attr_name)
97
+ def new_attribute(attr_name, attr_value)
98
+ return unless attribute_defined?(attr_name)
94
99
  assign_attribute(attr_name, attr_value)
95
100
  end
96
101
 
97
- def initialize_relationship(rel_type, rel_value)
98
- return unless include_relationship?(rel_type)
99
- assign_relationship(rel_type, rel_value["data"])
102
+ def new_relationship(rel_type, rel_value)
103
+ if has_one_defined?(rel_type)
104
+ assign_has_one_relationship(rel_type, rel_value["data"])
105
+ elsif has_many_defined?(rel_type)
106
+ assign_has_many_relationship(rel_type, rel_value["data"])
107
+ end
100
108
  end
101
109
  end
102
110
  end
@@ -2,8 +2,23 @@
2
2
 
3
3
  module JsonAPIObjectMapper
4
4
  class InvalidResource < StandardError
5
+ def initialize(msg = nil)
6
+ msg ||= "The deserializer class must be an inherited `JsonAPIObjectMapper::Deserialize::Resource` klass"
7
+ super
8
+ end
5
9
  end
6
10
 
7
11
  class InvalidParser < StandardError
12
+ def initialize(msg = nil)
13
+ msg ||= "Must provide a parsed `JsonAPIObjectMapper::Parser::Document` klass-document"
14
+ super
15
+ end
16
+ end
17
+
18
+ class InvalidEmbedKlass < StandardError
19
+ def initialize(msg = nil)
20
+ msg ||= "The `embed_with: ...` option, must be a inherited `JsonAPIObjectMapper::Deserialize::Resource` klass"
21
+ super
22
+ end
8
23
  end
9
24
  end
@@ -11,7 +11,7 @@ module JsonAPIObjectMapper
11
11
  attr_accessor :document, :includes
12
12
 
13
13
  def initialize(document)
14
- @document = document.is_a?(Hash) ? document : ::Oj.load(document)
14
+ @document = (document.is_a?(String) ? ::Oj.load(document) : document).freeze
15
15
  @includes = IncludedResources.load(@document["included"])
16
16
  @errors = deserialize_errors!.freeze
17
17
  freeze
@@ -16,8 +16,8 @@ module JsonAPIObjectMapper
16
16
  def initialize(included_resources = [])
17
17
  included_resources ||= []
18
18
  @resource = included_resources.each_with_object({}) do |include, hash|
19
- hash[format_key(include)] = include["attributes"]
20
- end
19
+ hash[format_key(include)] = include
20
+ end.freeze
21
21
 
22
22
  freeze
23
23
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonAPIObjectMapper
4
- VERSION = "0.6.5"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -3,7 +3,7 @@
3
3
  require "spec_helper"
4
4
 
5
5
  module JsonAPIObjectMapper
6
- module Deserialize
6
+ module Deserialize # rubocop:disable Metrics/ModuleLength
7
7
  RSpec.describe Resource do
8
8
  describe "Attributes" do
9
9
  let(:payload) do
@@ -39,7 +39,7 @@ module JsonAPIObjectMapper
39
39
  end
40
40
  end
41
41
 
42
- describe "Relationships" do
42
+ describe ".has_one Relationship" do
43
43
  let(:payload) do
44
44
  {
45
45
  "relationships" => {
@@ -53,7 +53,7 @@ module JsonAPIObjectMapper
53
53
  }
54
54
  end
55
55
 
56
- context "Has included Resource" do
56
+ context "Has one included Resource" do
57
57
  let(:included_payload) do
58
58
  payload.merge(
59
59
  "included" => [
@@ -72,7 +72,7 @@ module JsonAPIObjectMapper
72
72
  end
73
73
 
74
74
  actual = klass.call(included_payload)
75
- expect(actual.photo).to eq("image" => "good_day_sir.jpg")
75
+ expect(actual.photo).to include("attributes" => { "image" => "good_day_sir.jpg" })
76
76
  end
77
77
 
78
78
  it "Should Resolve and decode the resource as the embedded relationship class" do
@@ -100,7 +100,7 @@ module JsonAPIObjectMapper
100
100
  expect(actual.photo).to eq("id" => "1", "type" => "photo")
101
101
  end
102
102
 
103
- it "Should set the default relationship values if no includes can be found" do
103
+ it "Should assign attributes that exist from the included resource" do
104
104
  photo_klass = Class.new(described_class) do
105
105
  attribute :image
106
106
  end
@@ -109,9 +109,85 @@ module JsonAPIObjectMapper
109
109
  has_one :photo, embed_with: photo_klass
110
110
  end
111
111
 
112
- actual = core_klass.call(payload)
113
- expect(actual.photo).to be_a(Hash)
114
- expect(actual.photo).to eq("id" => "1", "type" => "photo")
112
+ actual = core_klass.load(payload)
113
+ expect(actual.photo).to be_a(photo_klass)
114
+ expect(actual.photo.id).to eq("1")
115
+ expect(actual.photo.type).to eq("photo")
116
+ expect(actual.photo.image).to be_nil
117
+ end
118
+ end
119
+ end
120
+
121
+ describe ".has_many Relationships" do
122
+ let(:payload) do
123
+ {
124
+ "relationships" => {
125
+ "photos" => {
126
+ "data" => [
127
+ { "type" => "photo", "id" => "1" },
128
+ { "type" => "photo", "id" => "99" },
129
+ ],
130
+ },
131
+ },
132
+ }
133
+ end
134
+
135
+ context "Has many included resources" do
136
+ let(:included_payload) do
137
+ payload.merge(
138
+ "included" => [
139
+ {
140
+ "id" => "1",
141
+ "type" => "photo",
142
+ "attributes" => { "image" => "good_day_sir.jpg" },
143
+ },
144
+ {
145
+ "id" => "99",
146
+ "type" => "photo",
147
+ "attributes" => { "image" => "i_said_good_day!.jpg" },
148
+ },
149
+ ],
150
+ )
151
+ end
152
+
153
+ it "Should store a collection of included values" do
154
+ klass = Class.new(described_class) do
155
+ has_many :photos
156
+ end
157
+
158
+ actual = klass.load(included_payload)
159
+ expect(actual.photos).to be_a(Array)
160
+ expect(actual.photos.first["id"]).to eq("1")
161
+ expect(actual.photos.first["type"]).to eq("photo")
162
+
163
+ expect(actual.photos.last["id"]).to eq("99")
164
+ expect(actual.photos.last["type"]).to eq("photo")
165
+ end
166
+
167
+ it "Should resolve the embed_with option to a collection of parsed results" do
168
+ photo_klass = Class.new(described_class)
169
+ klass = Class.new(described_class) do
170
+ has_many :photos, embed_with: photo_klass
171
+ end
172
+
173
+ actual = klass.load(included_payload)
174
+ expect(actual.photos).to be_a(Collection)
175
+ expect(actual.photos[0].id).to eq("1")
176
+ expect(actual.photos[1].id).to eq("99")
177
+ end
178
+ end
179
+
180
+ context "Has no included resources" do
181
+ it "Should set the hash of the unresolved type" do
182
+ klass = Class.new(described_class) do
183
+ has_many :photos
184
+ end
185
+
186
+ actual = klass.load(payload)
187
+ expect(actual.photos).to be_a(Array)
188
+ expect(actual.photos.first).to be_a(Hash)
189
+ expect(actual.photos.first["id"]).to eq("1")
190
+ expect(actual.photos.last["id"]).to eq("99")
115
191
  end
116
192
  end
117
193
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi-object-mapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Protacio-Karaszi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-08 00:00:00.000000000 Z
11
+ date: 2018-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: oj