jsonapi-materializer 1.0.0.rc6 → 1.0.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.
@@ -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