jsonapi-materializer 1.0.0.rc6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,101 +1,103 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require("spec_helper")
2
4
 
3
5
  RSpec.describe(JSONAPI::Materializer::Collection) do
4
- let(:described_class) {ArticleMaterializer::Collection}
5
- let(:collection) {described_class.new(:object => object, :includes => [["comments"], ["author"]])}
6
+ let(:described_class) { ArticleMaterializer::Collection }
7
+ let(:collection) { described_class.new(object:, includes: [["comments"], ["author"]]) }
6
8
 
7
9
  describe("#as_json") do
8
- subject {collection.as_json.deep_stringify_keys}
10
+ subject { collection.as_json.deep_stringify_keys }
9
11
 
10
12
  before do
11
- Account.create!(:id => 9, :name => "Dan Gebhardt", :twitter => "dgeb")
12
- Account.create!(:id => 2, :name => "DHH", :twitter => "DHH")
13
- Article.create!(:id => 1, :title => "JSON API paints my bikeshed!", :account => Account.find(9))
14
- Article.create!(:id => 2, :title => "Rails is Omakase", :account => Account.find(9))
15
- Article.create!(:id => 3, :title => "What is JSON:API?", :account => Account.find(9))
16
- Comment.create!(:id => 5, :body => "First!", :article => Article.find(1), :account => Account.find(2))
17
- Comment.create!(:id => 12, :body => "I like XML better", :article => Article.find(1), :account => Account.find(9))
13
+ Account.create!(id: 9, name: "Dan Gebhardt", twitter: "dgeb")
14
+ Account.create!(id: 2, name: "DHH", twitter: "DHH")
15
+ Article.create!(id: 1, title: "JSON API paints my bikeshed!", account: Account.find(9))
16
+ Article.create!(id: 2, title: "Rails is Omakase", account: Account.find(9))
17
+ Article.create!(id: 3, title: "What is JSON:API?", account: Account.find(9))
18
+ Comment.create!(id: 5, body: "First!", article: Article.find(1), account: Account.find(2))
19
+ Comment.create!(id: 12, body: "I like XML better", article: Article.find(1), account: Account.find(9))
18
20
  end
19
21
 
20
22
  context("when the list has items") do
21
- let(:object) {Kaminari.paginate_array(Article.all).page(1).per(1)}
23
+ let(:object) { Kaminari.paginate_array(Article.all).page(1).per(1) }
22
24
 
23
25
  it("has a data key at root with the resources") do
24
26
  expect(subject.fetch("data")).to(eq([{
25
- "id" => "1",
26
- "type" => "articles",
27
- "attributes" => {
28
- "title" => "JSON API paints my bikeshed!"
29
- },
30
- "relationships" => {
31
- "author" => {
32
- "data" => {"id" => "9", "type" => "people"},
33
- "links" => {
34
- "self" => "http://example.com/articles/1/relationships/author",
35
- "related" => "http://example.com/articles/1/author"
36
- }
37
- },
38
- "comments" => {
39
- "data" => [
40
- {"id" => "5", "type" => "comments"},
41
- {"id" => "12", "type" => "comments"}
42
- ],
43
- "links" => {
44
- "self" => "http://example.com/articles/1/relationships/comments",
45
- "related" => "http://example.com/articles/1/comments"
46
- }
47
- }
48
- },
49
- "links" => {
50
- "self" => "http://example.com/articles/1"
51
- }
52
- }]))
27
+ "id" => "1",
28
+ "type" => "articles",
29
+ "attributes" => {
30
+ "title" => "JSON API paints my bikeshed!"
31
+ },
32
+ "relationships" => {
33
+ "author" => {
34
+ "data" => { "id" => "9", "type" => "people" },
35
+ "links" => {
36
+ "self" => "http://example.com/articles/1/relationships/author",
37
+ "related" => "http://example.com/articles/1/author"
38
+ }
39
+ },
40
+ "comments" => {
41
+ "data" => [
42
+ { "id" => "5", "type" => "comments" },
43
+ { "id" => "12", "type" => "comments" }
44
+ ],
45
+ "links" => {
46
+ "self" => "http://example.com/articles/1/relationships/comments",
47
+ "related" => "http://example.com/articles/1/comments"
48
+ }
49
+ }
50
+ },
51
+ "links" => {
52
+ "self" => "http://example.com/articles/1"
53
+ }
54
+ }]))
53
55
  end
54
56
 
55
57
  it("has a links key at root with pagination") do
56
58
  expect(subject.fetch("links")).to(eq(
57
- "self" => "http://example.com/articles",
58
- "next" => "http://example.com/articles?page[offset]=2&page[limit]=1",
59
- "last" => "http://example.com/articles?page[offset]=3&page[limit]=1"
60
- ))
59
+ "self" => "http://example.com/articles",
60
+ "next" => "http://example.com/articles?page[offset]=2&page[limit]=1",
61
+ "last" => "http://example.com/articles?page[offset]=3&page[limit]=1"
62
+ ))
61
63
  end
62
64
 
63
65
  it("has a included key at root with included models") do
64
66
  expect(subject.fetch("included")).to(include(
65
- {
66
- "id" => "5",
67
- "type" => "comments",
68
- "attributes"=>{"body"=>"First!"},
69
- "relationships" => {
70
- "author" => {"data" => {"id" => "2", "type" => "people"}, "links" => {"self" => "http://example.com/comments/5/relationships/author", "related" => "http://example.com/comments/5/author"}},
71
- "article" => {"data" => {"id" => "1", "type" => "articles"}, "links" => {"self" => "http://example.com/comments/5/relationships/article", "related" => "http://example.com/comments/5/article"}}
72
- },
73
- "links" => {"self" => "http://example.com/comments/5"}
74
- },
75
- {
76
- "id" => "12",
77
- "type" => "comments",
78
- "attributes"=>{"body"=>"I like XML better"},
79
- "relationships" => {
80
- "author" => {"data" => {"id" => "9", "type" => "people"}, "links" => {"self" => "http://example.com/comments/12/relationships/author", "related" => "http://example.com/comments/12/author"}},
81
- "article" => {"data" => {"id" => "1", "type" => "articles"}, "links" => {"self" => "http://example.com/comments/12/relationships/article", "related" => "http://example.com/comments/12/article"}}
82
- },
83
- "links" => {"self" => "http://example.com/comments/12"}
84
- },
85
- {
86
- "id" => "9",
87
- "type" => "people",
88
- "attributes"=>{"name"=>"Dan Gebhardt"},
89
- "relationships" => {
90
- "comments" => {"data" => [{"id" => "12", "type" => "comments"}], "links" => {"self" => "http://example.com/people/9/relationships/comments", "related" => "http://example.com/people/9/comments"}},
91
- "articles" => {
92
- "data" => [{"id" => "1", "type" => "articles"}, {"id" => "2", "type" => "articles"}, {"id" => "3", "type" => "articles"}],
93
- "links" => {"self" => "http://example.com/people/9/relationships/articles", "related" => "http://example.com/people/9/articles"}
94
- }
95
- },
96
- "links" => {"self" => "http://example.com/people/9"}
97
- }
98
- ))
67
+ {
68
+ "id" => "5",
69
+ "type" => "comments",
70
+ "attributes" => { "body" => "First!" },
71
+ "relationships" => {
72
+ "author" => { "data" => { "id" => "2", "type" => "people" }, "links" => { "self" => "http://example.com/comments/5/relationships/author", "related" => "http://example.com/comments/5/author" } },
73
+ "article" => { "data" => { "id" => "1", "type" => "articles" }, "links" => { "self" => "http://example.com/comments/5/relationships/article", "related" => "http://example.com/comments/5/article" } }
74
+ },
75
+ "links" => { "self" => "http://example.com/comments/5" }
76
+ },
77
+ {
78
+ "id" => "12",
79
+ "type" => "comments",
80
+ "attributes" => { "body" => "I like XML better" },
81
+ "relationships" => {
82
+ "author" => { "data" => { "id" => "9", "type" => "people" }, "links" => { "self" => "http://example.com/comments/12/relationships/author", "related" => "http://example.com/comments/12/author" } },
83
+ "article" => { "data" => { "id" => "1", "type" => "articles" }, "links" => { "self" => "http://example.com/comments/12/relationships/article", "related" => "http://example.com/comments/12/article" } }
84
+ },
85
+ "links" => { "self" => "http://example.com/comments/12" }
86
+ },
87
+ {
88
+ "id" => "9",
89
+ "type" => "people",
90
+ "attributes" => { "name" => "Dan Gebhardt" },
91
+ "relationships" => {
92
+ "comments" => { "data" => [{ "id" => "12", "type" => "comments" }], "links" => { "self" => "http://example.com/people/9/relationships/comments", "related" => "http://example.com/people/9/comments" } },
93
+ "articles" => {
94
+ "data" => [{ "id" => "1", "type" => "articles" }, { "id" => "2", "type" => "articles" }, { "id" => "3", "type" => "articles" }],
95
+ "links" => { "self" => "http://example.com/people/9/relationships/articles", "related" => "http://example.com/people/9/articles" }
96
+ }
97
+ },
98
+ "links" => { "self" => "http://example.com/people/9" }
99
+ }
100
+ ))
99
101
  end
100
102
  end
101
103
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Configuration
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Controller
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Error
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Error
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Error
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Error
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  class Error < StandardError
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Resource
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Resource
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Resource
@@ -25,32 +27,28 @@ module JSONAPI
25
27
  def for(subject)
26
28
  @for ||= {}
27
29
  @for[checksum(subject)] ||= case type
28
- when :many then
29
- unlessing(fetch_relation(subject), -> {subject.includes.any? {|included| included.include?(from.to_s)} || fetch_relation(subject).loaded?}) do |subject|
30
- subject.select(:id)
31
- end.map do |related_object|
32
- materializer_class.new(
33
- **subject.raw,
34
- :object => related_object
35
- )
36
- end
37
- when :one then
38
- if fetch_relation(subject).present?
39
- materializer_class.new(
40
- **subject.raw,
41
- :object => fetch_relation(subject)
42
- )
43
- end
44
- end
45
- end
46
-
47
- private def fetch_relation(subject)
48
- @fetch_relationship ||= {}
49
- @fetch_relationship[checksum(subject)] ||= subject.object.public_send(from)
30
+ when :many
31
+ unlessing(fetch_relation(subject), -> { subject.includes.any? { |included| included.include?(from.to_s) } || fetch_relation(subject).loaded? }) do |subject|
32
+ subject.select(:id)
33
+ end.map do |related_object|
34
+ materializer_class.new(
35
+ **subject.raw,
36
+ object: related_object
37
+ )
38
+ end
39
+ when :one
40
+ if fetch_relation(subject).present?
41
+ materializer_class.new(
42
+ **subject.raw,
43
+ object: fetch_relation(subject)
44
+ )
45
+ end
46
+ end
50
47
  end
48
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
51
49
 
52
50
  def using(parent)
53
- Resource::Relationship.new(:related => self, :parent => parent)
51
+ Resource::Relationship.new(related: self, parent:)
54
52
  end
55
53
 
56
54
  def many?
@@ -61,15 +59,20 @@ module JSONAPI
61
59
  type == :one
62
60
  end
63
61
 
62
+ private def fetch_relation(subject)
63
+ @fetch_relationship ||= {}
64
+ @fetch_relationship[checksum(subject)] ||= subject.object.public_send(from)
65
+ end
66
+
64
67
  private def materializer_class
65
68
  class_name.constantize
66
69
  end
67
70
 
68
71
  private def unlessing(object, proc)
69
- unless proc.call()
70
- yield(object)
71
- else
72
+ if proc.call
72
73
  object
74
+ else
75
+ yield(object)
73
76
  end
74
77
  end
75
78
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Resource
@@ -18,12 +20,12 @@ module JSONAPI
18
20
 
19
21
  def as_json(*)
20
22
  {
21
- :data => data,
22
- :links => {
23
- :self => links_self,
24
- :related => links_related
23
+ data:,
24
+ links: {
25
+ self: links_self,
26
+ related: links_related
25
27
  },
26
- :meta => {}
28
+ meta: {}
27
29
  }.transform_values(&:presence).compact
28
30
  end
29
31
 
@@ -33,31 +35,32 @@ module JSONAPI
33
35
  ).pattern
34
36
  end
35
37
 
36
- private def links_related
38
+ def links_related
37
39
  Addressable::Template.new(
38
40
  "#{parent.links_self}/#{related.name}"
39
41
  ).pattern
40
42
  end
41
43
 
42
- private def data
44
+ def data
43
45
  return if related_parent_materializer.blank?
44
46
 
45
47
  @data ||= if related.many?
46
- related_parent_materializer.map do |child|
47
- {
48
- :id => child.attribute("id").for(child).to_s,
49
- :type => child.type.to_s
50
- }
51
- end
52
- else
53
- {
54
- :id => related_parent_materializer.attribute("id").for(related_parent_materializer).to_s,
55
- :type => related_parent_materializer.type.to_s
56
- }
57
- end
48
+ related_parent_materializer.map do |child|
49
+ {
50
+ id: child.attribute("id").for(child).to_s,
51
+ type: child.type.to_s
52
+ }
53
+ end
54
+ else
55
+ {
56
+ id: related_parent_materializer.attribute("id").for(related_parent_materializer).to_s,
57
+ type: related_parent_materializer.type.to_s
58
+ }
59
+ end
58
60
  end
61
+ # rubocop:enable Metrics/AbcSize
59
62
 
60
- private def related_parent_materializer
63
+ def related_parent_materializer
61
64
  related.for(parent)
62
65
  end
63
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JSONAPI
2
4
  module Materializer
3
5
  module Resource
@@ -9,11 +11,11 @@ module JSONAPI
9
11
  extend(ActiveSupport::Concern)
10
12
  include(ActiveModel::Model)
11
13
 
12
- MIXIN_HOOK = ->(*) do
14
+ MIXIN_HOOK = lambda do |*|
13
15
  @attributes = {}
14
16
  @relations = {}
15
17
 
16
- unless const_defined?("Collection")
18
+ unless const_defined?(:Collection)
17
19
  self::Collection = Class.new do
18
20
  include(JSONAPI::Materializer::Collection)
19
21
  end
@@ -42,60 +44,33 @@ module JSONAPI
42
44
 
43
45
  def as_data
44
46
  {
45
- :id => id,
46
- :type => type,
47
- :attributes => exposed(attributes.except(:id)).
48
- transform_values {|attribute| object.public_send(attribute.from)},
49
- :relationships => exposed(relations).
50
- transform_values {|relation| relation.using(self).as_json},
51
- :links => {
52
- :self => links_self
47
+ id:,
48
+ type:,
49
+ attributes: exposed(attributes.except(:id))
50
+ .transform_values { |attribute| object.public_send(attribute.from) },
51
+ relationships: exposed(relations)
52
+ .transform_values { |relation| relation.using(self).as_json },
53
+ links: {
54
+ self: links_self
53
55
  }
54
56
  }.transform_values(&:presence).compact
55
57
  end
56
-
57
- private def exposed(mapping)
58
- if selects.any?
59
- mapping.slice(*selects.dig(type))
60
- else
61
- mapping
62
- end
63
- end
58
+ # rubocop:enable Metrics/AbcSize
64
59
 
65
60
  def as_json(*)
66
61
  {
67
- :links => {
68
- :self => links_self
62
+ links: {
63
+ self: links_self
69
64
  },
70
- :data => as_data,
71
- :included => included
65
+ data: as_data,
66
+ included:
72
67
  }.transform_values(&:presence).compact
73
68
  end
74
69
 
75
- private def id
76
- object.public_send(identifier).to_s
77
- end
78
-
79
70
  def type
80
71
  self.class.configuration.type.to_s
81
72
  end
82
73
 
83
- private def attributes
84
- self.class.configuration.attributes
85
- end
86
-
87
- private def origin
88
- self.class.configuration.origin
89
- end
90
-
91
- private def identifier
92
- self.class.configuration.identifier
93
- end
94
-
95
- private def relations
96
- self.class.configuration.relations
97
- end
98
-
99
74
  def attribute(name)
100
75
  self.class.attribute(name)
101
76
  end
@@ -111,31 +86,20 @@ module JSONAPI
111
86
  end
112
87
 
113
88
  def selects
114
- (@selects || {}).transform_values {|list| list.map(&:to_sym)}
89
+ (@selects || {}).transform_values { |list| list.map(&:to_sym) }
115
90
  end
116
91
 
117
92
  def includes
118
93
  @includes || []
119
94
  end
120
95
 
121
- private def included
122
- @included ||= includes.flat_map do |path|
123
- path.reduce(self) do |subject, key|
124
- if subject.is_a?(Array)
125
- subject.map {|related_subject| related_subject.relation(key).for(subject)}
126
- else
127
- subject.relation(key).for(subject)
128
- end
129
- end
130
- end.map(&:as_data)
131
- end
132
-
133
96
  included do
134
97
  class_eval(&MIXIN_HOOK) unless @abstract_class
135
98
  end
136
99
 
137
100
  class_methods do
138
101
  def inherited(object)
102
+ super
139
103
  object.class_eval(&MIXIN_HOOK) unless object.instance_variable_defined?(:@abstract_class)
140
104
  end
141
105
 
@@ -153,51 +117,95 @@ module JSONAPI
153
117
 
154
118
  def has(name, from: name)
155
119
  @attributes[name] = Attribute.new(
156
- :owner => self,
157
- :name => name,
158
- :from => from
120
+ owner: self,
121
+ name:,
122
+ from:
159
123
  )
160
124
  end
161
125
 
162
- def has_one(name, from: name, class_name:)
126
+ # rubocop:disable Naming/PredicateName
127
+ def has_one(name, class_name:, from: name)
163
128
  @relations[name] = Relation.new(
164
- :owner => self,
165
- :type => :one,
166
- :name => name,
167
- :from => from,
168
- :class_name => class_name
129
+ owner: self,
130
+ type: :one,
131
+ name:,
132
+ from:,
133
+ class_name:
169
134
  )
170
135
  end
136
+ # rubocop:enable Naming/PredicateName
171
137
 
172
- def has_many(name, from: name, class_name:)
138
+ # rubocop:disable Naming/PredicateName
139
+ def has_many(name, class_name:, from: name)
173
140
  @relations[name] = Relation.new(
174
- :owner => self,
175
- :type => :many,
176
- :name => name,
177
- :from => from,
178
- :class_name => class_name
141
+ owner: self,
142
+ type: :many,
143
+ name:,
144
+ from:,
145
+ class_name:
179
146
  )
180
147
  end
148
+ # rubocop:enable Naming/PredicateName
181
149
 
182
150
  def configuration
183
151
  @configuration ||= Configuration.new(
184
- :owner => self,
185
- :type => @type,
186
- :origin => @origin,
187
- :identifier => @identifier,
188
- :attributes => @attributes,
189
- :relations => @relations
152
+ owner: self,
153
+ type: @type,
154
+ origin: @origin,
155
+ identifier: @identifier,
156
+ attributes: @attributes,
157
+ relations: @relations
190
158
  )
191
159
  end
192
160
 
193
161
  def attribute(name)
194
- configuration.attributes.fetch(name.to_sym) {raise(Error::ResourceAttributeNotFound, :name => name, :materializer => self)}
162
+ configuration.attributes.fetch(name.to_sym) { raise(Error::ResourceAttributeNotFound, name:, materializer: self) }
195
163
  end
196
164
 
197
165
  def relation(name)
198
- configuration.relations.fetch(name.to_sym) {raise(Error::ResourceRelationshipNotFound, :name => name, :materializer => self)}
166
+ configuration.relations.fetch(name.to_sym) { raise(Error::ResourceRelationshipNotFound, name:, materializer: self) }
199
167
  end
200
168
  end
169
+
170
+ private def exposed(mapping)
171
+ if selects.any?
172
+ mapping.slice(*selects[type])
173
+ else
174
+ mapping
175
+ end
176
+ end
177
+
178
+ private def id
179
+ object.public_send(identifier).to_s
180
+ end
181
+
182
+ private def attributes
183
+ self.class.configuration.attributes
184
+ end
185
+
186
+ private def origin
187
+ self.class.configuration.origin
188
+ end
189
+
190
+ private def identifier
191
+ self.class.configuration.identifier
192
+ end
193
+
194
+ private def relations
195
+ self.class.configuration.relations
196
+ end
197
+
198
+ private def included
199
+ @included ||= includes.flat_map do |path|
200
+ path.reduce(self) do |subject, key|
201
+ if subject.is_a?(Array)
202
+ subject.map { |related_subject| related_subject.relation(key).for(subject) }
203
+ else
204
+ subject.relation(key).for(subject)
205
+ end
206
+ end
207
+ end.map(&:as_data)
208
+ end
201
209
  end
202
210
  end
203
211
  end