serega 0.15.0 → 0.17.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 +121 -22
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +5 -5
- data/lib/serega/attribute_normalizer.rb +29 -11
- data/lib/serega/config.rb +1 -1
- data/lib/serega/object_serializer.rb +1 -1
- data/lib/serega/plan.rb +11 -11
- data/lib/serega/plan_point.rb +5 -5
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +1 -1
- data/lib/serega/plugins/batch/batch.rb +15 -15
- data/lib/serega/plugins/batch/lib/batch_config.rb +11 -8
- data/lib/serega/plugins/batch/lib/modules/attribute_normalizer.rb +26 -11
- data/lib/serega/plugins/batch/lib/validations/check_batch_opt_key.rb +5 -35
- data/lib/serega/plugins/batch/lib/validations/check_batch_opt_loader.rb +5 -35
- data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +2 -2
- data/lib/serega/plugins/camel_case/camel_case.rb +195 -0
- data/lib/serega/plugins/depth_limit/depth_limit.rb +185 -0
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +1 -1
- data/lib/serega/plugins/formatters/formatters.rb +88 -14
- data/lib/serega/plugins/if/if.rb +47 -23
- data/lib/serega/plugins/if/validations/check_opt_if.rb +4 -36
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -39
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +4 -45
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -39
- data/lib/serega/plugins/metadata/meta_attribute.rb +21 -5
- data/lib/serega/plugins/metadata/metadata.rb +22 -13
- data/lib/serega/plugins/metadata/validations/check_block.rb +10 -10
- data/lib/serega/plugins/metadata/validations/check_opt_const.rb +38 -0
- data/lib/serega/plugins/metadata/validations/check_opt_value.rb +61 -0
- data/lib/serega/plugins/metadata/validations/check_opts.rb +24 -10
- data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +1 -1
- data/lib/serega/plugins/preloads/lib/preload_paths.rb +12 -5
- data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +1 -1
- data/lib/serega/plugins/preloads/preloads.rb +12 -12
- data/lib/serega/plugins/root/root.rb +1 -1
- data/lib/serega/plugins/string_modifiers/string_modifiers.rb +1 -1
- data/lib/serega/utils/params_count.rb +50 -0
- data/lib/serega/utils/to_hash.rb +1 -1
- data/lib/serega/validations/attribute/check_block.rb +4 -5
- data/lib/serega/validations/attribute/check_opt_const.rb +1 -1
- data/lib/serega/validations/attribute/check_opt_delegate.rb +9 -4
- data/lib/serega/validations/attribute/{check_opt_key.rb → check_opt_method.rb} +8 -8
- data/lib/serega/validations/attribute/check_opt_value.rb +17 -13
- data/lib/serega/validations/check_attribute_params.rb +3 -2
- data/lib/serega/validations/check_initiate_params.rb +1 -1
- data/lib/serega/validations/check_serialize_params.rb +1 -1
- data/lib/serega/validations/utils/check_allowed_keys.rb +4 -2
- data/lib/serega/validations/utils/check_extra_keyword_arg.rb +33 -0
- data/lib/serega.rb +26 -11
- metadata +10 -4
@@ -22,45 +22,15 @@ class Serega
|
|
22
22
|
|
23
23
|
raise SeregaError, must_be_callable unless key.respond_to?(:call)
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
check_callable(key)
|
29
|
-
end
|
25
|
+
SeregaValidations::Utils::CheckExtraKeywordArg.call(key, "batch option :key")
|
26
|
+
params_count = SeregaUtils::ParamsCount.call(key, max_count: 2)
|
27
|
+
raise SeregaError, params_count_error if params_count > 2
|
30
28
|
end
|
31
29
|
|
32
30
|
private
|
33
31
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
raise SeregaError, block_parameters_error
|
38
|
-
end
|
39
|
-
|
40
|
-
def check_callable(callable)
|
41
|
-
return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
|
42
|
-
|
43
|
-
raise SeregaError, callable_parameters_error
|
44
|
-
end
|
45
|
-
|
46
|
-
def valid_parameters?(data, accepted_count:)
|
47
|
-
params = data.parameters
|
48
|
-
accepted_count.include?(params.count) && valid_parameters_types?(params)
|
49
|
-
end
|
50
|
-
|
51
|
-
def valid_parameters_types?(params)
|
52
|
-
params.all? do |param|
|
53
|
-
type = param[0]
|
54
|
-
(type == :req) || (type == :opt)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def block_parameters_error
|
59
|
-
"Invalid :batch option :key. When it is a Proc it can have maximum two regular parameters (object, context)"
|
60
|
-
end
|
61
|
-
|
62
|
-
def callable_parameters_error
|
63
|
-
"Invalid :batch option :key. When it is a callable object it must have two regular parameters (object, context)"
|
32
|
+
def params_count_error
|
33
|
+
"Invalid :batch option :key. It can accept maximum 2 parameters (object, context)"
|
64
34
|
end
|
65
35
|
|
66
36
|
def must_be_callable
|
@@ -22,45 +22,15 @@ class Serega
|
|
22
22
|
|
23
23
|
raise SeregaError, must_be_callable unless loader.respond_to?(:call)
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
check_callable(loader)
|
29
|
-
end
|
25
|
+
SeregaValidations::Utils::CheckExtraKeywordArg.call(loader, ":batch option :loader")
|
26
|
+
params_count = SeregaUtils::ParamsCount.call(loader, max_count: 3)
|
27
|
+
raise SeregaError, params_count_error if params_count > 3
|
30
28
|
end
|
31
29
|
|
32
30
|
private
|
33
31
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
raise SeregaError, block_parameters_error
|
38
|
-
end
|
39
|
-
|
40
|
-
def check_callable(callable)
|
41
|
-
return if valid_parameters?(callable.method(:call), accepted_count: 3..3)
|
42
|
-
|
43
|
-
raise SeregaError, callable_parameters_error
|
44
|
-
end
|
45
|
-
|
46
|
-
def valid_parameters?(data, accepted_count:)
|
47
|
-
params = data.parameters
|
48
|
-
accepted_count.include?(params.count) && valid_parameters_types?(params)
|
49
|
-
end
|
50
|
-
|
51
|
-
def valid_parameters_types?(params)
|
52
|
-
params.all? do |param|
|
53
|
-
type = param[0]
|
54
|
-
(type == :req) || (type == :opt)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def block_parameters_error
|
59
|
-
"Invalid :batch option :loader. When it is a Proc it can have maximum three regular parameters (keys, context, point)"
|
60
|
-
end
|
61
|
-
|
62
|
-
def callable_parameters_error
|
63
|
-
"Invalid :batch option :loader. When it is a callable object it must have three regular parameters (keys, context, point)"
|
32
|
+
def params_count_error
|
33
|
+
"Invalid :batch option :loader. It can accept maximum 3 parameters (keys, context, plan)"
|
64
34
|
end
|
65
35
|
|
66
36
|
def must_be_callable
|
@@ -23,7 +23,7 @@ class Serega
|
|
23
23
|
SeregaValidations::Utils::CheckOptIsHash.call(opts, :batch)
|
24
24
|
|
25
25
|
batch = opts[:batch]
|
26
|
-
SeregaValidations::Utils::CheckAllowedKeys.call(batch, %i[key loader default])
|
26
|
+
SeregaValidations::Utils::CheckAllowedKeys.call(batch, %i[key loader default], :batch)
|
27
27
|
|
28
28
|
check_batch_opt_key(batch, serializer_class)
|
29
29
|
check_batch_opt_loader(batch)
|
@@ -50,7 +50,7 @@ class Serega
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def check_usage_with_other_params(opts, block)
|
53
|
-
raise SeregaError, "Option :batch can not be used together with option :
|
53
|
+
raise SeregaError, "Option :batch can not be used together with option :method" if opts.key?(:method)
|
54
54
|
raise SeregaError, "Option :batch can not be used together with option :value" if opts.key?(:value)
|
55
55
|
raise SeregaError, "Option :batch can not be used together with option :const" if opts.key?(:const)
|
56
56
|
raise SeregaError, "Option :batch can not be used together with option :delegate" if opts.key?(:delegate)
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module SeregaPlugins
|
5
|
+
#
|
6
|
+
# Plugin :camel_case
|
7
|
+
#
|
8
|
+
# By default when we add attribute like `attribute :first_name` this means:
|
9
|
+
# - adding a `:first_name` key to resulted hash
|
10
|
+
# - adding a `#first_name` method call result as value
|
11
|
+
#
|
12
|
+
# But its often desired to response with *camelCased* keys.
|
13
|
+
# Earlier this can be achieved by specifying attribute name and method directly
|
14
|
+
# for each attribute: `attribute :firstName, method: first_name`
|
15
|
+
#
|
16
|
+
# Now this plugin transforms all attribute names automatically.
|
17
|
+
# We use simple regular expression to replace `_x` to `X` for the whole string.
|
18
|
+
# You can provide your own callable transformation when defining plugin,
|
19
|
+
# for example `plugin :camel_case, transform: ->(name) { name.camelize }`
|
20
|
+
#
|
21
|
+
# For any attribute camelCase-behavior can be skipped when
|
22
|
+
# `camel_case: false` attribute option provided.
|
23
|
+
#
|
24
|
+
# @example Define plugin
|
25
|
+
# class AppSerializer < Serega
|
26
|
+
# plugin :camel_case
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class UserSerializer < AppSerializer
|
30
|
+
# attribute :first_name
|
31
|
+
# attribute :last_name
|
32
|
+
# attribute :full_name, camel_case: false, value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# require "ostruct"
|
36
|
+
# user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
|
37
|
+
# UserSerializer.to_h(user) # {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
|
38
|
+
#
|
39
|
+
module CamelCase
|
40
|
+
# Default camel-case transformation
|
41
|
+
TRANSFORM_DEFAULT = proc { |attribute_name|
|
42
|
+
attribute_name.gsub!(/_[a-z]/) { |m| m[-1].upcase! }
|
43
|
+
attribute_name
|
44
|
+
}
|
45
|
+
|
46
|
+
# @return [Symbol] Plugin name
|
47
|
+
def self.plugin_name
|
48
|
+
:camel_case
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Applies plugin code to specific serializer
|
53
|
+
#
|
54
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
55
|
+
# @param _opts [Hash] Loaded plugins options
|
56
|
+
#
|
57
|
+
# @return [void]
|
58
|
+
#
|
59
|
+
def self.load_plugin(serializer_class, **_opts)
|
60
|
+
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
61
|
+
serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
|
62
|
+
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Adds config options and runs other callbacks after plugin was loaded
|
67
|
+
#
|
68
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
69
|
+
# @param opts [Hash] loaded plugins opts
|
70
|
+
#
|
71
|
+
# @return [void]
|
72
|
+
#
|
73
|
+
def self.after_load_plugin(serializer_class, **opts)
|
74
|
+
config = serializer_class.config
|
75
|
+
config.opts[:camel_case] = {}
|
76
|
+
config.camel_case.transform = opts[:transform] || TRANSFORM_DEFAULT
|
77
|
+
|
78
|
+
config.attribute_keys << :camel_case
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Config class additional/patched instance methods
|
83
|
+
#
|
84
|
+
# @see Serega::SeregaConfig
|
85
|
+
#
|
86
|
+
module ConfigInstanceMethods
|
87
|
+
# @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] `camel_case` plugin config
|
88
|
+
def camel_case
|
89
|
+
@camel_case ||= CamelCaseConfig.new(opts.fetch(:camel_case))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
|
95
|
+
#
|
96
|
+
# @see Serega::SeregaValidations::CheckAttributeParams
|
97
|
+
#
|
98
|
+
module CheckAttributeParamsInstanceMethods
|
99
|
+
private
|
100
|
+
|
101
|
+
def check_opts
|
102
|
+
super
|
103
|
+
CheckOptCamelCase.call(opts)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Validator for attribute :camel_case option
|
109
|
+
#
|
110
|
+
class CheckOptCamelCase
|
111
|
+
class << self
|
112
|
+
#
|
113
|
+
# Checks attribute :camel_case option must be boolean
|
114
|
+
#
|
115
|
+
# @param opts [Hash] Attribute options
|
116
|
+
#
|
117
|
+
# @raise [SeregaError] Attribute validation error
|
118
|
+
#
|
119
|
+
# @return [void]
|
120
|
+
#
|
121
|
+
def call(opts)
|
122
|
+
camel_case_option_exists = opts.key?(:camel_case)
|
123
|
+
return unless camel_case_option_exists
|
124
|
+
|
125
|
+
value = opts[:camel_case]
|
126
|
+
return if value.equal?(true) || value.equal?(false)
|
127
|
+
|
128
|
+
raise SeregaError, "Attribute option :camel_case must have a boolean value, but #{value.class} was provided"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# CamelCase config object
|
134
|
+
class CamelCaseConfig
|
135
|
+
attr_reader :opts
|
136
|
+
|
137
|
+
#
|
138
|
+
# Initializes CamelCaseConfig object
|
139
|
+
#
|
140
|
+
# @param opts [Hash] camel_case plugin options
|
141
|
+
# @option opts [#call] :transform Callable object that transforms original attribute name
|
142
|
+
#
|
143
|
+
# @return [Serega::SeregaPlugins::CamelCase::CamelCaseConfig] CamelCaseConfig object
|
144
|
+
#
|
145
|
+
def initialize(opts)
|
146
|
+
@opts = opts
|
147
|
+
end
|
148
|
+
|
149
|
+
# @return [#call] defined object that transforms name
|
150
|
+
def transform
|
151
|
+
opts.fetch(:transform)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Sets transformation callable object
|
155
|
+
#
|
156
|
+
# @param value [#call] transformation
|
157
|
+
#
|
158
|
+
# @return [#call] camel_case plugin transformation callable object
|
159
|
+
def transform=(value)
|
160
|
+
raise SeregaError, "Transform value must respond to #call" unless value.respond_to?(:call)
|
161
|
+
|
162
|
+
params = value.is_a?(Proc) ? value.parameters : value.method(:call).parameters
|
163
|
+
if params.count != 1 || !params.all? { |param| (param[0] == :req) || (param[0] == :opt) }
|
164
|
+
raise SeregaError, "Transform value must respond to #call and accept 1 regular parameter"
|
165
|
+
end
|
166
|
+
|
167
|
+
opts[:transform] = value
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
#
|
172
|
+
# SeregaAttributeNormalizer additional/patched instance methods
|
173
|
+
#
|
174
|
+
# @see SeregaAttributeNormalizer::AttributeInstanceMethods
|
175
|
+
#
|
176
|
+
module AttributeNormalizerInstanceMethods
|
177
|
+
private
|
178
|
+
|
179
|
+
#
|
180
|
+
# Patch for original `prepare_name` method
|
181
|
+
#
|
182
|
+
# Makes camelCased name
|
183
|
+
#
|
184
|
+
def prepare_name
|
185
|
+
res = super
|
186
|
+
return res if init_opts[:camel_case] == false
|
187
|
+
|
188
|
+
self.class.serializer_class.config.camel_case.transform.call(res.to_s).to_sym
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
register_plugin(CamelCase.plugin_name, CamelCase)
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Serega
|
4
|
+
module SeregaPlugins
|
5
|
+
#
|
6
|
+
# Plugin :depth_limit
|
7
|
+
#
|
8
|
+
# Helps to secure from malicious queries that require to serialize too much
|
9
|
+
# or from accidental serializing of objects with cyclic relations.
|
10
|
+
#
|
11
|
+
# Depth limit is checked when constructing a serialization plan, that is when
|
12
|
+
# `#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
|
13
|
+
# It can be useful to instantiate serializer before any other business logic
|
14
|
+
# to get possible errors earlier.
|
15
|
+
#
|
16
|
+
# Any class-level serialization methods also check depth limit as they also instantiate serializer.
|
17
|
+
#
|
18
|
+
# When depth limit is exceeded `Serega::DepthLimitError` is raised.
|
19
|
+
# Depth limit error details can be found in additional `Serega::DepthLimitError#details` method
|
20
|
+
#
|
21
|
+
# Limit can be checked or changed with next config options:
|
22
|
+
#
|
23
|
+
# - config.depth_limit.limit
|
24
|
+
# - config.depth_limit.limit=
|
25
|
+
#
|
26
|
+
# There are no default limit, but it should be set when enabling plugin.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
#
|
30
|
+
# class AppSerializer < Serega
|
31
|
+
# plugin :depth_limit, limit: 10 # set limit for all child classes
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# class UserSerializer < AppSerializer
|
35
|
+
# config.depth_limit.limit = 5 # overrides limit for UserSerializer
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
module DepthLimit
|
39
|
+
# @return [Symbol] Plugin name
|
40
|
+
def self.plugin_name
|
41
|
+
:depth_limit
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Applies plugin code to specific serializer
|
46
|
+
#
|
47
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
48
|
+
# @param _opts [Hash] Loaded plugins options
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
#
|
52
|
+
def self.load_plugin(serializer_class, **_opts)
|
53
|
+
serializer_class::SeregaPlan.include(PlanInstanceMethods)
|
54
|
+
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Adds config options and runs other callbacks after plugin was loaded
|
59
|
+
#
|
60
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
61
|
+
# @param opts [Hash] loaded plugins opts
|
62
|
+
#
|
63
|
+
# @return [void]
|
64
|
+
#
|
65
|
+
def self.after_load_plugin(serializer_class, **opts)
|
66
|
+
config = serializer_class.config
|
67
|
+
limit = opts.fetch(:limit) { raise SeregaError, "Please provide :limit option. Example: `plugin :depth_limit, limit: 10`" }
|
68
|
+
config.opts[:depth_limit] = {}
|
69
|
+
config.depth_limit.limit = limit
|
70
|
+
end
|
71
|
+
|
72
|
+
# DepthLimit config object
|
73
|
+
class DepthLimitConfig
|
74
|
+
attr_reader :opts
|
75
|
+
|
76
|
+
#
|
77
|
+
# Initializes DepthLimitConfig object
|
78
|
+
#
|
79
|
+
# @param opts [Hash] depth_limit plugin options
|
80
|
+
#
|
81
|
+
# @return [SeregaPlugins::DepthLimit::DepthLimitConfig] DepthLimitConfig object
|
82
|
+
#
|
83
|
+
def initialize(opts)
|
84
|
+
@opts = opts
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Integer] defined depth limit
|
88
|
+
def limit
|
89
|
+
opts.fetch(:limit)
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Set depth limit
|
94
|
+
#
|
95
|
+
# @param value [Integer] depth limit
|
96
|
+
#
|
97
|
+
# @return [Integer] depth limit
|
98
|
+
def limit=(value)
|
99
|
+
raise SeregaError, "Depth limit must be an Integer" unless value.is_a?(Integer)
|
100
|
+
|
101
|
+
opts[:limit] = value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Serega::SeregaConfig additional/patched class methods
|
107
|
+
#
|
108
|
+
# @see Serega::SeregaConfig
|
109
|
+
#
|
110
|
+
module ConfigInstanceMethods
|
111
|
+
# @return [Serega::SeregaPlugins::DepthLimit::DepthLimitConfig] current depth_limit config
|
112
|
+
def depth_limit
|
113
|
+
@depth_limit ||= DepthLimitConfig.new(opts.fetch(:depth_limit))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# SeregaPlan additional/patched instance methods
|
119
|
+
#
|
120
|
+
# @see SeregaPlan
|
121
|
+
#
|
122
|
+
module PlanInstanceMethods
|
123
|
+
#
|
124
|
+
# Initializes serialization plan
|
125
|
+
# Overrides original method (adds depth_limit validation)
|
126
|
+
#
|
127
|
+
def initialize(parent_plan_point, *)
|
128
|
+
check_depth_limit_exceeded(parent_plan_point)
|
129
|
+
super
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def check_depth_limit_exceeded(current_point)
|
135
|
+
plan = self
|
136
|
+
depth_level = 1
|
137
|
+
point = current_point
|
138
|
+
|
139
|
+
while point
|
140
|
+
depth_level += 1
|
141
|
+
plan = point.plan
|
142
|
+
point = plan.parent_plan_point
|
143
|
+
end
|
144
|
+
|
145
|
+
root_serializer = plan.class.serializer_class
|
146
|
+
root_depth_limit = root_serializer.config.depth_limit.limit
|
147
|
+
|
148
|
+
if depth_level > root_depth_limit
|
149
|
+
fields_chain = [current_point.name]
|
150
|
+
fields_chain << current_point.name while (current_point = current_point.plan.parent_plan_point)
|
151
|
+
details = "#{root_serializer} (depth limit: #{root_depth_limit}) -> #{fields_chain.reverse!.join(" -> ")}"
|
152
|
+
raise DepthLimitError.new("Depth limit was exceeded", details)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
register_plugin(DepthLimit.plugin_name, DepthLimit)
|
159
|
+
end
|
160
|
+
|
161
|
+
#
|
162
|
+
# Special error for depth_limit plugin
|
163
|
+
#
|
164
|
+
class DepthLimitError < SeregaError
|
165
|
+
#
|
166
|
+
# Details of why depth limit error happens.
|
167
|
+
#
|
168
|
+
# @return [String] error details
|
169
|
+
#
|
170
|
+
attr_reader :details
|
171
|
+
|
172
|
+
#
|
173
|
+
# Initializes new error
|
174
|
+
#
|
175
|
+
# @param message [String] Error message
|
176
|
+
# @param details [String] Error additional details
|
177
|
+
#
|
178
|
+
# @return [DepthLimitError] error instance
|
179
|
+
#
|
180
|
+
def initialize(message, details = nil)
|
181
|
+
super(message)
|
182
|
+
@details = details
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -40,7 +40,7 @@ class Serega
|
|
40
40
|
# @return [void]
|
41
41
|
#
|
42
42
|
def self.load_plugin(serializer_class, **_opts)
|
43
|
-
require_relative "
|
43
|
+
require_relative "validations/check_opt_many"
|
44
44
|
|
45
45
|
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
46
46
|
end
|
@@ -11,12 +11,15 @@ class Serega
|
|
11
11
|
#
|
12
12
|
# Attribute option `:format` now can be used with name of formatter or with callable instance.
|
13
13
|
#
|
14
|
+
# Formatters can accept up to 2 parameters (formatted object, context)
|
15
|
+
#
|
14
16
|
# @example
|
15
17
|
# class AppSerializer < Serega
|
16
18
|
# plugin :formatters, formatters: {
|
17
19
|
# iso8601: ->(value) { time.iso8601.round(6) },
|
18
20
|
# on_off: ->(value) { value ? 'ON' : 'OFF' },
|
19
21
|
# money: ->(value) { value.round(2) }
|
22
|
+
# date: DateTypeFormatter # callable
|
20
23
|
# }
|
21
24
|
# end
|
22
25
|
#
|
@@ -35,6 +38,7 @@ class Serega
|
|
35
38
|
# attribute :updated_at, format: :iso8601
|
36
39
|
#
|
37
40
|
# # Using `callable` formatter
|
41
|
+
# attribute :score_percent, format: PercentFormmatter # callable class
|
38
42
|
# attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
|
39
43
|
# end
|
40
44
|
#
|
@@ -68,6 +72,7 @@ class Serega
|
|
68
72
|
def self.load_plugin(serializer_class, **_opts)
|
69
73
|
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
70
74
|
serializer_class::SeregaAttributeNormalizer.include(AttributeNormalizerInstanceMethods)
|
75
|
+
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
71
76
|
end
|
72
77
|
|
73
78
|
#
|
@@ -107,6 +112,7 @@ class Serega
|
|
107
112
|
# @return [void]
|
108
113
|
def add(formatters)
|
109
114
|
formatters.each_pair do |key, value|
|
115
|
+
CheckFormatter.call(key, value)
|
110
116
|
opts[key] = value
|
111
117
|
end
|
112
118
|
end
|
@@ -124,6 +130,21 @@ class Serega
|
|
124
130
|
end
|
125
131
|
end
|
126
132
|
|
133
|
+
#
|
134
|
+
# Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
|
135
|
+
#
|
136
|
+
# @see Serega::SeregaValidations::CheckAttributeParams
|
137
|
+
#
|
138
|
+
module CheckAttributeParamsInstanceMethods
|
139
|
+
private
|
140
|
+
|
141
|
+
def check_opts
|
142
|
+
super
|
143
|
+
|
144
|
+
CheckOptFormat.call(opts, self.class.serializer_class)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
127
148
|
#
|
128
149
|
# Attribute class additional/patched instance methods
|
129
150
|
#
|
@@ -143,16 +164,10 @@ class Serega
|
|
143
164
|
def prepare_value_block
|
144
165
|
return super unless formatter
|
145
166
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
else
|
151
|
-
# Wrap original block into formatter block
|
152
|
-
proc do |object, context|
|
153
|
-
value = super.call(object, context)
|
154
|
-
formatter.call(value)
|
155
|
-
end
|
167
|
+
# Wrap original block into formatter block
|
168
|
+
proc do |object, context|
|
169
|
+
value = super.call(object, context)
|
170
|
+
formatter.call(value, context)
|
156
171
|
end
|
157
172
|
end
|
158
173
|
|
@@ -160,10 +175,69 @@ class Serega
|
|
160
175
|
formatter = init_opts[:format]
|
161
176
|
return unless formatter
|
162
177
|
|
163
|
-
if formatter.is_a?(Symbol)
|
164
|
-
|
165
|
-
|
166
|
-
|
178
|
+
formatter = self.class.serializer_class.config.formatters.opts.fetch(formatter) if formatter.is_a?(Symbol)
|
179
|
+
prepare_callable_proc(formatter)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
#
|
184
|
+
# Validator for attribute :format option
|
185
|
+
#
|
186
|
+
class CheckOptFormat
|
187
|
+
class << self
|
188
|
+
#
|
189
|
+
# Checks attribute :format option must be registered or valid callable with maximum 2 args
|
190
|
+
#
|
191
|
+
# @param opts [value] Attribute options
|
192
|
+
#
|
193
|
+
# @raise [SeregaError] Attribute validation error
|
194
|
+
#
|
195
|
+
# @return [void]
|
196
|
+
#
|
197
|
+
def call(opts, serializer_class)
|
198
|
+
return unless opts.key?(:format)
|
199
|
+
|
200
|
+
formatter = opts[:format]
|
201
|
+
|
202
|
+
if formatter.is_a?(Symbol)
|
203
|
+
check_formatter_defined(serializer_class, formatter)
|
204
|
+
else
|
205
|
+
CheckFormatter.call(:format, formatter)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def check_formatter_defined(serializer_class, formatter)
|
212
|
+
return if serializer_class.config.formatters.opts.key?(formatter)
|
213
|
+
|
214
|
+
raise Serega::SeregaError, "Formatter `#{formatter.inspect}` was not defined"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
#
|
220
|
+
# Validator for formatters defined as config options or directly as attribute :format option
|
221
|
+
#
|
222
|
+
class CheckFormatter
|
223
|
+
class << self
|
224
|
+
#
|
225
|
+
# Check formatter type and parameters
|
226
|
+
#
|
227
|
+
# @param formatter_name [Symbol] Name of formatter
|
228
|
+
# @param formatter [#call] Formatter callable object
|
229
|
+
#
|
230
|
+
# @return [void]
|
231
|
+
#
|
232
|
+
def call(formatter_name, formatter)
|
233
|
+
raise Serega::SeregaError, "Option #{formatter_name.inspect} must have callable value" unless formatter.respond_to?(:call)
|
234
|
+
|
235
|
+
SeregaValidations::Utils::CheckExtraKeywordArg.call(formatter, "#{formatter_name.inspect} value")
|
236
|
+
params_count = SeregaUtils::ParamsCount.call(formatter, max_count: 2)
|
237
|
+
|
238
|
+
if params_count > 2
|
239
|
+
raise SeregaError, "Formatter can have maximum 2 parameters (value to format, context)"
|
240
|
+
end
|
167
241
|
end
|
168
242
|
end
|
169
243
|
end
|