servactory 3.1.0.rc2 → 3.1.0.rc4

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +7 -2
  4. data/config/locales/ja.yml +134 -0
  5. data/lib/generators/README.md +2 -2
  6. data/lib/generators/servactory/extension/USAGE +1 -1
  7. data/lib/generators/servactory/install/USAGE +2 -2
  8. data/lib/generators/servactory/rspec/USAGE +4 -4
  9. data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
  10. data/lib/generators/servactory/service/USAGE +5 -5
  11. data/lib/servactory/actions/collection.rb +21 -5
  12. data/lib/servactory/actions/stages/collection.rb +19 -1
  13. data/lib/servactory/actions/tools/rules.rb +1 -1
  14. data/lib/servactory/actions/tools/runner.rb +1 -1
  15. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
  16. data/lib/servactory/configuration/config.rb +16 -0
  17. data/lib/servactory/configuration/configurable.rb +7 -4
  18. data/lib/servactory/configuration/factory.rb +30 -3
  19. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -1
  20. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +156 -29
  21. data/lib/servactory/context/warehouse/inputs.rb +1 -1
  22. data/lib/servactory/context/workspace/inputs.rb +2 -2
  23. data/lib/servactory/context/workspace/internals.rb +2 -2
  24. data/lib/servactory/context/workspace/outputs.rb +2 -2
  25. data/lib/servactory/context/workspace.rb +7 -7
  26. data/lib/servactory/info/builder.rb +4 -3
  27. data/lib/servactory/inputs/dsl.rb +5 -1
  28. data/lib/servactory/inputs/input.rb +1 -2
  29. data/lib/servactory/inputs/tools/validation.rb +10 -18
  30. data/lib/servactory/internals/dsl.rb +7 -1
  31. data/lib/servactory/maintenance/attributes/base.rb +4 -1
  32. data/lib/servactory/maintenance/attributes/collection.rb +61 -6
  33. data/lib/servactory/maintenance/options/collection.rb +71 -30
  34. data/lib/servactory/maintenance/options/option.rb +3 -12
  35. data/lib/servactory/maintenance/options/registrar.rb +3 -13
  36. data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
  37. data/lib/servactory/maintenance/validations/performer.rb +12 -23
  38. data/lib/servactory/outputs/dsl.rb +7 -1
  39. data/lib/servactory/result.rb +21 -8
  40. data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
  41. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
  42. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -125
  43. data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
  44. data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
  45. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +185 -50
  46. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +45 -10
  47. data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
  48. data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
  49. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
  50. data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
  51. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
  52. data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
  53. data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
  54. data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
  55. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
  56. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
  57. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
  58. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
  59. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
  60. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
  61. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
  62. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
  63. data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
  64. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +34 -11
  65. data/lib/servactory/tool_kit/dynamic_options/format.rb +3 -4
  66. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +3 -3
  67. data/lib/servactory/tool_kit/dynamic_options/max.rb +3 -3
  68. data/lib/servactory/tool_kit/dynamic_options/min.rb +3 -3
  69. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +35 -7
  70. data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
  71. data/lib/servactory/tool_kit/dynamic_options/schema.rb +40 -25
  72. data/lib/servactory/tool_kit/dynamic_options/target.rb +4 -4
  73. data/lib/servactory/utils.rb +13 -3
  74. data/lib/servactory/version.rb +1 -1
  75. data/lib/servactory.rb +3 -4
  76. metadata +20 -4
@@ -22,17 +22,38 @@ module Servactory
22
22
  # end
23
23
  # ```
24
24
  #
25
+ # ## Messages
26
+ #
27
+ # `.message` checks the message of the option chained right before it,
28
+ # `.required(message)` checks the required message, and `.must` takes
29
+ # the expected message of each rule by its name:
30
+ #
31
+ # ```ruby
32
+ # it { is_expected.to have_service_input(:status).inclusion(%w[active inactive]).message("Unknown status") }
33
+ # it { is_expected.to have_service_input(:status).type(String).message(:default) }
34
+ # it { is_expected.to have_service_input(:email).required(/is required/) }
35
+ # it { is_expected.to have_service_input(:age).must(:be_adult, be_positive: "Age must be positive") }
36
+ # it { is_expected.to have_service_input(:age).must(:be_positive).message(be_a(Proc)) }
37
+ # ```
38
+ #
39
+ # A String must equal the message, a Regexp must match it, an RSpec
40
+ # matcher is applied to the message as defined, and `:default` checks
41
+ # that no custom message is defined. See MessageSubmatcher and
42
+ # MustSubmatcher for the details.
43
+ #
25
44
  # ## Chain Methods
26
45
  #
27
46
  # - `.type(Class)` / `.types(Class, ...)` - expected type(s)
28
- # - `.required` / `.optional` - required status
47
+ # - `.required` / `.optional` - required status, `.required(message)` also checks the message
29
48
  # - `.default(value)` - expected default value
30
49
  # - `.consists_of(Class)` - for Array/Hash element types
31
50
  # - `.schema(Hash)` - expected schema definition
32
51
  # - `.inclusion(Array)` - expected inclusion values
33
- # - `.must(Array)` - custom validation rules
52
+ # - `.must(Array)` / `.must(*names, **messages)` - custom validation rules,
53
+ # optionally with the expected message of each rule
34
54
  # - `.target(value, name:)` - target validation
35
- # - `.message(String)` - expected error message (after other chain)
55
+ # - `.message(String | Regexp | matcher | :default)` - expected error message (after other chain);
56
+ # after `.must(:rule)` it is the expected message of that rule
36
57
  class HaveServiceInputMatcher < Base::AttributeMatcher
37
58
  for_attribute_type :input
38
59
 
@@ -69,7 +90,9 @@ module Servactory
69
90
 
70
91
  register_submatcher :must,
71
92
  class_name: "Shared::MustSubmatcher",
72
- transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] }
93
+ transform_args: (lambda do |args, _kwargs = {}|
94
+ Submatchers::Shared::MustSubmatcher.arguments_from(args)
95
+ end)
73
96
 
74
97
  register_submatcher :message,
75
98
  class_name: "Shared::MessageSubmatcher",
@@ -19,6 +19,9 @@ module Servactory
19
19
  # it { is_expected.to have_service_internal(:processed_data).type(Hash) }
20
20
  # it { is_expected.to have_service_internal(:items).type(Array).consists_of(Item) }
21
21
  # it { is_expected.to have_service_internal(:config).schema({ key: String }) }
22
+ # it { is_expected.to have_service_internal(:items).type(Array).message(/must be an Array/) }
23
+ # it { is_expected.to have_service_internal(:total).must(be_positive: "Total must be positive") }
24
+ # it { is_expected.to have_service_internal(:total).must(:be_positive).message(:default) }
22
25
  # end
23
26
  # ```
24
27
  #
@@ -28,9 +31,11 @@ module Servactory
28
31
  # - `.consists_of(Class)` - for Array/Hash element types
29
32
  # - `.schema(Hash)` - expected schema definition
30
33
  # - `.inclusion(Array)` - expected inclusion values
31
- # - `.must(Array)` - custom validation rules
34
+ # - `.must(Array)` / `.must(*names, **messages)` - custom validation rules,
35
+ # optionally with the expected message of each rule
32
36
  # - `.target(value, name:)` - target validation
33
- # - `.message(String)` - expected error message (after other chain)
37
+ # - `.message(String | Regexp | matcher | :default)` - expected error message (after other chain);
38
+ # after `.must(:rule)` it is the expected message of that rule
34
39
  #
35
40
  # ## Architecture
36
41
  #
@@ -72,7 +77,9 @@ module Servactory
72
77
 
73
78
  register_submatcher :must,
74
79
  class_name: "Shared::MustSubmatcher",
75
- transform_args: ->(args, _kwargs = {}) { [Array(args).flatten] }
80
+ transform_args: (lambda do |args, _kwargs = {}|
81
+ Submatchers::Shared::MustSubmatcher.arguments_from(args)
82
+ end)
76
83
 
77
84
  register_submatcher :message,
78
85
  class_name: "Shared::MessageSubmatcher",
@@ -18,21 +18,40 @@ module Servactory
18
18
  # ```ruby
19
19
  # it { is_expected.to have_service_input(:user_id).required }
20
20
  # it { is_expected.to have_service_input(:name).required("Name is mandatory") }
21
+ # it { is_expected.to have_service_input(:name).required(/is mandatory/) }
22
+ # it { is_expected.to have_service_input(:name).required(:default) }
21
23
  # ```
22
24
  #
23
25
  # ## Validation
24
26
  #
25
27
  # Checks the `:required` option in attribute data where `is: true`.
26
28
  # If a custom message is provided, also validates the message matches.
29
+ #
30
+ # The expected message follows the rules of `.message`: a String is
31
+ # compared with the message exactly, a Regexp is matched against it,
32
+ # an RSpec matcher is applied to the message as defined, and
33
+ # `:default` checks that the input defines no custom message.
34
+ # The default required message does not depend on the given value,
35
+ # so a String or Regexp is compared with it when there is no
36
+ # custom message.
37
+ #
38
+ # A Proc message is called with the keyword arguments the library
39
+ # passes to it: `service:`, `input:` and `value:`, which is `nil`.
40
+ # A Proc message that raises or does not return a String does not
41
+ # match, and the failure message shows the error or the returned value.
27
42
  class RequiredSubmatcher < Base::Submatcher
43
+ include Concerns::RequiredMessage
44
+
28
45
  # Creates a new required submatcher.
29
46
  #
30
47
  # @param context [Base::SubmatcherContext] The submatcher context
31
- # @param custom_message [String, nil] Optional expected error message
48
+ # @param custom_message [String, Regexp, Symbol, Object, nil] Optional expected error message,
49
+ # `:default` or an RSpec matcher
32
50
  # @return [RequiredSubmatcher] New submatcher instance
51
+ # @raise [ArgumentError] If the expected message is of another kind
33
52
  def initialize(context, custom_message = nil)
34
53
  super(context)
35
- @custom_message = custom_message
54
+ @expectation = Base::MessageExpectation.new(custom_message) unless custom_message.nil?
36
55
  end
37
56
 
38
57
  # Returns description for RSpec output.
@@ -48,41 +67,57 @@ module Servactory
48
67
  #
49
68
  # @return [Boolean] True if input is required with matching message
50
69
  def passes?
51
- required_data = attribute_data.fetch(:required)
52
- is_required = required_data.fetch(:is)
70
+ return false unless required?
71
+ return true if expectation.nil?
53
72
 
54
- return false unless is_required == true
55
- return true unless custom_message.present?
56
-
57
- actual_message = required_data.fetch(:message) || default_required_message
58
- actual_message.casecmp(custom_message).zero?
73
+ @message_mismatch = find_message_mismatch
74
+ @message_mismatch.nil?
59
75
  end
60
76
 
61
77
  # Builds the failure message for required validation.
62
78
  #
63
79
  # @return [String] Failure message with expected vs actual
64
80
  def build_failure_message
65
- <<~MESSAGE
66
- should be required
81
+ return not_required_failure_message unless required?
67
82
 
68
- expected required: true
69
- got required: false
70
- MESSAGE
83
+ "should be required with the expected message\n\n#{@message_mismatch.indent(2)}"
71
84
  end
72
85
 
73
86
  private
74
87
 
75
- attr_reader :custom_message
88
+ attr_reader :expectation
89
+
90
+ # Checks if the input has `required: true`.
91
+ #
92
+ # @return [Boolean] True if input is required
93
+ def required?
94
+ attribute_data.fetch(:required).fetch(:is) == true
95
+ end
96
+
97
+ # Compares the message of the required option with the expected message.
98
+ #
99
+ # Uses the custom message from `required: { message: }` if provided,
100
+ # otherwise the default message. A Proc message is called with the
101
+ # keyword arguments the library passes to it.
102
+ #
103
+ # @return [String, nil] Explanation of the mismatch, or nil if the messages match
104
+ def find_message_mismatch
105
+ expectation.mismatch_for(
106
+ required_custom_message,
107
+ default_message: default_required_message
108
+ ) { |message| call_required_message(message) }
109
+ end
76
110
 
77
- # Generates the default I18n message for required validation.
111
+ # Builds the failure message for an optional input.
78
112
  #
79
- # @return [String] Localized default required message
80
- def default_required_message
81
- I18n.t(
82
- "#{i18n_root_key}.inputs.validations.required.default_error.default",
83
- service_class_name: described_class.name,
84
- input_name: attribute_name
85
- )
113
+ # @return [String] Failure message with expected vs actual required state
114
+ def not_required_failure_message
115
+ <<~MESSAGE
116
+ should be required
117
+
118
+ expected required: true
119
+ got required: false
120
+ MESSAGE
86
121
  end
87
122
  end
88
123
  end
@@ -35,6 +35,10 @@ module Servactory
35
35
  # 5. `failure_inclusion_passes?` - fails with value outside inclusion
36
36
  # 6. `failure_target_passes?` - fails with value outside target
37
37
  class ValidWithSubmatcher < Base::Submatcher # rubocop:disable Metrics/ClassLength
38
+ include Concerns::OptionHelperRules
39
+ include Concerns::ProcMessage
40
+ include Concerns::RequiredMessage
41
+
38
42
  # Creates a new valid_with submatcher.
39
43
  #
40
44
  # @param context [Base::SubmatcherContext] The submatcher context
@@ -93,44 +97,59 @@ module Servactory
93
97
  # Checks that service fails with wrong type.
94
98
  #
95
99
  # @return [Boolean] True if type validation fails as expected
96
- def failure_type_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
97
- option_types = attribute_data.fetch(:types)
100
+ def failure_type_passes?
101
+ wrong_value = Servactory::TestKit::FakeType.new
98
102
 
99
103
  prepared_attributes = attributes.dup
100
- prepared_attributes[attribute_name] = Servactory::TestKit::FakeType.new
104
+ prepared_attributes[attribute_name] = wrong_value
105
+
106
+ expect_failure_with!(prepared_attributes, type_error_message_for(wrong_value))
107
+ end
108
+
109
+ # Builds the message expected for a value of wrong type.
110
+ #
111
+ # Uses the custom message from `type: { is:, message: }` if provided,
112
+ # otherwise the default message.
113
+ #
114
+ # @param value [Object] The value of wrong type
115
+ # @return [String] Expected error message
116
+ def type_error_message_for(value)
117
+ message = attribute_data.fetch(:type).fetch(:message)
118
+ expected_type = attribute_data.fetch(:types).join(", ")
119
+ given_type = value.class.name
120
+
121
+ return default_type_error_message(expected_type:, given_type:) if message.blank?
122
+ return message unless message.is_a?(Proc)
123
+
124
+ call_proc_message(message, :type, value:, expected_type:, given_type:)
125
+ end
101
126
 
102
- input_required_message = I18n.t(
127
+ # Builds the default message for a value of wrong type.
128
+ #
129
+ # @param expected_type [String] Expected type names
130
+ # @param given_type [String] Name of the given value's class
131
+ # @return [String] Default error message
132
+ def default_type_error_message(expected_type:, given_type:)
133
+ I18n.t(
103
134
  "#{i18n_root_key}.inputs.validations.type.default_error.default",
104
135
  service_class_name: described_class.name,
105
136
  input_name: attribute_name,
106
- expected_type: option_types.join(", "),
107
- given_type: Servactory::TestKit::FakeType.new.class.name
137
+ expected_type:,
138
+ given_type:
108
139
  )
109
-
110
- expect_failure_with!(prepared_attributes, input_required_message)
111
140
  end
112
141
 
113
142
  # Checks that required validation fails when input is nil.
114
143
  #
115
144
  # @return [Boolean] True if required validation fails as expected
116
- def failure_required_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
145
+ def failure_required_passes?
117
146
  input_required = attribute_data.fetch(:required).fetch(:is)
118
147
  return true unless input_required
119
148
 
120
149
  prepared_attributes = attributes.dup
121
150
  prepared_attributes[attribute_name] = nil
122
151
 
123
- input_required_message = attribute_data.fetch(:required).fetch(:message)
124
-
125
- if input_required_message.nil?
126
- input_required_message = I18n.t(
127
- "#{i18n_root_key}.inputs.validations.required.default_error.default",
128
- service_class_name: described_class.name,
129
- input_name: attribute_name
130
- )
131
- end
132
-
133
- expect_failure_with!(prepared_attributes, input_required_message)
152
+ expect_failure_with!(prepared_attributes, required_message)
134
153
  end
135
154
 
136
155
  # Checks that optional input accepts nil without failure.
@@ -149,7 +168,7 @@ module Servactory
149
168
  # Checks that inclusion validation fails with wrong value.
150
169
  #
151
170
  # @return [Boolean] True if inclusion validation fails as expected
152
- def failure_inclusion_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
171
+ def failure_inclusion_passes?
153
172
  input_inclusion_in = attribute_data.dig(:inclusion, :in)
154
173
  return true if input_inclusion_in.blank?
155
174
 
@@ -158,31 +177,43 @@ module Servactory
158
177
  prepared_attributes = attributes.dup
159
178
  prepared_attributes[attribute_name] = wrong_value
160
179
 
161
- input_required_message = attribute_data.fetch(:inclusion).fetch(:message)
162
-
163
- # If message is a Proc, we can't easily evaluate it in the test context
164
- # (it may require runtime args like input:, value:), so skip message comparison
165
- if input_required_message.is_a?(Proc)
166
- return expect_failure_with!(prepared_attributes, :skip_message_check)
167
- end
180
+ expect_failure_with!(prepared_attributes, inclusion_error_message_for(wrong_value, input_inclusion_in))
181
+ end
168
182
 
169
- if input_required_message.nil?
170
- input_required_message = I18n.t(
171
- "#{i18n_root_key}.inputs.validations.must.dynamic_options.inclusion.default",
172
- service_class_name: described_class.name,
173
- input_name: attribute_name,
174
- input_inclusion: input_inclusion_in.inspect,
175
- value: wrong_value.inspect
176
- )
177
- end
183
+ # Builds the message expected for a value outside the inclusion set.
184
+ #
185
+ # Uses the custom message from `inclusion: { in:, message: }` if provided,
186
+ # otherwise the default message.
187
+ #
188
+ # @param value [Object] The value outside the inclusion set
189
+ # @param option_value [Range, Array, Object] The inclusion constraint
190
+ # @return [Object] Expected error message
191
+ def inclusion_error_message_for(value, option_value)
192
+ message = attribute_data.fetch(:inclusion).fetch(:message)
193
+ return default_inclusion_error_message(value:, option_value:) if message.blank?
194
+
195
+ dynamic_option_message(message, :inclusion, value:, option_value:)
196
+ end
178
197
 
179
- expect_failure_with!(prepared_attributes, input_required_message)
198
+ # Builds the default message for a value outside the inclusion set.
199
+ #
200
+ # @param value [Object] The value outside the inclusion set
201
+ # @param option_value [Object] The inclusion constraint
202
+ # @return [String] Default error message
203
+ def default_inclusion_error_message(value:, option_value:)
204
+ I18n.t(
205
+ "#{i18n_root_key}.inputs.validations.must.dynamic_options.inclusion.default",
206
+ service_class_name: described_class.name,
207
+ input_name: attribute_name,
208
+ input_inclusion: option_value.inspect,
209
+ value: value.inspect
210
+ )
180
211
  end
181
212
 
182
213
  # Checks that target validation fails with wrong value.
183
214
  #
184
215
  # @return [Boolean] True if target validation fails as expected
185
- def failure_target_passes? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
216
+ def failure_target_passes?
186
217
  input_target_in = attribute_data.dig(:target, :in)
187
218
  return true if input_target_in.blank?
188
219
 
@@ -192,40 +223,74 @@ module Servactory
192
223
  prepared_attributes = attributes.dup
193
224
  prepared_attributes[attribute_name] = wrong_value
194
225
 
195
- input_required_message = attribute_data.fetch(:target).fetch(:message)
226
+ expect_failure_with!(prepared_attributes, target_error_message_for(wrong_value, input_target_in))
227
+ end
196
228
 
197
- # If message is a Proc, we can't easily evaluate it in the test context
198
- # (it may require runtime args like input:, value:), so skip message comparison
199
- if input_required_message.is_a?(Proc)
200
- return expect_failure_with!(prepared_attributes, :skip_message_check)
201
- end
229
+ # Builds the message expected for a value outside the target set.
230
+ #
231
+ # Uses the custom message from `target: { in:, message: }` if provided,
232
+ # otherwise the default message.
233
+ #
234
+ # @param value [Object] The value outside the target set
235
+ # @param option_value [Array, Object] The target constraint
236
+ # @return [Object] Expected error message
237
+ def target_error_message_for(value, option_value)
238
+ message = attribute_data.fetch(:target).fetch(:message)
239
+ return default_target_error_message(value:, option_value:) if message.blank?
240
+
241
+ dynamic_option_message(message, :target, value:, option_value:)
242
+ end
202
243
 
203
- if input_required_message.nil?
204
- input_required_message = I18n.t(
205
- "#{i18n_root_key}.inputs.validations.must.dynamic_options.target.default",
206
- service_class_name: described_class.name,
207
- input_name: attribute_name,
208
- expected_target: input_target_in.inspect,
209
- value: wrong_value.inspect
210
- )
211
- end
244
+ # Builds the default message for a value outside the target set.
245
+ #
246
+ # @param value [Object] The value outside the target set
247
+ # @param option_value [Object] The target constraint
248
+ # @return [String] Default error message
249
+ def default_target_error_message(value:, option_value:)
250
+ I18n.t(
251
+ "#{i18n_root_key}.inputs.validations.must.dynamic_options.target.default",
252
+ service_class_name: described_class.name,
253
+ input_name: attribute_name,
254
+ expected_target: option_value.inspect,
255
+ value: value.inspect
256
+ )
257
+ end
212
258
 
213
- expect_failure_with!(prepared_attributes, input_required_message)
259
+ # Returns the custom message of a dynamic option as the library builds it.
260
+ #
261
+ # A Proc message is called with the keywords the library passes to it
262
+ # when the value is outside the option, where the failure has no reason.
263
+ #
264
+ # @param message [Proc, Object] The custom message of the option
265
+ # @param option_name [Symbol] The option name, such as `inclusion`
266
+ # @param value [Object] The value outside the option
267
+ # @param option_value [Object] The value of the option
268
+ # @return [Object] Expected error message
269
+ def dynamic_option_message(message, option_name, value:, option_value:)
270
+ return message unless message.is_a?(Proc)
271
+
272
+ call_proc_message(
273
+ message,
274
+ :dynamic_option,
275
+ value:,
276
+ code: option_helper_rule_name(option_name),
277
+ reason: nil,
278
+ option_name:,
279
+ option_value:
280
+ )
214
281
  end
215
282
 
216
283
  # Calls service and verifies it fails with expected message.
217
284
  #
218
285
  # @param prepared_attributes [Hash] Attributes to pass to service
219
- # @param expected_message [String, Symbol, nil] Expected error message
286
+ # @param expected_message [Object, nil] Expected error message
220
287
  # @return [Boolean] True if service fails with expected message
221
288
  def expect_failure_with!(prepared_attributes, expected_message)
222
289
  described_class.call!(prepared_attributes).success?
223
290
  rescue Servactory::Exceptions::Input => e
224
- return true if expected_message == :skip_message_check # Just verify error was raised
225
291
  return false if expected_message.blank?
226
292
 
227
- message_to_compare = expected_message.is_a?(Proc) ? expected_message.call : expected_message
228
- message_to_compare.to_s.casecmp(e.message.to_s).zero?
293
+ expected_message.to_s.casecmp(e.message.to_s).zero?
229
294
  rescue Servactory::Exceptions::Internal, Servactory::Exceptions::Output
230
295
  true
231
296
  end