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