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
@@ -0,0 +1,126 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module ActiveRelationResourceFinder
|
3
|
+
class JoinTree
|
4
|
+
# Stores relationship paths starting from the resource_klass. This allows consolidation of duplicate paths from
|
5
|
+
# relationships, filters and sorts. This enables the determination of table aliases as they are joined.
|
6
|
+
|
7
|
+
attr_reader :resource_klass, :options, :source_relationship
|
8
|
+
|
9
|
+
def initialize(resource_klass:, options: {}, source_relationship: nil, filters: nil, sort_criteria: nil)
|
10
|
+
@resource_klass = resource_klass
|
11
|
+
@options = options
|
12
|
+
@source_relationship = source_relationship
|
13
|
+
|
14
|
+
@join_relationships = {}
|
15
|
+
|
16
|
+
add_sort_criteria(sort_criteria)
|
17
|
+
add_filters(filters)
|
18
|
+
end
|
19
|
+
|
20
|
+
# A hash of joins that can be used to create the required joins
|
21
|
+
def get_joins
|
22
|
+
walk_relation_node(@join_relationships)
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_filters(filters)
|
26
|
+
return if filters.blank?
|
27
|
+
filters.each_key do |filter|
|
28
|
+
# Do not add joins for filters with an apply callable. This can be overridden by setting perform_joins to true
|
29
|
+
next if resource_klass._allowed_filters[filter].try(:[], :apply) &&
|
30
|
+
!resource_klass._allowed_filters[filter].try(:[], :perform_joins)
|
31
|
+
|
32
|
+
add_join(filter)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_sort_criteria(sort_criteria)
|
37
|
+
return if sort_criteria.blank?
|
38
|
+
|
39
|
+
sort_criteria.each do |sort|
|
40
|
+
add_join(sort[:field], :left)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def add_join_relationship(parent_joins, join_name, relation_name, type)
|
47
|
+
parent_joins[join_name] ||= {relation_name: relation_name, relationship: {}, type: type}
|
48
|
+
if parent_joins[join_name][:type] == :left && type == :inner
|
49
|
+
parent_joins[join_name][:type] = :inner
|
50
|
+
end
|
51
|
+
parent_joins[join_name][:relationship]
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_join(path, default_type = :inner)
|
55
|
+
relationships, _field = resource_klass.parse_relationship_path(path)
|
56
|
+
|
57
|
+
current_joins = @join_relationships
|
58
|
+
|
59
|
+
terminated = false
|
60
|
+
|
61
|
+
relationships.each do |relationship|
|
62
|
+
if terminated
|
63
|
+
# ToDo: Relax this, if possible
|
64
|
+
# :nocov:
|
65
|
+
warn "Can not nest joins under polymorphic join"
|
66
|
+
# :nocov:
|
67
|
+
end
|
68
|
+
|
69
|
+
if relationship.polymorphic?
|
70
|
+
relation_names = relationship.polymorphic_relations
|
71
|
+
relation_names.each do |relation_name|
|
72
|
+
join_name = "#{relationship.name}[#{relation_name}]"
|
73
|
+
add_join_relationship(current_joins, join_name, relation_name, :left)
|
74
|
+
end
|
75
|
+
terminated = true
|
76
|
+
else
|
77
|
+
join_name = relationship.name
|
78
|
+
current_joins = add_join_relationship(current_joins, join_name, relationship.relation_name(options), default_type)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create a nested set of hashes from an array of path components. This will be used by the `join` methods.
|
84
|
+
# [post, comments] => { post: { comments: {} }
|
85
|
+
def relation_join_hash(path, path_hash = {})
|
86
|
+
relation = path.shift
|
87
|
+
if relation
|
88
|
+
path_hash[relation] = {}
|
89
|
+
relation_join_hash(path, path_hash[relation])
|
90
|
+
end
|
91
|
+
path_hash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the paths from shortest to longest, allowing the capture of the table alias for earlier paths. For
|
95
|
+
# example posts, posts.comments and then posts.comments.author joined in that order will alow each
|
96
|
+
# alias to be determined whereas just joining posts.comments.author will only record the author alias.
|
97
|
+
# ToDo: Dependence on this specialized logic should be removed in the future, if possible.
|
98
|
+
def walk_relation_node(node, paths = {}, current_relation_path = [], current_relationship_path = [])
|
99
|
+
node.each do |key, value|
|
100
|
+
if current_relation_path.empty? && source_relationship
|
101
|
+
current_relation_path << source_relationship.relation_name(options)
|
102
|
+
end
|
103
|
+
|
104
|
+
current_relation_path << value[:relation_name].to_s
|
105
|
+
current_relationship_path << key.to_s
|
106
|
+
|
107
|
+
rel_path = current_relationship_path.join('.')
|
108
|
+
paths[rel_path] ||= {
|
109
|
+
alias: nil,
|
110
|
+
join_type: value[:type],
|
111
|
+
relation_join_hash: relation_join_hash(current_relation_path.dup)
|
112
|
+
}
|
113
|
+
|
114
|
+
walk_relation_node(value[:relationship],
|
115
|
+
paths,
|
116
|
+
current_relation_path,
|
117
|
+
current_relationship_path)
|
118
|
+
|
119
|
+
current_relation_path.pop
|
120
|
+
current_relationship_path.pop
|
121
|
+
end
|
122
|
+
paths
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -9,9 +9,12 @@ module JSONAPI
|
|
9
9
|
base.extend ClassMethods
|
10
10
|
base.include Callbacks
|
11
11
|
base.cattr_reader :server_error_callbacks
|
12
|
-
base.define_jsonapi_resources_callbacks :process_operations
|
12
|
+
base.define_jsonapi_resources_callbacks :process_operations,
|
13
|
+
:transaction
|
13
14
|
end
|
14
15
|
|
16
|
+
attr_reader :response_document
|
17
|
+
|
15
18
|
def index
|
16
19
|
process_request
|
17
20
|
end
|
@@ -25,22 +28,18 @@ module JSONAPI
|
|
25
28
|
end
|
26
29
|
|
27
30
|
def create
|
28
|
-
return unless verify_content_type_header
|
29
31
|
process_request
|
30
32
|
end
|
31
33
|
|
32
34
|
def create_relationship
|
33
|
-
return unless verify_content_type_header
|
34
35
|
process_request
|
35
36
|
end
|
36
37
|
|
37
38
|
def update_relationship
|
38
|
-
return unless verify_content_type_header
|
39
39
|
process_request
|
40
40
|
end
|
41
41
|
|
42
42
|
def update
|
43
|
-
return unless verify_content_type_header
|
44
43
|
process_request
|
45
44
|
end
|
46
45
|
|
@@ -52,59 +51,93 @@ module JSONAPI
|
|
52
51
|
process_request
|
53
52
|
end
|
54
53
|
|
55
|
-
def
|
54
|
+
def show_related_resource
|
56
55
|
process_request
|
57
56
|
end
|
58
57
|
|
59
|
-
def
|
58
|
+
def index_related_resources
|
60
59
|
process_request
|
61
60
|
end
|
62
61
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
62
|
+
def get_related_resource
|
63
|
+
# :nocov:
|
64
|
+
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resource`"\
|
65
|
+
" action. Please use `show_related_resource` instead."
|
66
|
+
show_related_resource
|
67
|
+
# :nocov:
|
68
|
+
end
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
results = process_operations(operations)
|
78
|
-
render_results(results)
|
79
|
-
end
|
80
|
-
rescue => e
|
81
|
-
handle_exceptions(e)
|
70
|
+
def get_related_resources
|
71
|
+
# :nocov:
|
72
|
+
ActiveSupport::Deprecation.warn "In #{self.class.name} you exposed a `get_related_resources`"\
|
73
|
+
" action. Please use `index_related_resource` instead."
|
74
|
+
index_related_resources
|
75
|
+
# :nocov:
|
82
76
|
end
|
83
77
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
78
|
+
def process_request
|
79
|
+
@response_document = create_response_document
|
80
|
+
|
81
|
+
unless verify_content_type_header && verify_accept_header
|
82
|
+
render_response_document
|
83
|
+
return
|
87
84
|
end
|
88
|
-
end
|
89
85
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
request_parser = JSONAPI::RequestParser.new(
|
87
|
+
params,
|
88
|
+
context: context,
|
89
|
+
key_formatter: key_formatter,
|
90
|
+
server_error_callbacks: (self.class.server_error_callbacks || []))
|
91
|
+
|
92
|
+
transactional = request_parser.transactional?
|
93
|
+
|
94
|
+
begin
|
95
|
+
process_operations(transactional) do
|
96
|
+
run_callbacks :process_operations do
|
97
|
+
request_parser.each(response_document) do |op|
|
98
|
+
op.options[:serializer] = resource_serializer_klass.new(
|
99
|
+
op.resource_klass,
|
100
|
+
include_directives: op.options[:include_directives],
|
101
|
+
fields: op.options[:fields],
|
102
|
+
base_url: base_url,
|
103
|
+
key_formatter: key_formatter,
|
104
|
+
route_formatter: route_formatter,
|
105
|
+
serialization_options: serialization_options
|
106
|
+
)
|
107
|
+
op.options[:cache_serializer_output] = !JSONAPI.configuration.resource_cache.nil?
|
108
|
+
|
109
|
+
process_operation(op)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
if response_document.has_errors?
|
113
|
+
raise ActiveRecord::Rollback
|
114
|
+
end
|
94
115
|
end
|
95
|
-
|
116
|
+
rescue => e
|
117
|
+
handle_exceptions(e)
|
118
|
+
end
|
119
|
+
render_response_document
|
96
120
|
end
|
97
121
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
|
122
|
+
def process_operations(transactional)
|
123
|
+
if transactional
|
124
|
+
run_callbacks :transaction do
|
125
|
+
ActiveRecord::Base.transaction do
|
126
|
+
yield
|
127
|
+
end
|
128
|
+
end
|
129
|
+
else
|
130
|
+
begin
|
131
|
+
yield
|
132
|
+
rescue ActiveRecord::Rollback
|
133
|
+
# Can't rollback without transaction, so just ignore it
|
134
|
+
end
|
135
|
+
end
|
102
136
|
end
|
103
137
|
|
104
|
-
def
|
105
|
-
|
106
|
-
|
107
|
-
server_error_callbacks: @request.server_error_callbacks)
|
138
|
+
def process_operation(operation)
|
139
|
+
result = operation.process
|
140
|
+
response_document.add_result(result, operation)
|
108
141
|
end
|
109
142
|
|
110
143
|
private
|
@@ -117,20 +150,6 @@ module JSONAPI
|
|
117
150
|
@resource_serializer_klass ||= JSONAPI::ResourceSerializer
|
118
151
|
end
|
119
152
|
|
120
|
-
def resource_serializer
|
121
|
-
@resource_serializer ||= resource_serializer_klass.new(
|
122
|
-
resource_klass,
|
123
|
-
include_directives: @request ? @request.include_directives : nil,
|
124
|
-
fields: @request ? @request.fields : {},
|
125
|
-
base_url: base_url,
|
126
|
-
key_formatter: key_formatter,
|
127
|
-
route_formatter: route_formatter,
|
128
|
-
serialization_options: serialization_options,
|
129
|
-
controller: self
|
130
|
-
)
|
131
|
-
@resource_serializer
|
132
|
-
end
|
133
|
-
|
134
153
|
def base_url
|
135
154
|
@base_url ||= request.protocol + request.host_with_port
|
136
155
|
end
|
@@ -140,8 +159,10 @@ module JSONAPI
|
|
140
159
|
end
|
141
160
|
|
142
161
|
def verify_content_type_header
|
143
|
-
|
144
|
-
|
162
|
+
if ['create', 'create_relationship', 'update_relationship', 'update'].include?(params[:action])
|
163
|
+
unless request.content_type == JSONAPI::MEDIA_TYPE
|
164
|
+
fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
|
165
|
+
end
|
145
166
|
end
|
146
167
|
true
|
147
168
|
rescue => e
|
@@ -162,13 +183,12 @@ module JSONAPI
|
|
162
183
|
def valid_accept_media_type?
|
163
184
|
media_types = media_types_for('Accept')
|
164
185
|
|
165
|
-
media_types.blank? ||
|
166
|
-
|
167
|
-
|
168
|
-
end
|
186
|
+
media_types.blank? || media_types.any? do |media_type|
|
187
|
+
(media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
|
188
|
+
end
|
169
189
|
end
|
170
190
|
|
171
|
-
|
191
|
+
def media_types_for(header)
|
172
192
|
(request.headers[header] || '')
|
173
193
|
.scan(MEDIA_TYPE_MATCHER)
|
174
194
|
.to_a
|
@@ -203,57 +223,43 @@ module JSONAPI
|
|
203
223
|
end
|
204
224
|
|
205
225
|
def base_meta
|
206
|
-
|
207
|
-
base_response_meta
|
208
|
-
else
|
209
|
-
base_response_meta.merge(warnings: @request.warnings)
|
210
|
-
end
|
226
|
+
base_response_meta
|
211
227
|
end
|
212
228
|
|
213
229
|
def base_response_links
|
214
230
|
{}
|
215
231
|
end
|
216
232
|
|
217
|
-
def
|
218
|
-
|
219
|
-
result = JSONAPI::ErrorsOperationResult.new(errors[0].status, errors)
|
220
|
-
operation_results.add_result(result)
|
221
|
-
|
222
|
-
render_results(operation_results)
|
223
|
-
end
|
224
|
-
|
225
|
-
def render_results(operation_results)
|
226
|
-
response_doc = create_response_document(operation_results)
|
227
|
-
content = response_doc.contents
|
233
|
+
def render_response_document
|
234
|
+
content = response_document.contents
|
228
235
|
|
229
236
|
render_options = {}
|
230
|
-
if
|
237
|
+
if response_document.has_errors?
|
231
238
|
render_options[:json] = content
|
232
239
|
else
|
233
|
-
#
|
240
|
+
# Bypassing ActiveSupport allows us to use CompiledJson objects for cached response fragments
|
234
241
|
render_options[:body] = JSON.generate(content)
|
235
|
-
end
|
236
242
|
|
237
|
-
|
238
|
-
|
239
|
-
|
243
|
+
if (response_document.status == 201 && content[:data].class != Array) &&
|
244
|
+
content['data'] && content['data']['links'] && content['data']['links']['self']
|
245
|
+
render_options[:location] = content['data']['links']['self']
|
246
|
+
end
|
247
|
+
end
|
240
248
|
|
241
249
|
# For whatever reason, `render` ignores :status and :content_type when :body is set.
|
242
250
|
# But, we can just set those values directly in the Response object instead.
|
243
|
-
response.status =
|
251
|
+
response.status = response_document.status
|
244
252
|
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
245
253
|
|
246
254
|
render(render_options)
|
247
255
|
end
|
248
256
|
|
249
|
-
def create_response_document
|
257
|
+
def create_response_document
|
250
258
|
JSONAPI::ResponseDocument.new(
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
base_links: base_response_links,
|
256
|
-
request: @request
|
259
|
+
key_formatter: key_formatter,
|
260
|
+
base_meta: base_meta,
|
261
|
+
base_links: base_response_links,
|
262
|
+
request: request
|
257
263
|
)
|
258
264
|
end
|
259
265
|
|
@@ -261,21 +267,30 @@ module JSONAPI
|
|
261
267
|
# Note: Be sure to either call super(e) or handle JSONAPI::Exceptions::Error and raise unhandled exceptions
|
262
268
|
def handle_exceptions(e)
|
263
269
|
case e
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
fail e
|
270
|
+
when JSONAPI::Exceptions::Error
|
271
|
+
errors = e.errors
|
272
|
+
when ActionController::ParameterMissing
|
273
|
+
errors = JSONAPI::Exceptions::ParameterMissing.new(e.param).errors
|
269
274
|
else
|
270
|
-
|
271
|
-
|
272
|
-
|
275
|
+
if JSONAPI.configuration.exception_class_whitelisted?(e)
|
276
|
+
raise e
|
277
|
+
else
|
278
|
+
if self.class.server_error_callbacks
|
279
|
+
self.class.server_error_callbacks.each { |callback|
|
280
|
+
safe_run_callback(callback, e)
|
281
|
+
}
|
282
|
+
end
|
273
283
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
284
|
+
# Store exception for other middlewares
|
285
|
+
request.env['action_dispatch.exception'] ||= e
|
286
|
+
|
287
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
288
|
+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
|
289
|
+
errors = internal_server_error.errors
|
290
|
+
end
|
278
291
|
end
|
292
|
+
|
293
|
+
response_document.add_result(JSONAPI::ErrorsOperationResult.new(errors[0].status, errors), nil)
|
279
294
|
end
|
280
295
|
|
281
296
|
def safe_run_callback(callback, error)
|
@@ -284,7 +299,7 @@ module JSONAPI
|
|
284
299
|
rescue => e
|
285
300
|
Rails.logger.error { "Error in error handling callback: #{e.message} #{e.backtrace.join("\n")}" }
|
286
301
|
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
287
|
-
|
302
|
+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
|
288
303
|
end
|
289
304
|
end
|
290
305
|
|