serega 0.15.0 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|