jsonapi 0.1.1.beta1
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 +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
|