grape-entity 0.6.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +5 -1
  4. data/.rspec +1 -1
  5. data/.rubocop.yml +124 -2
  6. data/.rubocop_todo.yml +21 -32
  7. data/.travis.yml +16 -17
  8. data/CHANGELOG.md +66 -0
  9. data/Dangerfile +2 -0
  10. data/Gemfile +8 -8
  11. data/Guardfile +4 -2
  12. data/README.md +101 -4
  13. data/Rakefile +2 -2
  14. data/bench/serializing.rb +7 -0
  15. data/grape-entity.gemspec +10 -8
  16. data/lib/grape-entity.rb +2 -0
  17. data/lib/grape_entity.rb +2 -0
  18. data/lib/grape_entity/condition.rb +20 -11
  19. data/lib/grape_entity/condition/base.rb +2 -0
  20. data/lib/grape_entity/condition/block_condition.rb +3 -1
  21. data/lib/grape_entity/condition/hash_condition.rb +2 -0
  22. data/lib/grape_entity/condition/symbol_condition.rb +2 -0
  23. data/lib/grape_entity/delegator.rb +10 -9
  24. data/lib/grape_entity/delegator/base.rb +2 -0
  25. data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
  26. data/lib/grape_entity/delegator/hash_object.rb +4 -2
  27. data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
  28. data/lib/grape_entity/delegator/plain_object.rb +2 -0
  29. data/lib/grape_entity/entity.rb +112 -38
  30. data/lib/grape_entity/exposure.rb +64 -41
  31. data/lib/grape_entity/exposure/base.rb +20 -6
  32. data/lib/grape_entity/exposure/block_exposure.rb +2 -0
  33. data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
  34. data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
  35. data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
  36. data/lib/grape_entity/exposure/nesting_exposure.rb +35 -30
  37. data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +25 -15
  38. data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +6 -2
  39. data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
  40. data/lib/grape_entity/options.rb +44 -58
  41. data/lib/grape_entity/version.rb +3 -1
  42. data/spec/grape_entity/entity_spec.rb +243 -47
  43. data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
  44. data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
  45. data/spec/grape_entity/exposure_spec.rb +14 -2
  46. data/spec/grape_entity/hash_spec.rb +38 -1
  47. data/spec/grape_entity/options_spec.rb +66 -0
  48. data/spec/spec_helper.rb +17 -0
  49. metadata +31 -44
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rubygems'
4
4
  require 'bundler'
@@ -17,4 +17,4 @@ RSpec::Core::RakeTask.new(:spec)
17
17
  require 'rubocop/rake_task'
18
18
  RuboCop::RakeTask.new(:rubocop)
19
19
 
20
- task default: [:rubocop, :spec]
20
+ task default: %i[spec rubocop]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
4
  require 'grape-entity'
3
5
  require 'benchmark'
@@ -5,6 +7,7 @@ require 'benchmark'
5
7
  module Models
6
8
  class School
7
9
  attr_reader :classrooms
10
+
8
11
  def initialize
9
12
  @classrooms = []
10
13
  end
@@ -13,6 +16,7 @@ module Models
13
16
  class ClassRoom
14
17
  attr_reader :students
15
18
  attr_accessor :teacher
19
+
16
20
  def initialize(opts = {})
17
21
  @teacher = opts[:teacher]
18
22
  @students = []
@@ -21,6 +25,7 @@ module Models
21
25
 
22
26
  class Person
23
27
  attr_accessor :name
28
+
24
29
  def initialize(opts = {})
25
30
  @name = opts[:name]
26
31
  end
@@ -28,6 +33,7 @@ module Models
28
33
 
29
34
  class Teacher < Models::Person
30
35
  attr_accessor :tenure
36
+
31
37
  def initialize(opts = {})
32
38
  super(opts)
33
39
  @tenure = opts[:tenure]
@@ -36,6 +42,7 @@ module Models
36
42
 
37
43
  class Student < Models::Person
38
44
  attr_reader :grade
45
+
39
46
  def initialize(opts = {})
40
47
  super(opts)
41
48
  @grade = opts[:grade]
@@ -1,4 +1,6 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
4
  require 'grape_entity/version'
3
5
 
4
6
  Gem::Specification.new do |s|
@@ -12,20 +14,20 @@ Gem::Specification.new do |s|
12
14
  s.description = 'Extracted from Grape, A Ruby framework for rapid API development with great conventions.'
13
15
  s.license = 'MIT'
14
16
 
15
- s.rubyforge_project = 'grape-entity'
17
+ s.required_ruby_version = '>= 2.4'
16
18
 
19
+ s.add_runtime_dependency 'activesupport', '>= 3.0.0'
20
+ # FIXME: remove dependecy
17
21
  s.add_runtime_dependency 'multi_json', '>= 1.3.2'
18
- s.add_runtime_dependency 'activesupport'
19
22
 
20
23
  s.add_development_dependency 'bundler'
21
- s.add_development_dependency 'rake'
22
- s.add_development_dependency 'rubocop', '~> 0.40'
23
- s.add_development_dependency 'rspec', '~> 3.0'
24
- s.add_development_dependency 'rack-test'
25
24
  s.add_development_dependency 'maruku'
26
- s.add_development_dependency 'yard'
27
25
  s.add_development_dependency 'pry' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
28
26
  s.add_development_dependency 'pry-byebug' unless RUBY_PLATFORM.eql?('java') || RUBY_ENGINE.eql?('rbx')
27
+ s.add_development_dependency 'rack-test'
28
+ s.add_development_dependency 'rake'
29
+ s.add_development_dependency 'rspec', '~> 3.9'
30
+ s.add_development_dependency 'yard'
29
31
 
30
32
  s.files = `git ls-files`.split("\n")
31
33
  s.test_files = `git ls-files -- {test,spec}/*`.split("\n")
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape_entity'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/version'
2
4
  require 'active_support/core_ext/string/inflections'
3
5
  require 'active_support/core_ext/hash/reverse_merge'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape_entity/condition/base'
2
4
  require 'grape_entity/condition/block_condition'
3
5
  require 'grape_entity/condition/hash_condition'
@@ -6,19 +8,26 @@ require 'grape_entity/condition/symbol_condition'
6
8
  module Grape
7
9
  class Entity
8
10
  module Condition
9
- def self.new_if(arg)
10
- case arg
11
- when Hash then HashCondition.new false, arg
12
- when Proc then BlockCondition.new false, &arg
13
- when Symbol then SymbolCondition.new false, arg
11
+ class << self
12
+ def new_if(arg)
13
+ condition(false, arg)
14
14
  end
15
- end
16
15
 
17
- def self.new_unless(arg)
18
- case arg
19
- when Hash then HashCondition.new true, arg
20
- when Proc then BlockCondition.new true, &arg
21
- when Symbol then SymbolCondition.new true, arg
16
+ def new_unless(arg)
17
+ condition(true, arg)
18
+ end
19
+
20
+ private
21
+
22
+ def condition(inverse, arg)
23
+ condition_klass =
24
+ case arg
25
+ when Hash then HashCondition
26
+ when Proc then BlockCondition
27
+ when Symbol then SymbolCondition
28
+ end
29
+
30
+ condition_klass.new(inverse, arg)
22
31
  end
23
32
  end
24
33
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
4
6
  class BlockCondition < Base
5
7
  attr_reader :block
6
8
 
7
- def setup(&block)
9
+ def setup(block)
8
10
  @block = block
9
11
  end
10
12
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Condition
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape_entity/delegator/base'
2
4
  require 'grape_entity/delegator/hash_object'
3
5
  require 'grape_entity/delegator/openstruct_object'
@@ -8,15 +10,14 @@ module Grape
8
10
  class Entity
9
11
  module Delegator
10
12
  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
13
+ delegator_klass =
14
+ if object.is_a?(Hash) then HashObject
15
+ elsif defined?(OpenStruct) && object.is_a?(OpenStruct) then OpenStructObject
16
+ elsif object.respond_to?(:fetch, true) then FetchableObject
17
+ else PlainObject
18
+ end
19
+
20
+ delegator_klass.new(object)
20
21
  end
21
22
  end
22
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
4
6
  class HashObject < Base
5
- def delegate(attribute)
6
- object[attribute]
7
+ def delegate(attribute, hash_access: :to_sym)
8
+ object[attribute.send(hash_access)]
7
9
  end
8
10
  end
9
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Delegator
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'multi_json'
2
4
  require 'set'
3
5
 
@@ -103,7 +105,7 @@ module Grape
103
105
  @root_exposure ||= Exposure.new(nil, nesting: true)
104
106
  end
105
107
 
106
- attr_writer :root_exposure
108
+ attr_writer :root_exposure, :formatters
107
109
 
108
110
  # Returns all formatters that are registered for this and it's ancestors
109
111
  # @return [Hash] of formatters
@@ -111,7 +113,25 @@ module Grape
111
113
  @formatters ||= {}
112
114
  end
113
115
 
114
- attr_writer :formatters
116
+ def hash_access
117
+ @hash_access ||= :to_sym
118
+ end
119
+
120
+ def hash_access=(value)
121
+ @hash_access =
122
+ case value
123
+ when :to_s, :str, :string
124
+ :to_s
125
+ when :to_sym, :sym, :symbol
126
+ :to_sym
127
+ else
128
+ :to_sym
129
+ end
130
+ end
131
+
132
+ def delegation_opts
133
+ @delegation_opts ||= { hash_access: hash_access }
134
+ end
115
135
  end
116
136
 
117
137
  @formatters = {}
@@ -124,7 +144,26 @@ module Grape
124
144
  # This method is the primary means by which you will declare what attributes
125
145
  # should be exposed by the entity.
126
146
  #
147
+ # @option options :expose_nil When set to false the associated exposure will not
148
+ # be rendered if its value is nil.
149
+ #
127
150
  # @option options :as Declare an alias for the representation of this attribute.
151
+ # If a proc is presented it is evaluated in the context of the entity so object
152
+ # and the entity methods are available to it.
153
+ #
154
+ # @example as: a proc or lambda
155
+ #
156
+ # object = OpenStruct(awesomness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )
157
+ #
158
+ # class MyEntity < Grape::Entity
159
+ # expose :awesome, as: proc { object.awesomeness }
160
+ # expose :awesomeness, as: ->(object, opts) { object.other }
161
+ # end
162
+ #
163
+ # => { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }
164
+ #
165
+ # Note the parameters passed in via the lambda syntax.
166
+ #
128
167
  # @option options :if When passed a Hash, the attribute will only be exposed if the
129
168
  # runtime options match all the conditions passed in. When passed a lambda, the
130
169
  # lambda will execute with two arguments: the object being represented and the
@@ -147,17 +186,23 @@ module Grape
147
186
  # @option options :documentation Define documenation for an exposed
148
187
  # field, typically the value is a hash with two fields, type and desc.
149
188
  # @option options :merge This option allows you to merge an exposed field to the root
189
+ #
190
+ # rubocop:disable Layout/LineLength
150
191
  def self.expose(*args, &block)
151
192
  options = merge_options(args.last.is_a?(Hash) ? args.pop : {})
152
193
 
153
194
  if args.size > 1
195
+
154
196
  raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
197
+ raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
155
198
  raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
156
199
  end
157
200
 
158
- raise ArgumentError, 'You may not use block-setting when also using format_with' if block_given? && options[:format_with].respond_to?(:call)
159
-
160
201
  if block_given?
202
+ if options[:format_with].respond_to?(:call)
203
+ raise ArgumentError, 'You may not use block-setting when also using format_with'
204
+ end
205
+
161
206
  if block.parameters.any?
162
207
  options[:proc] = block
163
208
  else
@@ -167,24 +212,25 @@ module Grape
167
212
 
168
213
  @documentation = nil
169
214
  @nesting_stack ||= []
215
+ args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
216
+ end
217
+ # rubocop:enable Layout/LineLength
170
218
 
171
- # rubocop:disable Style/Next
172
- args.each do |attribute|
173
- exposure = Exposure.new(attribute, options)
219
+ def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
220
+ exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures
174
221
 
175
- if @nesting_stack.empty?
176
- root_exposures << exposure
177
- else
178
- @nesting_stack.last.nested_exposures << exposure
179
- end
222
+ exposure = Exposure.new(attribute, options)
180
223
 
181
- # Nested exposures are given in a block with no parameters.
182
- if exposure.nesting?
183
- @nesting_stack << exposure
184
- block.call
185
- @nesting_stack.pop
186
- end
187
- end
224
+ exposure_list.delete_by(attribute) if exposure.override?
225
+
226
+ exposure_list << exposure
227
+
228
+ # Nested exposures are given in a block with no parameters.
229
+ return unless exposure.nesting?
230
+
231
+ nesting_stack << exposure
232
+ block.call
233
+ nesting_stack.pop
188
234
  end
189
235
 
190
236
  # Returns exposures that have been declared for this Entity on the top level.
@@ -237,9 +283,7 @@ module Grape
237
283
  # #docmentation, any exposure without a documentation key will be ignored.
238
284
  def self.documentation
239
285
  @documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
240
- if exposure.documentation && !exposure.documentation.empty?
241
- memo[exposure.key] = exposure.documentation
242
- end
286
+ memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
243
287
  end
244
288
  end
245
289
 
@@ -271,6 +315,7 @@ module Grape
271
315
  #
272
316
  def self.format_with(name, &block)
273
317
  raise ArgumentError, 'You must pass a block for formatters' unless block_given?
318
+
274
319
  formatters[name.to_sym] = block
275
320
  end
276
321
 
@@ -392,6 +437,7 @@ module Grape
392
437
  # @option options :only [Array] all the fields that should be returned
393
438
  # @option options :except [Array] all the fields that should not be returned
394
439
  def self.represent(objects, options = {})
440
+ @present_collection ||= nil
395
441
  if objects.respond_to?(:to_ary) && !@present_collection
396
442
  root_element = root_element(:collection_root)
397
443
  inner = objects.to_ary.map { |object| new(object, options.reverse_merge(collection: true)).presented }
@@ -409,8 +455,9 @@ module Grape
409
455
  # This method returns the entity's root or collection root node, or its parent's
410
456
  # @param root_type: either :collection_root or just :root
411
457
  def self.root_element(root_type)
412
- if instance_variable_get("@#{root_type}")
413
- instance_variable_get("@#{root_type}")
458
+ instance_variable = "@#{root_type}"
459
+ if instance_variable_defined?(instance_variable) && instance_variable_get(instance_variable)
460
+ instance_variable_get(instance_variable)
414
461
  elsif superclass.respond_to? :root_element
415
462
  superclass.root_element(root_type)
416
463
  end
@@ -432,12 +479,11 @@ module Grape
432
479
 
433
480
  def initialize(object, options = {})
434
481
  @object = object
435
- @delegator = Delegator.new object
436
- @options = if options.is_a? Options
437
- options
438
- else
439
- Options.new options
440
- end
482
+ @options = options.is_a?(Options) ? options : Options.new(options)
483
+ @delegator = Delegator.new(object)
484
+
485
+ # Why not `arity > 1`? It might be negative https://ruby-doc.org/core-2.6.6/Method.html#method-i-arity
486
+ @delegator_accepts_opts = @delegator.method(:delegate).arity != 1
441
487
  end
442
488
 
443
489
  def root_exposures
@@ -472,7 +518,11 @@ module Grape
472
518
  end
473
519
 
474
520
  def exec_with_object(options, &block)
475
- instance_exec(object, options, &block)
521
+ if block.parameters.count == 1
522
+ instance_exec(object, &block)
523
+ else
524
+ instance_exec(object, options, &block)
525
+ end
476
526
  end
477
527
 
478
528
  def exec_with_attribute(attribute, &block)
@@ -484,28 +534,52 @@ module Grape
484
534
  end
485
535
 
486
536
  def delegate_attribute(attribute)
487
- if respond_to?(attribute, true) && Grape::Entity > method(attribute).owner
537
+ if is_defined_in_entity?(attribute)
488
538
  send(attribute)
539
+ elsif @delegator_accepts_opts
540
+ delegator.delegate(attribute, self.class.delegation_opts)
489
541
  else
490
542
  delegator.delegate(attribute)
491
543
  end
492
544
  end
493
545
 
546
+ def is_defined_in_entity?(attribute)
547
+ return false unless respond_to?(attribute, true)
548
+
549
+ ancestors = self.class.ancestors
550
+ ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
551
+ end
552
+
494
553
  alias as_json serializable_hash
495
554
 
496
555
  def to_json(options = {})
497
- options = options.to_h if options && options.respond_to?(:to_h)
556
+ options = options.to_h if options&.respond_to?(:to_h)
498
557
  MultiJson.dump(serializable_hash(options))
499
558
  end
500
559
 
501
560
  def to_xml(options = {})
502
- options = options.to_h if options && options.respond_to?(:to_h)
561
+ options = options.to_h if options&.respond_to?(:to_h)
503
562
  serializable_hash(options).to_xml(options)
504
563
  end
505
564
 
506
565
  # All supported options.
507
- OPTIONS = [
508
- :rewrite, :as, :if, :unless, :using, :with, :proc, :documentation, :format_with, :safe, :attr_path, :if_extras, :unless_extras, :merge
566
+ OPTIONS = %i[
567
+ rewrite
568
+ as
569
+ if
570
+ unless
571
+ using
572
+ with
573
+ proc
574
+ documentation
575
+ format_with
576
+ safe
577
+ attr_path
578
+ if_extras
579
+ unless_extras
580
+ merge
581
+ expose_nil
582
+ override
509
583
  ].to_set.freeze
510
584
 
511
585
  # Merges the given options with current block options.
@@ -515,7 +589,7 @@ module Grape
515
589
  opts = {}
516
590
 
517
591
  merge_logic = proc do |key, existing_val, new_val|
518
- if [:if, :unless].include?(key)
592
+ if %i[if unless].include?(key)
519
593
  if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
520
594
  existing_val.merge(new_val)
521
595
  elsif new_val.is_a?(Hash)
@@ -541,7 +615,7 @@ module Grape
541
615
  #
542
616
  # @param options [Hash] Exposure options.
543
617
  def self.valid_options(options)
544
- options.keys.each do |key|
618
+ options.each_key do |key|
545
619
  raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
546
620
  end
547
621