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