serega 0.38.0 → 0.40.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/README.md +198 -28
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +3 -3
- data/lib/serega/attribute_normalizer.rb +140 -9
- data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
- data/lib/serega/attribute_value_resolvers/hash_access.rb +116 -0
- data/lib/serega/config.rb +107 -1
- data/lib/serega/object_serializer.rb +3 -3
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +2 -0
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
- data/lib/serega/plugins/presenter/presenter.rb +99 -2
- data/lib/serega/plugins/root/root.rb +1 -1
- data/lib/serega/utils/collection_detector.rb +26 -0
- data/lib/serega/validations/attribute/check_block.rb +20 -55
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
- data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
- data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_delegate.rb +45 -5
- data/lib/serega/validations/attribute/check_opt_hash_access.rb +78 -0
- data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
- data/lib/serega/validations/check_attribute_params.rb +9 -7
- data/lib/serega.rb +22 -6
- metadata +5 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module AttributeValueResolvers
|
|
5
|
+
#
|
|
6
|
+
# Builds value resolver for attributes with the :hash_access option
|
|
7
|
+
#
|
|
8
|
+
class HashAccessResolver
|
|
9
|
+
# Allowed hash access modes
|
|
10
|
+
MODES = %i[symbol string].freeze
|
|
11
|
+
|
|
12
|
+
#
|
|
13
|
+
# Creates resolver that reads a key from Hash records
|
|
14
|
+
#
|
|
15
|
+
# @param name [Symbol, String] hash key
|
|
16
|
+
# @param mode [Symbol] hash access mode (:symbol, :string)
|
|
17
|
+
# @param allow_missing_key [Boolean] whether a missing key is read via `record[key]` rather than raising
|
|
18
|
+
#
|
|
19
|
+
# @return [HashAccessKeyword] resolver instance
|
|
20
|
+
#
|
|
21
|
+
def self.get(name, mode, allow_missing_key)
|
|
22
|
+
HashAccessKeyword.new(name, mode, allow_missing_key)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#
|
|
27
|
+
# Builds value resolver for attributes with the :delegate option using
|
|
28
|
+
# hash access on any of its steps
|
|
29
|
+
#
|
|
30
|
+
class HashAccessDelegateResolver
|
|
31
|
+
#
|
|
32
|
+
# Creates resolver that delegates through the provided step readers
|
|
33
|
+
#
|
|
34
|
+
# @param to_step [#call] reader of the intermediate object
|
|
35
|
+
# @param final_step [#call] reader of the final value
|
|
36
|
+
# @param delegate_allow_nil [Boolean] whether a nil intermediate object resolves to nil
|
|
37
|
+
#
|
|
38
|
+
# @return [HashAccessDelegate, HashAccessDelegateAllowNil] resolver instance
|
|
39
|
+
#
|
|
40
|
+
def self.get(to_step, final_step, delegate_allow_nil)
|
|
41
|
+
delegate_allow_nil ? HashAccessDelegateAllowNil.new(to_step, final_step) : HashAccessDelegate.new(to_step, final_step)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
#
|
|
46
|
+
# Value resolver for attributes with the :hash_access option
|
|
47
|
+
#
|
|
48
|
+
class HashAccessKeyword
|
|
49
|
+
def initialize(name, mode, allow_missing_key)
|
|
50
|
+
@key = (mode == :symbol) ? name.to_sym : name.to_s
|
|
51
|
+
@allow_missing_key = allow_missing_key
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
#
|
|
55
|
+
# Reads the key from the record
|
|
56
|
+
#
|
|
57
|
+
# @param object [Object] serialized object or delegation step value
|
|
58
|
+
# @return [Object] the value found
|
|
59
|
+
#
|
|
60
|
+
def call(object)
|
|
61
|
+
return object[@key] if @allow_missing_key
|
|
62
|
+
|
|
63
|
+
object.fetch(@key) do
|
|
64
|
+
default = object[@key]
|
|
65
|
+
next default unless default.nil?
|
|
66
|
+
|
|
67
|
+
raise KeyError.new("key not found: #{@key.inspect}", key: @key, receiver: object)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#
|
|
73
|
+
# Value resolver for attributes with :hash_access and :delegate (without :allow_nil) options
|
|
74
|
+
#
|
|
75
|
+
class HashAccessDelegate
|
|
76
|
+
def initialize(to_step, final_step)
|
|
77
|
+
@to_step = to_step
|
|
78
|
+
@final_step = final_step
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Delegates the value reading through the intermediate object
|
|
83
|
+
#
|
|
84
|
+
# @param object [Object] serialized object
|
|
85
|
+
# @return [Object] the value found
|
|
86
|
+
#
|
|
87
|
+
def call(object)
|
|
88
|
+
@final_step.call(@to_step.call(object))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
#
|
|
93
|
+
# Value resolver for attributes with :hash_access and :delegate (with :allow_nil) options
|
|
94
|
+
#
|
|
95
|
+
class HashAccessDelegateAllowNil
|
|
96
|
+
def initialize(to_step, final_step)
|
|
97
|
+
@to_step = to_step
|
|
98
|
+
@final_step = final_step
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#
|
|
102
|
+
# Delegates the value reading through the intermediate object,
|
|
103
|
+
# resolving a nil intermediate to nil
|
|
104
|
+
#
|
|
105
|
+
# @param object [Object] serialized object
|
|
106
|
+
# @return [Object, nil] the value found
|
|
107
|
+
#
|
|
108
|
+
def call(object)
|
|
109
|
+
intermediate = @to_step.call(object)
|
|
110
|
+
return if intermediate.nil?
|
|
111
|
+
|
|
112
|
+
@final_step.call(intermediate)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/serega/config.rb
CHANGED
|
@@ -24,6 +24,8 @@ class Serega
|
|
|
24
24
|
default
|
|
25
25
|
preload
|
|
26
26
|
batch
|
|
27
|
+
base_serializer
|
|
28
|
+
hash_access
|
|
27
29
|
].freeze,
|
|
28
30
|
serialize_keys: %i[context many].freeze,
|
|
29
31
|
check_attribute_name: true,
|
|
@@ -31,8 +33,11 @@ class Serega
|
|
|
31
33
|
delegate_default_allow_nil: false,
|
|
32
34
|
max_cached_plans_per_serializer_count: 0,
|
|
33
35
|
auto_preload: {has_delegate_option: false, has_serializer_option: false},
|
|
36
|
+
auto_preload_excluded_methods: %i[itself].freeze,
|
|
34
37
|
hide_by_default: false,
|
|
35
|
-
batch_id_option: :id
|
|
38
|
+
batch_id_option: :id,
|
|
39
|
+
base_serializer: nil,
|
|
40
|
+
hash_access: {default_mode: :symbol, default_allow_missing_key: false}
|
|
36
41
|
}.freeze
|
|
37
42
|
# :nocov:
|
|
38
43
|
|
|
@@ -114,6 +119,50 @@ class Serega
|
|
|
114
119
|
opts[:delegate_default_allow_nil] = value
|
|
115
120
|
end
|
|
116
121
|
|
|
122
|
+
# Returns :auto_preload_excluded_methods config option — methods that are
|
|
123
|
+
# never auto-preloaded, as they return the serialized object itself and
|
|
124
|
+
# not an association
|
|
125
|
+
# @return [Array<Symbol>] Current :auto_preload_excluded_methods config option
|
|
126
|
+
def auto_preload_excluded_methods
|
|
127
|
+
opts.fetch(:auto_preload_excluded_methods)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Sets :auto_preload_excluded_methods config option — methods that are
|
|
131
|
+
# never auto-preloaded. Applies to the `:method` option of attributes
|
|
132
|
+
# with a serializer and to the `delegate: {to: ...}` option.
|
|
133
|
+
#
|
|
134
|
+
# @param value [Array<Symbol>] Method names to skip in auto-preload
|
|
135
|
+
#
|
|
136
|
+
# @return [Array<Symbol>] New :auto_preload_excluded_methods config option
|
|
137
|
+
def auto_preload_excluded_methods=(value)
|
|
138
|
+
unless value.is_a?(Array) && value.all?(Symbol)
|
|
139
|
+
raise SeregaError, "Must be an Array of Symbols, #{value.inspect} provided"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
opts[:auto_preload_excluded_methods] = value
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns :base_serializer config option — the parent class for nested
|
|
146
|
+
# serializers defined with attribute blocks
|
|
147
|
+
# @return [Class, nil] Current :base_serializer config option
|
|
148
|
+
def base_serializer
|
|
149
|
+
opts.fetch(:base_serializer)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Sets :base_serializer config option — the parent class for nested
|
|
153
|
+
# serializers defined with attribute blocks. Usually a settings-only
|
|
154
|
+
# serializer, e.g. `config.base_serializer = self` in an application
|
|
155
|
+
# base serializer class.
|
|
156
|
+
#
|
|
157
|
+
# @param value [Class] Serega or its subclass
|
|
158
|
+
#
|
|
159
|
+
# @return [Class] New :base_serializer config option
|
|
160
|
+
def base_serializer=(value)
|
|
161
|
+
raise SeregaError, "Must be a Serega subclass, #{value.inspect} provided" if !value.is_a?(Class) || !(value <= Serega)
|
|
162
|
+
|
|
163
|
+
opts[:base_serializer] = value
|
|
164
|
+
end
|
|
165
|
+
|
|
117
166
|
# Returns :hide_by_default config option
|
|
118
167
|
# @return [Boolean, Symbol] Current :hide_by_default config option
|
|
119
168
|
def hide_by_default
|
|
@@ -158,6 +207,12 @@ class Serega
|
|
|
158
207
|
end
|
|
159
208
|
end
|
|
160
209
|
|
|
210
|
+
# Returns the hash_access config object
|
|
211
|
+
# @return [Serega::SeregaConfig::HashAccessConfig] hash_access config object
|
|
212
|
+
def hash_access
|
|
213
|
+
@hash_access ||= HashAccessConfig.new(opts.fetch(:hash_access))
|
|
214
|
+
end
|
|
215
|
+
|
|
161
216
|
# Returns :max_cached_plans_per_serializer_count config option
|
|
162
217
|
# @return [Boolean] Current :max_cached_plans_per_serializer_count config option
|
|
163
218
|
def max_cached_plans_per_serializer_count
|
|
@@ -205,6 +260,57 @@ class Serega
|
|
|
205
260
|
end
|
|
206
261
|
end
|
|
207
262
|
|
|
263
|
+
#
|
|
264
|
+
# Config for the `hash_access:` attribute option
|
|
265
|
+
#
|
|
266
|
+
class HashAccessConfig
|
|
267
|
+
# @return [Hash] hash_access config options
|
|
268
|
+
attr_reader :opts
|
|
269
|
+
|
|
270
|
+
#
|
|
271
|
+
# Initializes HashAccessConfig object
|
|
272
|
+
#
|
|
273
|
+
# @param opts [Hash] hash_access config options
|
|
274
|
+
#
|
|
275
|
+
# @return [Serega::SeregaConfig::HashAccessConfig]
|
|
276
|
+
#
|
|
277
|
+
def initialize(opts)
|
|
278
|
+
@opts = opts
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# @return [Symbol] mode used by `hash_access: true` (default :symbol)
|
|
282
|
+
def default_mode
|
|
283
|
+
opts.fetch(:default_mode)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Sets the mode used by `hash_access: true`
|
|
287
|
+
# @param value [Symbol] one of :symbol, :string
|
|
288
|
+
# @return [Symbol] new default mode
|
|
289
|
+
def default_mode=(value)
|
|
290
|
+
unless AttributeValueResolvers::HashAccessResolver::MODES.include?(value)
|
|
291
|
+
raise SeregaError, "Invalid hash_access default_mode #{value.inspect}. Allowed modes: :symbol, :string"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
opts[:default_mode] = value
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# @return [Boolean] allow_missing_key used when an attribute omits it
|
|
298
|
+
def default_allow_missing_key
|
|
299
|
+
opts.fetch(:default_allow_missing_key)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Sets the allow_missing_key used when an attribute omits it
|
|
303
|
+
# @param value [Boolean]
|
|
304
|
+
# @return [Boolean] new default allow_missing_key
|
|
305
|
+
def default_allow_missing_key=(value)
|
|
306
|
+
unless value == true || value == false
|
|
307
|
+
raise SeregaError, "Invalid hash_access default_allow_missing_key #{value.inspect}. Must be a Boolean"
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
opts[:default_allow_missing_key] = value
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
208
314
|
include SeregaConfigInstanceMethods
|
|
209
315
|
extend Serega::SeregaHelpers::SerializerClassHelper
|
|
210
316
|
end
|
|
@@ -97,14 +97,14 @@ class Serega
|
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# How to serialize `object`, deciding whether the result is a collection or a
|
|
100
|
-
# single object and
|
|
100
|
+
# single object and checking the object type only once:
|
|
101
101
|
# - :many — `many` is on and the object is a collection
|
|
102
102
|
# - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
|
|
103
103
|
# - :one — serialize the object on its own
|
|
104
104
|
def serialize_mode(object)
|
|
105
105
|
case many
|
|
106
|
-
when NilClass then
|
|
107
|
-
when TrueClass then
|
|
106
|
+
when NilClass then SeregaUtils::CollectionDetector.call(object) ? :many : :one
|
|
107
|
+
when TrueClass then SeregaUtils::CollectionDetector.call(object) ? :many : :many_for_one
|
|
108
108
|
else :one # many == false
|
|
109
109
|
end
|
|
110
110
|
end
|
|
@@ -91,6 +91,7 @@ class Serega
|
|
|
91
91
|
# The underlying records to preload onto. The :presenter plugin wraps every
|
|
92
92
|
# serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
|
|
93
93
|
# the real records, so unwrap them via #__getobj__ when presenter is used.
|
|
94
|
+
# Objects are wrapped only when the Presenter class has custom methods.
|
|
94
95
|
#
|
|
95
96
|
# @param serializer_class [Class<Serega>] Current serializer class
|
|
96
97
|
# @param objects [Array] objects serialized at the current level
|
|
@@ -99,6 +100,7 @@ class Serega
|
|
|
99
100
|
#
|
|
100
101
|
def self.records(serializer_class, objects)
|
|
101
102
|
return objects unless serializer_class.plugin_used?(:presenter)
|
|
103
|
+
return objects unless serializer_class.custom_presenter?
|
|
102
104
|
|
|
103
105
|
objects.map(&:__getobj__)
|
|
104
106
|
end
|
|
@@ -6,7 +6,8 @@ class Serega
|
|
|
6
6
|
# Plugin :explicit_many_option
|
|
7
7
|
#
|
|
8
8
|
# Plugin requires to add :many option when adding relationships
|
|
9
|
-
# (relationships are attributes with :serializer option
|
|
9
|
+
# (relationships are attributes with the :serializer option or a block
|
|
10
|
+
# defining a nested serializer)
|
|
10
11
|
#
|
|
11
12
|
# Adding this plugin makes it clearer to find if relationship returns array or single object
|
|
12
13
|
#
|
|
@@ -56,7 +57,7 @@ class Serega
|
|
|
56
57
|
def check_opts
|
|
57
58
|
super
|
|
58
59
|
|
|
59
|
-
CheckOptMany.call(opts)
|
|
60
|
+
CheckOptMany.call(opts, block)
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -10,23 +10,25 @@ class Serega
|
|
|
10
10
|
class << self
|
|
11
11
|
#
|
|
12
12
|
# Checks attribute :many option must be provided with relations
|
|
13
|
+
# (attributes with the :serializer option or a block defining
|
|
14
|
+
# a nested serializer)
|
|
13
15
|
#
|
|
14
16
|
# @param opts [Hash] Attribute options
|
|
17
|
+
# @param block [nil, Proc] Attribute block
|
|
15
18
|
#
|
|
16
19
|
# @raise [SeregaError] Attribute validation error
|
|
17
20
|
#
|
|
18
21
|
# @return [void]
|
|
19
22
|
#
|
|
20
|
-
def call(opts)
|
|
21
|
-
|
|
22
|
-
return unless serializer
|
|
23
|
+
def call(opts, block = nil)
|
|
24
|
+
return if !opts[:serializer] && !block
|
|
23
25
|
|
|
24
26
|
many_option_exists = opts.key?(:many)
|
|
25
27
|
return if many_option_exists
|
|
26
28
|
|
|
27
29
|
raise SeregaError,
|
|
28
30
|
"Attribute option :many [Boolean] must be provided" \
|
|
29
|
-
" for attributes with :serializer option"
|
|
31
|
+
" for attributes with :serializer option or a block"
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -15,13 +15,19 @@ class Serega
|
|
|
15
15
|
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
16
16
|
# - The serialization context is accessible via the private method __ctx__.
|
|
17
17
|
#
|
|
18
|
+
# Objects are wrapped in the Presenter only when the serializer's Presenter
|
|
19
|
+
# class (or an inherited one) was actually extended with custom methods —
|
|
20
|
+
# a bare `plugin :presenter` adds no wrapping overhead. The check is
|
|
21
|
+
# denormalized: `SerializerClass.custom_presenter?` is asked once per
|
|
22
|
+
# object serializer and the result is reused for the whole level.
|
|
23
|
+
#
|
|
18
24
|
# class UserSerializer < Serega
|
|
19
25
|
# plugin :presenter
|
|
20
26
|
#
|
|
21
27
|
# attribute :name
|
|
22
28
|
# attribute :role
|
|
23
29
|
#
|
|
24
|
-
#
|
|
30
|
+
# presenter do
|
|
25
31
|
# def name
|
|
26
32
|
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
27
33
|
# end
|
|
@@ -31,6 +37,9 @@ class Serega
|
|
|
31
37
|
# end
|
|
32
38
|
# end
|
|
33
39
|
# end
|
|
40
|
+
#
|
|
41
|
+
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
42
|
+
# Presenter class, so multiple blocks accumulate.
|
|
34
43
|
module Presenter
|
|
35
44
|
# @return [Symbol] Plugin name
|
|
36
45
|
def self.plugin_name
|
|
@@ -62,6 +71,11 @@ class Serega
|
|
|
62
71
|
presenter_class = Class.new(Presenter)
|
|
63
72
|
presenter_class.serializer_class = serializer_class
|
|
64
73
|
serializer_class.const_set(:Presenter, presenter_class)
|
|
74
|
+
|
|
75
|
+
# The presenter's unwrap method returns the serialized object itself,
|
|
76
|
+
# not an association — it must never be auto-preloaded.
|
|
77
|
+
config = serializer_class.config
|
|
78
|
+
config.auto_preload_excluded_methods = config.auto_preload_excluded_methods | [:__getobj__]
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
# Presenter class
|
|
@@ -97,6 +111,48 @@ class Serega
|
|
|
97
111
|
extend SeregaHelpers::SerializerClassHelper
|
|
98
112
|
extend Forwardable
|
|
99
113
|
include InstanceMethods
|
|
114
|
+
|
|
115
|
+
# Tracks whether user code was added to the Presenter class.
|
|
116
|
+
#
|
|
117
|
+
# These singleton methods are defined after the base class body above,
|
|
118
|
+
# so the plugin's own includes do not mark the base class as modified.
|
|
119
|
+
# Lazy delegators defined by #method_missing do mark the class, but
|
|
120
|
+
# they can appear only on presenters that are already wrapping.
|
|
121
|
+
class << self
|
|
122
|
+
#
|
|
123
|
+
# Checks if this Presenter class (or an inherited one) was extended
|
|
124
|
+
# with custom user code and therefore objects must be wrapped
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] whether custom presenter methods were defined
|
|
127
|
+
#
|
|
128
|
+
def modified?
|
|
129
|
+
return true if defined?(@modified)
|
|
130
|
+
return false if equal?(Presenter) # the plugin's base class — the walk stops here
|
|
131
|
+
|
|
132
|
+
superclass.modified?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Marks the class as modified, then includes the module
|
|
136
|
+
# @return [void]
|
|
137
|
+
def include(*modules)
|
|
138
|
+
@modified = true
|
|
139
|
+
super
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Marks the class as modified, then prepends the module
|
|
143
|
+
# @return [void]
|
|
144
|
+
def prepend(*modules)
|
|
145
|
+
@modified = true
|
|
146
|
+
super
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def method_added(name)
|
|
152
|
+
@modified = true
|
|
153
|
+
super
|
|
154
|
+
end
|
|
155
|
+
end
|
|
100
156
|
end
|
|
101
157
|
|
|
102
158
|
#
|
|
@@ -105,6 +161,36 @@ class Serega
|
|
|
105
161
|
# @see Serega
|
|
106
162
|
#
|
|
107
163
|
module ClassMethods
|
|
164
|
+
#
|
|
165
|
+
# Defines presenter methods — evaluates the block inside the
|
|
166
|
+
# serializer's own Presenter class. Multiple blocks accumulate.
|
|
167
|
+
#
|
|
168
|
+
# presenter do
|
|
169
|
+
# def name
|
|
170
|
+
# [first_name, last_name].compact.join(" ")
|
|
171
|
+
# end
|
|
172
|
+
# end
|
|
173
|
+
#
|
|
174
|
+
# @return [void]
|
|
175
|
+
#
|
|
176
|
+
def presenter(&block)
|
|
177
|
+
raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
|
|
178
|
+
|
|
179
|
+
self::Presenter.class_exec(&block)
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
#
|
|
184
|
+
# Checks if the serializer's Presenter class (or an inherited one) was
|
|
185
|
+
# extended with custom user code. When it was not, serialized objects
|
|
186
|
+
# are not wrapped in the Presenter at all.
|
|
187
|
+
#
|
|
188
|
+
# @return [Boolean] whether custom presenter methods were defined
|
|
189
|
+
#
|
|
190
|
+
def custom_presenter?
|
|
191
|
+
self::Presenter.modified?
|
|
192
|
+
end
|
|
193
|
+
|
|
108
194
|
private
|
|
109
195
|
|
|
110
196
|
def inherited(subclass)
|
|
@@ -122,14 +208,25 @@ class Serega
|
|
|
122
208
|
# @see Serega::SeregaObjectSerializer
|
|
123
209
|
#
|
|
124
210
|
module SeregaObjectSerializerInstanceMethods
|
|
211
|
+
# The custom-presenter check is made once per object serializer here
|
|
212
|
+
# and its result is reused for every enqueued chunk of the level.
|
|
213
|
+
def initialize(**opts)
|
|
214
|
+
super
|
|
215
|
+
@wrap_in_presenter = self.class.serializer_class.custom_presenter?
|
|
216
|
+
end
|
|
217
|
+
|
|
125
218
|
private
|
|
126
219
|
|
|
127
220
|
#
|
|
128
221
|
# Wraps each serialized object in Presenter.new(object, ctx) before it is
|
|
129
222
|
# enqueued, so the whole level — value resolution and batch loaders alike —
|
|
130
|
-
# sees presenters.
|
|
223
|
+
# sees presenters. Objects are not wrapped when the Presenter class has
|
|
224
|
+
# no custom methods — such wrapping would only add overhead and break
|
|
225
|
+
# class checks (object.is_a?, Hash === object) without changing anything.
|
|
131
226
|
#
|
|
132
227
|
def enqueue(objects)
|
|
228
|
+
return super unless @wrap_in_presenter
|
|
229
|
+
|
|
133
230
|
presenter = self.class.serializer_class::Presenter
|
|
134
231
|
presenters = objects.map { |object| presenter.new(object, context) }
|
|
135
232
|
super(presenters)
|
|
@@ -244,7 +244,7 @@ class Serega
|
|
|
244
244
|
return opts[:root] if opts.key?(:root)
|
|
245
245
|
|
|
246
246
|
root = self.class.config.root
|
|
247
|
-
(opts.fetch(:many) {
|
|
247
|
+
(opts.fetch(:many) { SeregaUtils::CollectionDetector.call(object) }) ? root.many : root.one
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module SeregaUtils
|
|
5
|
+
#
|
|
6
|
+
# Utility to check if an object should be serialized as a collection.
|
|
7
|
+
#
|
|
8
|
+
# Hashes and Structs are Enumerable, but enumerate their own member
|
|
9
|
+
# values, so they are treated as single objects.
|
|
10
|
+
#
|
|
11
|
+
class CollectionDetector
|
|
12
|
+
class << self
|
|
13
|
+
#
|
|
14
|
+
# Checks if provided object is a collection of objects
|
|
15
|
+
#
|
|
16
|
+
# @param object [Object] Serialized object
|
|
17
|
+
#
|
|
18
|
+
# @return [Boolean] whether object should be serialized as a collection
|
|
19
|
+
#
|
|
20
|
+
def call(object)
|
|
21
|
+
object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -10,25 +10,30 @@ class Serega
|
|
|
10
10
|
# Attribute `block` parameter validator
|
|
11
11
|
#
|
|
12
12
|
class CheckBlock
|
|
13
|
+
# Explains the changed attribute block behavior. Shown when the block
|
|
14
|
+
# looks like an old-style value block — it accepts parameters or
|
|
15
|
+
# defines no attributes.
|
|
16
|
+
ERROR_MESSAGE =
|
|
17
|
+
"Attribute block now defines a nested serializer:" \
|
|
18
|
+
" it is executed in the context of a new serializer class and must define its attributes." \
|
|
19
|
+
" Defining the attribute value with a block is not supported anymore," \
|
|
20
|
+
" use the `value: <callable>` option instead."
|
|
21
|
+
|
|
13
22
|
class << self
|
|
14
23
|
#
|
|
15
24
|
# Checks block parameter provided with attribute.
|
|
16
|
-
# Must have up to two arguments - object and context. Context can be
|
|
17
|
-
# also provided as keyword argument :ctx.
|
|
18
|
-
#
|
|
19
|
-
# @example without arguments
|
|
20
|
-
# attribute(:email) { CONSTANT_EMAIL }
|
|
21
|
-
#
|
|
22
|
-
# @example with one argument
|
|
23
|
-
# attribute(:email) { |obj| obj.confirmed_email }
|
|
24
25
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
26
|
+
# The block defines attributes of a nested anonymous serializer, so
|
|
27
|
+
# it is executed in the context of that serializer and must accept
|
|
28
|
+
# no parameters.
|
|
27
29
|
#
|
|
28
|
-
# @example
|
|
29
|
-
# attribute
|
|
30
|
+
# @example
|
|
31
|
+
# attribute :statistics, method: :itself do
|
|
32
|
+
# attribute :likes_count
|
|
33
|
+
# attribute :comments_count
|
|
34
|
+
# end
|
|
30
35
|
#
|
|
31
|
-
# @param block [Proc] Block that
|
|
36
|
+
# @param block [Proc] Block that defines nested serializer attributes
|
|
32
37
|
#
|
|
33
38
|
# @raise [SeregaError] SeregaError that block has invalid arguments
|
|
34
39
|
#
|
|
@@ -37,48 +42,8 @@ class Serega
|
|
|
37
42
|
def call(block)
|
|
38
43
|
return unless block
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def check_block(block)
|
|
46
|
-
signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [:ctx, :batches])
|
|
47
|
-
|
|
48
|
-
raise SeregaError, signature_error unless valid_signature?(signature)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def valid_signature?(signature)
|
|
52
|
-
case signature
|
|
53
|
-
when "0" # no parameters
|
|
54
|
-
true
|
|
55
|
-
when "1" # call(object)
|
|
56
|
-
true
|
|
57
|
-
when "1_ctx" # call(object, ctx:)
|
|
58
|
-
true
|
|
59
|
-
when "1_batches" # call(object, batches:)
|
|
60
|
-
true
|
|
61
|
-
when "1_batches_ctx" # call(object, batches:, ctx:)
|
|
62
|
-
true
|
|
63
|
-
when "2" # call(object, context)
|
|
64
|
-
true
|
|
65
|
-
when "2_batches_ctx" # call(object, context, batches:, ctx:) (proc with no params)
|
|
66
|
-
true
|
|
67
|
-
else
|
|
68
|
-
false
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def signature_error
|
|
73
|
-
<<~ERROR.strip
|
|
74
|
-
Invalid attribute block parameters, valid parameters signatures:
|
|
75
|
-
- () # no parameters
|
|
76
|
-
- (object) # one positional parameter
|
|
77
|
-
- (object, ctx:) # one positional parameter and :ctx keyword
|
|
78
|
-
- (object, batches:) # one positional parameter and :batches keyword
|
|
79
|
-
- (object, ctx:, batches:) # one positional parameter, :ctx, and :batches keywords
|
|
80
|
-
- (object, context) # two positional parameters
|
|
81
|
-
ERROR
|
|
45
|
+
signature = SeregaUtils::MethodSignature.call(block, pos_limit: 0)
|
|
46
|
+
raise SeregaError, ERROR_MESSAGE unless signature == "0"
|
|
82
47
|
end
|
|
83
48
|
end
|
|
84
49
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module SeregaValidations
|
|
5
|
+
module Attribute
|
|
6
|
+
#
|
|
7
|
+
# Attribute `:base_serializer` option validator
|
|
8
|
+
#
|
|
9
|
+
class CheckOptBaseSerializer
|
|
10
|
+
class << self
|
|
11
|
+
#
|
|
12
|
+
# Checks attribute :base_serializer option. It specifies the parent
|
|
13
|
+
# class for the nested serializer defined with the attribute block,
|
|
14
|
+
# so it makes sense only when a block is provided.
|
|
15
|
+
#
|
|
16
|
+
# @param opts [Hash] Attribute options
|
|
17
|
+
# @param block [nil, Proc] Attribute block (defines a nested serializer)
|
|
18
|
+
#
|
|
19
|
+
# @raise [SeregaError] SeregaError that option has invalid value
|
|
20
|
+
#
|
|
21
|
+
# @return [void]
|
|
22
|
+
#
|
|
23
|
+
def call(opts, block = nil)
|
|
24
|
+
return unless opts.key?(:base_serializer)
|
|
25
|
+
|
|
26
|
+
raise SeregaError, "Option :base_serializer can be used only with a block" unless block
|
|
27
|
+
|
|
28
|
+
value = opts[:base_serializer]
|
|
29
|
+
return if value.is_a?(Class) && (value <= Serega)
|
|
30
|
+
|
|
31
|
+
raise SeregaError, "Invalid option :base_serializer => #{value.inspect}. Must be a Serega subclass"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|