jsonapi-resources 0.9.12 → 0.10.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1 -1
  3. data/README.md +34 -11
  4. data/lib/bug_report_templates/rails_5_latest.rb +125 -0
  5. data/lib/bug_report_templates/rails_5_master.rb +140 -0
  6. data/lib/jsonapi-resources.rb +8 -3
  7. data/lib/jsonapi/active_relation_resource_finder.rb +640 -0
  8. data/lib/jsonapi/active_relation_resource_finder/join_tree.rb +126 -0
  9. data/lib/jsonapi/acts_as_resource_controller.rb +121 -106
  10. data/lib/jsonapi/{cached_resource_fragment.rb → cached_response_fragment.rb} +13 -30
  11. data/lib/jsonapi/compiled_json.rb +11 -1
  12. data/lib/jsonapi/configuration.rb +44 -18
  13. data/lib/jsonapi/error.rb +27 -0
  14. data/lib/jsonapi/exceptions.rb +43 -40
  15. data/lib/jsonapi/formatter.rb +3 -3
  16. data/lib/jsonapi/include_directives.rb +2 -45
  17. data/lib/jsonapi/link_builder.rb +87 -80
  18. data/lib/jsonapi/operation.rb +16 -5
  19. data/lib/jsonapi/operation_result.rb +74 -16
  20. data/lib/jsonapi/processor.rb +233 -112
  21. data/lib/jsonapi/relationship.rb +77 -53
  22. data/lib/jsonapi/request_parser.rb +378 -423
  23. data/lib/jsonapi/resource.rb +224 -524
  24. data/lib/jsonapi/resource_controller_metal.rb +2 -2
  25. data/lib/jsonapi/resource_fragment.rb +47 -0
  26. data/lib/jsonapi/resource_id_tree.rb +112 -0
  27. data/lib/jsonapi/resource_identity.rb +42 -0
  28. data/lib/jsonapi/resource_serializer.rb +133 -301
  29. data/lib/jsonapi/resource_set.rb +108 -0
  30. data/lib/jsonapi/resources/version.rb +1 -1
  31. data/lib/jsonapi/response_document.rb +100 -88
  32. data/lib/jsonapi/routing_ext.rb +21 -43
  33. metadata +29 -45
  34. data/lib/jsonapi/operation_dispatcher.rb +0 -88
  35. data/lib/jsonapi/operation_results.rb +0 -35
  36. 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 get_related_resource
54
+ def show_related_resource
56
55
  process_request
57
56
  end
58
57
 
59
- def get_related_resources
58
+ def index_related_resources
60
59
  process_request
61
60
  end
62
61
 
63
- def process_request
64
- return unless verify_accept_header
65
-
66
- @request = JSONAPI::RequestParser.new(params, context: context,
67
- key_formatter: key_formatter,
68
- server_error_callbacks: (self.class.server_error_callbacks || []))
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
- unless @request.errors.empty?
71
- render_errors(@request.errors)
72
- else
73
- operations = @request.operations
74
- unless JSONAPI.configuration.resource_cache.nil?
75
- operations.each {|op| op.options[:cache_serializer] = resource_serializer }
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 process_operations(operations)
85
- run_callbacks :process_operations do
86
- operation_dispatcher.process(operations)
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
- def transaction
91
- lambda { |&block|
92
- ActiveRecord::Base.transaction do
93
- block.yield
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 rollback
99
- lambda {
100
- fail ActiveRecord::Rollback
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 operation_dispatcher
105
- @operation_dispatcher ||= JSONAPI::OperationDispatcher.new(transaction: transaction,
106
- rollback: rollback,
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
- unless request.content_type == JSONAPI::MEDIA_TYPE
144
- fail JSONAPI::Exceptions::UnsupportedMediaTypeError.new(request.content_type)
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
- media_types.any? do |media_type|
167
- (media_type == JSONAPI::MEDIA_TYPE || media_type.start_with?(ALL_MEDIA_TYPES))
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
- def media_types_for(header)
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
- if @request.nil? || @request.warnings.empty?
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 render_errors(errors)
218
- operation_results = JSONAPI::OperationResults.new
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 operation_results.has_errors?
237
+ if response_document.has_errors?
231
238
  render_options[:json] = content
232
239
  else
233
- # Bypasing ActiveSupport allows us to use CompiledJson objects for cached response fragments
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
- render_options[:location] = content[:data]["links"][:self] if (
238
- response_doc.status == :created && content[:data].class != Array && content[:data]["links"]
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 = response_doc.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(operation_results)
257
+ def create_response_document
250
258
  JSONAPI::ResponseDocument.new(
251
- operation_results,
252
- operation_results.has_errors? ? nil : resource_serializer,
253
- key_formatter: key_formatter,
254
- base_meta: base_meta,
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
- when JSONAPI::Exceptions::Error
265
- render_errors(e.errors)
266
- else
267
- if JSONAPI.configuration.exception_class_whitelisted?(e)
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
- (self.class.server_error_callbacks || []).each { |callback|
271
- safe_run_callback(callback, e)
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
- internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
275
- Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
276
- render_errors(internal_server_error.errors)
277
- end
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
- render_errors(internal_server_error.errors)
302
+ return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
288
303
  end
289
304
  end
290
305