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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +121 -22
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +5 -5
  5. data/lib/serega/attribute_normalizer.rb +29 -11
  6. data/lib/serega/config.rb +1 -1
  7. data/lib/serega/object_serializer.rb +1 -1
  8. data/lib/serega/plan.rb +11 -11
  9. data/lib/serega/plan_point.rb +5 -5
  10. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +1 -1
  11. data/lib/serega/plugins/batch/batch.rb +15 -15
  12. data/lib/serega/plugins/batch/lib/batch_config.rb +11 -8
  13. data/lib/serega/plugins/batch/lib/modules/attribute_normalizer.rb +26 -11
  14. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_key.rb +5 -35
  15. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_loader.rb +5 -35
  16. data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +2 -2
  17. data/lib/serega/plugins/camel_case/camel_case.rb +195 -0
  18. data/lib/serega/plugins/depth_limit/depth_limit.rb +185 -0
  19. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +1 -1
  20. data/lib/serega/plugins/formatters/formatters.rb +88 -14
  21. data/lib/serega/plugins/if/if.rb +47 -23
  22. data/lib/serega/plugins/if/validations/check_opt_if.rb +4 -36
  23. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -39
  24. data/lib/serega/plugins/if/validations/check_opt_unless.rb +4 -45
  25. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -39
  26. data/lib/serega/plugins/metadata/meta_attribute.rb +21 -5
  27. data/lib/serega/plugins/metadata/metadata.rb +22 -13
  28. data/lib/serega/plugins/metadata/validations/check_block.rb +10 -10
  29. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +38 -0
  30. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +61 -0
  31. data/lib/serega/plugins/metadata/validations/check_opts.rb +24 -10
  32. data/lib/serega/plugins/preloads/lib/modules/attribute_normalizer.rb +1 -1
  33. data/lib/serega/plugins/preloads/lib/preload_paths.rb +12 -5
  34. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +1 -1
  35. data/lib/serega/plugins/preloads/preloads.rb +12 -12
  36. data/lib/serega/plugins/root/root.rb +1 -1
  37. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +1 -1
  38. data/lib/serega/utils/params_count.rb +50 -0
  39. data/lib/serega/utils/to_hash.rb +1 -1
  40. data/lib/serega/validations/attribute/check_block.rb +4 -5
  41. data/lib/serega/validations/attribute/check_opt_const.rb +1 -1
  42. data/lib/serega/validations/attribute/check_opt_delegate.rb +9 -4
  43. data/lib/serega/validations/attribute/{check_opt_key.rb → check_opt_method.rb} +8 -8
  44. data/lib/serega/validations/attribute/check_opt_value.rb +17 -13
  45. data/lib/serega/validations/check_attribute_params.rb +3 -2
  46. data/lib/serega/validations/check_initiate_params.rb +1 -1
  47. data/lib/serega/validations/check_serialize_params.rb +1 -1
  48. data/lib/serega/validations/utils/check_allowed_keys.rb +4 -2
  49. data/lib/serega/validations/utils/check_extra_keyword_arg.rb +33 -0
  50. data/lib/serega.rb +26 -11
  51. 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
- if key.is_a?(Proc)
26
- check_block(key)
27
- else
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 check_block(block)
35
- return if valid_parameters?(block, accepted_count: 0..2)
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
- if loader.is_a?(Proc)
26
- check_block(loader)
27
- else
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 check_block(block)
35
- return if valid_parameters?(block, accepted_count: 0..3)
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 :key" if opts.key?(:key)
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 "./validations/check_opt_many"
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
- if init_opts.key?(:const)
147
- # Format const value in advance
148
- const_value = formatter.call(init_opts[:const])
149
- proc { const_value }
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
- self.class.serializer_class.config.formatters.opts.fetch(formatter)
165
- else
166
- formatter # already callable
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