grape-entity 0.4.5 → 0.4.6

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: 77619537334dd75647477a452a937b672ca248cb
4
- data.tar.gz: f7c7659bd6f13e2f65efb8091b2ca00a37dfcd74
3
+ metadata.gz: 22fd603c80eeb68f64892de2f1ca6873c12b20ae
4
+ data.tar.gz: 63638985ec1e6d721d54885840ad66c54f14ac9c
5
5
  SHA512:
6
- metadata.gz: c73b5b28f9cb7ebcc7ba71e264029e725720cfa79593bcb891aeb5628ca3b50a26699043d38e79f1842a0336f1a66e881d636befa6f20d74e469131f86b1ac04
7
- data.tar.gz: 2d086dee3c598f34bedc613fe1da601e7eff2afb6f05d9cd8944c9ffac21e7f16f5e4cea2a686ecdbf1f60c0c6d9f08e918b0f247ed4b23a54df1ec13a6b9f45
6
+ metadata.gz: b1d88f629ac5188cd05ee77fa32641eb431a44b088c9ac17375c17100ccc6b314b04aad0ffae6f299af21d7fb4b6d33a6ff5c53ce9b64c50b7e5ca483761e0c4
7
+ data.tar.gz: e40e7c4e6fee4fef015cc554a38f8e6a44fb05e23142b0932263e059f97a9a9948d012e70f9e9a6f8084a83dd181894afb43d49c208a61712277ab09572f1809
data/.rubocop.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  Exclude:
3
3
  - vendor/**/*
4
+ - Guardfile
4
5
 
5
6
  inherit_from: .rubocop_todo.yml
data/.rubocop_todo.yml CHANGED
@@ -1,64 +1,42 @@
1
1
  # This configuration was generated by `rubocop --auto-gen-config`
2
- # on 2014-12-11 15:27:22 -0500 using RuboCop version 0.28.0.
2
+ # on 2015-05-21 22:47:03 +0700 using RuboCop version 0.31.0.
3
3
  # The point is for the user to remove these configuration records
4
4
  # one by one as the offenses are removed from the code base.
5
5
  # Note that changes in the inspected code, or installation of new
6
6
  # versions of RuboCop, may require this file to be generated again.
7
7
 
8
- # Offense count: 5
8
+ # Offense count: 8
9
9
  Metrics/AbcSize:
10
- Max: 53
10
+ Max: 51
11
11
 
12
12
  # Offense count: 1
13
13
  # Configuration parameters: CountComments.
14
14
  Metrics/ClassLength:
15
- Max: 301
15
+ Max: 328
16
16
 
17
- # Offense count: 4
17
+ # Offense count: 5
18
18
  Metrics/CyclomaticComplexity:
19
19
  Max: 17
20
20
 
21
- # Offense count: 175
21
+ # Offense count: 176
22
22
  # Configuration parameters: AllowURI, URISchemes.
23
23
  Metrics/LineLength:
24
- Max: 147
24
+ Max: 146
25
25
 
26
- # Offense count: 6
26
+ # Offense count: 7
27
27
  # Configuration parameters: CountComments.
28
28
  Metrics/MethodLength:
29
- Max: 34
29
+ Max: 32
30
30
 
31
- # Offense count: 4
31
+ # Offense count: 5
32
32
  Metrics/PerceivedComplexity:
33
33
  Max: 15
34
34
 
35
- # Offense count: 2
36
- # Cop supports --auto-correct.
37
- Style/Blocks:
38
- Enabled: false
39
-
40
- # Offense count: 30
35
+ # Offense count: 31
41
36
  Style/Documentation:
42
37
  Enabled: false
43
38
 
44
- # Offense count: 2
45
- Style/EachWithObject:
46
- Enabled: false
47
-
48
39
  # Offense count: 1
49
40
  # Configuration parameters: Exclude.
50
41
  Style/FileName:
51
42
  Enabled: false
52
-
53
- # Offense count: 16
54
- Style/Lambda:
55
- Enabled: false
56
-
57
- # Offense count: 1
58
- # Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
59
- Style/Next:
60
- Enabled: false
61
-
62
- # Offense count: 2
63
- Style/RegexpLiteral:
64
- MaxSlashes: 0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ 0.4.6 (2015-07-27)
2
+ ==================
3
+
4
+ * [#114](https://github.com/intridea/grape-entity/pull/114): Added 'only' option that selects which attributes should be returned - [@estevaoam](https://github.com/estevaoam).
5
+ * [#115](https://github.com/intridea/grape-entity/pull/115): Allowing 'root' to be inherited from parent to child entities - [@guidoprincess](https://github.com/guidoprincess).
6
+ * [#121](https://github.com/intridea/grape-entity/pull/122): Sublcassed Entity#documentation properly handles unexposed params - [@dan-corneanu](https://github.com/dan-corneanu).
7
+ * [#134](https://github.com/intridea/grape-entity/pull/134): Subclasses no longer affected in all cases by `unexpose` in parent - [@etehtsea](https://github.com/etehtsea).
8
+ * [#135](https://github.com/intridea/grape-entity/pull/135): Added `except` option - [@dan-corneanu](https://github.com/dan-corneanu).
9
+ * [#136](https://github.com/intridea/grape-entity/pull/136): Allow for strings in `only` and `except` options - [@bswinnerton](https://github.com/bswinnerton).
10
+ * [#147](https://github.com/intridea/grape-entity/pull/147): Expose `safe` attributes as `nil` if they cannot be evaluated: [#140](https://github.com/intridea/grape-entity/issues/140) - [@marshall-lee](http://github.com/marshall-lee).
11
+ * [#147](https://github.com/intridea/grape-entity/pull/147): Fix: private method values were not exposed with `safe` option: [#142](https://github.com/intridea/grape-entity/pull/142) - [@marshall-lee](http://github.com/marshall-lee).
12
+ * [#147](https://github.com/intridea/grape-entity/pull/147): Remove catching of `NoMethodError` because it can occur deep inside in a method call so this exception does not mean that attribute not exist - [@marshall-lee](http://github.com/marshall-lee).
13
+ * [#147](https://github.com/intridea/grape-entity/pull/147): `valid_exposures` is removed - [@marshall-lee](http://github.com/marshall-lee).
14
+
1
15
  0.4.5 (2015-03-10)
2
16
  ==================
3
17
 
data/Gemfile CHANGED
@@ -12,5 +12,5 @@ group :development, :test do
12
12
  gem 'json'
13
13
  gem 'rspec'
14
14
  gem 'rack-test', '~> 0.6.2', require: 'rack/test'
15
- gem 'rubocop', '0.28.0'
15
+ gem 'rubocop', '0.31.0'
16
16
  end
data/README.md CHANGED
@@ -23,13 +23,13 @@ module API
23
23
  expose :user_type, :user_id, if: lambda { |status, options| status.user.public? }
24
24
  expose :contact_info do
25
25
  expose :phone
26
- expose :address, using: API::Address
26
+ expose :address, using: API::Entities::Address
27
27
  end
28
28
  expose :digest do |status, options|
29
29
  Digest::MD5.hexdigest status.txt
30
30
  end
31
- expose :replies, using: API::Status, as: :replies
32
- expose :last_reply, using: API::Status do |status, options|
31
+ expose :replies, using: API::Entities::Status, as: :responses
32
+ expose :last_reply, using: API::Entities::Status do |status, options|
33
33
  status.replies.last
34
34
  end
35
35
 
@@ -78,13 +78,13 @@ The field lookup takes several steps
78
78
  Don't derive your model classes from `Grape::Entity`, expose them using a presenter.
79
79
 
80
80
  ```ruby
81
- expose :replies, using: API::Status, as: :replies
81
+ expose :replies, using: API::Entities::Status, as: :responses
82
82
  ```
83
83
 
84
84
  Presenter classes can also be specified in string format, which helps with circular dependencies.
85
85
 
86
86
  ```ruby
87
- expose :replies, using: "API::Status", as: :replies
87
+ expose :replies, using: "API::Entities::Status", as: :responses
88
88
  ```
89
89
 
90
90
  #### Conditional Exposure
@@ -116,7 +116,7 @@ Supply a block to define a hash using nested exposures.
116
116
  ```ruby
117
117
  expose :contact_info do
118
118
  expose :phone
119
- expose :address, using: API::Address
119
+ expose :address, using: API::Entities::Address
120
120
  end
121
121
  ```
122
122
 
@@ -124,7 +124,7 @@ You can also conditionally expose attributes in nested exposures:
124
124
  ```ruby
125
125
  expose :contact_info do
126
126
  expose :phone
127
- expose :address, using: API::Address
127
+ expose :address, using: API::Entities::Address
128
128
  expose :email, if: lambda { |instance, options| options[:type] == :full }
129
129
  end
130
130
  ```
@@ -148,7 +148,7 @@ As example:
148
148
  ```ruby
149
149
 
150
150
  present_collection true, :collection_name # `collection_name` is optional and defaults to `items`
151
- expose :collection_name, using: API:Items
151
+ expose :collection_name, using: API::Entities::Items
152
152
 
153
153
 
154
154
  ```
@@ -220,16 +220,55 @@ class MailingAddress < UserData
220
220
  end
221
221
  ```
222
222
 
223
+ #### Returning only the fields you want
223
224
 
225
+ After exposing the desired attributes, you can choose which one you need when representing some object or collection by using the only: and except: options. See the example:
224
226
 
227
+ ```ruby
228
+ class UserEntity
229
+ expose :id
230
+ expose :name
231
+ expose :email
232
+ end
233
+
234
+ class Entity
235
+ expose :id
236
+ expose :title
237
+ expose :user, using: UserEntity
238
+ end
239
+
240
+ data = Entity.represent(model, only: [:title, { user: [:name, :email] }])
241
+ data.as_json
242
+ ```
225
243
 
244
+ This will return something like this:
245
+
246
+ ```ruby
247
+ {
248
+ title: 'grape-entity is awesome!',
249
+ user: {
250
+ name: 'John Applet',
251
+ email: 'john@example.com'
252
+ }
253
+ }
254
+ ```
255
+
256
+ Instead of returning all the exposed attributes.
257
+
258
+
259
+ The same result can be achieved with the following exposure:
260
+
261
+ ```ruby
262
+ data = Entity.represent(model, except: [:id, { user: [:id] }])
263
+ data.as_json
264
+ ```
226
265
 
227
266
  #### Aliases
228
267
 
229
268
  Expose under a different name with `:as`.
230
269
 
231
270
  ```ruby
232
- expose :replies, using: API::Status, as: :replies
271
+ expose :replies, using: API::Entities::Status, as: :responses
233
272
  ```
234
273
 
235
274
  #### Format Before Exposing
@@ -271,6 +310,30 @@ end
271
310
  present s, with: Status, user: current_user
272
311
  ```
273
312
 
313
+ #### Passing Additional Option To Nested Exposure
314
+ There are sometimes that you want to pass additional option or parameter to nested exposure. Assume that you need to expose an address for a contact info, but it has both two different format: **full** and **simple**. You can pass an additional `full_format` option to specify that if the nested entity should render address in `:full` format.
315
+
316
+ ```ruby
317
+ # api/contact.rb
318
+ expose :contact_info do
319
+ expose :phone
320
+ expose :address do |instance, options|
321
+ # use `#merge` to extend options and then pass the new version of options to the nested entity
322
+ API::Entities::Address.represent instance.address, options.merge(full_format: instance.need_full_format?)
323
+ end
324
+ expose :email, if: lambda { |instance, options| options[:type] == :full }
325
+ end
326
+
327
+ # api/address.rb
328
+ expose :state, if: lambda {|instance, options| !!options[:full_format]} # the new option could be retrieved in options hash for conditional exposure
329
+ expose :city, if: lambda {|instance, options| !!options[:full_format]}
330
+ expose :stree do |instance, options|
331
+ # the new option could be retrieved in options hash for runtime exposure
332
+ !!options[:full_format] ? instance.full_street_name : instance.simple_street_name
333
+ end
334
+ ```
335
+ **Notice**: In the above code, you should pay attention to [**Safe Exposure**](#safe-exposure) yourself, for example, `instance.address` might be `nil`, in this situation, it is better to expose it as nil directly.
336
+
274
337
  ### Using the Exposure DSL
275
338
 
276
339
  Grape ships with a DSL to easily define entities within the context of an existing class:
@@ -393,4 +456,3 @@ MIT License. See [LICENSE](LICENSE) for details.
393
456
  ## Copyright
394
457
 
395
458
  Copyright (c) 2010-2014 Michael Bleigh, Intridea, Inc., and contributors.
396
-
data/lib/grape_entity.rb CHANGED
@@ -2,3 +2,4 @@ require 'active_support'
2
2
  require 'active_support/core_ext'
3
3
  require 'grape_entity/version'
4
4
  require 'grape_entity/entity'
5
+ require 'grape_entity/delegator'
@@ -0,0 +1,23 @@
1
+ require 'grape_entity/delegator/base'
2
+ require 'grape_entity/delegator/hash_object'
3
+ require 'grape_entity/delegator/openstruct_object'
4
+ require 'grape_entity/delegator/fetchable_object'
5
+ require 'grape_entity/delegator/plain_object'
6
+
7
+ module Grape
8
+ class Entity
9
+ module Delegator
10
+ def self.new(object)
11
+ if object.is_a?(Hash)
12
+ HashObject.new object
13
+ elsif defined?(OpenStruct) && object.is_a?(OpenStruct)
14
+ OpenStructObject.new object
15
+ elsif object.respond_to? :fetch, true
16
+ FetchableObject.new object
17
+ else
18
+ PlainObject.new object
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ module Grape
2
+ class Entity
3
+ module Delegator
4
+ class Base
5
+ attr_reader :object
6
+
7
+ def initialize(object)
8
+ @object = object
9
+ end
10
+
11
+ def delegatable?(_attribute)
12
+ true
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ class Entity
3
+ module Delegator
4
+ class FetchableObject < Base
5
+ def delegate(attribute)
6
+ object.fetch attribute
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ class Entity
3
+ module Delegator
4
+ class HashObject < Base
5
+ def delegate(attribute)
6
+ object[attribute]
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ class Entity
3
+ module Delegator
4
+ class OpenStructObject < Base
5
+ def delegate(attribute)
6
+ object.send attribute
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module Grape
2
+ class Entity
3
+ module Delegator
4
+ class PlainObject < Base
5
+ def delegate(attribute)
6
+ object.send attribute
7
+ end
8
+
9
+ def delegatable?(attribute)
10
+ object.respond_to? attribute, true
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -42,7 +42,7 @@ module Grape
42
42
  # end
43
43
  # end
44
44
  class Entity
45
- attr_reader :object, :options
45
+ attr_reader :object, :delegator, :options
46
46
 
47
47
  # The Entity DSL allows you to mix entity functionality into
48
48
  # your existing classes.
@@ -98,6 +98,28 @@ module Grape
98
98
  end
99
99
  end
100
100
 
101
+ class << self
102
+ # Returns exposures that have been declared for this Entity or
103
+ # ancestors. The keys are symbolized references to methods on the
104
+ # containing object, the values are the options that were passed into expose.
105
+ # @return [Hash] of exposures
106
+ attr_accessor :exposures
107
+ attr_accessor :root_exposures
108
+ # Returns all formatters that are registered for this and it's ancestors
109
+ # @return [Hash] of formatters
110
+ attr_accessor :formatters
111
+ attr_accessor :nested_attribute_names
112
+ attr_accessor :nested_exposures
113
+ end
114
+
115
+ def self.inherited(subclass)
116
+ subclass.exposures = exposures.try(:dup) || {}
117
+ subclass.root_exposures = root_exposures.try(:dup) || {}
118
+ subclass.nested_exposures = nested_exposures.try(:dup) || {}
119
+ subclass.nested_attribute_names = nested_attribute_names.try(:dup) || {}
120
+ subclass.formatters = formatters.try(:dup) || {}
121
+ end
122
+
101
123
  # This method is the primary means by which you will declare what attributes
102
124
  # should be exposed by the entity.
103
125
  #
@@ -137,17 +159,19 @@ module Grape
137
159
 
138
160
  @nested_attributes ||= []
139
161
 
162
+ # rubocop:disable Style/Next
140
163
  args.each do |attribute|
141
- unless @nested_attributes.empty?
164
+ if @nested_attributes.empty?
165
+ root_exposures[attribute] = options
166
+ else
142
167
  orig_attribute = attribute.to_sym
143
- attribute = "#{@nested_attributes.last}__#{attribute}"
144
- nested_attribute_names_hash[attribute.to_sym] = orig_attribute
168
+ attribute = "#{@nested_attributes.last}__#{attribute}".to_sym
169
+ nested_attribute_names[attribute] = orig_attribute
145
170
  options[:nested] = true
146
- nested_exposures_hash[@nested_attributes.last.to_sym] ||= {}
147
- nested_exposures_hash[@nested_attributes.last.to_sym][attribute.to_sym] = options
171
+ nested_exposures.deep_merge!(@nested_attributes.last.to_sym => { attribute => options })
148
172
  end
149
173
 
150
- exposures[attribute.to_sym] = options
174
+ exposures[attribute] = options
151
175
 
152
176
  # Nested exposures are given in a block with no parameters.
153
177
  if block_given? && block.parameters.empty?
@@ -177,74 +201,15 @@ module Grape
177
201
  @block_options.pop
178
202
  end
179
203
 
180
- # Returns a hash of exposures that have been declared for this Entity or ancestors. The keys
181
- # are symbolized references to methods on the containing object, the values are
182
- # the options that were passed into expose.
183
- def self.exposures
184
- return @exposures unless @exposures.nil?
185
-
186
- @exposures = {}
187
-
188
- if superclass.respond_to? :exposures
189
- @exposures = superclass.exposures.merge(@exposures)
190
- end
191
-
192
- @exposures
193
- end
194
-
195
- class << self
196
- attr_accessor :_nested_attribute_names_hash
197
- attr_accessor :_nested_exposures_hash
198
-
199
- def nested_attribute_names_hash
200
- self._nested_attribute_names_hash ||= {}
201
- end
202
-
203
- def nested_exposures_hash
204
- self._nested_exposures_hash ||= {}
205
- end
206
-
207
- def nested_attribute_names
208
- return @nested_attribute_names unless @nested_attribute_names.nil?
209
-
210
- @nested_attribute_names = {}.merge(nested_attribute_names_hash)
211
-
212
- if superclass.respond_to? :nested_attribute_names
213
- @nested_attribute_names = superclass.nested_attribute_names.deep_merge(@nested_attribute_names)
214
- end
215
-
216
- @nested_attribute_names
217
- end
218
-
219
- def nested_exposures
220
- return @nested_exposures unless @nested_exposures.nil?
221
-
222
- @nested_exposures = {}.merge(nested_exposures_hash)
223
-
224
- if superclass.respond_to? :nested_exposures
225
- @nested_exposures = superclass.nested_exposures.deep_merge(@nested_exposures)
226
- end
227
-
228
- @nested_exposures
229
- end
230
- end
231
-
232
204
  # Returns a hash, the keys are symbolized references to fields in the entity,
233
205
  # the values are document keys in the entity's documentation key. When calling
234
206
  # #docmentation, any exposure without a documentation key will be ignored.
235
207
  def self.documentation
236
- @documentation ||= exposures.inject({}) do |memo, (attribute, exposure_options)|
237
- unless exposure_options[:documentation].nil? || exposure_options[:documentation].empty?
208
+ @documentation ||= exposures.each_with_object({}) do |(attribute, exposure_options), memo|
209
+ if exposure_options[:documentation].present?
238
210
  memo[key_for(attribute)] = exposure_options[:documentation]
239
211
  end
240
- memo
241
212
  end
242
-
243
- if superclass.respond_to? :documentation
244
- @documentation = superclass.documentation.merge(@documentation)
245
- end
246
-
247
- @documentation
248
213
  end
249
214
 
250
215
  # This allows you to declare a Proc in which exposures can be formatted with.
@@ -278,17 +243,6 @@ module Grape
278
243
  formatters[name.to_sym] = block
279
244
  end
280
245
 
281
- # Returns a hash of all formatters that are registered for this and it's ancestors.
282
- def self.formatters
283
- @formatters ||= {}
284
-
285
- if superclass.respond_to? :formatters
286
- @formatters = superclass.formatters.merge(@formatters)
287
- end
288
-
289
- @formatters
290
- end
291
-
292
246
  # This allows you to set a root element name for your representation.
293
247
  #
294
248
  # @param plural [String] the root key to use when representing
@@ -404,13 +358,15 @@ module Grape
404
358
  # even if one is defined for the entity.
405
359
  # @option options :serializable [true or false] when true a serializable Hash will be returned
406
360
  #
361
+ # @option options :only [Array] all the fields that should be returned
362
+ # @option options :except [Array] all the fields that should not be returned
407
363
  def self.represent(objects, options = {})
408
364
  if objects.respond_to?(:to_ary) && ! @present_collection
409
- root_element = @collection_root
365
+ root_element = root_element(:collection_root)
410
366
  inner = objects.to_ary.map { |object| new(object, { collection: true }.merge(options)).presented }
411
367
  else
412
368
  objects = { @collection_name => objects } if @present_collection
413
- root_element = @root
369
+ root_element = root_element(:root)
414
370
  inner = new(objects, options).presented
415
371
  end
416
372
 
@@ -419,6 +375,16 @@ module Grape
419
375
  root_element ? { root_element => inner } : inner
420
376
  end
421
377
 
378
+ # This method returns the entity's root or collection root node, or its parent's
379
+ # @param root_type: either :collection_root or just :root
380
+ def self.root_element(root_type)
381
+ if instance_variable_get("@#{root_type}")
382
+ instance_variable_get("@#{root_type}")
383
+ elsif superclass.respond_to? :root_element
384
+ superclass.root_element(root_type)
385
+ end
386
+ end
387
+
422
388
  def presented
423
389
  if options[:serializable]
424
390
  serializable_hash
@@ -428,17 +394,17 @@ module Grape
428
394
  end
429
395
 
430
396
  def initialize(object, options = {})
431
- @object, @options = object, options
397
+ @object = object
398
+ @delegator = Delegator.new object
399
+ @options = options
432
400
  end
433
401
 
434
402
  def exposures
435
403
  self.class.exposures
436
404
  end
437
405
 
438
- def valid_exposures
439
- exposures.reject { |_, options| options[:nested] }.select do |attribute, exposure_options|
440
- valid_exposure?(attribute, exposure_options)
441
- end
406
+ def root_exposures
407
+ self.class.root_exposures
442
408
  end
443
409
 
444
410
  def documentation
@@ -458,24 +424,77 @@ module Grape
458
424
  # etc.
459
425
  def serializable_hash(runtime_options = {})
460
426
  return nil if object.nil?
427
+
461
428
  opts = options.merge(runtime_options || {})
462
- valid_exposures.inject({}) do |output, (attribute, exposure_options)|
463
- if conditions_met?(exposure_options, opts)
464
- partial_output = value_for(attribute, opts)
465
- output[self.class.key_for(attribute)] =
466
- if partial_output.respond_to? :serializable_hash
467
- partial_output.serializable_hash(runtime_options)
468
- elsif partial_output.is_a?(Array) && !partial_output.map { |o| o.respond_to? :serializable_hash }.include?(false)
469
- partial_output.map(&:serializable_hash)
470
- elsif partial_output.is_a?(Hash)
471
- partial_output.each do |key, value|
472
- partial_output[key] = value.serializable_hash if value.respond_to? :serializable_hash
473
- end
474
- else
475
- partial_output
429
+
430
+ root_exposures.each_with_object({}) do |(attribute, exposure_options), output|
431
+ next unless should_return_attribute?(attribute, opts) && conditions_met?(exposure_options, opts)
432
+
433
+ partial_output = value_for(attribute, opts)
434
+
435
+ output[self.class.key_for(attribute)] =
436
+ if partial_output.respond_to?(:serializable_hash)
437
+ partial_output.serializable_hash(runtime_options)
438
+ elsif partial_output.is_a?(Array) && partial_output.all? { |o| o.respond_to?(:serializable_hash) }
439
+ partial_output.map(&:serializable_hash)
440
+ elsif partial_output.is_a?(Hash)
441
+ partial_output.each do |key, value|
442
+ partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash)
476
443
  end
444
+ else
445
+ partial_output
446
+ end
447
+ end
448
+ end
449
+
450
+ def should_return_attribute?(attribute, options)
451
+ key = self.class.key_for(attribute)
452
+ only = only_fields(options).nil? ||
453
+ only_fields(options).include?(key)
454
+ except = except_fields(options) && except_fields(options).include?(key) &&
455
+ except_fields(options)[key] == true
456
+ only && !except
457
+ end
458
+
459
+ def only_fields(options, for_attribute = nil)
460
+ return nil unless options[:only]
461
+
462
+ @only_fields ||= options[:only].each_with_object({}) do |attribute, allowed_fields|
463
+ if attribute.is_a?(Hash)
464
+ attribute.each do |attr, nested_attrs|
465
+ allowed_fields[attr] ||= []
466
+ allowed_fields[attr] += nested_attrs
467
+ end
468
+ else
469
+ allowed_fields[attribute] = true
477
470
  end
478
- output
471
+ end.symbolize_keys
472
+
473
+ if for_attribute && @only_fields[for_attribute].is_a?(Array)
474
+ @only_fields[for_attribute]
475
+ elsif for_attribute.nil?
476
+ @only_fields
477
+ end
478
+ end
479
+
480
+ def except_fields(options, for_attribute = nil)
481
+ return nil unless options[:except]
482
+
483
+ @except_fields ||= options[:except].each_with_object({}) do |attribute, allowed_fields|
484
+ if attribute.is_a?(Hash)
485
+ attribute.each do |attr, nested_attrs|
486
+ allowed_fields[attr] ||= []
487
+ allowed_fields[attr] += nested_attrs
488
+ end
489
+ else
490
+ allowed_fields[attribute] = true
491
+ end
492
+ end.symbolize_keys
493
+
494
+ if for_attribute && @except_fields[for_attribute].is_a?(Array)
495
+ @except_fields[for_attribute]
496
+ elsif for_attribute.nil?
497
+ @except_fields
479
498
  end
480
499
  end
481
500
 
@@ -502,21 +521,30 @@ module Grape
502
521
  exposures[attribute.to_sym][:as] || name_for(attribute)
503
522
  end
504
523
 
505
- def self.nested_exposures_for(attribute)
506
- nested_exposures[attribute] || {}
524
+ def self.nested_exposures_for?(attribute)
525
+ nested_exposures.key?(attribute)
526
+ end
527
+
528
+ def nested_value_for(attribute, options)
529
+ nested_exposures = self.class.nested_exposures[attribute]
530
+ nested_attributes =
531
+ nested_exposures.map do |nested_attribute, nested_exposure_options|
532
+ if conditions_met?(nested_exposure_options, options)
533
+ [self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
534
+ end
535
+ end
536
+
537
+ Hash[nested_attributes.compact]
507
538
  end
508
539
 
509
540
  def value_for(attribute, options = {})
510
541
  exposure_options = exposures[attribute.to_sym]
511
-
512
- nested_exposures = self.class.nested_exposures_for(attribute)
542
+ return unless valid_exposure?(attribute, exposure_options)
513
543
 
514
544
  if exposure_options[:using]
515
545
  exposure_options[:using] = exposure_options[:using].constantize if exposure_options[:using].respond_to? :constantize
516
546
 
517
- using_options = options.dup
518
- using_options.delete(:collection)
519
- using_options[:root] = nil
547
+ using_options = options_for_using(attribute, options)
520
548
 
521
549
  if exposure_options[:proc]
522
550
  exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
@@ -538,15 +566,8 @@ module Grape
538
566
  instance_exec(delegate_attribute(attribute), &format_with)
539
567
  end
540
568
 
541
- elsif nested_exposures.any?
542
- nested_attributes =
543
- nested_exposures.map do |nested_attribute, nested_exposure_options|
544
- if conditions_met?(nested_exposure_options, options)
545
- [self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
546
- end
547
- end
548
-
549
- Hash[nested_attributes.compact]
569
+ elsif self.class.nested_exposures_for?(attribute)
570
+ nested_value_for(attribute, options)
550
571
  else
551
572
  delegate_attribute(attribute)
552
573
  end
@@ -556,28 +577,24 @@ module Grape
556
577
  name = self.class.name_for(attribute)
557
578
  if respond_to?(name, true)
558
579
  send(name)
559
- elsif object.is_a?(Hash)
560
- object[name]
561
- elsif object.respond_to?(name, true)
562
- object.send(name)
563
- elsif object.respond_to?(:fetch, true)
564
- object.fetch(name)
565
580
  else
566
- begin
567
- object.send(name)
568
- rescue NoMethodError
569
- raise NoMethodError, "#{self.class.name} missing attribute `#{name}' on #{object}"
570
- end
581
+ delegator.delegate(name)
571
582
  end
572
583
  end
573
584
 
574
585
  def valid_exposure?(attribute, exposure_options)
575
- nested_exposures = self.class.nested_exposures_for(attribute)
576
- (nested_exposures.any? && nested_exposures.all? { |a, o| valid_exposure?(a, o) }) || \
577
- exposure_options.key?(:proc) || \
578
- !exposure_options[:safe] || \
579
- object.respond_to?(self.class.name_for(attribute)) || \
580
- object.is_a?(Hash) && object.key?(self.class.name_for(attribute))
586
+ if self.class.nested_exposures_for?(attribute)
587
+ self.class.nested_exposures[attribute].all? { |a, o| valid_exposure?(a, o) }
588
+ elsif exposure_options.key?(:proc)
589
+ true
590
+ else
591
+ name = self.class.name_for(attribute)
592
+ if exposure_options[:safe]
593
+ delegator.delegatable?(name)
594
+ else
595
+ delegator.delegatable?(name) || fail(NoMethodError, "#{self.class.name} missing attribute `#{name}' on #{object}")
596
+ end
597
+ end
581
598
  end
582
599
 
583
600
  def conditions_met?(exposure_options, options)
@@ -612,6 +629,16 @@ module Grape
612
629
  true
613
630
  end
614
631
 
632
+ def options_for_using(attribute, options)
633
+ using_options = options.dup
634
+ using_options.delete(:collection)
635
+ using_options[:root] = nil
636
+ using_options[:only] = only_fields(using_options, attribute)
637
+ using_options[:except] = except_fields(using_options, attribute)
638
+
639
+ using_options
640
+ end
641
+
615
642
  # All supported options.
616
643
  OPTIONS = [
617
644
  :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :if_extras, :unless_extras