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.
data/Rakefile CHANGED
@@ -15,37 +15,7 @@ RSpec::Core::RakeTask.new(:rcov) do |spec|
15
15
  end
16
16
 
17
17
  task :spec
18
- task :default => :spec
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]
@@ -1 +1 @@
1
- require 'grape_entity'
1
+ require 'grape_entity'
@@ -1,6 +1,3 @@
1
+ require "active_support/core_ext"
1
2
  require "grape_entity/version"
2
-
3
- module Grape
4
- autoload :Entity, 'grape_entity/entity'
5
- end
6
-
3
+ require "grape_entity/entity"
@@ -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, :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(' ')}
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', { :object_fields => API::Entities::User.documentation }
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, :with => API::Entities::User, :type => type
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, :if => {:version => 'v2'}
84
+ # expose :new_attribute, if: { version: 'v2' }
85
85
  # end
86
86
  # end
87
87
  def entity(*exposures, &block)
88
- entity_class.expose *exposures if exposures.any?
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 :if => {:awesome => true} do
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, value|
176
- unless value[1][:documentation].nil? || value[1][:documentation].empty?
177
- memo[value[0]] = value[1][:documentation]
178
- end
179
- memo
180
- end
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, :format_with => :timestamp
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, :with => API::Entities::User
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, :with => API::Entities::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
- #  entity. Pass nil or false to represent the object or objects with no
289
- # root name even if one is defined for the entity.
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
- inner = if objects.respond_to?(:to_ary)
292
- objects.to_ary().map{|o| self.new(o, {:collection => true}.merge(options))}
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
- self.new(objects, options)
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
- options[:root]
299
- else
300
- objects.respond_to?(:to_ary) ? @collection_root : @root
301
- end
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
- exposures.inject({}) do |output, (attribute, exposure_options)|
332
- if (exposure_options.has_key?(:proc) || object.respond_to?(attribute)) && conditions_met?(exposure_options, opts)
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
- alias :as_json :serializable_hash
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 key_for(attribute)
362
- exposures[attribute.to_sym][:as] || attribute.to_sym
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
- if exposure_options[:proc]
369
- exposure_options[:proc].call(object, options)
370
- elsif exposure_options[:using]
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
- exposure_options[:using].represent(object.send(attribute), using_options)
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].call(object.send(attribute))
420
+ instance_exec(delegate_attribute(attribute), &formatters[format_with])
380
421
  elsif format_with.is_a?(Symbol)
381
- self.send(format_with, object.send(attribute))
422
+ send(format_with, delegate_attribute(attribute))
382
423
  elsif format_with.respond_to? :call
383
- format_with.call(object.send(attribute))
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
- object.send(attribute)
433
+ delegate_attribute(attribute)
387
434
  end
388
435
  end
389
436
 
390
- def conditions_met?(exposure_options, options)
391
- if_condition = exposure_options[:if]
392
- unless_condition = exposure_options[:unless]
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
- case if_condition
395
- when Hash; if_condition.each_pair{|k,v| return false if options[k.to_sym] != v }
396
- when Proc; return false unless if_condition.call(object, options)
397
- when Symbol; return false unless options[if_condition]
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
- case unless_condition
401
- when Hash; unless_condition.each_pair{|k,v| return false if options[k.to_sym] == v}
402
- when Proc; return false if unless_condition.call(object, options)
403
- when Symbol; return false if options[unless_condition]
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