jsonapi 0.1.1.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +48 -0
- data/LICENSE +21 -0
- data/README.md +141 -0
- data/Rakefile +7 -0
- data/jsonapi_parser.gemspec +25 -0
- data/lib/jsonapi/attributes.rb +41 -0
- data/lib/jsonapi/document.rb +126 -0
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/exceptions.rb +4 -0
- data/lib/jsonapi/include_directive/parser.rb +57 -0
- data/lib/jsonapi/include_directive.rb +67 -0
- data/lib/jsonapi/jsonapi.rb +19 -0
- data/lib/jsonapi/link.rb +41 -0
- data/lib/jsonapi/links.rb +34 -0
- data/lib/jsonapi/parse.rb +18 -0
- data/lib/jsonapi/relationship.rb +57 -0
- data/lib/jsonapi/relationships.rb +41 -0
- data/lib/jsonapi/resource/active_record.rb +151 -0
- data/lib/jsonapi/resource.rb +48 -0
- data/lib/jsonapi/resource_identifier.rb +36 -0
- data/lib/jsonapi/version.rb +3 -0
- data/lib/jsonapi.rb +18 -0
- data/spec/duplicates_spec.rb +95 -0
- data/spec/full_linkage_spec.rb +161 -0
- data/spec/include_directive/parser_spec.rb +59 -0
- data/spec/include_directive_spec.rb +47 -0
- data/spec/parser_spec.rb +97 -0
- data/spec/resource/to_activerecord_hash_spec.rb +76 -0
- metadata +137 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
# c.f. http://jsonapi.org/format/#document-links
|
3
|
+
class Links
|
4
|
+
def initialize(links_hash, options = {})
|
5
|
+
fail InvalidDocument, "the value of 'links' MUST be an object" unless
|
6
|
+
links_hash.is_a?(Hash)
|
7
|
+
|
8
|
+
@hash = links_hash
|
9
|
+
@links = {}
|
10
|
+
links_hash.each do |link_name, link_val|
|
11
|
+
@links[link_name.to_s] = Link.new(link_val, options)
|
12
|
+
define_singleton_method(link_name) do
|
13
|
+
@links[link_name.to_s]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
@hash
|
20
|
+
end
|
21
|
+
|
22
|
+
def defined?(link_name)
|
23
|
+
@links.key?(link_name.to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](link_name)
|
27
|
+
@links[link_name.to_s]
|
28
|
+
end
|
29
|
+
|
30
|
+
def keys
|
31
|
+
@links.keys
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Parse a JSON API document.
|
7
|
+
#
|
8
|
+
# @param document [Hash, String] the JSON API document.
|
9
|
+
# @param options [Hash] options
|
10
|
+
# @option options [Boolean] :id_optional (false) whether the resource
|
11
|
+
# objects in the primary data must have an id
|
12
|
+
# @return [JSON::API::Document]
|
13
|
+
def parse(document, options = {})
|
14
|
+
hash = document.is_a?(Hash) ? document : JSON.parse(document)
|
15
|
+
|
16
|
+
Document.new(hash, options)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
# c.f. http://jsonapi.org/format/#document-resource-object-relationships
|
3
|
+
class Relationship
|
4
|
+
attr_reader :data, :links, :meta
|
5
|
+
|
6
|
+
def initialize(relationship_hash, options = {})
|
7
|
+
@hash = relationship_hash
|
8
|
+
@options = options
|
9
|
+
@links_defined = relationship_hash.key?('links')
|
10
|
+
@data_defined = relationship_hash.key?('data')
|
11
|
+
@meta_defined = relationship_hash.key?('meta')
|
12
|
+
links_hash = relationship_hash['links'] || {}
|
13
|
+
@links = Links.new(links_hash, @options)
|
14
|
+
@data = parse_linkage(relationship_hash['data']) if
|
15
|
+
@data_defined
|
16
|
+
@meta = relationship_hash['meta'] if @meta_defined
|
17
|
+
|
18
|
+
validate!
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
@hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def collection?
|
26
|
+
@data.is_a?(Array)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def validate!
|
32
|
+
case
|
33
|
+
when !@links_defined && !@data_defined && !@meta_defined
|
34
|
+
fail InvalidDocument,
|
35
|
+
"a relationship object MUST contain at least one of 'links'," \
|
36
|
+
" 'data', or 'meta'"
|
37
|
+
when @links_defined &&
|
38
|
+
!@links.defined?(:self) &&
|
39
|
+
!@links.defined?(:related)
|
40
|
+
fail InvalidDocument,
|
41
|
+
"the 'links' object of a relationship object MUST contain at" \
|
42
|
+
" least one of 'self' or 'related'"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_linkage(linkage_hash)
|
47
|
+
collection = linkage_hash.is_a?(Array)
|
48
|
+
if collection
|
49
|
+
linkage_hash.map { |h| ResourceIdentifier.new(h, @options) }
|
50
|
+
elsif linkage_hash.nil?
|
51
|
+
nil
|
52
|
+
else
|
53
|
+
ResourceIdentifier.new(linkage_hash, @options)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
# c.f. http://jsonapi.org/format/#document-resource-object-relationships
|
3
|
+
class Relationships
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(relationships_hash, options = {})
|
7
|
+
fail InvalidDocument,
|
8
|
+
"the value of 'relationships' MUST be an object" unless
|
9
|
+
relationships_hash.is_a?(Hash)
|
10
|
+
|
11
|
+
@hash = relationships_hash
|
12
|
+
@relationships = {}
|
13
|
+
relationships_hash.each do |rel_name, rel_hash|
|
14
|
+
@relationships[rel_name.to_s] = Relationship.new(rel_hash, options)
|
15
|
+
define_singleton_method(rel_name) do
|
16
|
+
@relationships[rel_name.to_s]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_hash
|
22
|
+
@hash
|
23
|
+
end
|
24
|
+
|
25
|
+
def each(&block)
|
26
|
+
@relationships.each(&block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](rel_name)
|
30
|
+
@relationships[rel_name.to_s]
|
31
|
+
end
|
32
|
+
|
33
|
+
def defined?(rel_name)
|
34
|
+
@relationships.key?(rel_name.to_s)
|
35
|
+
end
|
36
|
+
|
37
|
+
def keys
|
38
|
+
@relationships.keys
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
class Resource
|
5
|
+
# Transform the resource object into a hash ready for ActiveRecord's
|
6
|
+
# new/create/update.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# payload = {
|
10
|
+
# 'data' => {
|
11
|
+
# 'type' => 'articles',
|
12
|
+
# 'id' => '1',
|
13
|
+
# 'attributes' => {
|
14
|
+
# 'title' => 'JSON API paints my bikeshed!',
|
15
|
+
# 'rating' => '5 stars'
|
16
|
+
# },
|
17
|
+
# 'relationships' => {
|
18
|
+
# 'author' => {
|
19
|
+
# 'data' => { 'type' => 'people', 'id' => '9' }
|
20
|
+
# },
|
21
|
+
# 'referree' => {
|
22
|
+
# 'data' => nil
|
23
|
+
# },
|
24
|
+
# 'publishing-journal' => {
|
25
|
+
# 'data' => nil
|
26
|
+
# },
|
27
|
+
# 'comments' => {
|
28
|
+
# 'data' => [
|
29
|
+
# { 'type' => 'comments', 'id' => '5' },
|
30
|
+
# { 'type' => 'comments', 'id' => '12' }
|
31
|
+
# ]
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
# }
|
35
|
+
# }
|
36
|
+
# document = JSON::API.parse(payload)
|
37
|
+
# options = {
|
38
|
+
# attributes: {
|
39
|
+
# except: [:rating]
|
40
|
+
# },
|
41
|
+
# relationships: {
|
42
|
+
# only: [:author, :'publishing-journal', :comments],
|
43
|
+
# polymorphic: [:author]
|
44
|
+
# },
|
45
|
+
# key_formatter: ->(x) { x.underscore }
|
46
|
+
# }
|
47
|
+
# document.data.to_activerecord_hash(options)
|
48
|
+
# # => {
|
49
|
+
# id: '1',
|
50
|
+
# title: 'JSON API paints my bikeshed!',
|
51
|
+
# author_id: '9',
|
52
|
+
# author_type: 'people',
|
53
|
+
# publishing_journal_id: nil,
|
54
|
+
# comment_ids: ['5', '12']
|
55
|
+
# }
|
56
|
+
#
|
57
|
+
# @param options [Hash]
|
58
|
+
# * :attributes (Hash)
|
59
|
+
# * :only (Array<Symbol,String>)
|
60
|
+
# * :except (Array<Symbol,String>)
|
61
|
+
# * :relationships (Hash)
|
62
|
+
# * :only (Array<Symbol,String>)
|
63
|
+
# * :except (Array<Symbol,String>)
|
64
|
+
# * :polymorphic (Array<Symbol,String>)
|
65
|
+
# * :key_formatter (lambda)
|
66
|
+
# @return [Hash]
|
67
|
+
def to_activerecord_hash(options = {})
|
68
|
+
options[:attributes] ||= {}
|
69
|
+
options[:relationships] ||= {}
|
70
|
+
hash = {}
|
71
|
+
hash[:id] = id unless id.nil?
|
72
|
+
hash.merge!(attributes_for_activerecord_hash(options))
|
73
|
+
hash.merge!(relationships_for_activerecord_hash(options))
|
74
|
+
|
75
|
+
hash
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def attributes_for_activerecord_hash(options)
|
81
|
+
attributes_hashes =
|
82
|
+
filter_keys(attributes.keys, options[:attributes]).map do |key|
|
83
|
+
attribute_for_activerecord_hash(key, options[:key_formatter])
|
84
|
+
end
|
85
|
+
|
86
|
+
attributes_hashes.reduce({}, :merge)
|
87
|
+
end
|
88
|
+
|
89
|
+
def attribute_for_activerecord_hash(key, key_formatter)
|
90
|
+
{ format_key(key, key_formatter).to_sym => attributes[key] }
|
91
|
+
end
|
92
|
+
|
93
|
+
def relationships_for_activerecord_hash(options)
|
94
|
+
relationship_hashes =
|
95
|
+
filter_keys(relationships.keys, options[:relationships]).map do |key|
|
96
|
+
polymorphic = (options[:relationships][:polymorphic] || [])
|
97
|
+
.include?(key.to_sym)
|
98
|
+
relationship_for_activerecord_hash(key,
|
99
|
+
options[:key_formatter],
|
100
|
+
polymorphic)
|
101
|
+
end
|
102
|
+
|
103
|
+
relationship_hashes.reduce({}, :merge)
|
104
|
+
end
|
105
|
+
|
106
|
+
def relationship_for_activerecord_hash(rel_name,
|
107
|
+
key_formatter,
|
108
|
+
polymorphic)
|
109
|
+
rel = relationships[rel_name]
|
110
|
+
key = format_key(rel_name, key_formatter)
|
111
|
+
|
112
|
+
if rel.collection?
|
113
|
+
to_many_relationship_for_activerecord_hash(key, rel)
|
114
|
+
else
|
115
|
+
to_one_relationship_for_activerecord_hash(key, rel, polymorphic)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_many_relationship_for_activerecord_hash(key, rel)
|
120
|
+
{ "#{key.singularize}_ids".to_sym => rel.data.map(&:id) }
|
121
|
+
end
|
122
|
+
|
123
|
+
def to_one_relationship_for_activerecord_hash(key, rel, polymorphic)
|
124
|
+
value = rel.data ? rel.data.id : nil
|
125
|
+
hash = { "#{key}_id".to_sym => value }
|
126
|
+
if polymorphic && !rel.data.nil?
|
127
|
+
hash["#{key}_type".to_sym] = rel.data.type.singularize.capitalize
|
128
|
+
end
|
129
|
+
|
130
|
+
hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def format_key(key, key_formatter)
|
134
|
+
if key_formatter
|
135
|
+
key_formatter.call(key)
|
136
|
+
else
|
137
|
+
key
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def filter_keys(keys, filter)
|
142
|
+
if filter[:only]
|
143
|
+
keys & filter[:only].map(&:to_s)
|
144
|
+
elsif filter[:except]
|
145
|
+
keys - filter[:except].map(&:to_s)
|
146
|
+
else
|
147
|
+
keys
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'jsonapi/resource/active_record'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
# c.f. http://jsonapi.org/format/#document-resource-objects
|
5
|
+
class Resource
|
6
|
+
attr_reader :id, :type, :attributes, :relationships, :links, :meta
|
7
|
+
|
8
|
+
def initialize(resource_hash, options = {})
|
9
|
+
@hash = resource_hash
|
10
|
+
@options = options.dup
|
11
|
+
@id_optional = @options.delete(:id_optional)
|
12
|
+
validate!(resource_hash)
|
13
|
+
@id = resource_hash['id']
|
14
|
+
@type = resource_hash['type']
|
15
|
+
@attributes_hash = resource_hash['attributes'] || {}
|
16
|
+
@attributes = Attributes.new(@attributes_hash, @options)
|
17
|
+
@relationships_hash = resource_hash['relationships'] || {}
|
18
|
+
@relationships = Relationships.new(@relationships_hash, @options)
|
19
|
+
@links_hash = resource_hash['links'] || {}
|
20
|
+
@links = Links.new(@links_hash, @options)
|
21
|
+
@meta = resource_hash['meta'] if resource_hash.key?('meta')
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_hash
|
25
|
+
@hash
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def validate!(resource_hash)
|
31
|
+
case
|
32
|
+
when !@id_optional && !resource_hash.key?('id')
|
33
|
+
# We might want to take care of
|
34
|
+
# > Exception: The id member is not required when the resource object
|
35
|
+
# > originates at the client and represents a new resource to be created
|
36
|
+
# > on the server.
|
37
|
+
# in the future.
|
38
|
+
fail InvalidDocument, "a resource object MUST contain an 'id'"
|
39
|
+
when !@id_optional && !resource_hash['id'].is_a?(String)
|
40
|
+
fail InvalidDocument, "the value of 'id' MUST be a string"
|
41
|
+
when !resource_hash.key?('type')
|
42
|
+
fail InvalidDocument, "a resource object MUST contain a 'type'"
|
43
|
+
when !resource_hash['type'].is_a?(String)
|
44
|
+
fail InvalidDocument, "the value of 'type' MUST be a string"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
# c.f. http://jsonapi.org/format/#document-resource-identifier-objects
|
3
|
+
class ResourceIdentifier
|
4
|
+
attr_reader :id, :type
|
5
|
+
|
6
|
+
def initialize(resource_identifier_hash, options = {})
|
7
|
+
@hash = resource_identifier_hash
|
8
|
+
|
9
|
+
validate!(resource_identifier_hash)
|
10
|
+
|
11
|
+
@id = resource_identifier_hash['id']
|
12
|
+
@type = resource_identifier_hash['type']
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_hash
|
16
|
+
@hash
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate!(resource_identifier_hash)
|
22
|
+
case
|
23
|
+
when !resource_identifier_hash.key?('id')
|
24
|
+
fail InvalidDocument,
|
25
|
+
"a resource identifier object MUST contain an 'id'"
|
26
|
+
when !resource_identifier_hash['id'].is_a?(String)
|
27
|
+
fail InvalidDocument, "the value of 'id' MUST be a string"
|
28
|
+
when !resource_identifier_hash.key?('type')
|
29
|
+
fail InvalidDocument,
|
30
|
+
"a resource identifier object MUST contain a 'type'"
|
31
|
+
when !resource_identifier_hash['type'].is_a?(String)
|
32
|
+
fail InvalidDocument, "the value of 'type' MUST be a string"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/jsonapi.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'jsonapi/exceptions'
|
2
|
+
require 'jsonapi/version'
|
3
|
+
|
4
|
+
require 'jsonapi/attributes'
|
5
|
+
require 'jsonapi/document'
|
6
|
+
require 'jsonapi/error'
|
7
|
+
require 'jsonapi/jsonapi'
|
8
|
+
require 'jsonapi/link'
|
9
|
+
require 'jsonapi/links'
|
10
|
+
require 'jsonapi/relationship'
|
11
|
+
require 'jsonapi/relationships'
|
12
|
+
require 'jsonapi/resource'
|
13
|
+
require 'jsonapi/resource_identifier'
|
14
|
+
|
15
|
+
require 'jsonapi/parse'
|
16
|
+
|
17
|
+
module JSONAPI
|
18
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'jsonapi'
|
2
|
+
|
3
|
+
describe JSONAPI, '#parse' do
|
4
|
+
it 'succeeds when there are no duplicates' do
|
5
|
+
payload = {
|
6
|
+
'data' => [
|
7
|
+
{
|
8
|
+
'type' => 'articles',
|
9
|
+
'id' => '1',
|
10
|
+
'attributes' => {
|
11
|
+
'title' => 'JSON API paints my bikeshed!'
|
12
|
+
},
|
13
|
+
'links' => {
|
14
|
+
'self' => 'http://example.com/articles/1'
|
15
|
+
},
|
16
|
+
'relationships' => {
|
17
|
+
'author' => {
|
18
|
+
'links' => {
|
19
|
+
'self' => 'http://example.com/articles/1/relationships/author',
|
20
|
+
'related' => 'http://example.com/articles/1/author'
|
21
|
+
},
|
22
|
+
'data' => { 'type' => 'people', 'id' => '9' }
|
23
|
+
},
|
24
|
+
'journal' => {
|
25
|
+
'data' => nil
|
26
|
+
},
|
27
|
+
'comments' => {
|
28
|
+
'links' => {
|
29
|
+
'self' => 'http://example.com/articles/1/relationships/comments',
|
30
|
+
'related' => 'http://example.com/articles/1/comments'
|
31
|
+
},
|
32
|
+
'data' => [
|
33
|
+
{ 'type' => 'comments', 'id' => '5' },
|
34
|
+
{ 'type' => 'comments', 'id' => '12' }
|
35
|
+
]
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}]
|
39
|
+
}
|
40
|
+
|
41
|
+
JSONAPI.parse(payload, verify_duplicates: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'fails when there are duplicates within primary data' do
|
45
|
+
payload = {
|
46
|
+
'data' => [
|
47
|
+
{
|
48
|
+
'type' => 'articles',
|
49
|
+
'id' => '1'
|
50
|
+
}, {
|
51
|
+
'type' => 'articles',
|
52
|
+
'id' => '1'
|
53
|
+
}]
|
54
|
+
}
|
55
|
+
|
56
|
+
expect { JSONAPI.parse(payload, verify_duplicates: true) }
|
57
|
+
.to raise_error(JSONAPI::InvalidDocument)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'fails when there are duplicates within included' do
|
61
|
+
payload = {
|
62
|
+
'data' => nil,
|
63
|
+
'included' => [
|
64
|
+
{
|
65
|
+
'type' => 'articles',
|
66
|
+
'id' => '1'
|
67
|
+
}, {
|
68
|
+
'type' => 'articles',
|
69
|
+
'id' => '1'
|
70
|
+
}]
|
71
|
+
}
|
72
|
+
|
73
|
+
expect { JSONAPI.parse(payload, verify_duplicates: true) }
|
74
|
+
.to raise_error(JSONAPI::InvalidDocument)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'fails when there are duplicates within primary data' do
|
78
|
+
payload = {
|
79
|
+
'data' => [
|
80
|
+
{
|
81
|
+
'type' => 'articles',
|
82
|
+
'id' => '1'
|
83
|
+
}
|
84
|
+
],
|
85
|
+
'included' => [
|
86
|
+
{
|
87
|
+
'type' => 'articles',
|
88
|
+
'id' => '1'
|
89
|
+
}]
|
90
|
+
}
|
91
|
+
|
92
|
+
expect { JSONAPI.parse(payload, verify_duplicates: true) }
|
93
|
+
.to raise_error(JSONAPI::InvalidDocument)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'jsonapi'
|
2
|
+
|
3
|
+
describe JSONAPI, '#parse' do
|
4
|
+
it 'succeeds when no included property is provided' do
|
5
|
+
payload = {
|
6
|
+
'data' => [
|
7
|
+
{
|
8
|
+
'type' => 'articles',
|
9
|
+
'id' => '1',
|
10
|
+
'attributes' => {
|
11
|
+
'title' => 'JSON API paints my bikeshed!'
|
12
|
+
},
|
13
|
+
'links' => {
|
14
|
+
'self' => 'http://example.com/articles/1'
|
15
|
+
},
|
16
|
+
'relationships' => {
|
17
|
+
'author' => {
|
18
|
+
'links' => {
|
19
|
+
'self' => 'http://example.com/articles/1/relationships/author',
|
20
|
+
'related' => 'http://example.com/articles/1/author'
|
21
|
+
},
|
22
|
+
'data' => { 'type' => 'people', 'id' => '9' }
|
23
|
+
},
|
24
|
+
'journal' => {
|
25
|
+
'data' => nil
|
26
|
+
},
|
27
|
+
'comments' => {
|
28
|
+
'links' => {
|
29
|
+
'self' => 'http://example.com/articles/1/relationships/comments',
|
30
|
+
'related' => 'http://example.com/articles/1/comments'
|
31
|
+
},
|
32
|
+
'data' => [
|
33
|
+
{ 'type' => 'comments', 'id' => '5' },
|
34
|
+
{ 'type' => 'comments', 'id' => '12' }
|
35
|
+
]
|
36
|
+
}
|
37
|
+
}
|
38
|
+
}]
|
39
|
+
}
|
40
|
+
|
41
|
+
JSONAPI.parse(payload, verify_linkage: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'succeeds when full linkage is respected' do
|
45
|
+
payload = {
|
46
|
+
'data' => [
|
47
|
+
{
|
48
|
+
'type' => 'articles',
|
49
|
+
'id' => '1',
|
50
|
+
'attributes' => {
|
51
|
+
'title' => 'JSON API paints my bikeshed!'
|
52
|
+
},
|
53
|
+
'links' => {
|
54
|
+
'self' => 'http://example.com/articles/1'
|
55
|
+
},
|
56
|
+
'relationships' => {
|
57
|
+
'author' => {
|
58
|
+
'links' => {
|
59
|
+
'self' => 'http://example.com/articles/1/relationships/author',
|
60
|
+
'related' => 'http://example.com/articles/1/author'
|
61
|
+
},
|
62
|
+
'data' => { 'type' => 'people', 'id' => '9' }
|
63
|
+
},
|
64
|
+
'journal' => {
|
65
|
+
'data' => nil
|
66
|
+
},
|
67
|
+
'comments' => {
|
68
|
+
'links' => {
|
69
|
+
'self' => 'http://example.com/articles/1/relationships/comments',
|
70
|
+
'related' => 'http://example.com/articles/1/comments'
|
71
|
+
},
|
72
|
+
'data' => [
|
73
|
+
{ 'type' => 'comments', 'id' => '5' },
|
74
|
+
{ 'type' => 'comments', 'id' => '12' }
|
75
|
+
]
|
76
|
+
}
|
77
|
+
}
|
78
|
+
}],
|
79
|
+
'included' => [
|
80
|
+
{
|
81
|
+
'type' => 'comments',
|
82
|
+
'id' => '5'
|
83
|
+
}, {
|
84
|
+
'type' => 'comments',
|
85
|
+
'id' => '12'
|
86
|
+
}, {
|
87
|
+
'type' => 'comments',
|
88
|
+
'id' => '13'
|
89
|
+
}, {
|
90
|
+
'type' => 'people',
|
91
|
+
'id' => '9',
|
92
|
+
'relationships' => {
|
93
|
+
'comments' => {
|
94
|
+
'data' => [
|
95
|
+
{
|
96
|
+
'type' => 'comments',
|
97
|
+
'id' => '13'
|
98
|
+
}
|
99
|
+
]
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
]
|
104
|
+
}
|
105
|
+
|
106
|
+
JSONAPI.parse(payload, verify_linkage: true)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'fails when full linkage is not respected' do
|
110
|
+
payload = {
|
111
|
+
'data' => [
|
112
|
+
{
|
113
|
+
'type' => 'articles',
|
114
|
+
'id' => '1',
|
115
|
+
'attributes' => {
|
116
|
+
'title' => 'JSON API paints my bikeshed!'
|
117
|
+
},
|
118
|
+
'links' => {
|
119
|
+
'self' => 'http://example.com/articles/1'
|
120
|
+
},
|
121
|
+
'relationships' => {
|
122
|
+
'author' => {
|
123
|
+
'links' => {
|
124
|
+
'self' => 'http://example.com/articles/1/relationships/author',
|
125
|
+
'related' => 'http://example.com/articles/1/author'
|
126
|
+
},
|
127
|
+
'data' => { 'type' => 'people', 'id' => '9' }
|
128
|
+
},
|
129
|
+
'journal' => {
|
130
|
+
'data' => nil
|
131
|
+
},
|
132
|
+
'comments' => {
|
133
|
+
'links' => {
|
134
|
+
'self' => 'http://example.com/articles/1/relationships/comments',
|
135
|
+
'related' => 'http://example.com/articles/1/comments'
|
136
|
+
},
|
137
|
+
'data' => [
|
138
|
+
{ 'type' => 'comments', 'id' => '5' },
|
139
|
+
{ 'type' => 'comments', 'id' => '12' }
|
140
|
+
]
|
141
|
+
}
|
142
|
+
}
|
143
|
+
}],
|
144
|
+
'included' => [
|
145
|
+
{
|
146
|
+
'type' => 'comments',
|
147
|
+
'id' => '5'
|
148
|
+
}, {
|
149
|
+
'type' => 'comments',
|
150
|
+
'id' => '12'
|
151
|
+
}, {
|
152
|
+
'type' => 'comments',
|
153
|
+
'id' => '13'
|
154
|
+
}
|
155
|
+
]
|
156
|
+
}
|
157
|
+
|
158
|
+
expect { JSONAPI.parse(payload, verify_linkage: true) }
|
159
|
+
.to raise_error(JSONAPI::InvalidDocument)
|
160
|
+
end
|
161
|
+
end
|