caprese 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 49d0eb87e9e10a1738d98c359dc8099eac6063bc
4
- data.tar.gz: 799eee055e9baefbe79bdd343a1dd1a613de62a0
3
+ metadata.gz: ca3bb7c32744fab18ebfc77dc7e6cca7df12fd1e
4
+ data.tar.gz: bbf6d26188979800498a2ae5310a7a56d860b9f5
5
5
  SHA512:
6
- metadata.gz: 0d547c69920b3a1ab3e2073e7c329627f65a79fbbbba83e1d6a75b715e38ad67f99e5bf7d00c49f793750df1cf58d1cfbeec16e3e5d7ebcc6f740ce441be9abf
7
- data.tar.gz: 94cbe72dea9c672aa3730ec6c4533ca7bdaaf395cd56f329c6382578817c4710d452cb38d3be3b0f8726f0f5da961c151f5670c076c80e4d6c0dc8811faa5d60
6
+ metadata.gz: 74c741443c8b6e7d363bdb2675096b27ec23c7147d39522a9fea8b404d7dc07c48ef0f7ccdcc67eb120b3492a35592a12946879a4e34b4f4351cb02039586c37
7
+ data.tar.gz: 2166985f7b64d5d99c5f671001430393b533d3d90a14b8231fc67ab1c79f0a233e90a46ab6f084f7c3121866521a38d85bef8b495731c19187c672111312d8e2
data/CHANGELOG.md CHANGED
@@ -168,4 +168,14 @@
168
168
 
169
169
  ## 0.4.1
170
170
 
171
- * Allows `:self` link to be overridden in serializers that subclass Caprese::Serializer
171
+ * Allows `:self` link to be overridden in serializers that subclass Caprese::Serializer
172
+
173
+ ## 0.5.0
174
+
175
+ * Add `relationship_scope(relationship_name, scope) => scope` method for all relationships in serializers, allowing override for custom scoping
176
+ * Refactor `assign_record_attributes` to `assign_changes_from_document`, which splits into multiple modular methods that handle relationship data errors with more source pointer detail
177
+ * Adds `ResourceDocumentInvalidError` for errors pertaining to the document instead of record assignment (`RecordInvalidError`)
178
+ * Allows `PATCH` requests to primary endpoints that update autosaving collection relationships to propagate the nested errors on attributes/relationships up to the primary data so error source pointers are just as detailed as they would be under `POST` requests already
179
+ * Fields are now assigned in a specific order: attributes, singular relationships, collection relationships
180
+ * Fix issue regarding `:base` field titles interpolated into error messages
181
+ * Add more detailed error responses to `update_relationship_definition` endpoints
data/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  Caprese is a Rails library for creating RESTful APIs in as few words as possible. It handles all CRUD operations on resources and their associations for you, and you can customize how these operations
4
4
  are carried out, allowing for infinite possibilities while focusing on work that matters to you, instead of writing repetitive code for each action of each resource in your application.
5
5
 
6
- For now, the only format that is supported by Caprese is the [JSON API schema](http://jsonapi.org/format/). In the future, Caprese will support a more straightforward (but less powerful) JSON format as well, for simpler use cases.
6
+ For now, the only format that is supported by Caprese is the [JSON API schema.](http://jsonapi.org/format/)
7
+
8
+ [![Coverage Status](https://coveralls.io/repos/github/nicklandgrebe/caprese/badge.svg)](https://coveralls.io/github/nicklandgrebe/caprese)
7
9
 
8
10
  ## Installation
9
11
 
@@ -18,6 +18,17 @@ module Caprese
18
18
  ]
19
19
  end
20
20
 
21
+ def self.document_errors(error_serializer, options)
22
+ error_attributes = error_serializer.as_json
23
+ [
24
+ {
25
+ code: error_attributes[:code],
26
+ detail: error_attributes[:message],
27
+ source: error_source(:pointer, nil, error_attributes[:field])
28
+ }
29
+ ]
30
+ end
31
+
21
32
  # Builds a JSON API Errors Object
22
33
  # {http://jsonapi.org/format/#errors JSON API Errors}
23
34
  #
@@ -89,12 +100,16 @@ module Caprese
89
100
  # parameter: 'pres'
90
101
  # }
91
102
  # end
92
- RESERVED_ATTRIBUTES = %w(type)
103
+ RESERVED_ATTRIBUTES = %w(id type)
93
104
  def self.error_source(source_type, record, attribute_name)
94
105
  case source_type
95
106
  when :pointer
107
+ if attribute_name == :base
108
+ {
109
+ pointer: JsonApi::JsonPointer.new(:base, record, attribute_name)
110
+ }
96
111
  # [type ...] and other primary data variables
97
- if RESERVED_ATTRIBUTES.include?(attribute_name.to_s)
112
+ elsif RESERVED_ATTRIBUTES.include?(attribute_name.to_s)
98
113
  {
99
114
  pointer: JsonApi::JsonPointer.new(:primary_data, record, attribute_name)
100
115
  }
@@ -9,7 +9,8 @@ module Caprese
9
9
  relationship_attribute: '/data/relationships/%s'.freeze,
10
10
  relationship_base: '/data/relationships/%s/data'.freeze,
11
11
  relationship_primary_data: '/data/relationships/%s/data/%s'.freeze,
12
- primary_data: '/data/%s'.freeze
12
+ primary_data: '/data/%s'.freeze,
13
+ base: '/data'.freeze
13
14
  }.freeze
14
15
 
15
16
  # Iterates over the field of an error and converts it to a pointer in JSON API format
@@ -50,7 +51,7 @@ module Caprese
50
51
  end
51
52
  end
52
53
  else
53
- format(POINTERS[pointer_type], *[value].flatten)
54
+ format(POINTERS[pointer_type], *Array.wrap(value))
54
55
  end
55
56
  end
56
57
  end
@@ -170,6 +170,8 @@ module Caprese
170
170
  hash[:errors] =
171
171
  if serializer.resource_errors?
172
172
  Error.resource_errors(serializer, instance_options)
173
+ elsif serializer.document_errors?
174
+ Error.document_errors(serializer, instance_options)
173
175
  else
174
176
  Error.param_errors(serializer, instance_options)
175
177
  end
@@ -5,7 +5,7 @@ module Caprese
5
5
  module Aliasing
6
6
  extend ActiveSupport::Concern
7
7
 
8
- # Records all of the field aliases engaged by the API request (called in `assign_record_attributes` using comparison)
8
+ # Records all of the field aliases engaged by the API request (called in `assign_changes_from_document` using comparison)
9
9
  # so that when the response is returned, the appropriate alias is used in reference to fields
10
10
  #
11
11
  # Success: @todo
@@ -12,10 +12,7 @@ module Caprese
12
12
  # @note The only instance this may be called, at least in JSON API settings, is a
13
13
  # missing params['data'] param
14
14
  rescue_from ActionController::ParameterMissing do |e|
15
- rescue_with_handler Error.new(
16
- field: e.param,
17
- code: :blank
18
- )
15
+ rescue_with_handler RequestDocumentInvalidError.new(field: :base)
19
16
  end
20
17
 
21
18
  rescue_from ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved do |e|
@@ -43,7 +40,7 @@ module Caprese
43
40
  fail_on_type_mismatch(data_params[:type])
44
41
 
45
42
  record = queried_record_scope.build
46
- assign_record_attributes(record, permitted_params_for(:create), data_params)
43
+ assign_changes_from_document(record, data_params, permitted_params_for(:create))
47
44
 
48
45
  execute_after_initialize_callbacks(record)
49
46
 
@@ -74,7 +71,7 @@ module Caprese
74
71
  def update
75
72
  fail_on_type_mismatch(data_params[:type])
76
73
 
77
- assign_record_attributes(queried_record, permitted_params_for(:update), data_params)
74
+ assign_changes_from_document(queried_record, data_params, permitted_params_for(:update))
78
75
 
79
76
  execute_before_update_callbacks(queried_record)
80
77
  execute_before_save_callbacks(queried_record)
@@ -157,7 +154,8 @@ module Caprese
157
154
  # @param [Array] params the params to search for the key in
158
155
  # @return [Array,Nil] the nested params for a given key
159
156
  def nested_params_for(key, params)
160
- params.detect { |p| p.is_a?(Hash) }.try(:[], key.to_sym)
157
+ key = key.to_sym
158
+ params.detect { |p| p.is_a?(Hash) && p.has_key?(key) }.try(:[], key)
161
159
  end
162
160
 
163
161
  # Flattens an array of the top level keys for a given set of params
@@ -192,7 +190,7 @@ module Caprese
192
190
  # }
193
191
  # create_params => [:price]
194
192
  #
195
- # assign_record_attributes(record, create_params, params)
193
+ # assign_changes_from_document(record, params, create_params)
196
194
  # => { price: '...', product: Product<@token='asj38k'> }
197
195
  #
198
196
  # @example
@@ -215,129 +213,180 @@ module Caprese
215
213
  #
216
214
  # create_params => [:price, order_items: [:title, :amount, :tax]]
217
215
  #
218
- # assign_record_attributes(record, create_params, params) # => {
216
+ # assign_changes_from_document(record, params, create_params) # => {
219
217
  # price: '...',
220
218
  # order_items: [OrderItem<@token=nil,@title='An order item',@amount=5.0,@tax=0.0>]
221
219
  # }
222
220
  #
223
221
  # @param [ActiveRecord::Base] record the record to build attribute into
224
- # @param [Array] permitted_params the permitted params for the action
225
222
  # @param [Parameters] data the data sent to the server to construct and assign to the record
223
+ # @param [Array] permitted_params the permitted params for the action
226
224
  # @option [String] parent_relationship_name the parent relationship assigning these attributes to the record, used to determine
227
225
  # engaged aliases @see concerns/aliasing
228
- def assign_record_attributes(record, permitted_params, data, parent_relationship_name: nil)
226
+ def assign_changes_from_document(record, data, permitted_params = [], parent_relationship_name: nil)
229
227
  # TODO: Make safe by enforcing that only a single alias/unalias can be engaged at once
230
- engaged_field_aliases_object = parent_relationship_name ? (engaged_field_aliases[parent_relationship_name] ||= {}) : engaged_field_aliases
228
+ aliases_document =
229
+ if parent_relationship_name
230
+ engaged_field_aliases[parent_relationship_name] ||= {}
231
+ else
232
+ engaged_field_aliases
233
+ end
231
234
 
232
- attributes = data[:attributes].try(:permit, *permitted_params).try(:inject, {}) do |out, (attribute_name, val)|
235
+ if data[:attributes]
236
+ assign_fields_to_record record, extract_attributes_from_document(
237
+ record,
238
+ data[:attributes],
239
+ permitted_params,
240
+ aliases_document
241
+ )
242
+ end
243
+
244
+ if data[:relationships]
245
+ collection_relationships, singular_relationships =
246
+ flattened_keys_for(permitted_params)
247
+ .select { |k|
248
+ begin
249
+ record.association(actual_field(k, record.class))
250
+ rescue ActiveRecord::AssociationNotFoundError
251
+ false
252
+ end
253
+ }
254
+ .partition { |k| record.association(actual_field(k, record.class)).reflection.collection? }
255
+ .map { |s|
256
+ s.map { |r| permitted_params.include?(r) ? r : { r => nested_params_for(r, permitted_params) } }
257
+ }
258
+
259
+ assign_fields_to_record record, extract_relationships_from_document(
260
+ record,
261
+ data[:relationships],
262
+ singular_relationships,
263
+ aliases_document
264
+ )
265
+
266
+ assign_fields_to_record record, extract_relationships_from_document(
267
+ record,
268
+ data[:relationships],
269
+ collection_relationships,
270
+ aliases_document
271
+ )
272
+ end
273
+ end
274
+
275
+ # Assigns fields to the record conditionally based on whether or not assign_attributes is available
276
+ # @note Allows non-ActiveRecord models to be handled
277
+ #
278
+ # @param [ActiveRecord::Base,Struct] record the record to assign fields to
279
+ # @param [Hash] fields the fields to assign to the record
280
+ def assign_fields_to_record(record, fields)
281
+ if record.respond_to?(:assign_attributes)
282
+ record.assign_attributes(fields)
283
+ else
284
+ fields.each { |k, v| record.send("#{k}=", v) }
285
+ end
286
+ end
287
+
288
+ # Builds an object of attributes to assign to a record, based on a document
289
+ #
290
+ # @param [ActiveRecord] record the record corresponding to the data document
291
+ # @param [Parameters] data the document to extract attributes from
292
+ # @param [Array<Symbol,Hash>] permitted_params the permitted attributes that can be assigned through this controller
293
+ # @param [Hash] aliases_document the aliases document reflects usage of aliases in the data document
294
+ # @return [Hash] the object of attributes to assign to the record
295
+ def extract_attributes_from_document(record, data, permitted_params, aliases_document)
296
+ data.permit(*permitted_params).each_with_object({}) do |(attribute_name, val), attributes|
233
297
  attribute_name = attribute_name.to_sym
234
298
  actual_attribute_name = actual_field(attribute_name, record.class)
235
299
 
236
300
  if attribute_name != actual_attribute_name
237
- engaged_field_aliases_object[attribute_name] = true
301
+ aliases_document[attribute_name] = true
238
302
  end
239
303
 
240
- out[actual_attribute_name] = val
241
- out
242
- end || {}
304
+ attributes[actual_attribute_name] = val
305
+ end
306
+ end
243
307
 
244
- data[:relationships]
245
- .try(:slice, *flattened_keys_for(permitted_params))
246
- .try(:each) do |relationship_name, relationship_data|
308
+ # Builds an object of relationships to assign to a record, based on a document
309
+ #
310
+ # @param [ActiveRecord] record the record corresponding to the data document
311
+ # @param [Parameters] data the document to extract relationships from
312
+ # @param [Array<Symbol,Hash>] permitted_relationships the permitted relationships that can be assigned through this controller
313
+ # @param [Hash] aliases_document the aliases document reflects usage of aliases in the data document
314
+ # @return [Hash] the object of relationships to assign to the record
315
+ def extract_relationships_from_document(record, data, permitted_relationships, aliases_document)
316
+ data
317
+ .slice(*flattened_keys_for(permitted_relationships))
318
+ .each_with_object({}) do |(relationship_name, relationship_data), relationships|
247
319
  relationship_name = relationship_name.to_sym
248
320
  actual_relationship_name = actual_field(relationship_name, record.class)
249
321
 
250
322
  if relationship_name != actual_relationship_name
251
- engaged_field_aliases_object[relationship_name] = {}
323
+ aliases_document[relationship_name] = {}
252
324
  end
253
325
 
254
- # TODO: Add checkme for relationship_name to ensure that format is correct (not Array when actually Record, vice versa)
255
- # No relationship exists as well
326
+ begin
327
+ raise RequestDocumentInvalidError.new(field: :base) unless relationship_data.has_key?(:data)
256
328
 
257
- attributes[actual_relationship_name] = records_for_relationship(
258
- record,
259
- nested_params_for(relationship_name, permitted_params),
260
- relationship_name,
261
- relationship_data
262
- )
263
- end
329
+ relationship_result = records_for_relationship(
330
+ record,
331
+ nested_params_for(relationship_name, permitted_relationships),
332
+ relationship_name,
333
+ relationship_data[:data]
334
+ )
264
335
 
265
- if record.respond_to?(:assign_attributes)
266
- record.assign_attributes(attributes)
267
- else
268
- attributes.each { |k, v| record.send("#{k}=", v) }
336
+ reflection = record.association(actual_relationship_name).reflection
337
+ if (reflection.collection? && !relationship_result.is_a?(Array)) ||
338
+ (!reflection.collection? && relationship_result.is_a?(Array))
339
+
340
+ raise RequestDocumentInvalidError.new(field: :base)
341
+ end
342
+
343
+ if record.persisted? && reflection.collection? &&
344
+ (inverse_reflection = record.class.reflect_on_association(actual_relationship_name).inverse_of)
345
+
346
+ relationship_result.each { |r| r.send("#{inverse_reflection.name}=", record) }
347
+ invalid_results = relationship_result.reject(&:valid?)
348
+ raise RecordInvalidError.new(invalid_results.first) if invalid_results.any?
349
+ end
350
+
351
+ relationships[actual_relationship_name] = relationship_result
352
+ rescue Caprese::RecordNotFoundError => e
353
+ record.errors.add(relationship_name, :not_found, t: e.t.slice(:value))
354
+ rescue RecordInvalidError => e
355
+ propagate_errors_to_parent(
356
+ record,
357
+ relationship_name,
358
+ e.record.errors.to_a
359
+ )
360
+ rescue RequestDocumentInvalidError => e
361
+ propagate_errors_to_parent(
362
+ record,
363
+ relationship_name,
364
+ [e]
365
+ )
366
+ end
269
367
  end
270
368
  end
271
369
 
272
370
  # Gets all the records for a relationship given a relationship data definition
273
371
  #
274
- # @param [ActiveRecord::Base] owner the owner of the relationship
372
+ # @param [ActiveRecord] owner the owner of the relationship
275
373
  # @param [Array] permitted_params the permitted params for the
276
374
  # @param [String] relationship_name the name of the relationship to get records for
277
375
  # @param [Hash,Array<Hash>] relationship_data the resource identifier data to use to find/build records
278
- # @return [ActiveRecord::Base,Array<ActiveRecord::Base>] the record(s) for the relationship
376
+ # @return [ActiveRecord,Array<ActiveRecord>] the record(s) for the relationship
279
377
  def records_for_relationship(owner, permitted_params, relationship_name, relationship_data)
280
- if relationship_data[:data].is_a?(Array)
281
- relationship_data[:data].map do |relationship_data_item|
282
- ref = record_for_relationship(owner, relationship_name, relationship_data_item)
283
-
284
- if ref && contains_constructable_data?(relationship_data_item)
285
- assign_record_attributes(ref, permitted_params, relationship_data_item, parent_relationship_name: relationship_name)
286
- end
378
+ result = Array.wrap(relationship_data).map do |relationship_data_item|
379
+ ref = record_for_resource_identifier(relationship_data_item)
287
380
 
288
- ref
289
- end
290
- elsif relationship_data[:data].present?
291
- ref = record_for_relationship(owner, relationship_name, relationship_data[:data])
292
-
293
- if ref && contains_constructable_data?(relationship_data[:data])
294
- assign_record_attributes(ref, permitted_params, relationship_data[:data], parent_relationship_name: relationship_name)
381
+ if ref && contains_constructable_data?(relationship_data_item)
382
+ assign_changes_from_document(ref, relationship_data_item, permitted_params, parent_relationship_name: relationship_name)
383
+ propagate_errors_to_parent(owner, relationship_name, ref.errors.to_a) if ref.errors.any?
295
384
  end
296
385
 
297
386
  ref
298
- elsif !relationship_data.has_key?(:data)
299
- raise Error.new(
300
- field: "/data/relationships/#{relationship_name}/data",
301
- code: :blank
302
- )
303
387
  end
304
- end
305
-
306
- # Given a resource identifier, finds or builds a resource for a relationship
307
- #
308
- # @param [ActiveRecord::Base] owner the owner of the relationship record
309
- # @param [String] relationship_name the name of the relationship
310
- # @param [Hash] resource_identifier the resource identifier for the resource
311
- # @return [ActiveRecord::Base] the found or built resource for the relationship
312
- def record_for_relationship(owner, relationship_name, resource_identifier)
313
- if resource_identifier[:type]
314
- # { type: '...', id: '...' }
315
- if (id = resource_identifier[:id])
316
- begin
317
- get_record!(
318
- resource_identifier[:type],
319
- Caprese.config.resource_primary_key,
320
- id
321
- )
322
- rescue Caprese::RecordNotFoundError => e
323
- owner.errors.add(relationship_name, :not_found, t: { value: id })
324
- nil
325
- end
326
388
 
327
- # { type: '...', attributes: { ... } }
328
- elsif contains_constructable_data?(resource_identifier)
329
- record_scope(resource_identifier[:type].to_sym).build
330
-
331
- # { type: '...' }
332
- else
333
- owner.errors.add(relationship_name)
334
- nil
335
- end
336
- else
337
- # { id: '...' } && { attributes: { ... } }
338
- owner.errors.add("#{relationship_name}.type")
339
- nil
340
- end
389
+ relationship_data.is_a?(Array) && result || result.first
341
390
  end
342
391
 
343
392
  # Indicates whether or not :attributes or :relationships keys are in a resource identifier,
@@ -349,6 +398,21 @@ module Caprese
349
398
  [:attributes, :relationships].any? { |k| resource_identifier.key?(k) }
350
399
  end
351
400
 
401
+ # Propagates errors to parent with nested field name
402
+ #
403
+ # @param [ActiveRecord] parent the parent to propagate errors to
404
+ # @param [String] relationship_name the name to use when nesting the errors
405
+ # @param [Array<Error>] errors the errors to propagate
406
+ def propagate_errors_to_parent(parent, relationship_name, errors)
407
+ errors.each do |error|
408
+ parent.errors.add(
409
+ error.field == :base ? relationship_name : "#{relationship_name}.#{error.field}",
410
+ error.code,
411
+ t: error.t.except(:field, :field_title)
412
+ )
413
+ end
414
+ end
415
+
352
416
  # Called in create, after the record is saved. When creating a new record, and assigning to it
353
417
  # existing has_many association relation, the records in the relation will be pushed onto the
354
418
  # appropriate target, but the relationship will not be persisted in their attributes until their
@@ -87,6 +87,38 @@ module Caprese
87
87
  record_class(type).all
88
88
  end
89
89
 
90
+ # Given a resource identifier, finds or builds a resource
91
+ #
92
+ # @param [Hash] resource_identifier the resource identifier for the resource
93
+ # @return [ActiveRecord::Base] the found or built resource for the relationship
94
+ def record_for_resource_identifier(resource_identifier)
95
+ if (type = resource_identifier[:type])
96
+ # { type: '...', id: '...' }
97
+ if (id = resource_identifier[:id])
98
+ begin
99
+ get_record!(
100
+ type,
101
+ Caprese.config.resource_primary_key,
102
+ id
103
+ )
104
+ rescue RecordNotFoundError => e
105
+ raise e unless record_scope(type.to_sym).is_a?(ActiveRecord::NullRelation)
106
+ end
107
+
108
+ # { type: '...', attributes: { ... } }
109
+ elsif contains_constructable_data?(resource_identifier)
110
+ record_scope(type.to_sym).build
111
+
112
+ # { type: '...' }
113
+ else
114
+ raise RequestDocumentInvalidError.new(field: :base)
115
+ end
116
+ else
117
+ # { id: '...' } && { attributes: { ... } }
118
+ raise RequestDocumentInvalidError.new(field: :type)
119
+ end
120
+ end
121
+
90
122
  # Gets a record in a scope using a column/value to search by
91
123
  #
92
124
  # @example
@@ -228,43 +228,58 @@ module Caprese
228
228
  #
229
229
  # PATCH/POST/DELETE /api/v1/:controller/:id/relationships/:relationship
230
230
  def update_relationship_definition
231
+ successful = false
232
+
231
233
  if queried_association &&
232
234
  flattened_keys_for(permitted_params_for(:update)).include?(params[:relationship].to_sym)
235
+ relationship_name = queried_association.reflection.name
233
236
 
234
- relationship_resources =
235
- Array.wrap(params[:data]).map do |resource_identifier|
236
- get_record!(
237
- resource_identifier[:type],
238
- column = self.config.resource_primary_key,
239
- resource_identifier[column]
240
- )
241
- end
237
+ relationship_resources = []
238
+ begin
239
+ relationship_resources = Array.wrap(records_for_relationship(
240
+ queried_record,
241
+ [],
242
+ relationship_name,
243
+ data_params
244
+ ))
245
+ rescue ActionController::ParameterMissing => e
246
+ # Only PATCH requests are allowed to have no :data (when clearing relationship)
247
+ raise e unless request.patch?
248
+ rescue Caprese::RecordNotFoundError => e
249
+ raise RequestDocumentInvalidError.new(field: :base, code: :not_found, t: e.t.slice(:value))
250
+ end
242
251
 
243
- relationship_name = queried_association.reflection.name
252
+ # Validate that if we assign queried_record as the inverse of the relationship, the relationship records are
253
+ # still valid
254
+ if !request.delete? && (inverse_reflection = queried_record.class.reflect_on_association(relationship_name).inverse_of)
255
+ relationship_resources.each { |r| r.send("#{inverse_reflection.name}=", queried_record) }
256
+ end
244
257
 
245
- successful =
246
- case queried_association.reflection.macro
247
- when :has_many
248
- if request.patch?
249
- queried_record.send("#{relationship_name}=", relationship_resources)
250
- elsif request.post?
251
- queried_record.send(relationship_name).push relationship_resources
252
- elsif request.delete?
253
- queried_record.send(relationship_name).delete relationship_resources
254
- end
255
- when :has_one
256
- if request.patch?
257
- queried_record.send("#{relationship_name}=", relationship_resources[0])
258
- relationship_resources[0].save if relationship_resources[0].present?
259
- end
260
- when :belongs_to
261
- if request.patch?
262
- queried_record.send("#{relationship_name}=", relationship_resources[0])
263
- queried_record.save
258
+ if relationship_resources.all?(&:valid?)
259
+ successful =
260
+ case queried_association.reflection.macro
261
+ when :has_many
262
+ if request.patch?
263
+ queried_record.send("#{relationship_name}=", relationship_resources)
264
+ elsif request.post?
265
+ queried_record.send(relationship_name).push relationship_resources
266
+ elsif request.delete?
267
+ queried_record.send(relationship_name).delete relationship_resources
268
+ end
269
+
270
+ true
271
+ when :has_one
272
+ if request.patch?
273
+ queried_record.send("#{relationship_name}=", relationship_resources[0])
274
+ relationship_resources[0].save if relationship_resources[0].present?
275
+ end
276
+ when :belongs_to
277
+ if request.patch?
278
+ queried_record.send("#{relationship_name}=", relationship_resources[0])
279
+ queried_record.save
280
+ end
264
281
  end
265
- end
266
- else
267
- successful = false
282
+ end
268
283
  end
269
284
 
270
285
  if successful
data/lib/caprese/error.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # TODO: Remove in favor of Rails 5 error details and dynamically setting i18n_scope
2
2
  module Caprese
3
3
  class Error < StandardError
4
- attr_reader :field, :code, :header
4
+ attr_reader :field, :code, :header, :t
5
5
 
6
6
  # Initializes a new error
7
7
  #
@@ -22,7 +22,9 @@ module Caprese
22
22
  @field = field
23
23
  @code = code
24
24
 
25
- @t = t
25
+ # field is nil if :base
26
+ field_name = field || model
27
+ @t = { field: field_name.to_s, field_title: field_name.to_s.titleize }.merge t
26
28
 
27
29
  @header = { status: :bad_request }
28
30
  end
@@ -100,14 +102,6 @@ module Caprese
100
102
  }
101
103
  end
102
104
 
103
- # Adds field and capitalized Field title to the translation params for every error
104
- # and returns them
105
- #
106
- # @return [Hash] the full translation params of the error
107
- def t
108
- @t.merge(field: @field, field_title: @field.to_s.titleize)
109
- end
110
-
111
105
  private
112
106
 
113
107
  # Checks whether or not a translation exists
@@ -1,6 +1,21 @@
1
1
  require 'caprese/error'
2
2
 
3
3
  module Caprese
4
+ # Thrown when a request document contained an error that made it unprocessable
5
+ #
6
+ # @param [Symbol] field the field of the document that contained the error
7
+ # @param [Symbol] code the code for the error
8
+ # @param [Hash] t the translation params for the error
9
+ class RequestDocumentInvalidError < Error
10
+ attr_reader :document
11
+
12
+ def initialize(field: nil, code: :invalid, t: {})
13
+ super
14
+ @document = true
15
+ @header = { status: :unprocessable_entity }
16
+ end
17
+ end
18
+
4
19
  # Thrown when a record was attempted to be persisted and was invalidated
5
20
  #
6
21
  # @param [ActiveRecord::Base] record the record that is invalid
@@ -5,7 +5,7 @@ module Caprese
5
5
  module Relationships
6
6
  extend ActiveSupport::Concern
7
7
 
8
- # Applies further scopes to a collection association when rendered as part of included document
8
+ # Applies further scopes to a singular or collection association when rendered as part of included document
9
9
  # @note Can be overridden to customize scoping at a per-relationship level
10
10
  #
11
11
  # @example
@@ -15,11 +15,13 @@ module Caprese
15
15
  # scope.by_merchant(...)
16
16
  # when :orders
17
17
  # scope.by_user(...)
18
+ # when :user
19
+ # # change singular user response
18
20
  # end
19
21
  # end
20
22
  #
21
23
  # @param [String] name the name of the association
22
- # @param [Relation] scope the scope corresponding to a collection association
24
+ # @param [Relation,Record] scope the scope corresponding to a collection association relation or singular record
23
25
  def relationship_scope(name, scope)
24
26
  scope
25
27
  end
@@ -31,10 +33,6 @@ module Caprese
31
33
  merge_serializer_option(name, options),
32
34
  &build_association_block(name)
33
35
  )
34
-
35
- define_method name do
36
- self.relationship_scope(name, object.send(object.class.caprese_unalias_field(name)))
37
- end
38
36
  end
39
37
 
40
38
  def has_one(name, options = {}, &block)
@@ -109,7 +107,7 @@ module Caprese
109
107
  end
110
108
  end
111
109
 
112
- :nil
110
+ serializer.relationship_scope(reflection_name, object.send(object.class.caprese_unalias_field(name)))
113
111
  end
114
112
  end
115
113
 
@@ -7,6 +7,10 @@ module Caprese
7
7
  object.try(:record).present?
8
8
  end
9
9
 
10
+ def document_errors?
11
+ object.try(:document).present?
12
+ end
13
+
10
14
  # Applies aliases to fields of RecordInvalid record's errors if aliases have been applied
11
15
  # @see controller/concerns/aliasing#engaged_field_aliases
12
16
  # Otherwise returns normal error fields as_json hash
@@ -1,3 +1,3 @@
1
1
  module Caprese
2
- VERSION = '0.4.1'
2
+ VERSION = '0.5.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: caprese
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Landgrebe
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2017-10-19 00:00:00.000000000 Z
13
+ date: 2017-12-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: active_model_serializers
@@ -272,3 +272,4 @@ signing_key:
272
272
  specification_version: 4
273
273
  summary: Opinionated Rails library for writing RESTful APIs
274
274
  test_files: []
275
+ has_rdoc: