grape-entity 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|