serega 0.15.0 → 0.17.0

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