grape-entity 0.4.5 → 0.4.6

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