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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'grape_entity/exposure/base'
2
4
  require 'grape_entity/exposure/represent_exposure'
3
5
  require 'grape_entity/exposure/block_exposure'
@@ -10,67 +12,88 @@ require 'grape_entity/condition'
10
12
  module Grape
11
13
  class Entity
12
14
  module Exposure
13
- def self.new(attribute, options)
14
- conditions = compile_conditions(options)
15
- base_args = [attribute, options, conditions]
16
-
17
- if options[:proc]
18
- block_exposure = BlockExposure.new(*base_args, &options[:proc])
19
- else
20
- delegator_exposure = DelegatorExposure.new(*base_args)
21
- end
15
+ class << self
16
+ def new(attribute, options)
17
+ conditions = compile_conditions(attribute, options)
18
+ base_args = [attribute, options, conditions]
22
19
 
23
- if options[:using]
20
+ passed_proc = options[:proc]
24
21
  using_class = options[:using]
22
+ format_with = options[:format_with]
25
23
 
26
- if options[:proc]
27
- RepresentExposure.new(*base_args, using_class, block_exposure)
24
+ if using_class
25
+ build_class_exposure(base_args, using_class, passed_proc)
26
+ elsif passed_proc
27
+ build_block_exposure(base_args, passed_proc)
28
+ elsif format_with
29
+ build_formatter_exposure(base_args, format_with)
30
+ elsif options[:nesting]
31
+ build_nesting_exposure(base_args)
28
32
  else
29
- RepresentExposure.new(*base_args, using_class, delegator_exposure)
33
+ build_delegator_exposure(base_args)
30
34
  end
35
+ end
31
36
 
32
- elsif options[:proc]
33
- block_exposure
37
+ private
34
38
 
35
- elsif options[:format_with]
36
- format_with = options[:format_with]
39
+ def compile_conditions(attribute, options)
40
+ if_conditions = [
41
+ options[:if_extras],
42
+ options[:if]
43
+ ].compact.flatten.map { |cond| Condition.new_if(cond) }
37
44
 
38
- if format_with.is_a? Symbol
39
- FormatterExposure.new(*base_args, format_with)
40
- elsif format_with.respond_to? :call
41
- FormatterBlockExposure.new(*base_args, &format_with)
42
- end
45
+ unless_conditions = [
46
+ options[:unless_extras],
47
+ options[:unless]
48
+ ].compact.flatten.map { |cond| Condition.new_unless(cond) }
43
49
 
44
- elsif options[:nesting]
45
- NestingExposure.new(*base_args)
50
+ unless_conditions << expose_nil_condition(attribute, options) if options[:expose_nil] == false
46
51
 
47
- else
48
- delegator_exposure
52
+ if_conditions + unless_conditions
49
53
  end
50
- end
51
54
 
52
- def self.compile_conditions(options)
53
- if_conditions = []
54
- unless options[:if_extras].nil?
55
- if_conditions.concat(options[:if_extras])
55
+ def expose_nil_condition(attribute, options)
56
+ Condition.new_unless(
57
+ proc do |object, _options|
58
+ if options[:proc].nil?
59
+ Delegator.new(object).delegate(attribute).nil?
60
+ else
61
+ exec_with_object(options, &options[:proc]).nil?
62
+ end
63
+ end
64
+ )
56
65
  end
57
- if_conditions << options[:if] unless options[:if].nil?
58
66
 
59
- if_conditions.map! do |cond|
60
- Condition.new_if cond
67
+ def build_class_exposure(base_args, using_class, passed_proc)
68
+ exposure =
69
+ if passed_proc
70
+ build_block_exposure(base_args, passed_proc)
71
+ else
72
+ build_delegator_exposure(base_args)
73
+ end
74
+
75
+ RepresentExposure.new(*base_args, using_class, exposure)
76
+ end
77
+
78
+ def build_formatter_exposure(base_args, format_with)
79
+ if format_with.is_a? Symbol
80
+ FormatterExposure.new(*base_args, format_with)
81
+ elsif format_with.respond_to?(:call)
82
+ FormatterBlockExposure.new(*base_args, &format_with)
83
+ end
61
84
  end
62
85
 
63
- unless_conditions = []
64
- unless options[:unless_extras].nil?
65
- unless_conditions.concat(options[:unless_extras])
86
+ def build_nesting_exposure(base_args)
87
+ NestingExposure.new(*base_args)
66
88
  end
67
- unless_conditions << options[:unless] unless options[:unless].nil?
68
89
 
69
- unless_conditions.map! do |cond|
70
- Condition.new_unless cond
90
+ def build_block_exposure(base_args, passed_proc)
91
+ BlockExposure.new(*base_args, &passed_proc)
71
92
  end
72
93
 
73
- if_conditions + unless_conditions
94
+ def build_delegator_exposure(base_args)
95
+ DelegatorExposure.new(*base_args)
96
+ end
74
97
  end
75
98
  end
76
99
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
4
6
  class Base
5
- attr_reader :attribute, :key, :is_safe, :documentation, :conditions, :for_merge
7
+ attr_reader :attribute, :is_safe, :documentation, :override, :conditions, :for_merge
6
8
 
7
9
  def self.new(attribute, options, conditions, *args, &block)
8
10
  super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
@@ -11,11 +13,13 @@ module Grape
11
13
  def initialize(attribute, options, conditions)
12
14
  @attribute = attribute.try(:to_sym)
13
15
  @options = options
14
- @key = (options[:as] || attribute).try(:to_sym)
16
+ key = options[:as] || attribute
17
+ @key = key.respond_to?(:to_sym) ? key.to_sym : key
15
18
  @is_safe = options[:safe]
16
19
  @for_merge = options[:merge]
17
20
  @attr_path_proc = options[:attr_path]
18
21
  @documentation = options[:documentation]
22
+ @override = options[:override]
19
23
  @conditions = conditions
20
24
  end
21
25
 
@@ -34,15 +38,14 @@ module Grape
34
38
  @conditions == other.conditions
35
39
  end
36
40
 
37
- def setup
38
- end
41
+ def setup; end
39
42
 
40
43
  def nesting?
41
44
  false
42
45
  end
43
46
 
44
47
  # if we have any nesting exposures with the same name.
45
- def deep_complex_nesting?
48
+ def deep_complex_nesting?(entity) # rubocop:disable Lint/UnusedMethodArgument
46
49
  false
47
50
  end
48
51
 
@@ -51,7 +54,10 @@ module Grape
51
54
  if @is_safe
52
55
  is_delegatable
53
56
  else
54
- is_delegatable || raise(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}")
57
+ is_delegatable || raise(
58
+ NoMethodError,
59
+ "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}"
60
+ )
55
61
  end
56
62
  end
57
63
 
@@ -103,6 +109,10 @@ module Grape
103
109
  end
104
110
  end
105
111
 
112
+ def key(entity = nil)
113
+ @key.respond_to?(:call) ? entity.exec_with_object(@options, &@key) : @key
114
+ end
115
+
106
116
  def with_attr_path(entity, options)
107
117
  path_part = attr_path(entity, options)
108
118
  options.with_attr_path(path_part) do
@@ -110,6 +120,10 @@ module Grape
110
120
  end
111
121
  end
112
122
 
123
+ def override?
124
+ @override
125
+ end
126
+
113
127
  protected
114
128
 
115
129
  attr_reader :options
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -29,45 +31,33 @@ module Grape
29
31
  end
30
32
 
31
33
  def value(entity, options)
32
- new_options = nesting_options_for(options)
33
- output = OutputBuilder.new
34
-
35
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
36
- exposure.with_attr_path(entity, new_options) do
37
- result = exposure.value(entity, new_options)
38
- out.add(exposure, result)
39
- end
34
+ map_entity_exposures(entity, options) do |exposure, nested_options|
35
+ exposure.value(entity, nested_options)
40
36
  end
41
37
  end
42
38
 
43
- def valid_value_for(key, entity, options)
44
- new_options = nesting_options_for(options)
45
-
46
- result = nil
47
- normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
48
- exposure.with_attr_path(entity, new_options) do
49
- result = exposure.valid_value(entity, new_options)
50
- end
39
+ def serializable_value(entity, options)
40
+ map_entity_exposures(entity, options) do |exposure, nested_options|
41
+ exposure.serializable_value(entity, nested_options)
51
42
  end
52
- result
53
43
  end
54
44
 
55
- def serializable_value(entity, options)
45
+ def valid_value_for(key, entity, options)
56
46
  new_options = nesting_options_for(options)
57
- output = OutputBuilder.new
58
47
 
59
- normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
48
+ key_exposures = normalized_exposures(entity, new_options).select { |e| e.key(entity) == key }
49
+
50
+ key_exposures.map do |exposure|
60
51
  exposure.with_attr_path(entity, new_options) do
61
- result = exposure.serializable_value(entity, new_options)
62
- out.add(exposure, result)
52
+ exposure.valid_value(entity, new_options)
63
53
  end
64
- end
54
+ end.last
65
55
  end
66
56
 
67
57
  # if we have any nesting exposures with the same name.
68
- # delegate :deep_complex_nesting?, to: :nested_exposures
69
- def deep_complex_nesting?
70
- nested_exposures.deep_complex_nesting?
58
+ # delegate :deep_complex_nesting?(entity), to: :nested_exposures
59
+ def deep_complex_nesting?(entity)
60
+ nested_exposures.deep_complex_nesting?(entity)
71
61
  end
72
62
 
73
63
  private
@@ -90,15 +80,16 @@ module Grape
90
80
 
91
81
  # This method 'merges' subsequent nesting exposures with the same name if it's needed
92
82
  def normalized_exposures(entity, options)
93
- return easy_normalized_exposures(entity, options) unless deep_complex_nesting? # optimization
83
+ return easy_normalized_exposures(entity, options) unless deep_complex_nesting?(entity) # optimization
94
84
 
95
85
  table = nested_exposures.each_with_object({}) do |exposure, output|
96
86
  should_expose = exposure.with_attr_path(entity, options) do
97
87
  exposure.should_expose?(entity, options)
98
88
  end
99
89
  next unless should_expose
100
- output[exposure.key] ||= []
101
- output[exposure.key] << exposure
90
+
91
+ output[exposure.key(entity)] ||= []
92
+ output[exposure.key(entity)] << exposure
102
93
  end
103
94
  table.map do |key, exposures|
104
95
  last_exposure = exposures.last
@@ -111,13 +102,27 @@ module Grape
111
102
  end
112
103
  new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
113
104
  NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
114
- new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
105
+ if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
106
+ new_exposure.instance_variable_set(:@deep_complex_nesting, true)
107
+ end
115
108
  end
116
109
  else
117
110
  last_exposure
118
111
  end
119
112
  end
120
113
  end
114
+
115
+ def map_entity_exposures(entity, options)
116
+ new_options = nesting_options_for(options)
117
+ output = OutputBuilder.new(entity)
118
+
119
+ normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
120
+ exposure.with_attr_path(entity, new_options) do
121
+ result = yield(exposure, new_options)
122
+ out.add(exposure, result)
123
+ end
124
+ end
125
+ end
121
126
  end
122
127
  end
123
128
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
@@ -7,12 +9,17 @@ module Grape
7
9
 
8
10
  def initialize(exposures)
9
11
  @exposures = exposures
12
+ @deep_complex_nesting = nil
10
13
  end
11
14
 
12
15
  def find_by(attribute)
13
16
  @exposures.find { |e| e.attribute == attribute }
14
17
  end
15
18
 
19
+ def select_by(attribute)
20
+ @exposures.select { |e| e.attribute == attribute }
21
+ end
22
+
16
23
  def <<(exposure)
17
24
  reset_memoization!
18
25
  @exposures << exposure
@@ -29,20 +36,20 @@ module Grape
29
36
  @exposures.clear
30
37
  end
31
38
 
32
- [
33
- :each,
34
- :to_ary, :to_a,
35
- :all?,
36
- :select,
37
- :each_with_object,
38
- :[],
39
- :==,
40
- :size,
41
- :count,
42
- :length,
43
- :empty?
39
+ %i[
40
+ each
41
+ to_ary to_a
42
+ all?
43
+ select
44
+ each_with_object
45
+ \[\]
46
+ ==
47
+ size
48
+ count
49
+ length
50
+ empty?
44
51
  ].each do |name|
45
- class_eval <<-RUBY, __FILE__, __LINE__
52
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
46
53
  def #{name}(*args, &block)
47
54
  @exposures.#{name}(*args, &block)
48
55
  end
@@ -50,10 +57,13 @@ module Grape
50
57
  end
51
58
 
52
59
  # Determine if we have any nesting exposures with the same name.
53
- def deep_complex_nesting?
60
+ def deep_complex_nesting?(entity)
54
61
  if @deep_complex_nesting.nil?
55
62
  all_nesting = select(&:nesting?)
56
- @deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
63
+ @deep_complex_nesting =
64
+ all_nesting
65
+ .group_by { |exposure| exposure.key(entity) }
66
+ .any? { |_key, exposures| exposures.length > 1 }
57
67
  else
58
68
  @deep_complex_nesting
59
69
  end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grape
2
4
  class Entity
3
5
  module Exposure
4
6
  class NestingExposure
5
7
  class OutputBuilder < SimpleDelegator
6
- def initialize
8
+ def initialize(entity)
9
+ @entity = entity
7
10
  @output_hash = {}
8
11
  @output_collection = []
9
12
  end
@@ -16,9 +19,10 @@ module Grape
16
19
  # If we have an array which should not be merged - save it with a key as a hash
17
20
  # If we have hash which should be merged - save it without a key (merge)
18
21
  return unless result
22
+
19
23
  @output_hash.merge! result, &merge_strategy(exposure.for_merge)
20
24
  else
21
- @output_hash[exposure.key] = result
25
+ @output_hash[exposure.key(@entity)] = result
22
26
  end
23
27
  end
24
28