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