json_api_ruby 0.4.0 → 0.4.1
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/CHANGELOG.md +3 -0
- data/lib/json_api_ruby/association.rb +58 -0
- data/lib/json_api_ruby/associations/meta.rb +48 -0
- data/lib/json_api_ruby/associations/to_many.rb +30 -0
- data/lib/json_api_ruby/associations/to_one.rb +28 -0
- data/lib/json_api_ruby/includes.rb +11 -1
- data/lib/json_api_ruby/resource.rb +1 -2
- data/lib/json_api_ruby/resources/dsl.rb +1 -1
- data/lib/json_api_ruby/serializer.rb +1 -1
- data/lib/json_api_ruby/version.rb +1 -1
- data/lib/json_api_ruby.rb +1 -0
- data/spec/json_api_ruby/association_spec.rb +155 -0
- data/spec/json_api_ruby/associations/meta_spec.rb +18 -0
- data/spec/json_api_ruby/associations/to_many_spec.rb +61 -0
- data/spec/json_api_ruby/associations/to_one_spec.rb +61 -0
- data/spec/json_api_ruby/includes_spec.rb +1 -1
- data/spec/json_api_ruby/resources/relationships_spec.rb +2 -153
- metadata +14 -3
- data/lib/json_api_ruby/resources/relationships.rb +0 -140
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0061d47ce650e9fcb4fb372e55830c44ffc2ee84
|
4
|
+
data.tar.gz: e21dcd60e6302c6cfc8c206fcd84cf15fad78ddb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 955e5c106e646ca173aa3b31b3f292abdc43d3122928aa856a52f7d09354d97f818b530a03e2cacfb7cf931341e9886f4664ea9205c4dc0525f2bcbfc8f59b94
|
7
|
+
data.tar.gz: c3e643b234cf81c68b5bbbcc0924f128e5364ec4500807c4d9b69eb71090dc84d9e67d755176ede994ec4cbe106a3628ba27925073e002d73d14b06a2a75e6e4
|
data/CHANGELOG.md
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
module JsonApi
|
2
|
+
class Association
|
3
|
+
# The resource object that "owns" this relationship
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
# class ArticleResource < JsonApi::Resource
|
7
|
+
# has_one :author
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# `ArticleResource` is the parent of the author object
|
11
|
+
attr_reader :parent
|
12
|
+
attr_reader :parent_model
|
13
|
+
|
14
|
+
# Determines whether the `data` attribute should be filled out and
|
15
|
+
# included
|
16
|
+
attr_reader :included
|
17
|
+
attr_reader :name
|
18
|
+
attr_reader :explicit_resource_class
|
19
|
+
|
20
|
+
|
21
|
+
# The resource object that represents this relationship
|
22
|
+
attr_reader :resources
|
23
|
+
|
24
|
+
def initialize(name, options)
|
25
|
+
@name = name
|
26
|
+
@resources = []
|
27
|
+
@parent = options.fetch(:parent_resource)
|
28
|
+
@parent_model = parent._model
|
29
|
+
@included = options.fetch(:included, JsonApi::Includes.new)
|
30
|
+
@explicit_resource_class = options.fetch(:explicit_resource_class)
|
31
|
+
end
|
32
|
+
|
33
|
+
def included?
|
34
|
+
included.has_name?(@name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
return_hash = {}
|
39
|
+
return_hash.merge!(relationship_links) if JsonApi.configuration.use_links
|
40
|
+
return_hash.merge!(data) if included?
|
41
|
+
return_hash
|
42
|
+
end
|
43
|
+
|
44
|
+
def relationship_links
|
45
|
+
{
|
46
|
+
'links' => {
|
47
|
+
'self' => JsonApi.configuration.base_url + parent.self_link_path + "/relationships/#{name}",
|
48
|
+
'related' => JsonApi.configuration.base_url + parent.self_link_path + "/#{name}"
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
require_relative 'associations/meta'
|
56
|
+
require_relative 'associations/to_one'
|
57
|
+
require_relative 'associations/to_many'
|
58
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module JsonApi
|
2
|
+
module Associations
|
3
|
+
UnknownCardinalityError = Class.new(StandardError)
|
4
|
+
|
5
|
+
class Meta
|
6
|
+
TO_ONE = :one
|
7
|
+
TO_MANY = :many
|
8
|
+
CARDINALITY = [TO_ONE, TO_MANY]
|
9
|
+
|
10
|
+
# The name of this relationship.
|
11
|
+
#
|
12
|
+
# This name comes from the resource object that defines the
|
13
|
+
# relationship. Example:
|
14
|
+
#
|
15
|
+
# class ArticleResource < JsonApi::Resource
|
16
|
+
# has_one :author # this is the name of this relationship
|
17
|
+
# end
|
18
|
+
attr_reader :name
|
19
|
+
|
20
|
+
attr_reader :cardinality
|
21
|
+
|
22
|
+
attr_reader :explicit_resource_class
|
23
|
+
|
24
|
+
def initialize(name, resource_class: nil, cardinality:)
|
25
|
+
@name = name.to_s
|
26
|
+
validate_cardinality(cardinality)
|
27
|
+
@cardinality = cardinality
|
28
|
+
@explicit_resource_class = resource_class
|
29
|
+
end
|
30
|
+
|
31
|
+
def validate_cardinality(cardinality)
|
32
|
+
unless(CARDINALITY.include?(cardinality))
|
33
|
+
fail UnknownCardinalityError.new
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_resources(options)
|
38
|
+
if cardinality == TO_ONE
|
39
|
+
relationship = ToOne.new(name, options.merge(explicit_resource_class: explicit_resource_class))
|
40
|
+
else
|
41
|
+
relationship = ToMany.new(name, options.merge(explicit_resource_class: explicit_resource_class))
|
42
|
+
end
|
43
|
+
relationship.build_resources(options)
|
44
|
+
relationship
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JsonApi
|
2
|
+
module Associations
|
3
|
+
class ToMany < JsonApi::Association
|
4
|
+
def data(options={})
|
5
|
+
data = resource_objects.map do |object|
|
6
|
+
object.identifier_hash
|
7
|
+
end
|
8
|
+
{'data' => data}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Build the related resources
|
12
|
+
def build_resources(options)
|
13
|
+
return unless included?
|
14
|
+
|
15
|
+
parent_model.send(name).each do |resource_model|
|
16
|
+
resource_class = JsonApi::Resources::Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent, resource_class: explicit_resource_class))
|
17
|
+
@resources << resource_class.new(resource_model, options.merge({include: included.next}))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def resource_objects
|
22
|
+
@resources
|
23
|
+
end
|
24
|
+
|
25
|
+
def cardinality
|
26
|
+
:many
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module JsonApi
|
2
|
+
module Associations
|
3
|
+
# convenience classes
|
4
|
+
class ToOne < JsonApi::Association
|
5
|
+
def data(options={})
|
6
|
+
identifier_hash = resource_object.identifier_hash if resource_object
|
7
|
+
{'data' => identifier_hash}
|
8
|
+
end
|
9
|
+
|
10
|
+
def build_resources(options)
|
11
|
+
return unless included?
|
12
|
+
resource_model = parent_model.send(name)
|
13
|
+
return if resource_model.blank?
|
14
|
+
|
15
|
+
resource_class = JsonApi::Resources::Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent, resource_class: explicit_resource_class))
|
16
|
+
@resources << resource_class.new(resource_model, options.merge({include: included.next}))
|
17
|
+
end
|
18
|
+
|
19
|
+
def resource_object
|
20
|
+
@resources.first
|
21
|
+
end
|
22
|
+
|
23
|
+
def cardinality
|
24
|
+
:one
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -3,7 +3,7 @@ module JsonApi
|
|
3
3
|
class Includes
|
4
4
|
attr_reader :name
|
5
5
|
attr_writer :includes
|
6
|
-
|
6
|
+
attr_writer :next
|
7
7
|
|
8
8
|
# Returns:
|
9
9
|
# #<Includes>
|
@@ -20,17 +20,27 @@ module JsonApi
|
|
20
20
|
zipped = first_array.zip(*other_arrays)
|
21
21
|
first_array = zipped.shift
|
22
22
|
first_include.includes = first_array.uniq.compact
|
23
|
+
|
23
24
|
zipped.inject(first_include) do |base_include, object_name|
|
24
25
|
new_include = self.new
|
25
26
|
new_include.includes = object_name.uniq.compact
|
26
27
|
base_include.next = new_include
|
27
28
|
new_include
|
28
29
|
end
|
30
|
+
|
29
31
|
first_include
|
30
32
|
end
|
31
33
|
|
34
|
+
def has_name?(name)
|
35
|
+
includes.include?(name)
|
36
|
+
end
|
37
|
+
|
32
38
|
def includes
|
33
39
|
@includes || []
|
34
40
|
end
|
41
|
+
|
42
|
+
def next
|
43
|
+
@next || self.class.new
|
44
|
+
end
|
35
45
|
end
|
36
46
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require_relative 'resources/base'
|
2
|
-
require_relative 'resources/relationships'
|
3
2
|
require_relative 'resources/dsl'
|
4
3
|
|
5
4
|
module JsonApi
|
@@ -58,7 +57,7 @@ module JsonApi
|
|
58
57
|
def initialize(model, options={})
|
59
58
|
options.stringify_keys!
|
60
59
|
@_model = model
|
61
|
-
@includes = options.fetch('include',
|
60
|
+
@includes = options.fetch('include', Includes.new)
|
62
61
|
build_object_graph # base module method
|
63
62
|
end
|
64
63
|
end
|
@@ -37,7 +37,7 @@ module JsonApi
|
|
37
37
|
private
|
38
38
|
def add_relationship(object_name, cardinality, options)
|
39
39
|
@relationships ||= []
|
40
|
-
@relationships <<
|
40
|
+
@relationships << JsonApi::Associations::Meta.new(object_name, options.merge(cardinality: cardinality))
|
41
41
|
create_accessor_methods(object_name)
|
42
42
|
end
|
43
43
|
|
@@ -25,7 +25,7 @@ module JsonApi
|
|
25
25
|
def initialize(object, options)
|
26
26
|
@meta = options.fetch('meta', Hash.new).stringify_keys
|
27
27
|
@object = object
|
28
|
-
@includes = options.fetch('include',
|
28
|
+
@includes = options.fetch('include', Includes.new)
|
29
29
|
@resource_class = options.fetch('resource_class', nil)
|
30
30
|
end
|
31
31
|
|
data/lib/json_api_ruby.rb
CHANGED
@@ -3,6 +3,7 @@ end
|
|
3
3
|
|
4
4
|
require_relative 'json_api_ruby/error_resource'
|
5
5
|
require_relative 'json_api_ruby/serializer'
|
6
|
+
require_relative 'json_api_ruby/association'
|
6
7
|
require_relative 'json_api_ruby/resource'
|
7
8
|
require_relative 'json_api_ruby/exceptions'
|
8
9
|
require_relative 'json_api_ruby/configuration'
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JsonApi::Association do
|
4
|
+
let(:article) do
|
5
|
+
Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:article_resource) do
|
9
|
+
ArticleResource.new(article)
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:author_relation) do
|
13
|
+
article_resource.class.relationships.find do |rel|
|
14
|
+
rel.name == 'author'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:comments_relation) do
|
19
|
+
article_resource.class.relationships.find do |rel|
|
20
|
+
rel.name == 'comments'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'exposes its cardinality' do
|
25
|
+
expect(author_relation.cardinality).to eq :one
|
26
|
+
expect(comments_relation.cardinality).to eq :many
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'link serialization' do
|
30
|
+
subject(:links_object) do
|
31
|
+
rel = author_relation.build_resources(parent_resource: article_resource)
|
32
|
+
rel.to_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'includes a links object' do
|
36
|
+
expect(links_object).to include('links')
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'has a "self" object' do
|
40
|
+
expect(links_object['links']).to include('self')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'has a "related" object' do
|
44
|
+
expect(links_object['links']).to include('related')
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'self object' do
|
48
|
+
it 'is a link to the relationship on the parent object' do
|
49
|
+
expect(links_object['links']['self']).to eq "http://localhost:3000/articles/#{article.uuid}/relationships/author"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'related object' do
|
54
|
+
it 'is a link to the base resource of the related object' do
|
55
|
+
expect(links_object['links']['related']).to eq "http://localhost:3000/articles/#{article.uuid}/author"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with links turned off' do
|
60
|
+
before { JsonApi.configuration.use_links = false }
|
61
|
+
after { JsonApi.configuration.use_links = true }
|
62
|
+
it 'returns an empty hash' do
|
63
|
+
expect(links_object['links']).to be_blank
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'data serialization' do
|
69
|
+
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
70
|
+
subject(:serialized_object) do
|
71
|
+
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
72
|
+
rel.to_hash
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'has a data top level object' do
|
76
|
+
expect(serialized_object).to include('data')
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'data object' do
|
80
|
+
context 'when an array' do
|
81
|
+
let(:included_data) { JsonApi::Includes.parse_includes('comments') }
|
82
|
+
subject(:serialized_object) do
|
83
|
+
rel = comments_relation.build_resources(parent_resource: article_resource, included: included_data)
|
84
|
+
rel.to_hash
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'has an identity hash for each object' do
|
88
|
+
expect(serialized_object['data'].flat_map(&:keys).uniq).to eq ['id', 'type']
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when a single object' do
|
93
|
+
it 'has an identity hash' do
|
94
|
+
expect(serialized_object['data'].keys).to eq ['id', 'type']
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe 'identity hash' do
|
101
|
+
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
102
|
+
it 'returns an identity hash given a model and parent resource' do
|
103
|
+
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
104
|
+
serialized_object = rel.to_hash
|
105
|
+
expect(serialized_object['data'].keys).to eq ['id', 'type']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'relationship serialization' do
|
110
|
+
context 'when the option "include" is true' do
|
111
|
+
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
112
|
+
it 'includes the data object' do
|
113
|
+
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
114
|
+
serialized_data = rel.to_hash
|
115
|
+
expect(serialized_data).to include('data')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'when the option "include" is falsey' do
|
120
|
+
it 'does not include data object' do
|
121
|
+
rel = author_relation.build_resources(parent_resource: article_resource)
|
122
|
+
serialized_data = rel.to_hash
|
123
|
+
expect(serialized_data).to_not include('data')
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe 'passing in a resource class' do
|
129
|
+
let(:simple_article_resource) do
|
130
|
+
SimpleArticleResource.new(article, include: JsonApi::Includes.parse_includes(['author', 'comments']))
|
131
|
+
end
|
132
|
+
|
133
|
+
let(:author_relation) do
|
134
|
+
simple_article_resource.relationships.find do |rel|
|
135
|
+
rel.name == 'author'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
let(:comment_relation) do
|
140
|
+
simple_article_resource.relationships.find do |rel|
|
141
|
+
rel.name == 'comments'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'uses the passed in resource_class for author' do
|
146
|
+
expect(author_relation.resource_object).to be_an_instance_of(SimplePersonResource)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'uses the passed in resource_class for each comment' do
|
150
|
+
comment_relation.resource_objects.each do |resource_object|
|
151
|
+
expect(resource_object).to be_an_instance_of(SimpleCommentResource)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe JsonApi::Associations::Meta do
|
4
|
+
let(:parent_resource) { PersonResource.new(:person) }
|
5
|
+
it 'fails on an invalid cardinality' do
|
6
|
+
expect { JsonApi::Associations::Meta.new('person', resource_class: PersonResource, cardinality: :yeahbuddy) }.to raise_error(JsonApi::Associations::UnknownCardinalityError)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'builds a ToOne association' do
|
10
|
+
new_meta = JsonApi::Associations::Meta.new('person', resource_class: PersonResource, cardinality: :one)
|
11
|
+
expect(new_meta.build_resources({parent_resource: parent_resource})).to be_a(JsonApi::Associations::ToOne)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'builds a ToMany association' do
|
15
|
+
new_meta = JsonApi::Associations::Meta.new('person', resource_class: PersonResource, cardinality: :many)
|
16
|
+
expect(new_meta.build_resources({parent_resource: parent_resource})).to be_a(JsonApi::Associations::ToMany)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ResourceObject
|
4
|
+
def identifier_hash
|
5
|
+
{}
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe JsonApi::Associations::ToMany do
|
10
|
+
subject(:assoc_instance) do
|
11
|
+
assoc = described_class.new('tomanytest', parent_resource: double('person_resource', _model: nil), explicit_resource_class: nil)
|
12
|
+
assoc.instance_variable_set(:@resources, [ResourceObject.new])
|
13
|
+
assoc
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'builds a data hash' do
|
17
|
+
expect(assoc_instance.data).to eq({'data' => [{}]})
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns nil when no resources are present' do
|
21
|
+
assoc = described_class.new('toonetest', parent_resource: double('person_resource', _model: nil), explicit_resource_class: nil)
|
22
|
+
expect(assoc.data).to eq({'data' => []})
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns its resource objects' do
|
26
|
+
expect(assoc_instance.resource_objects).to be_an Array
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns `many` for its cardinality' do
|
30
|
+
expect(assoc_instance.cardinality).to eq :many
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'is included' do
|
34
|
+
it 'builds its included resources' do
|
35
|
+
article = Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
36
|
+
article_resource = ArticleResource.new(article)
|
37
|
+
options = {
|
38
|
+
parent_resource: article_resource,
|
39
|
+
explicit_resource_class: CommentResource,
|
40
|
+
included: JsonApi::Includes.parse_includes(['comments'])
|
41
|
+
}
|
42
|
+
comment_assoc = described_class.new('comments', options)
|
43
|
+
comment_assoc.build_resources(options)
|
44
|
+
expect(comment_assoc.resource_objects.count).to eq article.comments.count
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'is not included' do
|
49
|
+
it 'returns nil' do
|
50
|
+
article = Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
51
|
+
article_resource = ArticleResource.new(article)
|
52
|
+
options = {
|
53
|
+
parent_resource: article_resource,
|
54
|
+
explicit_resource_class: CommentResource
|
55
|
+
}
|
56
|
+
comment_assoc = described_class.new('comments', options)
|
57
|
+
comment_assoc.build_resources(options)
|
58
|
+
expect(comment_assoc.resource_objects.count).to eq 0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ResourceObject
|
4
|
+
def identifier_hash
|
5
|
+
{}
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
describe JsonApi::Associations::ToOne do
|
10
|
+
subject(:assoc_instance) do
|
11
|
+
assoc = described_class.new('toonetest', parent_resource: double('person_resource', _model: nil), explicit_resource_class: nil)
|
12
|
+
assoc.instance_variable_set(:@resources, [ResourceObject.new])
|
13
|
+
assoc
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'builds a data hash' do
|
17
|
+
expect(assoc_instance.data).to eq({'data' => {}})
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns nil when no resources are present' do
|
21
|
+
assoc = described_class.new('toonetest', parent_resource: double('person_resource', _model: nil), explicit_resource_class: nil)
|
22
|
+
expect(assoc.data).to eq({'data' => nil})
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns its resource objects' do
|
26
|
+
expect(assoc_instance.resource_object).to be_a ResourceObject
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'returns `many` for its cardinality' do
|
30
|
+
expect(assoc_instance.cardinality).to eq :one
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'is included' do
|
34
|
+
it 'builds its included resources' do
|
35
|
+
article = Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
36
|
+
article_resource = ArticleResource.new(article)
|
37
|
+
options = {
|
38
|
+
parent_resource: article_resource,
|
39
|
+
explicit_resource_class: CommentResource,
|
40
|
+
included: JsonApi::Includes.parse_includes(['comments'])
|
41
|
+
}
|
42
|
+
comment_assoc = described_class.new('comments', options)
|
43
|
+
comment_assoc.build_resources(options)
|
44
|
+
expect(comment_assoc.resource_object).to be_a CommentResource
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'is not included' do
|
49
|
+
it 'returns nil' do
|
50
|
+
article = Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
51
|
+
article_resource = ArticleResource.new(article)
|
52
|
+
options = {
|
53
|
+
parent_resource: article_resource,
|
54
|
+
explicit_resource_class: CommentResource
|
55
|
+
}
|
56
|
+
comment_assoc = described_class.new('comments', options)
|
57
|
+
comment_assoc.build_resources(options)
|
58
|
+
expect(comment_assoc.resource_object).to be_nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -4,7 +4,7 @@ describe JsonApi::Includes do
|
|
4
4
|
context 'with nil' do
|
5
5
|
subject(:parsed_with_nil) { described_class.parse_includes(nil) }
|
6
6
|
specify { expect(parsed_with_nil.includes).to eq [] }
|
7
|
-
specify { expect(parsed_with_nil.next).to
|
7
|
+
specify { expect(parsed_with_nil.next).to be_an_instance_of(JsonApi::Includes) }
|
8
8
|
end
|
9
9
|
|
10
10
|
context 'with nested resources using the "." syntax' do
|
@@ -1,155 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
RSpec.describe JsonApi::Resources::RelationshipMeta do
|
4
|
-
|
5
|
-
Article.new('How to raise Triops', 'Triops are hardy little creatures whose eggs can be frozen for as long as 40 years')
|
6
|
-
end
|
7
|
-
|
8
|
-
let(:article_resource) do
|
9
|
-
ArticleResource.new(article)
|
10
|
-
end
|
11
|
-
|
12
|
-
let(:author_relation) do
|
13
|
-
article_resource.class.relationships.find do |rel|
|
14
|
-
rel.name == 'author'
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
let(:comments_relation) do
|
19
|
-
article_resource.class.relationships.find do |rel|
|
20
|
-
rel.name == 'comments'
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
it 'exposes its cardinality' do
|
25
|
-
expect(author_relation.cardinality).to eq :one
|
26
|
-
expect(comments_relation.cardinality).to eq :many
|
27
|
-
end
|
28
|
-
|
29
|
-
describe 'link serialization' do
|
30
|
-
subject(:links_object) do
|
31
|
-
rel = author_relation.build_resources(parent_resource: article_resource)
|
32
|
-
rel.to_hash
|
33
|
-
end
|
34
|
-
|
35
|
-
it 'includes a links object' do
|
36
|
-
expect(links_object).to include('links')
|
37
|
-
end
|
38
|
-
|
39
|
-
it 'has a "self" object' do
|
40
|
-
expect(links_object['links']).to include('self')
|
41
|
-
end
|
42
|
-
|
43
|
-
it 'has a "related" object' do
|
44
|
-
expect(links_object['links']).to include('related')
|
45
|
-
end
|
46
|
-
|
47
|
-
describe 'self object' do
|
48
|
-
it 'is a link to the relationship on the parent object' do
|
49
|
-
expect(links_object['links']['self']).to eq "http://localhost:3000/articles/#{article.uuid}/relationships/author"
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
describe 'related object' do
|
54
|
-
it 'is a link to the base resource of the related object' do
|
55
|
-
expect(links_object['links']['related']).to eq "http://localhost:3000/articles/#{article.uuid}/author"
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
context 'with links turned off' do
|
60
|
-
before { JsonApi.configuration.use_links = false }
|
61
|
-
after { JsonApi.configuration.use_links = true }
|
62
|
-
it 'returns an empty hash' do
|
63
|
-
expect(links_object['links']).to be_blank
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
describe 'data serialization' do
|
69
|
-
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
70
|
-
subject(:serialized_object) do
|
71
|
-
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
72
|
-
rel.to_hash
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'has a data top level object' do
|
76
|
-
expect(serialized_object).to include('data')
|
77
|
-
end
|
78
|
-
|
79
|
-
describe 'data object' do
|
80
|
-
context 'when an array' do
|
81
|
-
let(:included_data) { JsonApi::Includes.parse_includes('comments') }
|
82
|
-
subject(:serialized_object) do
|
83
|
-
rel = comments_relation.build_resources(parent_resource: article_resource, included: included_data)
|
84
|
-
rel.to_hash
|
85
|
-
end
|
86
|
-
|
87
|
-
it 'has an identity hash for each object' do
|
88
|
-
expect(serialized_object['data'].flat_map(&:keys).uniq).to eq ['id', 'type']
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
context 'when a single object' do
|
93
|
-
it 'has an identity hash' do
|
94
|
-
expect(serialized_object['data'].keys).to eq ['id', 'type']
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
describe 'identity hash' do
|
101
|
-
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
102
|
-
it 'returns an identity hash given a model and parent resource' do
|
103
|
-
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
104
|
-
serialized_object = rel.to_hash
|
105
|
-
expect(serialized_object['data'].keys).to eq ['id', 'type']
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
describe 'relationship serialization' do
|
110
|
-
context 'when the option "include" is true' do
|
111
|
-
let(:included_data) { JsonApi::Includes.parse_includes('author') }
|
112
|
-
it 'includes the data object' do
|
113
|
-
rel = author_relation.build_resources(parent_resource: article_resource, included: included_data)
|
114
|
-
serialized_data = rel.to_hash
|
115
|
-
expect(serialized_data).to include('data')
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
context 'when the option "include" is falsey' do
|
120
|
-
it 'does not include data object' do
|
121
|
-
rel = author_relation.build_resources(parent_resource: article_resource)
|
122
|
-
serialized_data = rel.to_hash
|
123
|
-
expect(serialized_data).to_not include('data')
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
describe 'passing in a resource class' do
|
129
|
-
let(:simple_article_resource) do
|
130
|
-
SimpleArticleResource.new(article, include: JsonApi::Includes.parse_includes(['author', 'comments']))
|
131
|
-
end
|
132
|
-
|
133
|
-
let(:author_relation) do
|
134
|
-
simple_article_resource.relationships.find do |rel|
|
135
|
-
rel.name == 'author'
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
let(:comment_relation) do
|
140
|
-
simple_article_resource.relationships.find do |rel|
|
141
|
-
rel.name == 'comments'
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
it 'uses the passed in resource_class for author' do
|
146
|
-
expect(author_relation.resource_object).to be_an_instance_of(SimplePersonResource)
|
147
|
-
end
|
148
|
-
|
149
|
-
it 'uses the passed in resource_class for each comment' do
|
150
|
-
comment_relation.resource_objects.each do |resource_object|
|
151
|
-
expect(resource_object).to be_an_instance_of(SimpleCommentResource)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
3
|
+
# RSpec.describe JsonApi::Resources::RelationshipMeta do
|
4
|
+
# end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_api_ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tracey Eubanks
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -111,6 +111,10 @@ files:
|
|
111
111
|
- Rakefile
|
112
112
|
- json_api_ruby.gemspec
|
113
113
|
- lib/json_api_ruby.rb
|
114
|
+
- lib/json_api_ruby/association.rb
|
115
|
+
- lib/json_api_ruby/associations/meta.rb
|
116
|
+
- lib/json_api_ruby/associations/to_many.rb
|
117
|
+
- lib/json_api_ruby/associations/to_one.rb
|
114
118
|
- lib/json_api_ruby/configuration.rb
|
115
119
|
- lib/json_api_ruby/error_resource.rb
|
116
120
|
- lib/json_api_ruby/exceptions.rb
|
@@ -119,9 +123,12 @@ files:
|
|
119
123
|
- lib/json_api_ruby/resources/base.rb
|
120
124
|
- lib/json_api_ruby/resources/discovery.rb
|
121
125
|
- lib/json_api_ruby/resources/dsl.rb
|
122
|
-
- lib/json_api_ruby/resources/relationships.rb
|
123
126
|
- lib/json_api_ruby/serializer.rb
|
124
127
|
- lib/json_api_ruby/version.rb
|
128
|
+
- spec/json_api_ruby/association_spec.rb
|
129
|
+
- spec/json_api_ruby/associations/meta_spec.rb
|
130
|
+
- spec/json_api_ruby/associations/to_many_spec.rb
|
131
|
+
- spec/json_api_ruby/associations/to_one_spec.rb
|
125
132
|
- spec/json_api_ruby/configuration_spec.rb
|
126
133
|
- spec/json_api_ruby/includes_spec.rb
|
127
134
|
- spec/json_api_ruby/resource_spec.rb
|
@@ -157,6 +164,10 @@ signing_key:
|
|
157
164
|
specification_version: 4
|
158
165
|
summary: Extremely lightweight implementation of JSON API
|
159
166
|
test_files:
|
167
|
+
- spec/json_api_ruby/association_spec.rb
|
168
|
+
- spec/json_api_ruby/associations/meta_spec.rb
|
169
|
+
- spec/json_api_ruby/associations/to_many_spec.rb
|
170
|
+
- spec/json_api_ruby/associations/to_one_spec.rb
|
160
171
|
- spec/json_api_ruby/configuration_spec.rb
|
161
172
|
- spec/json_api_ruby/includes_spec.rb
|
162
173
|
- spec/json_api_ruby/resource_spec.rb
|
@@ -1,140 +0,0 @@
|
|
1
|
-
module JsonApi
|
2
|
-
module Resources
|
3
|
-
|
4
|
-
class RelationshipMeta
|
5
|
-
# The name of this relationship.
|
6
|
-
#
|
7
|
-
# This name comes from the resource object that defines the
|
8
|
-
# relationship. Example:
|
9
|
-
#
|
10
|
-
# class ArticleResource < JsonApi::Resource
|
11
|
-
# has_one :author # this is the name of this relationship
|
12
|
-
# end
|
13
|
-
attr_reader :name
|
14
|
-
|
15
|
-
attr_reader :cardinality
|
16
|
-
|
17
|
-
attr_reader :explicit_resource_class
|
18
|
-
|
19
|
-
def initialize(name, options)
|
20
|
-
@name = name.to_s
|
21
|
-
@cardinality = options.fetch(:cardinality)
|
22
|
-
@explicit_resource_class = options.fetch(:resource_class, nil)
|
23
|
-
end
|
24
|
-
|
25
|
-
def build_resources(options)
|
26
|
-
if cardinality == :one
|
27
|
-
relationship = ToOneRelationship.new(name, options.merge(explicit_resource_class: explicit_resource_class))
|
28
|
-
else
|
29
|
-
relationship = ToManyRelationship.new(name, options.merge(explicit_resource_class: explicit_resource_class))
|
30
|
-
end
|
31
|
-
relationship.build_resources(options)
|
32
|
-
relationship
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
class Relationship
|
37
|
-
# The resource object that "owns" this relationship
|
38
|
-
#
|
39
|
-
# Example:
|
40
|
-
# class ArticleResource < JsonApi::Resource
|
41
|
-
# has_one :author
|
42
|
-
# end
|
43
|
-
#
|
44
|
-
# `ArticleResource` is the parent of the author object
|
45
|
-
attr_reader :parent
|
46
|
-
attr_reader :parent_model
|
47
|
-
|
48
|
-
# Determines whether the `data` attribute should be filled out and
|
49
|
-
# included
|
50
|
-
attr_reader :included
|
51
|
-
attr_reader :name
|
52
|
-
attr_reader :explicit_resource_class
|
53
|
-
|
54
|
-
|
55
|
-
# The resource object that represents this relationship
|
56
|
-
attr_reader :resources
|
57
|
-
|
58
|
-
def initialize(name, options)
|
59
|
-
@name = name
|
60
|
-
@resources = []
|
61
|
-
@parent = options.fetch(:parent_resource)
|
62
|
-
@parent_model = parent._model
|
63
|
-
@included = options.fetch(:included, {})
|
64
|
-
@explicit_resource_class = options.fetch(:explicit_resource_class)
|
65
|
-
end
|
66
|
-
|
67
|
-
def included?
|
68
|
-
included.present? && included.includes.include?(@name)
|
69
|
-
end
|
70
|
-
|
71
|
-
def to_hash
|
72
|
-
return_hash = {}
|
73
|
-
return_hash.merge!(relationship_links) if JsonApi.configuration.use_links
|
74
|
-
return_hash.merge!(data) if included?
|
75
|
-
return_hash
|
76
|
-
end
|
77
|
-
|
78
|
-
def relationship_links
|
79
|
-
{
|
80
|
-
'links' => {
|
81
|
-
'self' => JsonApi.configuration.base_url + parent.self_link_path + "/relationships/#{name}",
|
82
|
-
'related' => JsonApi.configuration.base_url + parent.self_link_path + "/#{name}"
|
83
|
-
}
|
84
|
-
}
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
# convenience classes
|
89
|
-
class ToOneRelationship < Relationship
|
90
|
-
def data(options={})
|
91
|
-
identifier_hash = resource_object.identifier_hash if resource_object
|
92
|
-
{'data' => identifier_hash}
|
93
|
-
end
|
94
|
-
|
95
|
-
def build_resources(options)
|
96
|
-
return unless included?
|
97
|
-
resource_model = parent_model.send(name)
|
98
|
-
return if resource_model.blank?
|
99
|
-
|
100
|
-
resource_class = Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent, resource_class: explicit_resource_class))
|
101
|
-
@resources << resource_class.new(resource_model, options.merge({include: included.next}))
|
102
|
-
end
|
103
|
-
|
104
|
-
def resource_object
|
105
|
-
@resources.first
|
106
|
-
end
|
107
|
-
|
108
|
-
def cardinality
|
109
|
-
:one
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
class ToManyRelationship < Relationship
|
114
|
-
def data(options={})
|
115
|
-
data = resource_objects.map do |object|
|
116
|
-
object.identifier_hash
|
117
|
-
end
|
118
|
-
{'data' => data}
|
119
|
-
end
|
120
|
-
|
121
|
-
# Build the related resources
|
122
|
-
def build_resources(options)
|
123
|
-
return unless included?
|
124
|
-
|
125
|
-
parent_model.send(name).each do |resource_model|
|
126
|
-
resource_class = Discovery.resource_for_name(resource_model, options.merge(parent_resource: parent, resource_class: explicit_resource_class))
|
127
|
-
@resources << resource_class.new(resource_model, options.merge({include: included.next}))
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
def resource_objects
|
132
|
-
@resources
|
133
|
-
end
|
134
|
-
|
135
|
-
def cardinality
|
136
|
-
:many
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|