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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +12 -12
- data/CHANGELOG.md +18 -1
- data/README.md +30 -4
- data/Rakefile +1 -1
- data/lib/grape_entity.rb +6 -2
- data/lib/grape_entity/condition.rb +26 -0
- data/lib/grape_entity/condition/base.rb +35 -0
- data/lib/grape_entity/condition/block_condition.rb +21 -0
- data/lib/grape_entity/condition/hash_condition.rb +25 -0
- data/lib/grape_entity/condition/symbol_condition.rb +21 -0
- data/lib/grape_entity/entity.rb +85 -238
- data/lib/grape_entity/exposure.rb +77 -0
- data/lib/grape_entity/exposure/base.rb +118 -0
- data/lib/grape_entity/exposure/block_exposure.rb +29 -0
- data/lib/grape_entity/exposure/delegator_exposure.rb +11 -0
- data/lib/grape_entity/exposure/formatter_block_exposure.rb +25 -0
- data/lib/grape_entity/exposure/formatter_exposure.rb +30 -0
- data/lib/grape_entity/exposure/nesting_exposure.rb +128 -0
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +70 -0
- data/lib/grape_entity/exposure/represent_exposure.rb +47 -0
- data/lib/grape_entity/options.rb +142 -0
- data/lib/grape_entity/version.rb +1 -1
- data/spec/grape_entity/entity_spec.rb +378 -154
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +34 -0
- data/spec/grape_entity/exposure_spec.rb +90 -0
- metadata +47 -26
@@ -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,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
|