jsonapi-resources 0.9.12 → 0.10.0.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 +5 -5
- data/LICENSE.txt +1 -1
- data/README.md +34 -11
- data/lib/bug_report_templates/rails_5_latest.rb +125 -0
- data/lib/bug_report_templates/rails_5_master.rb +140 -0
- data/lib/jsonapi-resources.rb +8 -3
- data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
- data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
- data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
- data/lib/jsonapi/compiled_json.rb +11 -1
- data/lib/jsonapi/configuration.rb +44 -18
- data/lib/jsonapi/error.rb +27 -0
- data/lib/jsonapi/exceptions.rb +43 -40
- data/lib/jsonapi/formatter.rb +3 -3
- data/lib/jsonapi/include_directives.rb +2 -45
- data/lib/jsonapi/link_builder.rb +87 -80
- data/lib/jsonapi/operation.rb +16 -5
- data/lib/jsonapi/operation_result.rb +74 -16
- data/lib/jsonapi/processor.rb +233 -112
- data/lib/jsonapi/relationship.rb +77 -53
- data/lib/jsonapi/request_parser.rb +378 -423
- data/lib/jsonapi/resource.rb +224 -524
- data/lib/jsonapi/resource_controller_metal.rb +2 -2
- data/lib/jsonapi/resource_fragment.rb +47 -0
- data/lib/jsonapi/resource_id_tree.rb +112 -0
- data/lib/jsonapi/resource_identity.rb +42 -0
- data/lib/jsonapi/resource_serializer.rb +133 -301
- data/lib/jsonapi/resource_set.rb +108 -0
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +100 -88
- data/lib/jsonapi/routing_ext.rb +21 -43
- metadata +29 -45
- data/lib/jsonapi/operation_dispatcher.rb +0 -88
- data/lib/jsonapi/operation_results.rb +0 -35
- data/lib/jsonapi/relationship_builder.rb +0 -167
@@ -5,10 +5,10 @@ module JSONAPI
|
|
5
5
|
ActionController::Rendering,
|
6
6
|
ActionController::Renderers::All,
|
7
7
|
ActionController::StrongParameters,
|
8
|
-
|
8
|
+
ActionController::ForceSSL,
|
9
9
|
ActionController::Instrumentation,
|
10
10
|
JSONAPI::ActsAsResourceController
|
11
|
-
].
|
11
|
+
].freeze
|
12
12
|
|
13
13
|
MODULES.each do |mod|
|
14
14
|
include mod
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
|
3
|
+
# A ResourceFragment holds a ResourceIdentity and associated partial resource data.
|
4
|
+
#
|
5
|
+
# The following partial resource data may be stored
|
6
|
+
# cache - the value of the cache field for the resource instance
|
7
|
+
# related - a hash of arrays of related resource identities, grouped by relationship name
|
8
|
+
# related_from - a set of related resource identities that loaded the fragment
|
9
|
+
#
|
10
|
+
# Todo: optionally use these for faster responses by bypassing model instantiation)
|
11
|
+
# attributes - resource attributes
|
12
|
+
|
13
|
+
class ResourceFragment
|
14
|
+
attr_reader :identity, :attributes, :related_from, :related
|
15
|
+
|
16
|
+
attr_accessor :primary, :cache
|
17
|
+
|
18
|
+
alias :cache_field :cache #ToDo: Rename one or the other
|
19
|
+
|
20
|
+
def initialize(identity)
|
21
|
+
@identity = identity
|
22
|
+
@cache = nil
|
23
|
+
@attributes = {}
|
24
|
+
@related = {}
|
25
|
+
@primary = false
|
26
|
+
@related_from = Set.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize_related(relationship_name)
|
30
|
+
@related ||= {}
|
31
|
+
@related[relationship_name.to_sym] ||= Set.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_related_identity(relationship_name, identity)
|
35
|
+
initialize_related(relationship_name)
|
36
|
+
@related[relationship_name.to_sym] << identity
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_related_from(identity)
|
40
|
+
@related_from << identity
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_attribute(name, value)
|
44
|
+
@attributes[name] = value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
|
3
|
+
# A tree structure representing the resource structure of the requested resource(s). This is an intermediate structure
|
4
|
+
# used to keep track of the resources, by identity, found at different included relationships. It will be flattened and
|
5
|
+
# the resource instances will be fetched from the cache or the record store.
|
6
|
+
class ResourceIdTree
|
7
|
+
|
8
|
+
attr_reader :fragments, :related_resource_id_trees
|
9
|
+
|
10
|
+
# Gets the related Resource Id Tree for a relationship, and creates it first if it does not exist
|
11
|
+
#
|
12
|
+
# @param relationship [JSONAPI::Relationship]
|
13
|
+
#
|
14
|
+
# @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship
|
15
|
+
def fetch_related_resource_id_tree(relationship)
|
16
|
+
relationship_name = relationship.name.to_sym
|
17
|
+
@related_resource_id_trees[relationship_name] ||= RelatedResourceIdTree.new(relationship, self)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def init_included_relationships(fragment, include_related)
|
23
|
+
include_related && include_related.each_key do |relationship_name|
|
24
|
+
fragment.initialize_related(relationship_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class PrimaryResourceIdTree < ResourceIdTree
|
30
|
+
|
31
|
+
# Creates a PrimaryResourceIdTree with no resources and no related ResourceIdTrees
|
32
|
+
def initialize
|
33
|
+
@fragments ||= {}
|
34
|
+
@related_resource_id_trees ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
# Adds each Resource Fragment to the Resources hash
|
38
|
+
#
|
39
|
+
# @param fragments [Hash]
|
40
|
+
# @param include_related [Hash]
|
41
|
+
#
|
42
|
+
# @return [null]
|
43
|
+
def add_resource_fragments(fragments, include_related)
|
44
|
+
fragments.each_value do |fragment|
|
45
|
+
add_resource_fragment(fragment, include_related)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds a Resource Fragment to the Resources hash
|
50
|
+
#
|
51
|
+
# @param fragment [JSONAPI::ResourceFragment]
|
52
|
+
# @param include_related [Hash]
|
53
|
+
#
|
54
|
+
# @return [null]
|
55
|
+
def add_resource_fragment(fragment, include_related)
|
56
|
+
fragment.primary = true
|
57
|
+
|
58
|
+
init_included_relationships(fragment, include_related)
|
59
|
+
|
60
|
+
@fragments[fragment.identity] = fragment
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class RelatedResourceIdTree < ResourceIdTree
|
65
|
+
|
66
|
+
attr_reader :parent_relationship, :source_resource_id_tree
|
67
|
+
|
68
|
+
# Creates a RelatedResourceIdTree with no resources and no related ResourceIdTrees. A connection to the parent
|
69
|
+
# ResourceIdTree is maintained.
|
70
|
+
#
|
71
|
+
# @param parent_relationship [JSONAPI::Relationship]
|
72
|
+
# @param source_resource_id_tree [JSONAPI::ResourceIdTree]
|
73
|
+
#
|
74
|
+
# @return [JSONAPI::RelatedResourceIdTree] the new or existing resource id tree for the requested relationship
|
75
|
+
def initialize(parent_relationship, source_resource_id_tree)
|
76
|
+
@fragments ||= {}
|
77
|
+
@related_resource_id_trees ||= {}
|
78
|
+
|
79
|
+
@parent_relationship = parent_relationship
|
80
|
+
@parent_relationship_name = parent_relationship.name.to_sym
|
81
|
+
@source_resource_id_tree = source_resource_id_tree
|
82
|
+
end
|
83
|
+
|
84
|
+
# Adds each Resource Fragment to the Resources hash
|
85
|
+
#
|
86
|
+
# @param fragments [Hash]
|
87
|
+
# @param include_related [Hash]
|
88
|
+
#
|
89
|
+
# @return [null]
|
90
|
+
def add_resource_fragments(fragments, include_related)
|
91
|
+
fragments.each_value do |fragment|
|
92
|
+
add_resource_fragment(fragment, include_related)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Adds a Resource Fragment to the fragments hash
|
97
|
+
#
|
98
|
+
# @param fragment [JSONAPI::ResourceFragment]
|
99
|
+
# @param include_related [Hash]
|
100
|
+
#
|
101
|
+
# @return [null]
|
102
|
+
def add_resource_fragment(fragment, include_related)
|
103
|
+
init_included_relationships(fragment, include_related)
|
104
|
+
|
105
|
+
fragment.related_from.each do |rid|
|
106
|
+
@source_resource_id_tree.fragments[rid].add_related_identity(parent_relationship.name, fragment.identity)
|
107
|
+
end
|
108
|
+
|
109
|
+
@fragments[fragment.identity] = fragment
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
|
3
|
+
# ResourceIdentity describes a unique identity of a resource in the system.
|
4
|
+
# This consists of a Resource class and an identifier that is unique within
|
5
|
+
# that Resource class. ResourceIdentities are intended to be used as hash
|
6
|
+
# keys to provide ordered mixing of resource types in result sets.
|
7
|
+
#
|
8
|
+
#
|
9
|
+
# == Creating a ResourceIdentity
|
10
|
+
#
|
11
|
+
# rid = ResourceIdentity.new(PostResource, 12)
|
12
|
+
#
|
13
|
+
class ResourceIdentity
|
14
|
+
attr_reader :resource_klass, :id
|
15
|
+
|
16
|
+
def initialize(resource_klass, id)
|
17
|
+
@resource_klass = resource_klass
|
18
|
+
@id = id
|
19
|
+
end
|
20
|
+
|
21
|
+
def ==(other)
|
22
|
+
# :nocov:
|
23
|
+
eql?(other)
|
24
|
+
# :nocov:
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(other)
|
28
|
+
other.is_a?(ResourceIdentity) && other.resource_klass == @resource_klass && other.id == @id
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
[@resource_klass, @id].hash
|
33
|
+
end
|
34
|
+
|
35
|
+
# Creates a string representation of the identifier.
|
36
|
+
def to_s
|
37
|
+
# :nocov:
|
38
|
+
"#{resource_klass}:#{id}"
|
39
|
+
# :nocov:
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
class ResourceSerializer
|
3
3
|
|
4
|
-
attr_reader :link_builder, :key_formatter, :serialization_options,
|
4
|
+
attr_reader :link_builder, :key_formatter, :serialization_options,
|
5
5
|
:fields, :include_directives, :always_include_to_one_linkage_data,
|
6
6
|
:always_include_to_many_linkage_data
|
7
7
|
|
@@ -19,7 +19,6 @@ module JSONAPI
|
|
19
19
|
|
20
20
|
def initialize(primary_resource_klass, options = {})
|
21
21
|
@primary_resource_klass = primary_resource_klass
|
22
|
-
@primary_class_name = primary_resource_klass._type
|
23
22
|
@fields = options.fetch(:fields, {})
|
24
23
|
@include = options.fetch(:include, [])
|
25
24
|
@include_directives = options[:include_directives]
|
@@ -42,67 +41,72 @@ module JSONAPI
|
|
42
41
|
@_supplying_relationship_fields = {}
|
43
42
|
end
|
44
43
|
|
45
|
-
# Converts a
|
46
|
-
def
|
47
|
-
@top_level_sources = Set.new([source].flatten(1).compact.map {|s| top_level_source_key(s) })
|
48
|
-
|
49
|
-
is_resource_collection = source.respond_to?(:to_ary)
|
50
|
-
|
51
|
-
@included_objects = {}
|
52
|
-
|
53
|
-
process_source_objects(source, @include_directives.include_directives)
|
44
|
+
# Converts a resource_set to a hash, conforming to the JSONAPI structure
|
45
|
+
def serialize_resource_set_to_hash_single(resource_set)
|
54
46
|
|
55
47
|
primary_objects = []
|
48
|
+
included_objects = []
|
56
49
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
else
|
69
|
-
if source.try(:id)
|
70
|
-
case source
|
71
|
-
when CachedResourceFragment then primary_objects.push(@included_objects[source.type][source.id][:object_hash])
|
72
|
-
when Resource then primary_objects.push(@included_objects[source.class._type][source.id][:object_hash])
|
73
|
-
else raise "Unknown source type #{source.inspect}"
|
50
|
+
resource_set.resource_klasses.each_value do |resource_klass|
|
51
|
+
resource_klass.each_value do |resource|
|
52
|
+
serialized_resource = object_hash(resource[:resource], resource[:relationships])
|
53
|
+
|
54
|
+
if resource[:primary]
|
55
|
+
primary_objects.push(serialized_resource)
|
56
|
+
else
|
57
|
+
included_objects.push(serialized_resource)
|
74
58
|
end
|
75
59
|
end
|
76
60
|
end
|
77
61
|
|
62
|
+
fail "To Many primary objects for show" if (primary_objects.count > 1)
|
63
|
+
primary_hash = { 'data' => primary_objects[0] }
|
64
|
+
|
65
|
+
primary_hash['included'] = included_objects if included_objects.size > 0
|
66
|
+
primary_hash
|
67
|
+
end
|
68
|
+
|
69
|
+
def serialize_resource_set_to_hash_plural(resource_set)
|
70
|
+
|
71
|
+
primary_objects = []
|
78
72
|
included_objects = []
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
73
|
+
|
74
|
+
resource_set.resource_klasses.each_value do |resource_klass|
|
75
|
+
resource_klass.each_value do |resource|
|
76
|
+
serialized_resource = object_hash(resource[:resource], resource[:relationships])
|
77
|
+
|
78
|
+
if resource[:primary]
|
79
|
+
primary_objects.push(serialized_resource)
|
80
|
+
else
|
81
|
+
included_objects.push(serialized_resource)
|
83
82
|
end
|
84
83
|
end
|
85
84
|
end
|
86
85
|
|
87
|
-
primary_hash = { data
|
86
|
+
primary_hash = { 'data' => primary_objects }
|
88
87
|
|
89
|
-
primary_hash[
|
88
|
+
primary_hash['included'] = included_objects if included_objects.size > 0
|
90
89
|
primary_hash
|
91
90
|
end
|
92
91
|
|
93
|
-
def
|
92
|
+
def serialize_related_resource_set_to_hash_plural(resource_set, _source_resource)
|
93
|
+
return serialize_resource_set_to_hash_plural(resource_set)
|
94
|
+
end
|
95
|
+
|
96
|
+
def serialize_to_links_hash(source, requested_relationship, resource_ids)
|
94
97
|
if requested_relationship.is_a?(JSONAPI::Relationship::ToOne)
|
95
|
-
data = to_one_linkage(
|
98
|
+
data = to_one_linkage(resource_ids[0])
|
96
99
|
else
|
97
|
-
data = to_many_linkage(
|
100
|
+
data = to_many_linkage(resource_ids)
|
98
101
|
end
|
99
102
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
103
|
+
{
|
104
|
+
'links' => {
|
105
|
+
'self' => self_link(source, requested_relationship),
|
106
|
+
'related' => related_link(source, requested_relationship)
|
107
|
+
},
|
108
|
+
'data' => data
|
109
|
+
}
|
106
110
|
end
|
107
111
|
|
108
112
|
def query_link(query_params)
|
@@ -113,6 +117,10 @@ module JSONAPI
|
|
113
117
|
@key_formatter.format(key)
|
114
118
|
end
|
115
119
|
|
120
|
+
def unformat_key(key)
|
121
|
+
@key_formatter.unformat(key)
|
122
|
+
end
|
123
|
+
|
116
124
|
def format_value(value, format)
|
117
125
|
@value_formatter_type_cache.get(format).format(value)
|
118
126
|
end
|
@@ -132,30 +140,35 @@ module JSONAPI
|
|
132
140
|
supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
|
133
141
|
supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
|
134
142
|
link_builder_base_url: link_builder.base_url,
|
143
|
+
route_formatter_class: link_builder.route_formatter.uncached.class.name,
|
135
144
|
key_formatter_class: key_formatter.uncached.class.name,
|
136
145
|
always_include_to_one_linkage_data: always_include_to_one_linkage_data,
|
137
146
|
always_include_to_many_linkage_data: always_include_to_many_linkage_data
|
138
147
|
}
|
139
148
|
end
|
140
149
|
|
141
|
-
|
142
|
-
def object_hash(source, include_directives = {})
|
150
|
+
def object_hash(source, relationship_data)
|
143
151
|
obj_hash = {}
|
144
152
|
|
145
|
-
if source.
|
146
|
-
|
153
|
+
return obj_hash if source.nil?
|
154
|
+
|
155
|
+
fetchable_fields = Set.new(source.fetchable_fields)
|
156
|
+
|
157
|
+
if source.is_a?(JSONAPI::CachedResponseFragment)
|
158
|
+
id_format = source.resource_klass._attribute_options(:id)[:format]
|
159
|
+
|
160
|
+
id_format = 'id' if id_format == :default
|
161
|
+
obj_hash['id'] = format_value(source.id, id_format)
|
147
162
|
obj_hash['type'] = source.type
|
148
163
|
|
149
164
|
obj_hash['links'] = source.links_json if source.links_json
|
150
165
|
obj_hash['attributes'] = source.attributes_json if source.attributes_json
|
151
166
|
|
152
|
-
relationships = cached_relationships_hash(source,
|
153
|
-
obj_hash['relationships'] = relationships unless relationships.
|
167
|
+
relationships = cached_relationships_hash(source, fetchable_fields, relationship_data)
|
168
|
+
obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
|
154
169
|
|
155
170
|
obj_hash['meta'] = source.meta_json if source.meta_json
|
156
171
|
else
|
157
|
-
fetchable_fields = Set.new(source.fetchable_fields)
|
158
|
-
|
159
172
|
# TODO Should this maybe be using @id_formatter instead, for consistency?
|
160
173
|
id_format = source.class._attribute_options(:id)[:format]
|
161
174
|
# protect against ids that were declared as an attribute, but did not have a format set.
|
@@ -170,8 +183,8 @@ module JSONAPI
|
|
170
183
|
attributes = attributes_hash(source, fetchable_fields)
|
171
184
|
obj_hash['attributes'] = attributes unless attributes.empty?
|
172
185
|
|
173
|
-
relationships = relationships_hash(source, fetchable_fields,
|
174
|
-
obj_hash['relationships'] = relationships unless relationships.
|
186
|
+
relationships = relationships_hash(source, fetchable_fields, relationship_data)
|
187
|
+
obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
|
175
188
|
|
176
189
|
meta = meta_hash(source)
|
177
190
|
obj_hash['meta'] = meta unless meta.empty?
|
@@ -182,19 +195,6 @@ module JSONAPI
|
|
182
195
|
|
183
196
|
private
|
184
197
|
|
185
|
-
# Process the primary source object(s). This will then serialize associated object recursively based on the
|
186
|
-
# requested includes. Fields are controlled fields option for each resource type, such
|
187
|
-
# as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
|
188
|
-
# The fields options controls both fields and included links references.
|
189
|
-
def process_source_objects(source, include_directives)
|
190
|
-
if source.respond_to?(:to_ary)
|
191
|
-
source.each { |resource| process_source_objects(resource, include_directives) }
|
192
|
-
else
|
193
|
-
return {} if source.nil?
|
194
|
-
add_resource(source, include_directives, true)
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
198
|
def supplying_attribute_fields(resource_klass)
|
199
199
|
@_supplying_attribute_fields.fetch resource_klass do
|
200
200
|
attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
|
@@ -249,9 +249,7 @@ module JSONAPI
|
|
249
249
|
|
250
250
|
def links_hash(source)
|
251
251
|
links = custom_links_hash(source)
|
252
|
-
|
253
|
-
links['self'] = link_builder.self_link(source)
|
254
|
-
end
|
252
|
+
links['self'] = link_builder.self_link(source) unless links.key?('self')
|
255
253
|
links.compact
|
256
254
|
end
|
257
255
|
|
@@ -260,118 +258,61 @@ module JSONAPI
|
|
260
258
|
(custom_links.is_a?(Hash) && custom_links) || {}
|
261
259
|
end
|
262
260
|
|
263
|
-
def
|
264
|
-
|
265
|
-
when CachedResourceFragment then "#{source.resource_klass}_#{source.id}"
|
266
|
-
when Resource then "#{source.class}_#{@id_formatter.format(source.id)}"
|
267
|
-
else raise "Unknown source type #{source.inspect}"
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
def self_referential_and_already_in_source(resource)
|
272
|
-
resource && @top_level_sources.include?(top_level_source_key(resource))
|
273
|
-
end
|
274
|
-
|
275
|
-
def relationships_hash(source, fetchable_fields, include_directives = {})
|
276
|
-
if source.is_a?(CachedResourceFragment)
|
277
|
-
return cached_relationships_hash(source, include_directives)
|
278
|
-
end
|
279
|
-
|
280
|
-
include_directives[:include_related] ||= {}
|
281
|
-
|
282
|
-
relationships = source.class._relationships.select{|k,v| fetchable_fields.include?(k) }
|
261
|
+
def relationships_hash(source, fetchable_fields, relationship_data)
|
262
|
+
relationships = source.class._relationships.select{|k,_v| fetchable_fields.include?(k) }
|
283
263
|
field_set = supplying_relationship_fields(source.class) & relationships.keys
|
284
264
|
|
285
265
|
relationships.each_with_object({}) do |(name, relationship), hash|
|
286
|
-
|
287
|
-
include_linkage = ia && ia[:include]
|
288
|
-
include_linked_children = ia && !ia[:include_related].empty?
|
289
|
-
|
266
|
+
include_data = false
|
290
267
|
if field_set.include?(name)
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
# through the relationships.
|
298
|
-
if include_linkage || include_linked_children
|
299
|
-
resources = if source.preloaded_fragments.has_key?(format_key(name))
|
300
|
-
source.preloaded_fragments[format_key(name)].values
|
301
|
-
else
|
302
|
-
options = { filters: ia && ia[:include_filters] || {} }
|
303
|
-
[source.public_send(name, options)].flatten(1).compact
|
304
|
-
end
|
305
|
-
resources.each do |resource|
|
306
|
-
next if self_referential_and_already_in_source(resource)
|
307
|
-
id = resource.id
|
308
|
-
relationships_only = already_serialized?(relationship.type, id)
|
309
|
-
if include_linkage && !relationships_only
|
310
|
-
add_resource(resource, ia)
|
311
|
-
elsif include_linked_children || relationships_only
|
312
|
-
relationships_hash(resource, fetchable_fields, ia)
|
268
|
+
if relationship_data[name]
|
269
|
+
include_data = true
|
270
|
+
if relationship.is_a?(JSONAPI::Relationship::ToOne)
|
271
|
+
rids = relationship_data[name].first
|
272
|
+
else
|
273
|
+
rids = relationship_data[name]
|
313
274
|
end
|
314
275
|
end
|
276
|
+
|
277
|
+
hash[format_key(name)] = link_object(source, relationship, rids, include_data)
|
315
278
|
end
|
316
279
|
end
|
317
280
|
end
|
318
281
|
|
319
|
-
def cached_relationships_hash(source,
|
320
|
-
|
321
|
-
return h unless include_directives.has_key?(:include_related)
|
282
|
+
def cached_relationships_hash(source, fetchable_fields, relationship_data)
|
283
|
+
relationships = {}
|
322
284
|
|
323
|
-
|
324
|
-
|
285
|
+
source.relationships.try(:each_pair) do |k,v|
|
286
|
+
if fetchable_fields.include?(unformat_key(k).to_sym)
|
287
|
+
relationships[k.to_sym] = v
|
288
|
+
end
|
325
289
|
end
|
326
290
|
|
327
|
-
|
328
|
-
relationships.each do |rel_name, relationship|
|
329
|
-
key = @key_formatter.format(rel_name)
|
330
|
-
to_many = relationship.is_a? JSONAPI::Relationship::ToMany
|
291
|
+
field_set = supplying_relationship_fields(source.resource_klass).collect {|k| format_key(k).to_sym } & relationships.keys
|
331
292
|
|
332
|
-
|
333
|
-
if
|
334
|
-
|
335
|
-
|
336
|
-
|
293
|
+
relationships.each_with_object({}) do |(name, relationship), hash|
|
294
|
+
if field_set.include?(name)
|
295
|
+
|
296
|
+
relationship_name = unformat_key(name).to_sym
|
297
|
+
relationship_klass = source.resource_klass._relationships[relationship_name]
|
337
298
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
real_res = source.to_real_resource
|
299
|
+
if relationship_klass.is_a?(JSONAPI::Relationship::ToOne)
|
300
|
+
# include_linkage = @always_include_to_one_linkage_data | relationship_klass.always_include_linkage_data
|
301
|
+
if relationship_data[relationship_name]
|
302
|
+
rids = relationship_data[relationship_name].first
|
303
|
+
relationship['data'] = to_one_linkage(rids)
|
344
304
|
end
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
if h.has_key?(key)
|
352
|
-
# The hash already has everything we need except the :data field
|
353
|
-
data = {
|
354
|
-
type: format_key(f.is_a?(Resource) ? f.class._type : f.type),
|
355
|
-
id: @id_formatter.format(id)
|
356
|
-
}
|
357
|
-
|
358
|
-
if to_many
|
359
|
-
h[key][:data] << data
|
360
|
-
else
|
361
|
-
h[key][:data] = data
|
362
|
-
end
|
305
|
+
else
|
306
|
+
# include_linkage = relationship_klass.always_include_linkage_data
|
307
|
+
if relationship_data[relationship_name]
|
308
|
+
rids = relationship_data[relationship_name]
|
309
|
+
relationship['data'] = to_many_linkage(rids)
|
363
310
|
end
|
364
311
|
end
|
312
|
+
|
313
|
+
hash[format_key(name)] = relationship
|
365
314
|
end
|
366
315
|
end
|
367
|
-
|
368
|
-
return h
|
369
|
-
end
|
370
|
-
|
371
|
-
def already_serialized?(type, id)
|
372
|
-
type = format_key(type)
|
373
|
-
id = @id_formatter.format(id)
|
374
|
-
@included_objects.key?(type) && @included_objects[type].key?(id)
|
375
316
|
end
|
376
317
|
|
377
318
|
def self_link(source, relationship)
|
@@ -382,169 +323,60 @@ module JSONAPI
|
|
382
323
|
link_builder.relationships_related_link(source, relationship)
|
383
324
|
end
|
384
325
|
|
385
|
-
def
|
386
|
-
links = {}
|
387
|
-
links['self'] = self_link(source, relationship) unless relationship.exclude_link?(:self)
|
388
|
-
links['related'] = related_link(source, relationship) unless relationship.exclude_link?(:related)
|
389
|
-
links.compact
|
390
|
-
end
|
391
|
-
|
392
|
-
def to_one_linkage(source, relationship)
|
393
|
-
linkage_id = foreign_key_value(source, relationship)
|
394
|
-
linkage_type = format_key(relationship.type_for_source(source))
|
395
|
-
return unless linkage_id.present? && linkage_type.present?
|
396
|
-
|
397
|
-
{
|
398
|
-
type: linkage_type,
|
399
|
-
id: linkage_id,
|
400
|
-
}
|
401
|
-
end
|
402
|
-
|
403
|
-
def to_many_linkage(source, relationship)
|
326
|
+
def to_many_linkage(rids)
|
404
327
|
linkage = []
|
405
|
-
include_config = include_directives.include_config(relationship.name.to_sym) if include_directives
|
406
|
-
include_filters = include_config[:include_filters] if include_config
|
407
|
-
options = { filters: include_filters || {} }
|
408
328
|
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
assoc = source.public_send("records_for_#{relationship.name}", options)
|
415
|
-
# Avoid hitting the database again for values already pre-loaded
|
416
|
-
if assoc.respond_to?(:loaded?) and assoc.loaded?
|
417
|
-
assoc.map do |obj|
|
418
|
-
[obj.type.underscore.pluralize, obj.id]
|
419
|
-
end
|
420
|
-
else
|
421
|
-
assoc = assoc.unscope(:includes) if assoc.is_a?(ActiveRecord::Relation)
|
422
|
-
assoc.pluck(:type, :id).map do |type, id|
|
423
|
-
[type.underscore.pluralize, id]
|
424
|
-
end
|
425
|
-
end
|
426
|
-
else
|
427
|
-
source.public_send(relationship.name, options).map do |value|
|
428
|
-
[relationship.type, value.id]
|
329
|
+
rids && rids.each do |details|
|
330
|
+
id = details.id
|
331
|
+
type = details.resource_klass.try(:_type)
|
332
|
+
if type && id
|
333
|
+
linkage.append({'type' => format_key(type), 'id' => @id_formatter.format(id)})
|
429
334
|
end
|
430
335
|
end
|
431
336
|
|
432
|
-
linkage_types_and_values.each do |type, value|
|
433
|
-
if type && value
|
434
|
-
linkage.append({type: format_key(type), id: @id_formatter.format(value)})
|
435
|
-
end
|
436
|
-
end
|
437
337
|
linkage
|
438
338
|
end
|
439
339
|
|
440
|
-
def
|
441
|
-
|
442
|
-
relationship_object_hash = {}
|
443
|
-
|
444
|
-
links = default_relationship_links(source, relationship)
|
340
|
+
def to_one_linkage(rid)
|
341
|
+
return unless rid
|
445
342
|
|
446
|
-
|
447
|
-
|
448
|
-
|
343
|
+
{
|
344
|
+
'type' => format_key(rid.resource_klass._type),
|
345
|
+
'id' => @id_formatter.format(rid.id),
|
346
|
+
}
|
449
347
|
end
|
450
348
|
|
451
|
-
def
|
452
|
-
|
453
|
-
|
349
|
+
def link_object_to_one(source, relationship, rid, include_data)
|
350
|
+
link_object_hash = {}
|
351
|
+
link_object_hash['links'] = {}
|
352
|
+
link_object_hash['links']['self'] = self_link(source, relationship)
|
353
|
+
link_object_hash['links']['related'] = related_link(source, relationship)
|
354
|
+
link_object_hash['data'] = to_one_linkage(rid) if include_data
|
355
|
+
link_object_hash
|
356
|
+
end
|
454
357
|
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
358
|
+
def link_object_to_many(source, relationship, rids, include_data)
|
359
|
+
link_object_hash = {}
|
360
|
+
link_object_hash['links'] = {}
|
361
|
+
link_object_hash['links']['self'] = self_link(source, relationship)
|
362
|
+
link_object_hash['links']['related'] = related_link(source, relationship)
|
363
|
+
link_object_hash['data'] = to_many_linkage(rids) if include_data
|
364
|
+
link_object_hash
|
459
365
|
end
|
460
366
|
|
461
|
-
def
|
367
|
+
def link_object(source, relationship, rid, include_data)
|
462
368
|
if relationship.is_a?(JSONAPI::Relationship::ToOne)
|
463
|
-
|
369
|
+
link_object_to_one(source, relationship, rid, include_data)
|
464
370
|
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
|
465
|
-
|
466
|
-
end
|
467
|
-
end
|
468
|
-
|
469
|
-
# Extracts the foreign key value for a to_one relationship.
|
470
|
-
def foreign_key_value(source, relationship)
|
471
|
-
# If you have changed the key_name, don't even try to look at `"#{relationship.name}_id"`
|
472
|
-
# just load the association and call the custom key_name
|
473
|
-
foreign_key_type_changed = relationship.options[:foreign_key_type_changed] || false
|
474
|
-
related_resource_id =
|
475
|
-
if source.preloaded_fragments.has_key?(format_key(relationship.name))
|
476
|
-
source.preloaded_fragments[format_key(relationship.name)].values.first.try(:id)
|
477
|
-
elsif !foreign_key_type_changed && source.respond_to?("#{relationship.name}_id")
|
478
|
-
# If you have direct access to the underlying id, you don't have to load the relationship
|
479
|
-
# which can save quite a lot of time when loading a lot of data.
|
480
|
-
# This does not apply to e.g. has_one :through relationships.
|
481
|
-
source.public_send("#{relationship.name}_id")
|
482
|
-
else
|
483
|
-
source.public_send(relationship.name).try(:id)
|
484
|
-
end
|
485
|
-
return nil unless related_resource_id
|
486
|
-
@id_formatter.format(related_resource_id)
|
487
|
-
end
|
488
|
-
|
489
|
-
def foreign_key_types_and_values(source, relationship)
|
490
|
-
if relationship.is_a?(JSONAPI::Relationship::ToMany)
|
491
|
-
if relationship.polymorphic?
|
492
|
-
assoc = source._model.public_send(relationship.name)
|
493
|
-
# Avoid hitting the database again for values already pre-loaded
|
494
|
-
if assoc.respond_to?(:loaded?) and assoc.loaded?
|
495
|
-
assoc.map do |obj|
|
496
|
-
[obj.type.underscore.pluralize, @id_formatter.format(obj.id)]
|
497
|
-
end
|
498
|
-
else
|
499
|
-
assoc.pluck(:type, :id).map do |type, id|
|
500
|
-
[type.underscore.pluralize, @id_formatter.format(id)]
|
501
|
-
end
|
502
|
-
end
|
503
|
-
else
|
504
|
-
source.public_send(relationship.name).map do |value|
|
505
|
-
[relationship.type, @id_formatter.format(value.id)]
|
506
|
-
end
|
507
|
-
end
|
508
|
-
end
|
509
|
-
end
|
510
|
-
|
511
|
-
# Sets that an object should be included in the primary document of the response.
|
512
|
-
def set_primary(type, id)
|
513
|
-
type = format_key(type)
|
514
|
-
@included_objects[type][id][:primary] = true
|
515
|
-
end
|
516
|
-
|
517
|
-
def add_resource(source, include_directives, primary = false)
|
518
|
-
type = source.is_a?(JSONAPI::CachedResourceFragment) ? source.type : source.class._type
|
519
|
-
id = source.id
|
520
|
-
|
521
|
-
@included_objects[type] ||= {}
|
522
|
-
existing = @included_objects[type][id]
|
523
|
-
|
524
|
-
if existing.nil?
|
525
|
-
obj_hash = object_hash(source, include_directives)
|
526
|
-
@included_objects[type][id] = {
|
527
|
-
primary: primary,
|
528
|
-
object_hash: obj_hash,
|
529
|
-
includes: Set.new(include_directives[:include_related].keys)
|
530
|
-
}
|
531
|
-
else
|
532
|
-
include_related = Set.new(include_directives[:include_related].keys)
|
533
|
-
unless existing[:includes].superset?(include_related)
|
534
|
-
obj_hash = object_hash(source, include_directives)
|
535
|
-
@included_objects[type][id][:object_hash].deep_merge!(obj_hash)
|
536
|
-
@included_objects[type][id][:includes].add(include_related)
|
537
|
-
@included_objects[type][id][:primary] = existing[:primary] | primary
|
538
|
-
end
|
371
|
+
link_object_to_many(source, relationship, rid, include_data)
|
539
372
|
end
|
540
373
|
end
|
541
374
|
|
542
375
|
def generate_link_builder(primary_resource_klass, options)
|
543
376
|
LinkBuilder.new(
|
544
377
|
base_url: options.fetch(:base_url, ''),
|
545
|
-
primary_resource_klass: primary_resource_klass,
|
546
378
|
route_formatter: options.fetch(:route_formatter, JSONAPI.configuration.route_formatter),
|
547
|
-
|
379
|
+
primary_resource_klass: primary_resource_klass,
|
548
380
|
)
|
549
381
|
end
|
550
382
|
end
|