jsonapi-materializer 1.0.0.rc.pre.1 → 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.
- checksums.yaml +4 -4
- data/LICENSE +440 -0
- data/README.md +1 -76
- data/Rakefile +2 -1
- data/lib/jsonapi/materializer/collection.rb +22 -20
- data/lib/jsonapi/materializer/collection_spec.rb +79 -78
- data/lib/jsonapi/materializer/configuration.rb +2 -0
- data/lib/jsonapi/materializer/controller.rb +2 -0
- data/lib/jsonapi/materializer/error/invalid_accept_header.rb +2 -0
- data/lib/jsonapi/materializer/error/missing_accept_header.rb +2 -0
- data/lib/jsonapi/materializer/error/resource_attribute_not_found.rb +2 -0
- data/lib/jsonapi/materializer/error/resource_relationship_not_found.rb +2 -0
- data/lib/jsonapi/materializer/error.rb +2 -0
- data/lib/jsonapi/materializer/resource/attribute.rb +2 -18
- data/lib/jsonapi/materializer/resource/configuration.rb +2 -0
- data/lib/jsonapi/materializer/resource/relation.rb +29 -44
- data/lib/jsonapi/materializer/resource/relationship.rb +23 -20
- data/lib/jsonapi/materializer/resource.rb +88 -108
- data/lib/jsonapi/materializer/resource_spec.rb +10 -8
- data/lib/jsonapi/materializer/version.rb +3 -1
- data/lib/jsonapi/materializer.rb +6 -5
- data/lib/jsonapi/materializer_spec.rb +2 -3
- data/lib/jsonapi-materializer.rb +2 -0
- metadata +25 -206
- data/lib/jsonapi/materializer/context.rb +0 -8
- data/lib/jsonapi/materializer/version_spec.rb +0 -7
@@ -1,102 +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(:
|
6
|
-
let(:collection) {described_class.new(:object => object, :includes => [["comments"], ["author"]], :context => {:policy => policy.new})}
|
6
|
+
let(:described_class) { ArticleMaterializer::Collection }
|
7
|
+
let(:collection) { described_class.new(object:, includes: [["comments"], ["author"]]) }
|
7
8
|
|
8
9
|
describe("#as_json") do
|
9
|
-
subject {collection.as_json.deep_stringify_keys}
|
10
|
+
subject { collection.as_json.deep_stringify_keys }
|
10
11
|
|
11
12
|
before do
|
12
|
-
Account.create!(:
|
13
|
-
Account.create!(:
|
14
|
-
Article.create!(:
|
15
|
-
Article.create!(:
|
16
|
-
Article.create!(:
|
17
|
-
Comment.create!(:
|
18
|
-
Comment.create!(:
|
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))
|
19
20
|
end
|
20
21
|
|
21
22
|
context("when the list has items") do
|
22
|
-
let(:object) {Kaminari.paginate_array(Article.all).page(1).per(1)}
|
23
|
+
let(:object) { Kaminari.paginate_array(Article.all).page(1).per(1) }
|
23
24
|
|
24
25
|
it("has a data key at root with the resources") do
|
25
26
|
expect(subject.fetch("data")).to(eq([{
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
+
}]))
|
54
55
|
end
|
55
56
|
|
56
57
|
it("has a links key at root with pagination") do
|
57
58
|
expect(subject.fetch("links")).to(eq(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
+
))
|
62
63
|
end
|
63
64
|
|
64
65
|
it("has a included key at root with included models") do
|
65
66
|
expect(subject.fetch("included")).to(include(
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
+
))
|
100
101
|
end
|
101
102
|
end
|
102
103
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSONAPI
|
2
4
|
module Materializer
|
3
5
|
module Resource
|
@@ -7,12 +9,10 @@ module JSONAPI
|
|
7
9
|
attr_accessor(:owner)
|
8
10
|
attr_accessor(:name)
|
9
11
|
attr_accessor(:from)
|
10
|
-
attr_accessor(:visible)
|
11
12
|
|
12
13
|
validates_presence_of(:owner)
|
13
14
|
validates_presence_of(:name)
|
14
15
|
validates_presence_of(:from)
|
15
|
-
validate(:visible_callable)
|
16
16
|
|
17
17
|
def initialize(**keyword_arguments)
|
18
18
|
super(**keyword_arguments)
|
@@ -24,25 +24,9 @@ module JSONAPI
|
|
24
24
|
subject.object.public_send(from)
|
25
25
|
end
|
26
26
|
|
27
|
-
def visible?(subject)
|
28
|
-
return visible if [true, false].include?(visible)
|
29
|
-
return subject.send(visible, self) if visible.is_a?(Symbol)
|
30
|
-
return visible.call(self) if visible.respond_to?(:call)
|
31
|
-
|
32
|
-
true
|
33
|
-
end
|
34
|
-
|
35
27
|
private def materializer_class
|
36
28
|
class_name.constantize
|
37
29
|
end
|
38
|
-
|
39
|
-
private def visible_callable
|
40
|
-
return if [true, false].include?(visible)
|
41
|
-
return if visible.is_a?(Symbol)
|
42
|
-
return if visible.respond_to?(:call)
|
43
|
-
|
44
|
-
errors.add(:visible, "not callable or boolean")
|
45
|
-
end
|
46
30
|
end
|
47
31
|
end
|
48
32
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JSONAPI
|
2
4
|
module Materializer
|
3
5
|
module Resource
|
@@ -9,14 +11,12 @@ module JSONAPI
|
|
9
11
|
attr_accessor(:type)
|
10
12
|
attr_accessor(:from)
|
11
13
|
attr_accessor(:class_name)
|
12
|
-
attr_accessor(:visible)
|
13
14
|
|
14
15
|
validates_presence_of(:owner)
|
15
16
|
validates_presence_of(:name)
|
16
17
|
validates_presence_of(:type)
|
17
18
|
validates_presence_of(:from)
|
18
19
|
validates_presence_of(:class_name)
|
19
|
-
validate(:visible_callable)
|
20
20
|
|
21
21
|
def initialize(**keyword_arguments)
|
22
22
|
super(**keyword_arguments)
|
@@ -27,40 +27,28 @@ module JSONAPI
|
|
27
27
|
def for(subject)
|
28
28
|
@for ||= {}
|
29
29
|
@for[checksum(subject)] ||= case type
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
def visible?(subject)
|
50
|
-
return visible if [true, false].include?(visible)
|
51
|
-
return subject.send(visible, self) if visible.is_a?(Symbol)
|
52
|
-
return visible.call(self) if visible.respond_to?(:call)
|
53
|
-
|
54
|
-
true
|
55
|
-
end
|
56
|
-
|
57
|
-
private def fetch_relation(subject)
|
58
|
-
@fetch_relationship ||= {}
|
59
|
-
@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
|
60
47
|
end
|
48
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
61
49
|
|
62
50
|
def using(parent)
|
63
|
-
Resource::Relationship.new(:
|
51
|
+
Resource::Relationship.new(related: self, parent:)
|
64
52
|
end
|
65
53
|
|
66
54
|
def many?
|
@@ -71,23 +59,20 @@ module JSONAPI
|
|
71
59
|
type == :one
|
72
60
|
end
|
73
61
|
|
74
|
-
private def
|
75
|
-
|
62
|
+
private def fetch_relation(subject)
|
63
|
+
@fetch_relationship ||= {}
|
64
|
+
@fetch_relationship[checksum(subject)] ||= subject.object.public_send(from)
|
76
65
|
end
|
77
66
|
|
78
|
-
private def
|
79
|
-
|
80
|
-
return if visible.is_a?(Symbol)
|
81
|
-
return if visible.respond_to?(:call)
|
82
|
-
|
83
|
-
errors.add(:visible, "not callable or boolean")
|
67
|
+
private def materializer_class
|
68
|
+
class_name.constantize
|
84
69
|
end
|
85
70
|
|
86
71
|
private def unlessing(object, proc)
|
87
|
-
|
88
|
-
yield(object)
|
89
|
-
else
|
72
|
+
if proc.call
|
90
73
|
object
|
74
|
+
else
|
75
|
+
yield(object)
|
91
76
|
end
|
92
77
|
end
|
93
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
|
-
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
23
|
+
data:,
|
24
|
+
links: {
|
25
|
+
self: links_self,
|
26
|
+
related: links_related
|
25
27
|
},
|
26
|
-
:
|
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
|
-
|
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
|
-
|
44
|
+
def data
|
43
45
|
return if related_parent_materializer.blank?
|
44
46
|
|
45
47
|
@data ||= if related.many?
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
63
|
+
def related_parent_materializer
|
61
64
|
related.for(parent)
|
62
65
|
end
|
63
66
|
end
|