serega 0.8.3 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 116b00d5c709f65a90899a9eda49f6e2c95cc9a47781a1cddf2499057553f427
4
- data.tar.gz: 5f8fe19470218be68c09704964228be0381e677f5bd3cbcf9688f92974e9392d
3
+ metadata.gz: 6a94d74dd45c74e499cd1a3abec082d8776019538b67bf24fc840512271adb6b
4
+ data.tar.gz: a22bc547e6ac94b0988e2cfef2af31ad659beb53058a90159e7f46f6470c3390
5
5
  SHA512:
6
- metadata.gz: 047de5335017cf65ee0f887124f81a755b94e19eea0e53a56ae24cb81698e7bdcc7e8eb6eba9256b5160a24bb011a5c0a1c1628cca77c7e7951b8883629ad96f
7
- data.tar.gz: 5836df28c6e1cf2238d8c8471280dc806dd41a0cbbe474a101118d801e460830cda97dcb419e0885f9106d1e4e5ae795776ff5cc475555bdab6cb4c5408c1c94
6
+ metadata.gz: d89c6180d4af0273eee6dd6179a0ac6f0157409f07ae6c18985941ecd8971bd17eb8b547a764d97afd12cdc08e565cb8749e9d9e0a70d8ab8add5dbad3e349db
7
+ data.tar.gz: 4cd808491f38a469ac705f6884007a3d71cead7f12d8e9730ec8c15ee1da97cb2d0d7503aafc0925a5bf41c7b660f7aa71a62da6fda295e713b62114ee83842a
data/README.md CHANGED
@@ -20,6 +20,7 @@ It has some great features:
20
20
  - Built-in object presenter ([presenter][presenter] plugin)
21
21
  - Adding custom metadata (via [metadata][metadata] or [context_metadata][context_metadata] plugins)
22
22
  - Attributes formatters ([formatters][formatters] plugin)
23
+ - Conditional attributes ([if][if] plugin)
23
24
 
24
25
  ## Installation
25
26
 
@@ -94,9 +95,11 @@ class UserSerializer < Serega
94
95
  # It allows to specify associations to preload to attribute value
95
96
  attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
96
97
 
97
- # Option `:hide_nil` can be specified when enabled `:hide_nil` plugin
98
- # It is literally hides attribute if its value is nil
99
- attribute :email, hide_nil: true
98
+ # Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified when enabled `:if` plugin
99
+ # They hide attribute key and value from response when conditions satisfied
100
+ # See more usage examples in :if plugin section.
101
+ attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
102
+ attribute :email, if_value: :present?
100
103
 
101
104
  # Option `:format` can be specified when enabled `:formatters` plugin
102
105
  # It changes attribute value
@@ -108,6 +111,23 @@ class UserSerializer < Serega
108
111
  end
109
112
  ```
110
113
 
114
+ ---
115
+
116
+ ⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "_", "-", "~" characters.
117
+ We have this check as:
118
+ - Attributes names can be used in URL without encoding.
119
+ - Plugin [string_modifiers][string_modifiers] already uses "," and "()" as attribute names delimeters.
120
+ - We are protected from errors when added some non-english character looking as english.
121
+
122
+ This names check can be disabled globally or per-serializer via:
123
+ ```ruby
124
+ Serega.config.check_attribute_name = false
125
+
126
+ class SomeSerializer < Serega
127
+ config.check_attribute_name = false
128
+ end
129
+ ```
130
+
111
131
  ### Serializing
112
132
 
113
133
  We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and same instance methods `#to_h`, `#to_json`, `#as_json`.
@@ -600,16 +620,45 @@ PostSerializer.new(with: "user(email)").to_h(post)
600
620
  PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
601
621
  ```
602
622
 
603
- ### Plugin :hide_nil
623
+ ### Plugin :if
604
624
 
605
- Allows to hide attributes with `nil` values
625
+ Plugin adds `:if`, `:unless`, `:if_value`, `:unless_value` options to
626
+ attributes so we can remove attributes from response in various ways.
606
627
 
607
- ```ruby
608
- class UserSerializer < Serega
609
- plugin :hide_nil
628
+ Use `:if` and `:unless` when you want to hide attributes before finding attribute value,
629
+ and use `:if_value` and `:unless_value` to hide attributes after we find final value.
610
630
 
611
- attribute :email, hide_nil: true
612
- end
631
+ Options `:if` and `:unless` accept currently serialized object and context as parameters.
632
+ Options `:if_value` and `:unless_value` accept already found serialized value and context as parameters.
633
+
634
+ Options `:if_value` and `:unless_value` cannot be used with :serializer option, as
635
+ serialized objects have no "serialized value". Use `:if` and `:unless` in this case.
636
+
637
+ See also a `:hide` option that is available without any plugins to hide
638
+ attribute without conditions. Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
639
+
640
+ ```ruby
641
+ class UserSerializer < Serega
642
+ attribute :email, if: :active? # if user.active?
643
+ attribute :email, if: proc {|user| user.active?} # same
644
+ attribute :email, if: proc {|user, ctx| user == ctx[:current_user]} # using context
645
+ attribute :email, if: CustomPolicy.method(:view_email?) # You can provide own callable object
646
+
647
+ attribute :email, unless: :hidden? # unless user.hidden?
648
+ attribute :email, unless: proc {|user| user.hidden?} # same
649
+ attribute :email, unless: proc {|user, context| context[:show_emails]} # using context
650
+ attribute :email, unless: CustomPolicy.method(:hide_email?) # You can provide own callable object
651
+
652
+ attribute :email, if_value: :present? # if email.present?
653
+ attribute :email, if_value: proc {|email| email.present?} # same
654
+ attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]} # using context
655
+ attribute :email, if_value: CustomPolicy.method(:view_email?) # You can provide own callable object
656
+
657
+ attribute :email, unless_value: :blank? # unless email.blank?
658
+ attribute :email, unless_value: proc {|email| email.blank?} # same
659
+ attribute :email, unless_value: proc {|email, context| context[:show_emails]} # using context
660
+ attribute :email, unless_value: CustomPolicy.method(:hide_email?) # You can provide own callable object
661
+ end
613
662
  ```
614
663
 
615
664
  ## Errors
@@ -648,3 +697,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
648
697
  [presenter]: #plugin-presenter
649
698
  [root]: #plugin-root
650
699
  [string_modifiers]: #plugin-string_modifiers
700
+ [if]: #plugin-if
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.8.3
1
+ 0.10.0
@@ -51,6 +51,10 @@ class Serega
51
51
  end
52
52
 
53
53
  # Shows current opts[:hide] option
54
+ #
55
+ # Patched in:
56
+ # - plugin :preloads (returns true by default if config option auto_hide_attribute_with_preloads is enabled)
57
+ #
54
58
  # @return [Boolean, nil] Attribute :hide option value
55
59
  def hide
56
60
  opts[:hide]
@@ -82,7 +86,11 @@ class Serega
82
86
  end
83
87
  end
84
88
 
85
- # Returns final block that will be used to find attribute value
89
+ # Returns final block used to find attribute value
90
+ #
91
+ # Patched in:
92
+ # - plugin :formatters (wraps resulted block in formatter block and formats :const values)
93
+ #
86
94
  # @return [Proc] Proc to find attribute value
87
95
  def value_block
88
96
  return @value_block if instance_variable_defined?(:@value_block)
data/lib/serega/config.rb CHANGED
@@ -17,6 +17,7 @@ class Serega
17
17
  initiate_keys: %i[only with except check_initiate_params].freeze,
18
18
  attribute_keys: %i[key value serializer many hide const delegate].freeze,
19
19
  serialize_keys: %i[context many].freeze,
20
+ check_attribute_name: true,
20
21
  check_initiate_params: true,
21
22
  max_cached_map_per_serializer_count: 0,
22
23
  to_json: (SeregaJSON.adapter == :oj) ? SeregaJSON::OjDump : SeregaJSON::JSONDump,
@@ -102,6 +103,21 @@ class Serega
102
103
  opts[:max_cached_map_per_serializer_count] = value
103
104
  end
104
105
 
106
+ # Returns whether attributes names check is disabled
107
+ def check_attribute_name
108
+ opts.fetch(:check_attribute_name)
109
+ end
110
+
111
+ # Sets :check_attribute_name config option
112
+ #
113
+ # @param value [Boolean] Set :check_attribute_name config option
114
+ #
115
+ # @return [Boolean] New :check_attribute_name config option
116
+ def check_attribute_name=(value)
117
+ raise SeregaError, "Must have boolean value, #{value.inspect} provided" if (value != true) && (value != false)
118
+ opts[:check_attribute_name] = value
119
+ end
120
+
105
121
  # Returns current `to_json` adapter
106
122
  # @return [#call] Callable that used to construct JSON
107
123
  def to_json
@@ -41,18 +41,30 @@ class Serega
41
41
  object.map { |obj| serialize_object(obj) }
42
42
  end
43
43
 
44
+ # Patched in:
45
+ # - plugin :presenter (makes presenter_object and serializes it)
44
46
  def serialize_object(object)
45
47
  points.each_with_object({}) do |point, container|
46
- attach_value(object, point, container)
48
+ serialize_point(object, point, container)
47
49
  end
48
50
  end
49
51
 
52
+ # Patched in:
53
+ # - plugin :if (conditionally skips serializing this point)
54
+ def serialize_point(object, point, container)
55
+ attach_value(object, point, container)
56
+ end
57
+
58
+ # Patched in:
59
+ # - plugin :batch (remembers key for batch loading values instead of attaching)
50
60
  def attach_value(object, point, container)
51
61
  value = point.value(object, context)
52
62
  final_value = final_value(value, point)
53
63
  attach_final_value(final_value, point, container)
54
64
  end
55
65
 
66
+ # Patched in:
67
+ # - plugin :if (conditionally skips attaching)
56
68
  def attach_final_value(final_value, point, container)
57
69
  container[point.name] = final_value
58
70
  end
@@ -262,14 +262,15 @@ class Serega
262
262
 
263
263
  def attach_value(object, point, container)
264
264
  batch = point.batch
265
+ return super unless batch
265
266
 
266
- if batch
267
- key = batch.key.call(object, context)
268
- opts[:batch_loaders].get(point, self).remember(key, container)
269
- container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
270
- else
271
- super
272
- end
267
+ remember_key_for_batch_loading(batch, object, point, container)
268
+ end
269
+
270
+ def remember_key_for_batch_loading(batch, object, point, container)
271
+ key = batch.key.call(object, context)
272
+ opts[:batch_loaders].get(point, self).remember(key, container)
273
+ container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
273
274
  end
274
275
  end
275
276
  end
@@ -68,6 +68,9 @@ class Serega
68
68
  end
69
69
  end
70
70
 
71
+ # Patched in:
72
+ # - plugin batch (extension :activerecord_preloads - preloads data to found values)
73
+ # - plugin batch (extension :formatters - formats values)
71
74
  def keys_values
72
75
  ids = keys.keys
73
76
 
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ #
6
+ # Plugin adds `:if`, `:unless`, `:if_value`, `:unless_value` options to
7
+ # attributes so we can remove attributes from response in various ways.
8
+ #
9
+ # Use `:if` and `:unless` when you want to hide attributes before finding attribute value,
10
+ # and use `:if_value` and `:unless_value` to hide attributes after we find final value.
11
+ #
12
+ # Options `:if` and `:unless` accept currently serialized object and context as parameters.
13
+ # Options `:if_value` and `:unless_value` accept already found serialized value and context as parameters.
14
+ #
15
+ # Options `:if_value` and `:unless_value` cannot be used with :serializer option, as
16
+ # serialized objects have no "serialized value". Use `:if` and `:unless` in this case.
17
+ #
18
+ # See also a `:hide` option that is available without any plugins to hide
19
+ # attribute without conditions. Look at README.md#selecting-fields for `:hide` usage examples.
20
+ #
21
+ # Examples:
22
+ # class UserSerializer < Serega
23
+ # attribute :email, if: :active? # if user.active?
24
+ # attribute :email, if: proc {|user| user.active?} # same
25
+ # attribute :email, if: proc {|user, ctx| user == ctx[:current_user]} # using context
26
+ # attribute :email, if: CustomPolicy.method(:view_email?) # You can provide own callable object
27
+ #
28
+ # attribute :email, unless: :hidden? # unless user.hidden?
29
+ # attribute :email, unless: proc {|user| user.hidden?} # same
30
+ # attribute :email, unless: proc {|user, context| context[:show_emails]} # using context
31
+ # attribute :email, unless: CustomPolicy.method(:hide_email?) # You can provide own callable object
32
+ #
33
+ # attribute :email, if_value: :present? # if email.present?
34
+ # attribute :email, if_value: proc {|email| email.present?} # same
35
+ # attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]} # using context
36
+ # attribute :email, if_value: CustomPolicy.method(:view_email?) # You can provide own callable object
37
+ #
38
+ # attribute :email, unless_value: :blank? # unless email.blank?
39
+ # attribute :email, unless_value: proc {|email| email.blank?} # same
40
+ # attribute :email, unless_value: proc {|email, context| context[:show_emails]} # using context
41
+ # attribute :email, unless_value: CustomPolicy.method(:hide_email?) # You can provide own callable object
42
+ # end
43
+ #
44
+ module If
45
+ # @return [Symbol] Plugin name
46
+ def self.plugin_name
47
+ :if
48
+ end
49
+
50
+ #
51
+ # Applies plugin code to specific serializer
52
+ #
53
+ # @param serializer_class [Class<Serega>] Current serializer class
54
+ # @param _opts [Hash] Loaded plugins options
55
+ #
56
+ # @return [void]
57
+ #
58
+ def self.load_plugin(serializer_class, **_opts)
59
+ require_relative "./validations/check_opt_if"
60
+ require_relative "./validations/check_opt_if_value"
61
+ require_relative "./validations/check_opt_unless"
62
+ require_relative "./validations/check_opt_unless_value"
63
+
64
+ serializer_class::SeregaMapPoint.include(MapPointInstanceMethods)
65
+ serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
66
+ serializer_class::SeregaObjectSerializer.include(SeregaObjectSerializerInstanceMethods)
67
+ end
68
+
69
+ #
70
+ # Adds config options and runs other callbacks after plugin was loaded
71
+ #
72
+ # @param serializer_class [Class<Serega>] Current serializer class
73
+ # @param opts [Hash] loaded plugins opts
74
+ #
75
+ # @return [void]
76
+ #
77
+ def self.after_load_plugin(serializer_class, **opts)
78
+ serializer_class.config.attribute_keys << :if << :if_value << :unless << :unless_value
79
+ end
80
+
81
+ #
82
+ # Serega::SeregaMapPoint additional/patched instance methods
83
+ #
84
+ # @see Serega::SeregaMapPoint::InstanceMethods
85
+ #
86
+ module MapPointInstanceMethods
87
+ #
88
+ # @return [Boolean] Should we show attribute or not
89
+ # Conditions for this checks are specified by :if and :unless attribute options.
90
+ #
91
+ def satisfy_if_conditions?(obj, ctx)
92
+ check_if_unless(obj, ctx, :if, :unless)
93
+ end
94
+
95
+ #
96
+ # @return [Boolean] Should we show attribute with specific value or not.
97
+ # Conditions for this checks are specified by :if_value and :unless_value attribute options.
98
+ #
99
+ def satisfy_if_value_conditions?(value, ctx)
100
+ check_if_unless(value, ctx, :if_value, :unless_value)
101
+ end
102
+
103
+ private
104
+
105
+ def check_if_unless(obj, ctx, opt_if_name, opt_unless_name)
106
+ opt_if = attribute.opts[opt_if_name]
107
+ opt_unless = attribute.opts[opt_unless_name]
108
+ return true if opt_if.nil? && opt_unless.nil?
109
+
110
+ res_if =
111
+ case opt_if
112
+ when NilClass then true
113
+ when Symbol then obj.public_send(opt_if)
114
+ else opt_if.call(obj, ctx)
115
+ end
116
+
117
+ res_unless =
118
+ case opt_unless
119
+ when NilClass then true
120
+ when Symbol then !obj.public_send(opt_unless)
121
+ else !opt_unless.call(obj, ctx)
122
+ end
123
+
124
+ res_if && res_unless
125
+ end
126
+ end
127
+
128
+ #
129
+ # Serega::SeregaValidations::CheckAttributeParams additional/patched class methods
130
+ #
131
+ # @see Serega::SeregaValidations::CheckAttributeParams
132
+ #
133
+ module CheckAttributeParamsInstanceMethods
134
+ private
135
+
136
+ def check_opts
137
+ super
138
+
139
+ CheckOptIf.call(opts)
140
+ CheckOptUnless.call(opts)
141
+ CheckOptIfValue.call(opts)
142
+ CheckOptUnlessValue.call(opts)
143
+ end
144
+ end
145
+
146
+ #
147
+ # SeregaObjectSerializer additional/patched class methods
148
+ #
149
+ # @see Serega::SeregaObjectSerializer
150
+ #
151
+ module SeregaObjectSerializerInstanceMethods
152
+ private
153
+
154
+ def serialize_point(object, point, _container)
155
+ return unless point.satisfy_if_conditions?(object, context)
156
+ super
157
+ end
158
+
159
+ def attach_final_value(value, point, _container)
160
+ return unless point.satisfy_if_value_conditions?(value, context)
161
+ super
162
+ end
163
+ end
164
+ end
165
+
166
+ register_plugin(If.plugin_name, If)
167
+ end
168
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :if option
8
+ #
9
+ class CheckOptIf
10
+ class << self
11
+ #
12
+ # Checks attribute :if option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:if)
22
+
23
+ check_type(opts[:if])
24
+ end
25
+
26
+ private
27
+
28
+ def check_type(value)
29
+ return if value.is_a?(Symbol)
30
+
31
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
32
+
33
+ if value.is_a?(Proc)
34
+ check_block(value)
35
+ else
36
+ check_callable(value)
37
+ end
38
+ end
39
+
40
+ def check_block(block)
41
+ return if valid_parameters?(block, accepted_count: 0..2)
42
+
43
+ raise SeregaError, block_parameters_error
44
+ end
45
+
46
+ def check_callable(callable)
47
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
48
+
49
+ raise SeregaError, callable_parameters_error
50
+ end
51
+
52
+ def valid_parameters?(data, accepted_count:)
53
+ params = data.parameters
54
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
55
+ end
56
+
57
+ def valid_parameters_types?(params)
58
+ params.all? do |param|
59
+ type = param[0]
60
+ (type == :req) || (type == :opt)
61
+ end
62
+ end
63
+
64
+ def block_parameters_error
65
+ "Invalid attribute option :if. When it is a Proc it can have maximum two regular parameters (object, context)"
66
+ end
67
+
68
+ def callable_parameters_error
69
+ "Invalid attribute option :if. When it is a callable object it must have two regular parameters (object, context)"
70
+ end
71
+
72
+ def must_be_callable
73
+ "Invalid attribute option :if. It must be a Symbol, a Proc or respond to :call"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :if_value option
8
+ #
9
+ class CheckOptIfValue
10
+ class << self
11
+ #
12
+ # Checks attribute :if_value option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:if_value)
22
+
23
+ check_usage_with_other_params(opts)
24
+ check_type(opts[:if_value])
25
+ end
26
+
27
+ private
28
+
29
+ def check_type(value)
30
+ return if value.is_a?(Symbol)
31
+
32
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
33
+
34
+ if value.is_a?(Proc)
35
+ check_block(value)
36
+ else
37
+ check_callable(value)
38
+ end
39
+ end
40
+
41
+ def check_usage_with_other_params(opts)
42
+ raise SeregaError, "Option :if_value can not be used together with option :serializer" if opts.key?(:serializer)
43
+ end
44
+
45
+ def check_block(block)
46
+ return if valid_parameters?(block, accepted_count: 0..2)
47
+
48
+ raise SeregaError, block_parameters_error
49
+ end
50
+
51
+ def check_callable(callable)
52
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
53
+
54
+ raise SeregaError, callable_parameters_error
55
+ end
56
+
57
+ def valid_parameters?(data, accepted_count:)
58
+ params = data.parameters
59
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
60
+ end
61
+
62
+ def valid_parameters_types?(params)
63
+ params.all? do |param|
64
+ type = param[0]
65
+ (type == :req) || (type == :opt)
66
+ end
67
+ end
68
+
69
+ def block_parameters_error
70
+ "Invalid attribute option :if_value. When it is a Proc it can have maximum two regular parameters (object, context)"
71
+ end
72
+
73
+ def callable_parameters_error
74
+ "Invalid attribute option :if_value. When it is a callable object it must have two regular parameters (object, context)"
75
+ end
76
+
77
+ def must_be_callable
78
+ "Invalid attribute option :if_value. It must be a Symbol, a Proc or respond to :call"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaPlugins
5
+ module If
6
+ #
7
+ # Validator for attribute :unless option
8
+ #
9
+ class CheckOptUnless
10
+ class << self
11
+ #
12
+ # Checks attribute :unless option that must be [nil, Symbol, Proc, #call]
13
+ #
14
+ # @param opts [Hash] Attribute options
15
+ #
16
+ # @raise [SeregaError] Attribute validation error
17
+ #
18
+ # @return [void]
19
+ #
20
+ def call(opts)
21
+ return unless opts.key?(:unless)
22
+
23
+ check_type(opts[:unless])
24
+ end
25
+
26
+ private
27
+
28
+ #
29
+ # Checks attribute :unless option
30
+ #
31
+ # @param value [nil, Symbol, Proc, #call] Attribute :unless option
32
+ #
33
+ # @raise [SeregaError] validation error
34
+ #
35
+ # @return [void]
36
+ #
37
+ def check_type(value)
38
+ return if value.is_a?(Symbol)
39
+
40
+ raise SeregaError, must_be_callable unless value.respond_to?(:call)
41
+
42
+ if value.is_a?(Proc)
43
+ check_block(value)
44
+ else
45
+ check_callable(value)
46
+ end
47
+ end
48
+
49
+ def check_block(block)
50
+ return if valid_parameters?(block, accepted_count: 0..2)
51
+
52
+ raise SeregaError, block_parameters_error
53
+ end
54
+
55
+ def check_callable(callable)
56
+ return if valid_parameters?(callable.method(:call), accepted_count: 2..2)
57
+
58
+ raise SeregaError, callable_parameters_error
59
+ end
60
+
61
+ def valid_parameters?(data, accepted_count:)
62
+ params = data.parameters
63
+ accepted_count.include?(params.count) && valid_parameters_types?(params)
64
+ end
65
+
66
+ def valid_parameters_types?(params)
67
+ params.all? do |param|
68
+ type = param[0]
69
+ (type == :req) || (type == :opt)
70
+ end
71
+ end
72
+
73
+ def block_parameters_error
74
+ "Invalid attribute option :unless. When it is a Proc it can have maximum two regular parameters (object, context)"
75
+ end
76
+
77
+ def callable_parameters_error
78
+ "Invalid attribute option :unless. When it is a callable object it must have two regular parameters (object, context)"
79
+ end
80
+
81
+ def must_be_callable
82
+ "Invalid attribute option :unless. It must be a Symbol, a Proc or respond to :call"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end