grape-entity 0.6.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.coveralls.yml +1 -0
- data/.gitignore +5 -1
- data/.rubocop.yml +82 -2
- data/.rubocop_todo.yml +16 -33
- data/.travis.yml +18 -17
- data/CHANGELOG.md +75 -0
- data/Dangerfile +2 -0
- data/Gemfile +6 -1
- data/Guardfile +4 -2
- data/README.md +101 -4
- data/Rakefile +2 -2
- data/UPGRADING.md +31 -2
- data/bench/serializing.rb +7 -0
- data/grape-entity.gemspec +10 -10
- data/lib/grape-entity.rb +2 -0
- data/lib/grape_entity.rb +3 -0
- data/lib/grape_entity/condition.rb +20 -11
- data/lib/grape_entity/condition/base.rb +3 -1
- data/lib/grape_entity/condition/block_condition.rb +3 -1
- data/lib/grape_entity/condition/hash_condition.rb +2 -0
- data/lib/grape_entity/condition/symbol_condition.rb +2 -0
- data/lib/grape_entity/delegator.rb +10 -9
- data/lib/grape_entity/delegator/base.rb +2 -0
- data/lib/grape_entity/delegator/fetchable_object.rb +2 -0
- data/lib/grape_entity/delegator/hash_object.rb +4 -2
- data/lib/grape_entity/delegator/openstruct_object.rb +2 -0
- data/lib/grape_entity/delegator/plain_object.rb +2 -0
- data/lib/grape_entity/deprecated.rb +13 -0
- data/lib/grape_entity/entity.rb +115 -36
- data/lib/grape_entity/exposure.rb +64 -41
- data/lib/grape_entity/exposure/base.rb +21 -8
- data/lib/grape_entity/exposure/block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/delegator_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_block_exposure.rb +2 -0
- data/lib/grape_entity/exposure/formatter_exposure.rb +2 -0
- data/lib/grape_entity/exposure/nesting_exposure.rb +36 -30
- data/lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb +26 -15
- data/lib/grape_entity/exposure/nesting_exposure/output_builder.rb +10 -2
- data/lib/grape_entity/exposure/represent_exposure.rb +3 -1
- data/lib/grape_entity/options.rb +44 -58
- data/lib/grape_entity/version.rb +3 -1
- data/spec/grape_entity/entity_spec.rb +270 -47
- data/spec/grape_entity/exposure/nesting_exposure/nested_exposures_spec.rb +6 -4
- data/spec/grape_entity/exposure/represent_exposure_spec.rb +5 -3
- data/spec/grape_entity/exposure_spec.rb +14 -2
- data/spec/grape_entity/hash_spec.rb +38 -1
- data/spec/grape_entity/options_spec.rb +66 -0
- data/spec/spec_helper.rb +17 -0
- metadata +32 -43
@@ -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
|
-
|
33
|
-
|
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
|
44
|
-
|
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
|
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).
|
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
|
-
|
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
|
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,16 +80,18 @@ 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
|
-
|
101
|
-
output[exposure.key]
|
90
|
+
|
91
|
+
output[exposure.key(entity)] ||= []
|
92
|
+
output[exposure.key(entity)] << exposure
|
102
93
|
end
|
94
|
+
|
103
95
|
table.map do |key, exposures|
|
104
96
|
last_exposure = exposures.last
|
105
97
|
|
@@ -111,13 +103,27 @@ module Grape
|
|
111
103
|
end
|
112
104
|
new_nested_exposures = nesting_tail.flat_map(&:nested_exposures)
|
113
105
|
NestingExposure.new(key, {}, [], new_nested_exposures).tap do |new_exposure|
|
114
|
-
|
106
|
+
if nesting_tail.any? { |exposure| exposure.deep_complex_nesting?(entity) }
|
107
|
+
new_exposure.instance_variable_set(:@deep_complex_nesting, true)
|
108
|
+
end
|
115
109
|
end
|
116
110
|
else
|
117
111
|
last_exposure
|
118
112
|
end
|
119
113
|
end
|
120
114
|
end
|
115
|
+
|
116
|
+
def map_entity_exposures(entity, options)
|
117
|
+
new_options = nesting_options_for(options)
|
118
|
+
output = OutputBuilder.new(entity)
|
119
|
+
|
120
|
+
normalized_exposures(entity, new_options).each_with_object(output) do |exposure, out|
|
121
|
+
exposure.with_attr_path(entity, new_options) do
|
122
|
+
result = yield(exposure, new_options)
|
123
|
+
out.add(exposure, result)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
121
127
|
end
|
122
128
|
end
|
123
129
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
class Entity
|
3
5
|
module Exposure
|
@@ -14,6 +16,10 @@ module Grape
|
|
14
16
|
@exposures.find { |e| e.attribute == attribute }
|
15
17
|
end
|
16
18
|
|
19
|
+
def select_by(attribute)
|
20
|
+
@exposures.select { |e| e.attribute == attribute }
|
21
|
+
end
|
22
|
+
|
17
23
|
def <<(exposure)
|
18
24
|
reset_memoization!
|
19
25
|
@exposures << exposure
|
@@ -30,31 +36,36 @@ module Grape
|
|
30
36
|
@exposures.clear
|
31
37
|
end
|
32
38
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition
|
40
|
+
%i[
|
41
|
+
each
|
42
|
+
to_ary to_a
|
43
|
+
all?
|
44
|
+
select
|
45
|
+
each_with_object
|
46
|
+
\[\]
|
47
|
+
==
|
48
|
+
size
|
49
|
+
count
|
50
|
+
length
|
51
|
+
empty?
|
45
52
|
].each do |name|
|
46
|
-
class_eval <<-RUBY, __FILE__, __LINE__
|
53
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
54
|
def #{name}(*args, &block)
|
48
55
|
@exposures.#{name}(*args, &block)
|
49
56
|
end
|
50
57
|
RUBY
|
51
58
|
end
|
59
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
52
60
|
|
53
61
|
# Determine if we have any nesting exposures with the same name.
|
54
|
-
def deep_complex_nesting?
|
62
|
+
def deep_complex_nesting?(entity)
|
55
63
|
if @deep_complex_nesting.nil?
|
56
64
|
all_nesting = select(&:nesting?)
|
57
|
-
@deep_complex_nesting =
|
65
|
+
@deep_complex_nesting =
|
66
|
+
all_nesting
|
67
|
+
.group_by { |exposure| exposure.key(entity) }
|
68
|
+
.any? { |_key, exposures| exposures.length > 1 }
|
58
69
|
else
|
59
70
|
@deep_complex_nesting
|
60
71
|
end
|
@@ -1,11 +1,16 @@
|
|
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 = []
|
12
|
+
|
13
|
+
super
|
9
14
|
end
|
10
15
|
|
11
16
|
def add(exposure, result)
|
@@ -16,9 +21,10 @@ module Grape
|
|
16
21
|
# If we have an array which should not be merged - save it with a key as a hash
|
17
22
|
# If we have hash which should be merged - save it without a key (merge)
|
18
23
|
return unless result
|
24
|
+
|
19
25
|
@output_hash.merge! result, &merge_strategy(exposure.for_merge)
|
20
26
|
else
|
21
|
-
@output_hash[exposure.key] = result
|
27
|
+
@output_hash[exposure.key(@entity)] = result
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
@@ -45,6 +51,7 @@ module Grape
|
|
45
51
|
output
|
46
52
|
end
|
47
53
|
|
54
|
+
# rubocop:disable Lint/EmptyBlock
|
48
55
|
# In case if we want to solve collisions providing lambda to :merge option
|
49
56
|
def merge_strategy(for_merge)
|
50
57
|
if for_merge.respond_to? :call
|
@@ -53,6 +60,7 @@ module Grape
|
|
53
60
|
-> {}
|
54
61
|
end
|
55
62
|
end
|
63
|
+
# rubocop:enable Lint/EmptyBlock
|
56
64
|
end
|
57
65
|
end
|
58
66
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Grape
|
2
4
|
class Entity
|
3
5
|
module Exposure
|
@@ -21,7 +23,7 @@ module Grape
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def value(entity, options)
|
24
|
-
new_options = options.for_nesting(key)
|
26
|
+
new_options = options.for_nesting(key(entity))
|
25
27
|
using_class.represent(@subexposure.value(entity, options), new_options)
|
26
28
|
end
|
27
29
|
|
data/lib/grape_entity/options.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
1
5
|
module Grape
|
2
6
|
class Entity
|
3
7
|
class Options
|
8
|
+
extend Forwardable
|
9
|
+
|
4
10
|
attr_reader :opts_hash
|
5
11
|
|
12
|
+
def_delegators :opts_hash, :dig, :key?, :fetch, :[], :empty
|
13
|
+
|
6
14
|
def initialize(opts_hash = {})
|
7
15
|
@opts_hash = opts_hash
|
8
16
|
@has_only = !opts_hash[:only].nil?
|
@@ -11,54 +19,33 @@ module Grape
|
|
11
19
|
@should_return_key_cache = {}
|
12
20
|
end
|
13
21
|
|
14
|
-
def
|
15
|
-
|
16
|
-
end
|
22
|
+
def merge(new_opts)
|
23
|
+
return self if new_opts.empty?
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
25
|
+
merged = if new_opts.instance_of? Options
|
26
|
+
@opts_hash.merge(new_opts.opts_hash)
|
27
|
+
else
|
28
|
+
@opts_hash.merge(new_opts)
|
29
|
+
end
|
21
30
|
|
22
|
-
|
23
|
-
@opts_hash.key? key
|
24
|
-
end
|
25
|
-
|
26
|
-
def merge(new_opts)
|
27
|
-
if new_opts.empty?
|
28
|
-
self
|
29
|
-
else
|
30
|
-
merged = if new_opts.instance_of? Options
|
31
|
-
@opts_hash.merge(new_opts.opts_hash)
|
32
|
-
else
|
33
|
-
@opts_hash.merge(new_opts)
|
34
|
-
end
|
35
|
-
Options.new(merged)
|
36
|
-
end
|
31
|
+
Options.new(merged)
|
37
32
|
end
|
38
33
|
|
39
34
|
def reverse_merge(new_opts)
|
40
|
-
if new_opts.empty?
|
41
|
-
self
|
42
|
-
else
|
43
|
-
merged = if new_opts.instance_of? Options
|
44
|
-
new_opts.opts_hash.merge(@opts_hash)
|
45
|
-
else
|
46
|
-
new_opts.merge(@opts_hash)
|
47
|
-
end
|
48
|
-
Options.new(merged)
|
49
|
-
end
|
50
|
-
end
|
35
|
+
return self if new_opts.empty?
|
51
36
|
|
52
|
-
|
53
|
-
|
37
|
+
merged = if new_opts.instance_of? Options
|
38
|
+
new_opts.opts_hash.merge(@opts_hash)
|
39
|
+
else
|
40
|
+
new_opts.merge(@opts_hash)
|
41
|
+
end
|
42
|
+
|
43
|
+
Options.new(merged)
|
54
44
|
end
|
55
45
|
|
56
46
|
def ==(other)
|
57
|
-
|
58
|
-
|
59
|
-
else
|
60
|
-
other
|
61
|
-
end
|
47
|
+
other_hash = other.is_a?(Options) ? other.opts_hash : other
|
48
|
+
@opts_hash == other_hash
|
62
49
|
end
|
63
50
|
|
64
51
|
def should_return_key?(key)
|
@@ -66,7 +53,7 @@ module Grape
|
|
66
53
|
|
67
54
|
only = only_fields.nil? ||
|
68
55
|
only_fields.key?(key)
|
69
|
-
except = except_fields
|
56
|
+
except = except_fields&.key?(key) &&
|
70
57
|
except_fields[key] == true
|
71
58
|
only && !except
|
72
59
|
end
|
@@ -96,36 +83,35 @@ module Grape
|
|
96
83
|
end
|
97
84
|
|
98
85
|
def with_attr_path(part)
|
86
|
+
return yield unless part
|
87
|
+
|
99
88
|
stack = (opts_hash[:attr_path] ||= [])
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
result
|
105
|
-
else
|
106
|
-
yield
|
107
|
-
end
|
89
|
+
stack.push part
|
90
|
+
result = yield
|
91
|
+
stack.pop
|
92
|
+
result
|
108
93
|
end
|
109
94
|
|
110
95
|
private
|
111
96
|
|
112
97
|
def build_for_nesting(key)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
98
|
+
Options.new(
|
99
|
+
opts_hash.dup.reject { |current_key| current_key == :collection }.merge(
|
100
|
+
root: nil,
|
101
|
+
only: only_fields(key),
|
102
|
+
except: except_fields(key),
|
103
|
+
attr_path: opts_hash[:attr_path]
|
104
|
+
)
|
105
|
+
)
|
121
106
|
end
|
122
107
|
|
123
108
|
def build_symbolized_hash(attribute, hash)
|
124
|
-
|
109
|
+
case attribute
|
110
|
+
when Hash
|
125
111
|
attribute.each do |attr, nested_attrs|
|
126
112
|
hash[attr.to_sym] = build_symbolized_hash(nested_attrs, {})
|
127
113
|
end
|
128
|
-
|
114
|
+
when Array
|
129
115
|
return attribute.each { |x| build_symbolized_hash(x, {}) }
|
130
116
|
else
|
131
117
|
hash[attribute.to_sym] = true
|
data/lib/grape_entity/version.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
require 'ostruct'
|
3
5
|
|
@@ -27,7 +29,11 @@ describe Grape::Entity do
|
|
27
29
|
end
|
28
30
|
|
29
31
|
it 'makes sure that :format_with as a proc cannot be used with a block' do
|
30
|
-
|
32
|
+
# rubocop:disable Style/BlockDelimiters
|
33
|
+
# rubocop:disable Lint/EmptyBlock
|
34
|
+
expect { subject.expose :name, format_with: proc {} do p 'hi' end }.to raise_error ArgumentError
|
35
|
+
# rubocop:enable Lint/EmptyBlock
|
36
|
+
# rubocop:enable Style/BlockDelimiters
|
31
37
|
end
|
32
38
|
|
33
39
|
it 'makes sure unknown options are not silently ignored' do
|
@@ -64,6 +70,150 @@ describe Grape::Entity do
|
|
64
70
|
end
|
65
71
|
end
|
66
72
|
|
73
|
+
context 'with :expose_nil option' do
|
74
|
+
let(:a) { nil }
|
75
|
+
let(:b) { nil }
|
76
|
+
let(:c) { 'value' }
|
77
|
+
|
78
|
+
context 'when model is a PORO' do
|
79
|
+
let(:model) { Model.new(a, b, c) }
|
80
|
+
|
81
|
+
before do
|
82
|
+
stub_const 'Model', Class.new
|
83
|
+
Model.class_eval do
|
84
|
+
attr_accessor :a, :b, :c
|
85
|
+
|
86
|
+
def initialize(a, b, c)
|
87
|
+
@a = a
|
88
|
+
@b = b
|
89
|
+
@c = c
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'when expose_nil option is not provided' do
|
95
|
+
it 'exposes nil attributes' do
|
96
|
+
subject.expose(:a)
|
97
|
+
subject.expose(:b)
|
98
|
+
subject.expose(:c)
|
99
|
+
expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when expose_nil option is true' do
|
104
|
+
it 'exposes nil attributes' do
|
105
|
+
subject.expose(:a, expose_nil: true)
|
106
|
+
subject.expose(:b, expose_nil: true)
|
107
|
+
subject.expose(:c)
|
108
|
+
expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'when expose_nil option is false' do
|
113
|
+
it 'does not expose nil attributes' do
|
114
|
+
subject.expose(:a, expose_nil: false)
|
115
|
+
subject.expose(:b, expose_nil: false)
|
116
|
+
subject.expose(:c)
|
117
|
+
expect(subject.represent(model).serializable_hash).to eq(c: 'value')
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'is only applied per attribute' do
|
121
|
+
subject.expose(:a, expose_nil: false)
|
122
|
+
subject.expose(:b)
|
123
|
+
subject.expose(:c)
|
124
|
+
expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'raises an error when applied to multiple attribute exposures' do
|
128
|
+
expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'when expose_nil option is false and block passed' do
|
133
|
+
it 'does not expose if block returns nil' do
|
134
|
+
subject.expose(:a, expose_nil: false) do |_obj, _options|
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
subject.expose(:b)
|
138
|
+
subject.expose(:c)
|
139
|
+
expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'exposes is block returns a value' do
|
143
|
+
subject.expose(:a, expose_nil: false) do |_obj, _options|
|
144
|
+
100
|
145
|
+
end
|
146
|
+
subject.expose(:b)
|
147
|
+
subject.expose(:c)
|
148
|
+
expect(subject.represent(model).serializable_hash).to eq(a: 100, b: nil, c: 'value')
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'when model is a hash' do
|
154
|
+
let(:model) { { a: a, b: b, c: c } }
|
155
|
+
|
156
|
+
context 'when expose_nil option is not provided' do
|
157
|
+
it 'exposes nil attributes' do
|
158
|
+
subject.expose(:a)
|
159
|
+
subject.expose(:b)
|
160
|
+
subject.expose(:c)
|
161
|
+
expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'when expose_nil option is true' do
|
166
|
+
it 'exposes nil attributes' do
|
167
|
+
subject.expose(:a, expose_nil: true)
|
168
|
+
subject.expose(:b, expose_nil: true)
|
169
|
+
subject.expose(:c)
|
170
|
+
expect(subject.represent(model).serializable_hash).to eq(a: nil, b: nil, c: 'value')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'when expose_nil option is false' do
|
175
|
+
it 'does not expose nil attributes' do
|
176
|
+
subject.expose(:a, expose_nil: false)
|
177
|
+
subject.expose(:b, expose_nil: false)
|
178
|
+
subject.expose(:c)
|
179
|
+
expect(subject.represent(model).serializable_hash).to eq(c: 'value')
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'is only applied per attribute' do
|
183
|
+
subject.expose(:a, expose_nil: false)
|
184
|
+
subject.expose(:b)
|
185
|
+
subject.expose(:c)
|
186
|
+
expect(subject.represent(model).serializable_hash).to eq(b: nil, c: 'value')
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'raises an error when applied to multiple attribute exposures' do
|
190
|
+
expect { subject.expose(:a, :b, :c, expose_nil: false) }.to raise_error ArgumentError
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'with nested structures' do
|
196
|
+
let(:model) { { a: a, b: b, c: { d: nil, e: nil, f: { g: nil, h: nil } } } }
|
197
|
+
|
198
|
+
context 'when expose_nil option is false' do
|
199
|
+
it 'does not expose nil attributes' do
|
200
|
+
subject.expose(:a, expose_nil: false)
|
201
|
+
subject.expose(:b)
|
202
|
+
subject.expose(:c) do
|
203
|
+
subject.expose(:d, expose_nil: false)
|
204
|
+
subject.expose(:e)
|
205
|
+
subject.expose(:f) do
|
206
|
+
subject.expose(:g, expose_nil: false)
|
207
|
+
subject.expose(:h)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
expect(subject.represent(model).serializable_hash).to eq(b: nil, c: { e: nil, f: { h: nil } })
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
67
217
|
context 'with a block' do
|
68
218
|
it 'errors out if called with multiple attributes' do
|
69
219
|
expect { subject.expose(:name, :email) { true } }.to raise_error ArgumentError
|
@@ -112,6 +262,53 @@ describe Grape::Entity do
|
|
112
262
|
end
|
113
263
|
end
|
114
264
|
|
265
|
+
describe 'blocks' do
|
266
|
+
class SomeObject
|
267
|
+
def method_without_args
|
268
|
+
'result'
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
describe 'with block passed in' do
|
273
|
+
specify do
|
274
|
+
subject.expose :that_method_without_args do |object|
|
275
|
+
object.method_without_args
|
276
|
+
end
|
277
|
+
|
278
|
+
object = SomeObject.new
|
279
|
+
|
280
|
+
value = subject.represent(object).value_for(:that_method_without_args)
|
281
|
+
expect(value).to eq('result')
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
context 'with block passed in via &' do
|
286
|
+
if RUBY_VERSION.start_with?('3')
|
287
|
+
specify do
|
288
|
+
subject.expose :that_method_without_args, &:method_without_args
|
289
|
+
subject.expose :method_without_args, as: :that_method_without_args_again
|
290
|
+
|
291
|
+
object = SomeObject.new
|
292
|
+
expect do
|
293
|
+
subject.represent(object).value_for(:that_method_without_args)
|
294
|
+
end.to raise_error Grape::Entity::Deprecated
|
295
|
+
|
296
|
+
value2 = subject.represent(object).value_for(:that_method_without_args_again)
|
297
|
+
expect(value2).to eq('result')
|
298
|
+
end
|
299
|
+
else
|
300
|
+
specify do
|
301
|
+
subject.expose :that_method_without_args_again, &:method_without_args
|
302
|
+
|
303
|
+
object = SomeObject.new
|
304
|
+
|
305
|
+
value2 = subject.represent(object).value_for(:that_method_without_args_again)
|
306
|
+
expect(value2).to eq('result')
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
115
312
|
context 'with no parameters passed to the block' do
|
116
313
|
it 'adds a nested exposure' do
|
117
314
|
subject.expose :awesome do
|
@@ -131,7 +328,7 @@ describe Grape::Entity do
|
|
131
328
|
expect(another_nested).to_not be_nil
|
132
329
|
expect(another_nested.using_class_name).to eq('Awesome')
|
133
330
|
expect(moar_nested).to_not be_nil
|
134
|
-
expect(moar_nested.key).to eq(:weee)
|
331
|
+
expect(moar_nested.key(subject)).to eq(:weee)
|
135
332
|
end
|
136
333
|
|
137
334
|
it 'represents the exposure as a hash of its nested.root_exposures' do
|
@@ -324,6 +521,24 @@ describe Grape::Entity do
|
|
324
521
|
expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'bar')
|
325
522
|
expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(email: nil, name: 'foo')
|
326
523
|
end
|
524
|
+
|
525
|
+
it 'not overrides exposure by default' do
|
526
|
+
subject.expose :name
|
527
|
+
child_class = Class.new(subject)
|
528
|
+
child_class.expose :name, as: :child_name
|
529
|
+
|
530
|
+
expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
|
531
|
+
expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar', child_name: 'bar')
|
532
|
+
end
|
533
|
+
|
534
|
+
it 'overrides parent class exposure when option is specified' do
|
535
|
+
subject.expose :name
|
536
|
+
child_class = Class.new(subject)
|
537
|
+
child_class.expose :name, as: :child_name, override: true
|
538
|
+
|
539
|
+
expect(subject.represent({ name: 'bar' }, serializable: true)).to eq(name: 'bar')
|
540
|
+
expect(child_class.represent({ name: 'bar' }, serializable: true)).to eq(child_name: 'bar')
|
541
|
+
end
|
327
542
|
end
|
328
543
|
|
329
544
|
context 'register formatters' do
|
@@ -496,7 +711,7 @@ describe Grape::Entity do
|
|
496
711
|
end
|
497
712
|
|
498
713
|
exposure = subject.find_exposure(:awesome_thing)
|
499
|
-
expect(exposure.key).to eq :extra_smooth
|
714
|
+
expect(exposure.key(subject)).to eq :extra_smooth
|
500
715
|
end
|
501
716
|
|
502
717
|
it 'merges nested :if option' do
|
@@ -596,6 +811,34 @@ describe Grape::Entity do
|
|
596
811
|
exposure = subject.find_exposure(:awesome_thing)
|
597
812
|
expect(exposure.documentation).to eq(desc: 'Other description.')
|
598
813
|
end
|
814
|
+
|
815
|
+
it 'propagates expose_nil option' do
|
816
|
+
subject.class_eval do
|
817
|
+
with_options(expose_nil: false) do
|
818
|
+
expose :awesome_thing
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
exposure = subject.find_exposure(:awesome_thing)
|
823
|
+
expect(exposure.conditions[0].inversed?).to be true
|
824
|
+
expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
|
825
|
+
end
|
826
|
+
|
827
|
+
it 'overrides nested :expose_nil option' do
|
828
|
+
subject.class_eval do
|
829
|
+
with_options(expose_nil: true) do
|
830
|
+
expose :awesome_thing, expose_nil: false
|
831
|
+
expose :other_awesome_thing
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
exposure = subject.find_exposure(:awesome_thing)
|
836
|
+
expect(exposure.conditions[0].inversed?).to be true
|
837
|
+
expect(exposure.conditions[0].block.call(awesome_thing: nil)).to be true
|
838
|
+
# Conditions are only added for exposures that do not expose nil
|
839
|
+
exposure = subject.find_exposure(:other_awesome_thing)
|
840
|
+
expect(exposure.conditions[0]).to be_nil
|
841
|
+
end
|
599
842
|
end
|
600
843
|
|
601
844
|
describe '.represent' do
|
@@ -653,7 +896,7 @@ describe Grape::Entity do
|
|
653
896
|
context 'with specified fields' do
|
654
897
|
it 'returns only specified fields with only option' do
|
655
898
|
subject.expose(:id, :name, :phone)
|
656
|
-
representation = subject.represent(OpenStruct.new, only: [
|
899
|
+
representation = subject.represent(OpenStruct.new, only: %i[id name], serializable: true)
|
657
900
|
expect(representation).to eq(id: nil, name: nil)
|
658
901
|
end
|
659
902
|
|
@@ -666,7 +909,7 @@ describe Grape::Entity do
|
|
666
909
|
it 'returns only fields specified in the only option and not specified in the except option' do
|
667
910
|
subject.expose(:id, :name, :phone)
|
668
911
|
representation = subject.represent(OpenStruct.new,
|
669
|
-
only: [
|
912
|
+
only: %i[name phone],
|
670
913
|
except: [:phone], serializable: true)
|
671
914
|
expect(representation).to eq(name: nil)
|
672
915
|
end
|
@@ -736,7 +979,7 @@ describe Grape::Entity do
|
|
736
979
|
subject.expose(:id, :name, :phone)
|
737
980
|
subject.expose(:user, using: user_entity)
|
738
981
|
|
739
|
-
representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: [
|
982
|
+
representation = subject.represent(OpenStruct.new(user: {}), only: [:id, :name, { user: %i[name email] }], serializable: true)
|
740
983
|
expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
|
741
984
|
end
|
742
985
|
|
@@ -759,7 +1002,7 @@ describe Grape::Entity do
|
|
759
1002
|
subject.expose(:user, using: user_entity)
|
760
1003
|
|
761
1004
|
representation = subject.represent(OpenStruct.new(user: {}),
|
762
|
-
only: [:id, :name, :phone, user: [
|
1005
|
+
only: [:id, :name, :phone, { user: %i[id name email] }],
|
763
1006
|
except: [:phone, { user: [:id] }], serializable: true)
|
764
1007
|
expect(representation).to eq(id: nil, name: nil, user: { name: nil, email: nil })
|
765
1008
|
end
|
@@ -771,7 +1014,7 @@ describe Grape::Entity do
|
|
771
1014
|
subject.expose(:name)
|
772
1015
|
end
|
773
1016
|
|
774
|
-
representation = subject.represent(OpenStruct.new, condition: true, only: [
|
1017
|
+
representation = subject.represent(OpenStruct.new, condition: true, only: %i[id name], serializable: true)
|
775
1018
|
expect(representation).to eq(id: nil, name: nil)
|
776
1019
|
end
|
777
1020
|
|
@@ -781,7 +1024,7 @@ describe Grape::Entity do
|
|
781
1024
|
subject.expose(:name, :mobile_phone)
|
782
1025
|
end
|
783
1026
|
|
784
|
-
representation = subject.represent(OpenStruct.new, condition: true, except: [
|
1027
|
+
representation = subject.represent(OpenStruct.new, condition: true, except: %i[phone mobile_phone], serializable: true)
|
785
1028
|
expect(representation).to eq(id: nil, name: nil)
|
786
1029
|
end
|
787
1030
|
|
@@ -863,7 +1106,7 @@ describe Grape::Entity do
|
|
863
1106
|
subject.expose(:id)
|
864
1107
|
subject.expose(:name, as: :title)
|
865
1108
|
|
866
|
-
representation = subject.represent(OpenStruct.new, condition: true, only: [
|
1109
|
+
representation = subject.represent(OpenStruct.new, condition: true, only: %i[id title], serializable: true)
|
867
1110
|
expect(representation).to eq(id: nil, title: nil)
|
868
1111
|
end
|
869
1112
|
|
@@ -890,7 +1133,7 @@ describe Grape::Entity do
|
|
890
1133
|
subject.expose(:nephew, using: nephew_entity)
|
891
1134
|
|
892
1135
|
representation = subject.represent(OpenStruct.new(user: {}),
|
893
|
-
only: [
|
1136
|
+
only: %i[id name user], except: [:nephew], serializable: true)
|
894
1137
|
expect(representation).to eq(id: nil, name: nil, user: { id: nil, name: nil, email: nil })
|
895
1138
|
end
|
896
1139
|
end
|
@@ -1178,6 +1421,18 @@ describe Grape::Entity do
|
|
1178
1421
|
expect(res).to have_key :nonexistent_attribute
|
1179
1422
|
end
|
1180
1423
|
|
1424
|
+
it 'exposes attributes defined through module inclusion' do
|
1425
|
+
module SharedAttributes
|
1426
|
+
def a_value
|
1427
|
+
3.14
|
1428
|
+
end
|
1429
|
+
end
|
1430
|
+
fresh_class.include(SharedAttributes)
|
1431
|
+
fresh_class.expose :a_value
|
1432
|
+
res = fresh_class.new(model).serializable_hash
|
1433
|
+
expect(res[:a_value]).to eq(3.14)
|
1434
|
+
end
|
1435
|
+
|
1181
1436
|
it 'does not expose attributes that are generated by a block but have not passed criteria' do
|
1182
1437
|
fresh_class.expose :nonexistent_attribute,
|
1183
1438
|
proc: ->(_, _) { 'I exist, but it is not yet my time to shine' },
|
@@ -1341,8 +1596,8 @@ describe Grape::Entity do
|
|
1341
1596
|
it 'allows to pass different :only and :except params using the same instance' do
|
1342
1597
|
fresh_class.expose :a, :b, :c
|
1343
1598
|
presenter = fresh_class.new(a: 1, b: 2, c: 3)
|
1344
|
-
expect(presenter.serializable_hash(only: [
|
1345
|
-
expect(presenter.serializable_hash(only: [
|
1599
|
+
expect(presenter.serializable_hash(only: %i[a b])).to eq(a: 1, b: 2)
|
1600
|
+
expect(presenter.serializable_hash(only: %i[b c])).to eq(b: 2, c: 3)
|
1346
1601
|
end
|
1347
1602
|
end
|
1348
1603
|
end
|
@@ -1463,10 +1718,12 @@ describe Grape::Entity do
|
|
1463
1718
|
end
|
1464
1719
|
end
|
1465
1720
|
|
1721
|
+
# rubocop:disable Lint/EmptyBlock
|
1466
1722
|
fresh_class.class_eval do
|
1467
1723
|
expose :first_friend, using: EntitySpec::FriendEntity do |_user, _opts|
|
1468
1724
|
end
|
1469
1725
|
end
|
1726
|
+
# rubocop:enable Lint/EmptyBlock
|
1470
1727
|
|
1471
1728
|
rep = subject.value_for(:first_friend)
|
1472
1729
|
expect(rep).to be_kind_of EntitySpec::FriendEntity
|
@@ -1765,39 +2022,5 @@ describe Grape::Entity do
|
|
1765
2022
|
end
|
1766
2023
|
end
|
1767
2024
|
end
|
1768
|
-
|
1769
|
-
describe Grape::Entity::Options do
|
1770
|
-
module EntitySpec
|
1771
|
-
class Crystalline
|
1772
|
-
attr_accessor :prop1, :prop2
|
1773
|
-
|
1774
|
-
def initialize
|
1775
|
-
@prop1 = 'value1'
|
1776
|
-
@prop2 = 'value2'
|
1777
|
-
end
|
1778
|
-
end
|
1779
|
-
|
1780
|
-
class CrystallineEntity < Grape::Entity
|
1781
|
-
expose :prop1, if: ->(_, options) { options.fetch(:signal) }
|
1782
|
-
expose :prop2, if: ->(_, options) { options.fetch(:beam, 'destructive') == 'destructive' }
|
1783
|
-
end
|
1784
|
-
end
|
1785
|
-
|
1786
|
-
context '#fetch' do
|
1787
|
-
it 'without passing in a required option raises KeyError' do
|
1788
|
-
expect { EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new).as_json }.to raise_error KeyError
|
1789
|
-
end
|
1790
|
-
|
1791
|
-
it 'passing in a required option will expose the values' do
|
1792
|
-
crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true)
|
1793
|
-
expect(crystalline_entity.as_json).to eq(prop1: 'value1', prop2: 'value2')
|
1794
|
-
end
|
1795
|
-
|
1796
|
-
it 'with an option that is not default will not expose that value' do
|
1797
|
-
crystalline_entity = EntitySpec::CrystallineEntity.represent(EntitySpec::Crystalline.new, signal: true, beam: 'intermittent')
|
1798
|
-
expect(crystalline_entity.as_json).to eq(prop1: 'value1')
|
1799
|
-
end
|
1800
|
-
end
|
1801
|
-
end
|
1802
2025
|
end
|
1803
2026
|
end
|