grape-entity 0.6.0 → 0.8.1

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