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.
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