grape-entity 0.4.8 → 0.5.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.
@@ -0,0 +1,77 @@
1
+ require 'grape_entity/exposure/base'
2
+ require 'grape_entity/exposure/represent_exposure'
3
+ require 'grape_entity/exposure/block_exposure'
4
+ require 'grape_entity/exposure/delegator_exposure'
5
+ require 'grape_entity/exposure/formatter_exposure'
6
+ require 'grape_entity/exposure/formatter_block_exposure'
7
+ require 'grape_entity/exposure/nesting_exposure'
8
+ require 'grape_entity/condition'
9
+
10
+ module Grape
11
+ class Entity
12
+ 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
22
+
23
+ if options[:using]
24
+ using_class = options[:using]
25
+
26
+ if options[:proc]
27
+ RepresentExposure.new(*base_args, using_class, block_exposure)
28
+ else
29
+ RepresentExposure.new(*base_args, using_class, delegator_exposure)
30
+ end
31
+
32
+ elsif options[:proc]
33
+ block_exposure
34
+
35
+ elsif options[:format_with]
36
+ format_with = options[:format_with]
37
+
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
43
+
44
+ elsif options[:nesting]
45
+ NestingExposure.new(*base_args)
46
+
47
+ else
48
+ delegator_exposure
49
+ end
50
+ end
51
+
52
+ def self.compile_conditions(options)
53
+ if_conditions = []
54
+ unless options[:if_extras].nil?
55
+ if_conditions.concat(options[:if_extras])
56
+ end
57
+ if_conditions << options[:if] unless options[:if].nil?
58
+
59
+ if_conditions.map! do |cond|
60
+ Condition.new_if cond
61
+ end
62
+
63
+ unless_conditions = []
64
+ unless options[:unless_extras].nil?
65
+ unless_conditions.concat(options[:unless_extras])
66
+ end
67
+ unless_conditions << options[:unless] unless options[:unless].nil?
68
+
69
+ unless_conditions.map! do |cond|
70
+ Condition.new_unless cond
71
+ end
72
+
73
+ if_conditions + unless_conditions
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,118 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class Base
5
+ attr_reader :attribute, :key, :is_safe, :documentation, :conditions
6
+
7
+ def self.new(attribute, options, conditions, *args, &block)
8
+ super(attribute, options, conditions).tap { |e| e.setup(*args, &block) }
9
+ end
10
+
11
+ def initialize(attribute, options, conditions)
12
+ @attribute = attribute.try(:to_sym)
13
+ @options = options
14
+ @key = (options[:as] || attribute).try(:to_sym)
15
+ @is_safe = options[:safe]
16
+ @attr_path_proc = options[:attr_path]
17
+ @documentation = options[:documentation]
18
+ @conditions = conditions
19
+ end
20
+
21
+ def dup(&block)
22
+ self.class.new(*dup_args, &block)
23
+ end
24
+
25
+ def dup_args
26
+ [@attribute, @options, @conditions.map(&:dup)]
27
+ end
28
+
29
+ def ==(other)
30
+ self.class == other.class &&
31
+ @attribute == other.attribute &&
32
+ @options == other.options &&
33
+ @conditions == other.conditions
34
+ end
35
+
36
+ def setup
37
+ end
38
+
39
+ def nesting?
40
+ false
41
+ end
42
+
43
+ # if we have any nesting exposures with the same name.
44
+ def deep_complex_nesting?
45
+ false
46
+ end
47
+
48
+ def valid?(entity)
49
+ is_delegatable = entity.delegator.delegatable?(@attribute) || entity.respond_to?(@attribute, true)
50
+ if @is_safe
51
+ is_delegatable
52
+ else
53
+ is_delegatable || fail(NoMethodError, "#{entity.class.name} missing attribute `#{@attribute}' on #{entity.object}")
54
+ end
55
+ end
56
+
57
+ def value(_entity, _options)
58
+ fail NotImplementedError
59
+ end
60
+
61
+ def serializable_value(entity, options)
62
+ partial_output = valid_value(entity, options)
63
+
64
+ if partial_output.respond_to?(:serializable_hash)
65
+ partial_output.serializable_hash(options)
66
+ elsif partial_output.is_a?(Array) && partial_output.all? { |o| o.respond_to?(:serializable_hash) }
67
+ partial_output.map(&:serializable_hash)
68
+ elsif partial_output.is_a?(Hash)
69
+ partial_output.each do |key, value|
70
+ partial_output[key] = value.serializable_hash if value.respond_to?(:serializable_hash)
71
+ end
72
+ else
73
+ partial_output
74
+ end
75
+ end
76
+
77
+ def valid_value(entity, options)
78
+ value(entity, options) if valid?(entity)
79
+ end
80
+
81
+ def should_return_key?(options)
82
+ options.should_return_key?(@key)
83
+ end
84
+
85
+ def conditional?
86
+ !@conditions.empty?
87
+ end
88
+
89
+ def conditions_met?(entity, options)
90
+ @conditions.all? { |condition| condition.met? entity, options }
91
+ end
92
+
93
+ def should_expose?(entity, options)
94
+ should_return_key?(options) && conditions_met?(entity, options)
95
+ end
96
+
97
+ def attr_path(entity, options)
98
+ if @attr_path_proc
99
+ entity.exec_with_object(options, &@attr_path_proc)
100
+ else
101
+ @key
102
+ end
103
+ end
104
+
105
+ def with_attr_path(entity, options)
106
+ path_part = attr_path(entity, options)
107
+ options.with_attr_path(path_part) do
108
+ yield
109
+ end
110
+ end
111
+
112
+ protected
113
+
114
+ attr_reader :options
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,29 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class BlockExposure < Base
5
+ attr_reader :block
6
+
7
+ def value(entity, options)
8
+ entity.exec_with_object(options, &@block)
9
+ end
10
+
11
+ def dup
12
+ super(&@block)
13
+ end
14
+
15
+ def ==(other)
16
+ super && @block == other.block
17
+ end
18
+
19
+ def valid?(_entity)
20
+ true
21
+ end
22
+
23
+ def setup(&block)
24
+ @block = block
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class DelegatorExposure < Base
5
+ def value(entity, _options)
6
+ entity.delegate_attribute(attribute)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class FormatterBlockExposure < Base
5
+ attr_reader :format_with
6
+
7
+ def setup(&format_with)
8
+ @format_with = format_with
9
+ end
10
+
11
+ def dup
12
+ super(&@format_with)
13
+ end
14
+
15
+ def ==(other)
16
+ super && @format_with == other.format_with
17
+ end
18
+
19
+ def value(entity, _options)
20
+ entity.exec_with_attribute(attribute, &@format_with)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class FormatterExposure < Base
5
+ attr_reader :format_with
6
+
7
+ def setup(format_with)
8
+ @format_with = format_with
9
+ end
10
+
11
+ def dup_args
12
+ [*super, format_with]
13
+ end
14
+
15
+ def ==(other)
16
+ super && @format_with == other.format_with
17
+ end
18
+
19
+ def value(entity, _options)
20
+ formatters = entity.class.formatters
21
+ if formatters[@format_with]
22
+ entity.exec_with_attribute(attribute, &formatters[@format_with])
23
+ else
24
+ entity.send(@format_with, entity.delegate_attribute(attribute))
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,128 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class NestingExposure < Base
5
+ attr_reader :nested_exposures
6
+
7
+ def setup(nested_exposures = [])
8
+ @nested_exposures = NestedExposures.new(nested_exposures)
9
+ end
10
+
11
+ def dup_args
12
+ [*super, @nested_exposures.map(&:dup)]
13
+ end
14
+
15
+ def ==(other)
16
+ super && @nested_exposures == other.nested_exposures
17
+ end
18
+
19
+ def nesting?
20
+ true
21
+ end
22
+
23
+ def find_nested_exposure(attribute)
24
+ nested_exposures.find_by(attribute)
25
+ end
26
+
27
+ def valid?(entity)
28
+ nested_exposures.all? { |e| e.valid?(entity) }
29
+ end
30
+
31
+ def value(entity, options)
32
+ new_options = nesting_options_for(options)
33
+
34
+ normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
35
+ exposure.with_attr_path(entity, new_options) do
36
+ result = exposure.value(entity, new_options)
37
+ output[exposure.key] = result
38
+ end
39
+ end
40
+ end
41
+
42
+ def valid_value_for(key, entity, options)
43
+ new_options = nesting_options_for(options)
44
+
45
+ result = nil
46
+ normalized_exposures(entity, new_options).select { |e| e.key == key }.each do |exposure|
47
+ exposure.with_attr_path(entity, new_options) do
48
+ result = exposure.valid_value(entity, new_options)
49
+ end
50
+ end
51
+ result
52
+ end
53
+
54
+ def serializable_value(entity, options)
55
+ new_options = nesting_options_for(options)
56
+
57
+ normalized_exposures(entity, new_options).each_with_object({}) do |exposure, output|
58
+ exposure.with_attr_path(entity, new_options) do
59
+ result = exposure.serializable_value(entity, new_options)
60
+ output[exposure.key] = result
61
+ end
62
+ end
63
+ end
64
+
65
+ # if we have any nesting exposures with the same name.
66
+ # delegate :deep_complex_nesting?, to: :nested_exposures
67
+ def deep_complex_nesting?
68
+ nested_exposures.deep_complex_nesting?
69
+ end
70
+
71
+ private
72
+
73
+ def nesting_options_for(options)
74
+ if @key
75
+ options.for_nesting(@key)
76
+ else
77
+ options
78
+ end
79
+ end
80
+
81
+ def easy_normalized_exposures(entity, options)
82
+ nested_exposures.select do |exposure|
83
+ exposure.with_attr_path(entity, options) do
84
+ exposure.should_expose?(entity, options)
85
+ end
86
+ end
87
+ end
88
+
89
+ # This method 'merges' subsequent nesting exposures with the same name if it's needed
90
+ def normalized_exposures(entity, options)
91
+ return easy_normalized_exposures(entity, options) unless deep_complex_nesting? # optimization
92
+
93
+ table = nested_exposures.each_with_object({}) do |exposure, output|
94
+ should_expose = exposure.with_attr_path(entity, options) do
95
+ exposure.should_expose?(entity, options)
96
+ end
97
+ next unless should_expose
98
+ output[exposure.key] ||= []
99
+ output[exposure.key] << exposure
100
+ end
101
+ table.map do |key, exposures|
102
+ last_exposure = exposures.last
103
+
104
+ if last_exposure.nesting?
105
+ # For the given key if the last candidates for exposing are nesting then combine them.
106
+ nesting_tail = []
107
+ exposures.reverse_each do |exposure|
108
+ if exposure.nesting?
109
+ nesting_tail.unshift exposure
110
+ else
111
+ break
112
+ end
113
+ end
114
+ new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
115
+ NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
116
+ new_exposure.instance_variable_set(:@deep_complex_nesting, true) if nesting_tail.any?(&:deep_complex_nesting?)
117
+ end
118
+ else
119
+ last_exposure
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ require 'grape_entity/exposure/nesting_exposure/nested_exposures'
@@ -0,0 +1,70 @@
1
+ module Grape
2
+ class Entity
3
+ module Exposure
4
+ class NestingExposure
5
+ class NestedExposures
6
+ include Enumerable
7
+
8
+ def initialize(exposures)
9
+ @exposures = exposures
10
+ end
11
+
12
+ def find_by(attribute)
13
+ @exposures.find { |e| e.attribute == attribute }
14
+ end
15
+
16
+ def <<(exposure)
17
+ reset_memoization!
18
+ @exposures << exposure
19
+ end
20
+
21
+ def delete_by(*attributes)
22
+ reset_memoization!
23
+ @exposures.reject! { |e| attributes.include? e.attribute }
24
+ end
25
+
26
+ def clear
27
+ reset_memoization!
28
+ @exposures.clear
29
+ end
30
+
31
+ [
32
+ :each,
33
+ :to_ary, :to_a,
34
+ :all?,
35
+ :select,
36
+ :each_with_object,
37
+ :[],
38
+ :==,
39
+ :size,
40
+ :count,
41
+ :length,
42
+ :empty?
43
+ ].each do |name|
44
+ class_eval <<-RUBY, __FILE__, __LINE__
45
+ def #{name}(*args, &block)
46
+ @exposures.#{name}(*args, &block)
47
+ end
48
+ RUBY
49
+ end
50
+
51
+ # Determine if we have any nesting exposures with the same name.
52
+ def deep_complex_nesting?
53
+ if @deep_complex_nesting.nil?
54
+ all_nesting = select(&:nesting?)
55
+ @deep_complex_nesting = all_nesting.group_by(&:key).any? { |_key, exposures| exposures.length > 1 }
56
+ else
57
+ @deep_complex_nesting
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def reset_memoization!
64
+ @deep_complex_nesting = nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end