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 +4 -4
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +5 -5
- data/README.md +2 -0
- data/lib/jsonapi-object-mapper/deserialize/collection.rb +10 -2
- data/lib/jsonapi-object-mapper/deserialize/dsl.rb +62 -20
- data/lib/jsonapi-object-mapper/deserialize/resource.rb +26 -18
- data/lib/jsonapi-object-mapper/exceptions.rb +15 -0
- data/lib/jsonapi-object-mapper/parser/document.rb +1 -1
- data/lib/jsonapi-object-mapper/parser/included_resources.rb +2 -2
- data/lib/jsonapi-object-mapper/version.rb +1 -1
- data/spec/deserialize/resource_spec.rb +84 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c55b670a2ffe1d7d8d64a77103e3652694ad29076072cda48899f65228fb8232
|
4
|
+
data.tar.gz: 5e597a8fe9650ef363ce6bf6902d3b5de7b3b316a49b50107e57018191c93e57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b037952245ea17ff6baed1cde1fe3d17620c29d268be9df0230ece3d48f51ac1b92ca9d99b9f21ae563a3e1e5d6cde23825e8b9a7294d21f8844bc7fb3ddad6
|
7
|
+
data.tar.gz: 187087f89ba2fd1d2fa18faaca2ea9f2f2312875466b24fe4593ff933d964ccd36b5eefdb8e114f60b3afe04636730cb2f7642ec62845936cc950189ccf7a2ab
|
data/.rubocop.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
jsonapi-object-mapper (0.
|
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.
|
16
|
+
oj (3.6.4)
|
17
17
|
parallel (1.12.1)
|
18
|
-
parser (2.5.1.
|
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.
|
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
|
+
[](https://badge.fury.io/rb/jsonapi-object-mapper) [](https://travis-ci.com/GeorgeKaraszi/jsonapi-object-mapper) [](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
|
15
|
-
raise InvalidParser
|
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
|
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,
|
32
|
-
|
33
|
-
|
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
|
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
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
@_class_relationships[key
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
115
|
+
def attribute_defined?(attribute_name)
|
87
116
|
self.class.attr_blocks.key?(attribute_name)
|
88
117
|
end
|
89
118
|
|
90
|
-
def
|
91
|
-
self.class.
|
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 :
|
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("@
|
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("@
|
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
|
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
|
-
|
79
|
-
|
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(:
|
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(:
|
94
|
+
@relationships.each_pair(&method(:new_relationship))
|
90
95
|
end
|
91
96
|
|
92
|
-
def
|
93
|
-
return unless
|
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
|
98
|
-
|
99
|
-
|
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?(
|
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
|
20
|
-
end
|
19
|
+
hash[format_key(include)] = include
|
20
|
+
end.freeze
|
21
21
|
|
22
22
|
freeze
|
23
23
|
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 "
|
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
|
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
|
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.
|
113
|
-
expect(actual.photo).to be_a(
|
114
|
-
expect(actual.photo).to eq("
|
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.
|
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-
|
11
|
+
date: 2018-07-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: oj
|