jsonapi-object-mapper 0.6.5 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
|
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
|