grape-entity 0.3.0 → 0.4.0
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/.gitignore +1 -0
- data/.rubocop.yml +69 -0
- data/.travis.yml +5 -6
- data/CHANGELOG.md +35 -0
- data/Gemfile +7 -0
- data/Guardfile +0 -1
- data/README.md +320 -0
- data/Rakefile +3 -33
- data/lib/grape-entity.rb +1 -1
- data/lib/grape_entity.rb +2 -5
- data/lib/grape_entity/entity.rb +190 -71
- data/lib/grape_entity/version.rb +1 -1
- data/spec/grape_entity/entity_spec.rb +539 -158
- metadata +6 -5
- data/CHANGELOG.markdown +0 -21
- data/README.markdown +0 -197
data/Rakefile
CHANGED
@@ -15,37 +15,7 @@ RSpec::Core::RakeTask.new(:rcov) do |spec|
|
|
15
15
|
end
|
16
16
|
|
17
17
|
task :spec
|
18
|
-
|
18
|
+
require 'rubocop/rake_task'
|
19
|
+
Rubocop::RakeTask.new(:rubocop)
|
19
20
|
|
20
|
-
|
21
|
-
# TODO: setup a place for documentation and then get this going again.
|
22
|
-
#
|
23
|
-
# begin
|
24
|
-
# require 'yard'
|
25
|
-
# DOC_FILES = ['lib/**/*.rb', 'README.markdown']
|
26
|
-
#
|
27
|
-
# YARD::Rake::YardocTask.new(:doc) do |t|
|
28
|
-
# t.files = DOC_FILES
|
29
|
-
# end
|
30
|
-
#
|
31
|
-
# namespace :doc do
|
32
|
-
# YARD::Rake::YardocTask.new(:pages) do |t|
|
33
|
-
# t.files = DOC_FILES
|
34
|
-
# t.options = ['-o', '../grape.doc']
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# namespace :pages do
|
38
|
-
# desc 'Generate and publish YARD docs to GitHub pages.'
|
39
|
-
# task :publish => ['doc:pages'] do
|
40
|
-
# Dir.chdir(File.dirname(__FILE__) + '/../grape.doc') do
|
41
|
-
# system("git add .")
|
42
|
-
# system("git add -u")
|
43
|
-
# system("git commit -m 'Generating docs for version #{version}.'")
|
44
|
-
# system("git push origin gh-pages")
|
45
|
-
# end
|
46
|
-
# end
|
47
|
-
# end
|
48
|
-
# end
|
49
|
-
# rescue LoadError
|
50
|
-
# puts "You need to install YARD."
|
51
|
-
# end
|
21
|
+
task default: [:rubocop, :spec]
|
data/lib/grape-entity.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
require 'grape_entity'
|
1
|
+
require 'grape_entity'
|
data/lib/grape_entity.rb
CHANGED
data/lib/grape_entity/entity.rb
CHANGED
@@ -12,11 +12,11 @@ module Grape
|
|
12
12
|
# module Entities
|
13
13
|
# class User < Grape::Entity
|
14
14
|
# expose :first_name, :last_name, :screen_name, :location
|
15
|
-
# expose :field, :
|
16
|
-
# expose :latest_status, :
|
17
|
-
# expose :email, :
|
18
|
-
# expose :new_attribute, :
|
19
|
-
# expose(:name){|model,options| [model.first_name, model.last_name].join(' ')}
|
15
|
+
# expose :field, documentation: { type: "string", desc: "describe the field" }
|
16
|
+
# expose :latest_status, using: API::Status, as: :status, unless: { collection: true }
|
17
|
+
# expose :email, if: { type: :full }
|
18
|
+
# expose :new_attribute, if: { version: 'v2' }
|
19
|
+
# expose(:name) { |model, options| [model.first_name, model.last_name].join(' ') }
|
20
20
|
# end
|
21
21
|
# end
|
22
22
|
# end
|
@@ -32,11 +32,11 @@ module Grape
|
|
32
32
|
# class Users < Grape::API
|
33
33
|
# version 'v2'
|
34
34
|
#
|
35
|
-
# desc 'User index', { :
|
35
|
+
# desc 'User index', { params: API::Entities::User.documentation }
|
36
36
|
# get '/users' do
|
37
37
|
# @users = User.all
|
38
38
|
# type = current_user.admin? ? :full : :default
|
39
|
-
# present @users, :
|
39
|
+
# present @users, with: API::Entities::User, type: type
|
40
40
|
# end
|
41
41
|
# end
|
42
42
|
# end
|
@@ -48,16 +48,16 @@ module Grape
|
|
48
48
|
module DSL
|
49
49
|
def self.included(base)
|
50
50
|
base.extend ClassMethods
|
51
|
-
ancestor_entity_class = base.ancestors.detect{|a| a.entity_class if a.respond_to?(:entity_class)}
|
51
|
+
ancestor_entity_class = base.ancestors.detect { |a| a.entity_class if a.respond_to?(:entity_class) }
|
52
52
|
base.const_set(:Entity, Class.new(ancestor_entity_class || Grape::Entity)) unless const_defined?(:Entity)
|
53
53
|
end
|
54
54
|
|
55
55
|
module ClassMethods
|
56
56
|
# Returns the automatically-created entity class for this
|
57
57
|
# Class.
|
58
|
-
def entity_class(search_ancestors=true)
|
58
|
+
def entity_class(search_ancestors = true)
|
59
59
|
klass = const_get(:Entity) if const_defined?(:Entity)
|
60
|
-
klass ||= ancestors.detect{|a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
|
60
|
+
klass ||= ancestors.detect { |a| a.entity_class(false) if a.respond_to?(:entity_class) } if search_ancestors
|
61
61
|
klass
|
62
62
|
end
|
63
63
|
|
@@ -70,7 +70,7 @@ module Grape
|
|
70
70
|
#
|
71
71
|
# class User
|
72
72
|
# include Grape::Entity::DSL
|
73
|
-
#
|
73
|
+
#
|
74
74
|
# entity :name, :email
|
75
75
|
# end
|
76
76
|
#
|
@@ -78,14 +78,14 @@ module Grape
|
|
78
78
|
#
|
79
79
|
# class User
|
80
80
|
# include Grape::Entity::DSL
|
81
|
-
#
|
81
|
+
#
|
82
82
|
# entity :name, :email do
|
83
83
|
# expose :latest_status, using: Status::Entity, if: :include_status
|
84
|
-
# expose :new_attribute, :
|
84
|
+
# expose :new_attribute, if: { version: 'v2' }
|
85
85
|
# end
|
86
86
|
# end
|
87
87
|
def entity(*exposures, &block)
|
88
|
-
entity_class.expose
|
88
|
+
entity_class.expose(*exposures) if exposures.any?
|
89
89
|
entity_class.class_eval(&block) if block_given?
|
90
90
|
entity_class
|
91
91
|
end
|
@@ -123,8 +123,7 @@ module Grape
|
|
123
123
|
# @option options :documentation Define documenation for an exposed
|
124
124
|
# field, typically the value is a hash with two fields, type and desc.
|
125
125
|
def self.expose(*args, &block)
|
126
|
-
options = args.last.is_a?(Hash) ? args.pop : {}
|
127
|
-
options = (@block_options ||= []).inject({}){|final, step| final.merge!(step)}.merge(options)
|
126
|
+
options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
|
128
127
|
|
129
128
|
if args.size > 1
|
130
129
|
raise ArgumentError, "You may not use the :as option on multi-attribute exposures." if options[:as]
|
@@ -133,24 +132,36 @@ module Grape
|
|
133
132
|
|
134
133
|
raise ArgumentError, "You may not use block-setting when also using format_with" if block_given? && options[:format_with].respond_to?(:call)
|
135
134
|
|
136
|
-
options[:proc] = block if block_given?
|
135
|
+
options[:proc] = block if block_given? && block.parameters.any?
|
137
136
|
|
137
|
+
@nested_attributes ||= []
|
138
138
|
args.each do |attribute|
|
139
|
+
unless @nested_attributes.empty?
|
140
|
+
attribute = "#{@nested_attributes.last}__#{attribute}"
|
141
|
+
end
|
142
|
+
|
139
143
|
exposures[attribute.to_sym] = options
|
144
|
+
|
145
|
+
# Nested exposures are given in a block with no parameters.
|
146
|
+
if block_given? && block.parameters.empty?
|
147
|
+
@nested_attributes << attribute
|
148
|
+
block.call
|
149
|
+
@nested_attributes.pop
|
150
|
+
end
|
140
151
|
end
|
141
152
|
end
|
142
153
|
|
143
154
|
# Set options that will be applied to any exposures declared inside the block.
|
144
155
|
#
|
145
156
|
# @example Multi-exposure if
|
146
|
-
#
|
157
|
+
#
|
147
158
|
# class MyEntity < Grape::Entity
|
148
|
-
# with_options :
|
159
|
+
# with_options if: { awesome: true } do
|
149
160
|
# expose :awesome, :sweet
|
150
161
|
# end
|
151
162
|
# end
|
152
163
|
def self.with_options(options)
|
153
|
-
(@block_options ||= []).push(options)
|
164
|
+
(@block_options ||= []).push(valid_options(options))
|
154
165
|
yield
|
155
166
|
@block_options.pop
|
156
167
|
end
|
@@ -172,12 +183,12 @@ module Grape
|
|
172
183
|
# the values are document keys in the entity's documentation key. When calling
|
173
184
|
# #docmentation, any exposure without a documentation key will be ignored.
|
174
185
|
def self.documentation
|
175
|
-
@documentation ||= exposures.inject({}) do |memo,
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
186
|
+
@documentation ||= exposures.inject({}) do |memo, (attribute, exposure_options)|
|
187
|
+
unless exposure_options[:documentation].nil? || exposure_options[:documentation].empty?
|
188
|
+
memo[key_for(attribute)] = exposure_options[:documentation]
|
189
|
+
end
|
190
|
+
memo
|
191
|
+
end
|
181
192
|
|
182
193
|
if superclass.respond_to? :documentation
|
183
194
|
@documentation = superclass.documentation.merge(@documentation)
|
@@ -192,8 +203,6 @@ module Grape
|
|
192
203
|
# @param name [Symbol] the name of the formatter
|
193
204
|
# @param block [Proc] the block that will interpret the exposed attribute
|
194
205
|
#
|
195
|
-
#
|
196
|
-
#
|
197
206
|
# @example Formatter declaration
|
198
207
|
#
|
199
208
|
# module API
|
@@ -203,7 +212,7 @@ module Grape
|
|
203
212
|
# date.strftime('%m/%d/%Y')
|
204
213
|
# end
|
205
214
|
#
|
206
|
-
# expose :birthday, :last_signed_in, :
|
215
|
+
# expose :birthday, :last_signed_in, format_with: :timestamp
|
207
216
|
# end
|
208
217
|
# end
|
209
218
|
# end
|
@@ -256,20 +265,20 @@ module Grape
|
|
256
265
|
# class Users < Grape::API
|
257
266
|
# version 'v2'
|
258
267
|
#
|
259
|
-
# # this will render { "users": [ {"id":"1"}, {"id":"2"} ] }
|
268
|
+
# # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ] }
|
260
269
|
# get '/users' do
|
261
270
|
# @users = User.all
|
262
|
-
# present @users, :
|
271
|
+
# present @users, with: API::Entities::User
|
263
272
|
# end
|
264
273
|
#
|
265
|
-
# # this will render { "user": {"id":"1"} }
|
274
|
+
# # this will render { "user" : { "id" : "1" } }
|
266
275
|
# get '/users/:id' do
|
267
276
|
# @user = User.find(params[:id])
|
268
|
-
# present @user, :
|
277
|
+
# present @user, with: API::Entities::User
|
269
278
|
# end
|
270
279
|
# end
|
271
280
|
# end
|
272
|
-
def self.root(plural, singular=nil)
|
281
|
+
def self.root(plural, singular = nil)
|
273
282
|
@collection_root = plural
|
274
283
|
@root = singular
|
275
284
|
end
|
@@ -284,21 +293,24 @@ module Grape
|
|
284
293
|
# @param options [Hash] Options that will be passed through to each entity
|
285
294
|
# representation.
|
286
295
|
#
|
287
|
-
# @option options :root [String] override the default root name set for the
|
288
|
-
#
|
289
|
-
#
|
296
|
+
# @option options :root [String] override the default root name set for the entity.
|
297
|
+
# Pass nil or false to represent the object or objects with no root name
|
298
|
+
# even if one is defined for the entity.
|
290
299
|
def self.represent(objects, options = {})
|
291
|
-
|
292
|
-
objects.to_ary
|
300
|
+
if objects.respond_to?(:to_ary)
|
301
|
+
inner = objects.to_ary.map { |object| new(object, { collection: true }.merge(options)) }
|
302
|
+
inner = inner.map(&:serializable_hash) if options[:serializable]
|
293
303
|
else
|
294
|
-
|
304
|
+
inner = new(objects, options)
|
305
|
+
inner = inner.serializable_hash if options[:serializable]
|
295
306
|
end
|
296
307
|
|
297
308
|
root_element = if options.has_key?(:root)
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
309
|
+
options[:root]
|
310
|
+
else
|
311
|
+
objects.respond_to?(:to_ary) ? @collection_root : @root
|
312
|
+
end
|
313
|
+
|
302
314
|
root_element ? { root_element => inner } : inner
|
303
315
|
end
|
304
316
|
|
@@ -310,6 +322,12 @@ module Grape
|
|
310
322
|
self.class.exposures
|
311
323
|
end
|
312
324
|
|
325
|
+
def valid_exposures
|
326
|
+
exposures.select do |attribute, exposure_options|
|
327
|
+
valid_exposure?(attribute, exposure_options)
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
313
331
|
def documentation
|
314
332
|
self.class.documentation
|
315
333
|
end
|
@@ -328,14 +346,18 @@ module Grape
|
|
328
346
|
def serializable_hash(runtime_options = {})
|
329
347
|
return nil if object.nil?
|
330
348
|
opts = options.merge(runtime_options || {})
|
331
|
-
|
332
|
-
if
|
349
|
+
valid_exposures.inject({}) do |output, (attribute, exposure_options)|
|
350
|
+
if conditions_met?(exposure_options, opts)
|
333
351
|
partial_output = value_for(attribute, opts)
|
334
|
-
output[key_for(attribute)] =
|
352
|
+
output[self.class.key_for(attribute)] =
|
335
353
|
if partial_output.respond_to? :serializable_hash
|
336
354
|
partial_output.serializable_hash(runtime_options)
|
337
|
-
elsif partial_output.kind_of?(Array) && !partial_output.map {|o| o.respond_to? :serializable_hash}.include?(false)
|
338
|
-
partial_output.map {|o| o.serializable_hash}
|
355
|
+
elsif partial_output.kind_of?(Array) && !partial_output.map { |o| o.respond_to? :serializable_hash }.include?(false)
|
356
|
+
partial_output.map { |o| o.serializable_hash }
|
357
|
+
elsif partial_output.kind_of?(Hash)
|
358
|
+
partial_output.each do |key, value|
|
359
|
+
partial_output[key] = value.serializable_hash if value.respond_to? :serializable_hash
|
360
|
+
end
|
339
361
|
else
|
340
362
|
partial_output
|
341
363
|
end
|
@@ -344,7 +366,7 @@ module Grape
|
|
344
366
|
end
|
345
367
|
end
|
346
368
|
|
347
|
-
|
369
|
+
alias_method :as_json, :serializable_hash
|
348
370
|
|
349
371
|
def to_json(options = {})
|
350
372
|
options = options.to_h if options && options.respond_to?(:to_h)
|
@@ -358,52 +380,149 @@ module Grape
|
|
358
380
|
|
359
381
|
protected
|
360
382
|
|
361
|
-
def
|
362
|
-
|
383
|
+
def self.name_for(attribute)
|
384
|
+
attribute.to_s.split('__').last.to_sym
|
385
|
+
end
|
386
|
+
|
387
|
+
def self.key_for(attribute)
|
388
|
+
exposures[attribute.to_sym][:as] || name_for(attribute)
|
389
|
+
end
|
390
|
+
|
391
|
+
def self.nested_exposures_for(attribute)
|
392
|
+
exposures.select { |a, _| a.to_s =~ /^#{attribute}__/ }
|
363
393
|
end
|
364
394
|
|
365
395
|
def value_for(attribute, options = {})
|
366
396
|
exposure_options = exposures[attribute.to_sym]
|
367
397
|
|
368
|
-
|
369
|
-
|
370
|
-
|
398
|
+
nested_exposures = self.class.nested_exposures_for(attribute)
|
399
|
+
|
400
|
+
if exposure_options[:using]
|
401
|
+
exposure_options[:using] = exposure_options[:using].constantize if exposure_options[:using].respond_to? :constantize
|
402
|
+
|
371
403
|
using_options = options.dup
|
372
404
|
using_options.delete(:collection)
|
373
405
|
using_options[:root] = nil
|
374
|
-
|
406
|
+
|
407
|
+
if exposure_options[:proc]
|
408
|
+
exposure_options[:using].represent(instance_exec(object, options, &exposure_options[:proc]), using_options)
|
409
|
+
else
|
410
|
+
exposure_options[:using].represent(delegate_attribute(attribute), using_options)
|
411
|
+
end
|
412
|
+
|
413
|
+
elsif exposure_options[:proc]
|
414
|
+
instance_exec(object, options, &exposure_options[:proc])
|
415
|
+
|
375
416
|
elsif exposure_options[:format_with]
|
376
417
|
format_with = exposure_options[:format_with]
|
377
418
|
|
378
419
|
if format_with.is_a?(Symbol) && formatters[format_with]
|
379
|
-
formatters[format_with]
|
420
|
+
instance_exec(delegate_attribute(attribute), &formatters[format_with])
|
380
421
|
elsif format_with.is_a?(Symbol)
|
381
|
-
|
422
|
+
send(format_with, delegate_attribute(attribute))
|
382
423
|
elsif format_with.respond_to? :call
|
383
|
-
|
424
|
+
instance_exec(delegate_attribute(attribute), &format_with)
|
384
425
|
end
|
426
|
+
|
427
|
+
elsif nested_exposures.any?
|
428
|
+
Hash[nested_exposures.map do |nested_attribute, _|
|
429
|
+
[self.class.key_for(nested_attribute), value_for(nested_attribute, options)]
|
430
|
+
end]
|
431
|
+
|
385
432
|
else
|
386
|
-
|
433
|
+
delegate_attribute(attribute)
|
387
434
|
end
|
388
435
|
end
|
389
436
|
|
390
|
-
def
|
391
|
-
|
392
|
-
|
437
|
+
def delegate_attribute(attribute)
|
438
|
+
name = self.class.name_for(attribute)
|
439
|
+
if respond_to?(name, true)
|
440
|
+
send(name)
|
441
|
+
else
|
442
|
+
object.send(name)
|
443
|
+
end
|
444
|
+
end
|
393
445
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
446
|
+
def valid_exposure?(attribute, exposure_options)
|
447
|
+
nested_exposures = self.class.nested_exposures_for(attribute)
|
448
|
+
(nested_exposures.any? && nested_exposures.all? { |a, o| valid_exposure?(a, o) }) || \
|
449
|
+
exposure_options.has_key?(:proc) || \
|
450
|
+
!exposure_options[:safe] || \
|
451
|
+
object.respond_to?(self.class.name_for(attribute))
|
452
|
+
end
|
453
|
+
|
454
|
+
def conditions_met?(exposure_options, options)
|
455
|
+
if_conditions = (exposure_options[:if_extras] || []).dup
|
456
|
+
if_conditions << exposure_options[:if] unless exposure_options[:if].nil?
|
457
|
+
|
458
|
+
if_conditions.each do |if_condition|
|
459
|
+
case if_condition
|
460
|
+
when Hash then if_condition.each_pair { |k, v| return false if options[k.to_sym] != v }
|
461
|
+
when Proc then return false unless instance_exec(object, options, &if_condition)
|
462
|
+
when Symbol then return false unless options[if_condition]
|
463
|
+
end
|
398
464
|
end
|
399
465
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
466
|
+
unless_conditions = (exposure_options[:unless_extras] || []).dup
|
467
|
+
unless_conditions << exposure_options[:unless] unless exposure_options[:unless].nil?
|
468
|
+
|
469
|
+
unless_conditions.each do |unless_condition|
|
470
|
+
case unless_condition
|
471
|
+
when Hash then unless_condition.each_pair { |k, v| return false if options[k.to_sym] == v }
|
472
|
+
when Proc then return false if instance_exec(object, options, &unless_condition)
|
473
|
+
when Symbol then return false if options[unless_condition]
|
474
|
+
end
|
404
475
|
end
|
405
476
|
|
406
477
|
true
|
407
478
|
end
|
479
|
+
|
480
|
+
private
|
481
|
+
|
482
|
+
# All supported options.
|
483
|
+
OPTIONS = [
|
484
|
+
:as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :if_extras, :unless_extras
|
485
|
+
].to_set.freeze
|
486
|
+
|
487
|
+
# Merges the given options with current block options.
|
488
|
+
#
|
489
|
+
# @param options [Hash] Exposure options.
|
490
|
+
def self.merge_options(options)
|
491
|
+
opts = {}
|
492
|
+
|
493
|
+
merge_logic = proc do |key, existing_val, new_val|
|
494
|
+
if [:if, :unless].include?(key)
|
495
|
+
if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
|
496
|
+
existing_val.merge(new_val)
|
497
|
+
elsif new_val.is_a?(Hash)
|
498
|
+
(opts["#{key}_extras".to_sym] ||= []) << existing_val
|
499
|
+
new_val
|
500
|
+
else
|
501
|
+
(opts["#{key}_extras".to_sym] ||= []) << new_val
|
502
|
+
existing_val
|
503
|
+
end
|
504
|
+
else
|
505
|
+
new_val
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
@block_options ||= []
|
510
|
+
opts.merge @block_options.inject({}) { |final, step|
|
511
|
+
final.merge(step, &merge_logic)
|
512
|
+
}.merge(valid_options(options), &merge_logic)
|
513
|
+
end
|
514
|
+
|
515
|
+
# Raises an error if the given options include unknown keys.
|
516
|
+
# Renames aliased options.
|
517
|
+
#
|
518
|
+
# @param options [Hash] Exposure options.
|
519
|
+
def self.valid_options(options)
|
520
|
+
options.keys.each do |key|
|
521
|
+
raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
|
522
|
+
end
|
523
|
+
|
524
|
+
options[:using] = options.delete(:with) if options.has_key?(:with)
|
525
|
+
options
|
526
|
+
end
|
408
527
|
end
|
409
528
|
end
|