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 +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +11 -33
- data/CHANGELOG.md +14 -0
- data/Gemfile +1 -1
- data/README.md +72 -10
- data/lib/grape_entity.rb +1 -0
- data/lib/grape_entity/delegator.rb +23 -0
- data/lib/grape_entity/delegator/base.rb +17 -0
- data/lib/grape_entity/delegator/fetchable_object.rb +11 -0
- data/lib/grape_entity/delegator/hash_object.rb +11 -0
- data/lib/grape_entity/delegator/openstruct_object.rb +11 -0
- data/lib/grape_entity/delegator/plain_object.rb +15 -0
- data/lib/grape_entity/entity.rb +161 -134
- data/lib/grape_entity/version.rb +1 -1
- data/spec/grape_entity/entity_spec.rb +304 -79
- metadata +32 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 22fd603c80eeb68f64892de2f1ca6873c12b20ae
|
4
|
+
data.tar.gz: 63638985ec1e6d721d54885840ad66c54f14ac9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1d88f629ac5188cd05ee77fa32641eb431a44b088c9ac17375c17100ccc6b314b04aad0ffae6f299af21d7fb4b6d33a6ff5c53ce9b64c50b7e5ca483761e0c4
|
7
|
+
data.tar.gz: e40e7c4e6fee4fef015cc554a38f8e6a44fb05e23142b0932263e059f97a9a9948d012e70f9e9a6f8084a83dd181894afb43d49c208a61712277ab09572f1809
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
CHANGED
@@ -1,64 +1,42 @@
|
|
1
1
|
# This configuration was generated by `rubocop --auto-gen-config`
|
2
|
-
# on
|
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:
|
8
|
+
# Offense count: 8
|
9
9
|
Metrics/AbcSize:
|
10
|
-
Max:
|
10
|
+
Max: 51
|
11
11
|
|
12
12
|
# Offense count: 1
|
13
13
|
# Configuration parameters: CountComments.
|
14
14
|
Metrics/ClassLength:
|
15
|
-
Max:
|
15
|
+
Max: 328
|
16
16
|
|
17
|
-
# Offense count:
|
17
|
+
# Offense count: 5
|
18
18
|
Metrics/CyclomaticComplexity:
|
19
19
|
Max: 17
|
20
20
|
|
21
|
-
# Offense count:
|
21
|
+
# Offense count: 176
|
22
22
|
# Configuration parameters: AllowURI, URISchemes.
|
23
23
|
Metrics/LineLength:
|
24
|
-
Max:
|
24
|
+
Max: 146
|
25
25
|
|
26
|
-
# Offense count:
|
26
|
+
# Offense count: 7
|
27
27
|
# Configuration parameters: CountComments.
|
28
28
|
Metrics/MethodLength:
|
29
|
-
Max:
|
29
|
+
Max: 32
|
30
30
|
|
31
|
-
# Offense count:
|
31
|
+
# Offense count: 5
|
32
32
|
Metrics/PerceivedComplexity:
|
33
33
|
Max: 15
|
34
34
|
|
35
|
-
# Offense count:
|
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
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: :
|
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: :
|
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: :
|
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
|
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: :
|
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
@@ -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
|
data/lib/grape_entity/entity.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
168
|
+
attribute = "#{@nested_attributes.last}__#{attribute}".to_sym
|
169
|
+
nested_attribute_names[attribute] = orig_attribute
|
145
170
|
options[:nested] = true
|
146
|
-
|
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
|
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.
|
237
|
-
|
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 =
|
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 =
|
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
|
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
|
439
|
-
|
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
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
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
|
-
|
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
|
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
|
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
|
542
|
-
|
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
|
-
|
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
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
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
|