2
2
require 'set'
3
3
4
4
module Grape
5
+
6
+ # The AttributeNotFoundError class indicates that an attribute defined
7
+ # by an exposure was not found on the target object of an entity.
8
+ class AttributeNotFoundError < StandardError
9
+ attr_reader :attribute
10
+
11
+ def initialize ( message , attribute )
12
+ super ( message )
13
+ @attribute = attribute . to_sym
14
+ end
15
+ end
16
+
5
17
# An Entity is a lightweight structure that allows you to easily
6
18
# represent data from your application in a consistent and abstracted
7
19
# way in your API. Entities can also provide documentation for the
@@ -123,6 +135,9 @@ def entity(options = {})
123
135
# block to the expose call to achieve the same effect.
124
136
# @option options :documentation Define documenation for an exposed
125
137
# field, typically the value is a hash with two fields, type and desc.
138
+ # @option options [Symbol, Proc] :object Specifies the target object to get
139
+ # an attribute value from. A [Symbol] references a method on the [#object].
140
+ # A [Proc] should return an alternate object.
126
141
def self . expose ( *args , &block )
127
142
options = merge_options ( args . last . is_a? ( Hash ) ? args . pop : { } )
128
143
@@ -171,6 +186,99 @@ def self.with_options(options)
171
186
@block_options . pop
172
187
end
173
188
189
+ # Merge exposures from another entity into the current entity
190
+ # as a way to "flatten" multiple models for use in formats such as "CSV".
191
+ #
192
+ # @overload merge_with(*entity_classes, &block)
193
+ # @param entity_classes [Entity] list of entities to copy exposures from
194
+ # (The last parameter can be a [Hash] with options)
195
+ # @param block [Proc] A block that returns the target object to retrieve attribute
196
+ # values from.
197
+ #
198
+ # @overload merge_with(*entity_classes, options, &block)
199
+ # @param entity_classes [Entity] list of entities to copy exposures from
200
+ # (The last parameter can be a [Hash] with options)
201
+ # @param options [Hash] Options merged into each exposure that is copied from
202
+ # the specified entities. Some additional options determine how exposures are
203
+ # copied.
204
+ # @see expose
205
+ # @param block [Proc] A block that returns the target object to retrieve attribute
206
+ # values from. Stored in the [expose] :object option.
207
+ # @option options [Symbol, Array<Symbol>] :except Attributes to skip when copying exposures
208
+ # @option options [Symbol, Array<Symbol>] :only Attributes to include when copying exposures
209
+ # @option options [String] :prefix String to prefix attributes with
210
+ # @option options [String] :suffix String to suffix attributes with
211
+ # @option options :if Criteria that are evaluated to determine if an exposure
212
+ # should be represented. If a copied exposure already has the :if option specified,
213
+ # a [Proc] is created that wraps both :if conditions.
214
+ # @see expose Check out the description of the default :if option
215
+ # @option options :unless Criteria that are evaluated to determine if an exposure
216
+ # should be represented. If a copied exposure already has the :unless option specified,
217
+ # a [Proc] is created that wraps both :unless conditions.
218
+ # @see expose Check out the description of the default :unless option
219
+ # @param block [Proc] A block that returns the target object to retrieve attribute
220
+ # values from.
221
+ #
222
+ # @raise ArgumentError Entity classes must inherit from [Entity]
223
+ #
224
+ # @example Merge child entity into parent
225
+ #
226
+ # class Address < Grape::Entity
227
+ # expose :id, :street, :city, :state, :zip
228
+ # end
229
+ #
230
+ # class Contact < Grape::Entity
231
+ # expose :id, :name
232
+ # expose :addresses, using: Address, unless: { format: :csv }
233
+ # merge_with Address, if: { format: :csv }, except: :id do
234
+ # object.addresses.first
235
+ # end
236
+ # end
237
+ def self . merge_with ( *entity_classes , &block )
238
+ merge_options = entity_classes . last . is_a? ( Hash ) ? entity_classes . pop . dup : { }
239
+ except_attributes = [ merge_options . delete ( :except ) ] . flatten . compact
240
+ only_attributes = [ merge_options . delete ( :only ) ] . flatten . compact
241
+ prefix = merge_options . delete ( :prefix )
242
+ suffix = merge_options . delete ( :suffix )
243
+
244
+ merge_options [ :object ] = block if block_given?
245
+
246
+ entity_classes . each do |entity_class |
247
+ raise ArgumentError , "#{ entity_class } must be a Grape::Entity" unless entity_class < Entity
248
+
249
+ merged_entities [ entity_class ] = merge_options
250
+
251
+ entity_class . exposures . each_pair do |attribute , original_options |
252
+ next if except_attributes . any? && except_attributes . include? ( attribute )
253
+ next if only_attributes . any? && !only_attributes . include? ( attribute )
254
+
255
+ original_options = original_options . dup
256
+ exposure_options = original_options . merge ( merge_options )
257
+
258
+ [ :if , :unless ] . each do |condition |
259
+ if merge_options . has_key? ( condition ) && original_options . has_key? ( condition )
260
+
261
+ # only overwrite original_options[:object] if a new object is specified
262
+ if merge_options . has_key? :object
263
+ original_options [ :object ] = merge_options [ :object ]
264
+ end
265
+
266
+ exposure_options [ condition ] = proc { |object , instance_options |
267
+ conditions_met? ( original_options , instance_options ) &&
268
+ conditions_met? ( merge_options , instance_options )
269
+ }
270
+ end
271
+ end
272
+
273
+ expose :"#{ prefix } #{ attribute } #{ suffix } " , exposure_options
274
+ end
275
+ end
276
+ end
277
+
278
+ def self . merged_entities
279
+ @merged_entities ||= superclass . respond_to? ( :merged_entities ) ? superclass . exposures . dup : { }
280
+ end
281
+
174
282
# Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
175
283
# are symbolized references to methods on the containing object, the values are
176
284
# the options that were passed into expose.
@@ -430,7 +538,7 @@ def value_for(attribute, options = {})
430
538
if exposure_options [ :proc ]
431
539
exposure_options [ :using ] . represent ( instance_exec ( object , options , &exposure_options [ :proc ] ) , using_options )
432
540
else
433
- exposure_options [ :using ] . represent ( delegate_attribute ( attribute ) , using_options )
541
+ exposure_options [ :using ] . represent ( delegate_attribute ( attribute , exposure_options ) , using_options )
434
542
end
435
543
436
544
elsif exposure_options [ :proc ]
@@ -440,11 +548,11 @@ def value_for(attribute, options = {})
440
548
format_with = exposure_options [ :format_with ]
441
549
442
550
if format_with . is_a? ( Symbol ) && formatters [ format_with ]
443
- instance_exec ( delegate_attribute ( attribute ) , &formatters [ format_with ] )
551
+ instance_exec ( delegate_attribute ( attribute , exposure_options ) , &formatters [ format_with ] )
444
552
elsif format_with . is_a? ( Symbol )
445
- send ( format_with , delegate_attribute ( attribute ) )
553
+ send ( format_with , delegate_attribute ( attribute , exposure_options ) )
446
554
elsif format_with . respond_to? :call
447
- instance_exec ( delegate_attribute ( attribute ) , &format_with )
555
+ instance_exec ( delegate_attribute ( attribute , exposure_options ) , &format_with )
448
556
end
449
557
450
558
elsif nested_exposures . any?
@@ -453,16 +561,43 @@ def value_for(attribute, options = {})
453
561
end ]
454
562
455
563
else
456
- delegate_attribute ( attribute )
564
+ delegate_attribute ( attribute , exposure_options )
457
565
end
458
566
end
459
567
460
- def delegate_attribute ( attribute )
461
- name = self . class . name_for ( attribute )
462
- if respond_to? ( name , true )
463
- send ( name )
568
+ # Detects what target object to retrieve the attribute value from.
569
+ #
570
+ # @param attribute [Symbol] Name of attribute to get a value from the target object
571
+ # @param alternate_object [Symbol, Proc] Specifies a target object to use
572
+ # instead of [#object] by referencing a method on the instance with a symbol,
573
+ # or evaluating a [Proc] and using the result as the target object. The original
574
+ # [#object] is used if no alternate object is specified.
575
+ #
576
+ # @raise [AttributeNotFoundError]
577
+ def delegate_attribute ( attribute , options = { } )
578
+ target_object = select_target_object ( options )
579
+
580
+ if respond_to? ( attribute , true )
581
+ send ( attribute )
582
+ elsif target_object . respond_to? ( attribute , true )
583
+ target_object . send ( attribute )
584
+ elsif target_object . respond_to? ( :[] , true )
585
+ target_object . send ( :[] , attribute )
464
586
else
465
- object . send ( name )
587
+ raise AttributeNotFoundError . new ( attribute . to_s , attribute )
588
+ end
589
+ end
590
+
591
+ def select_target_object ( options )
592
+ alternate_object = options [ :object ]
593
+
594
+ case alternate_object
595
+ when Symbol
596
+ send ( alternate_object )
597
+ when Proc
598
+ instance_exec ( &alternate_object )
599
+ else
600
+ object
466
601
end
467
602
end
468
603
@@ -481,7 +616,7 @@ def conditions_met?(exposure_options, options)
481
616
if_conditions . each do |if_condition |
482
617
case if_condition
483
618
when Hash then if_condition . each_pair { |k , v | return false if options [ k . to_sym ] != v }
484
- when Proc then return false unless instance_exec ( object , options , &if_condition )
619
+ when Proc then return false unless instance_exec ( select_target_object ( exposure_options ) , options , &if_condition )
485
620
when Symbol then return false unless options [ if_condition ]
486
621
end
487
622
end
@@ -492,7 +627,7 @@ def conditions_met?(exposure_options, options)
492
627
unless_conditions . each do |unless_condition |
493
628
case unless_condition
494
629
when Hash then unless_condition . each_pair { |k , v | return false if options [ k . to_sym ] == v }
495
- when Proc then return false if instance_exec ( object , options , &unless_condition )
630
+ when Proc then return false if instance_exec ( select_target_object ( exposure_options ) , options , &unless_condition )
496
631
when Symbol then return false if options [ unless_condition ]
497
632
end
498
633
end
0 commit comments