servactory 3.1.0.rc3 → 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.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/lib/generators/README.md +1 -1
- data/lib/generators/servactory/extension/USAGE +1 -1
- data/lib/generators/servactory/install/USAGE +2 -2
- data/lib/generators/servactory/rspec/USAGE +4 -4
- data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
- data/lib/generators/servactory/service/USAGE +5 -5
- data/lib/servactory/actions/collection.rb +21 -5
- data/lib/servactory/actions/stages/collection.rb +19 -1
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +1 -1
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
- data/lib/servactory/configuration/configurable.rb +1 -1
- data/lib/servactory/configuration/factory.rb +30 -3
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +2 -1
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +150 -37
- data/lib/servactory/context/warehouse/inputs.rb +1 -1
- data/lib/servactory/context/workspace/inputs.rb +2 -2
- data/lib/servactory/context/workspace/internals.rb +2 -2
- data/lib/servactory/context/workspace/outputs.rb +2 -2
- data/lib/servactory/context/workspace.rb +7 -7
- data/lib/servactory/info/builder.rb +4 -3
- data/lib/servactory/inputs/dsl.rb +5 -1
- data/lib/servactory/inputs/input.rb +1 -2
- data/lib/servactory/internals/dsl.rb +7 -1
- data/lib/servactory/maintenance/attributes/base.rb +4 -1
- data/lib/servactory/maintenance/attributes/collection.rb +61 -6
- data/lib/servactory/maintenance/options/collection.rb +46 -15
- data/lib/servactory/maintenance/options/option.rb +3 -12
- data/lib/servactory/maintenance/options/registrar.rb +2 -12
- data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
- data/lib/servactory/outputs/dsl.rb +7 -1
- data/lib/servactory/result.rb +21 -8
- data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -129
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
- data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +110 -45
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +26 -8
- data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
- data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +29 -8
- data/lib/servactory/tool_kit/dynamic_options/format.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/max.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/min.rb +1 -1
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +33 -5
- data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +34 -21
- data/lib/servactory/tool_kit/dynamic_options/target.rb +1 -1
- data/lib/servactory/utils.rb +13 -3
- data/lib/servactory/version.rb +1 -1
- data/lib/servactory.rb +3 -0
- metadata +10 -2
|
@@ -13,33 +13,113 @@ module Servactory
|
|
|
13
13
|
# Validates that an attribute has the expected custom validation rules
|
|
14
14
|
# defined via the `must` option. Excludes dynamic options that are
|
|
15
15
|
# tested separately (consists_of, schema, be_inclusion, be_target).
|
|
16
|
+
# Optionally validates the messages of the rules.
|
|
16
17
|
#
|
|
17
18
|
# ## Usage
|
|
18
19
|
#
|
|
19
20
|
# ```ruby
|
|
20
21
|
# it { is_expected.to have_service_input(:email).must([:be_valid_email]) }
|
|
21
22
|
# it { is_expected.to have_service_input(:age).must([:be_positive, :be_adult]) }
|
|
23
|
+
# it { is_expected.to have_service_input(:age).must(:be_positive, be_adult: "Must be an adult") }
|
|
24
|
+
# it { is_expected.to have_service_input(:age).must(be_positive: /positive/, be_adult: :default) }
|
|
25
|
+
# it { is_expected.to have_service_input(:age).must(:be_adult).message("Must be an adult") }
|
|
22
26
|
# ```
|
|
23
27
|
#
|
|
24
28
|
# ## Comparison
|
|
25
29
|
#
|
|
26
30
|
# Uses set difference to compare validation names - order doesn't matter.
|
|
31
|
+
# Bare names and the keys of expected messages together must name
|
|
32
|
+
# every rule of the attribute.
|
|
33
|
+
#
|
|
34
|
+
# ## Messages
|
|
35
|
+
#
|
|
36
|
+
# `.message` chained right after `.must` naming one rule is the same
|
|
37
|
+
# as the keyed form for that rule. After `.must` naming several
|
|
38
|
+
# rules, `.message` raises ArgumentError.
|
|
39
|
+
#
|
|
40
|
+
# An expected message follows the rules of `.message`: a String is
|
|
41
|
+
# compared with the message exactly, a Regexp is matched against it,
|
|
42
|
+
# an RSpec matcher is applied to the message as defined, and
|
|
43
|
+
# `:default` checks that the rule defines no custom message.
|
|
44
|
+
#
|
|
45
|
+
# Without a custom message, the rule uses the default message of
|
|
46
|
+
# the library, which depends only on the service, the attribute and
|
|
47
|
+
# the rule name, so a String or Regexp is compared with it.
|
|
48
|
+
#
|
|
49
|
+
# A Proc message is called with the keyword arguments the library
|
|
50
|
+
# passes to it: `service:`, the attribute (`input:`, `internal:` or
|
|
51
|
+
# `output:`), `value:`, `code:`, `reason:` and `meta:`. Values known
|
|
52
|
+
# only while the service runs are `nil`. A Proc message that raises
|
|
53
|
+
# with them, for example because it does not accept all of them,
|
|
54
|
+
# does not match.
|
|
55
|
+
#
|
|
56
|
+
# Rules generated by option helpers, such as `be_in_format` of the
|
|
57
|
+
# `format` option or `be_greater_than_or_equal_to` of `min`, may be
|
|
58
|
+
# listed by name, but their messages depend on the validated value:
|
|
59
|
+
# expecting a message for them raises ArgumentError. Check these
|
|
60
|
+
# messages with `raise_error` instead.
|
|
27
61
|
class MustSubmatcher < Base::Submatcher
|
|
62
|
+
include Concerns::OptionHelperRules
|
|
63
|
+
include Concerns::ProcMessage
|
|
64
|
+
|
|
65
|
+
# NOTE: Dynamic options that are also `must` but tested separately
|
|
66
|
+
BUILT_IN_RULE_NAMES = %i[consists_of schema be_inclusion be_target].freeze
|
|
67
|
+
|
|
68
|
+
# Splits the arguments of the `must` chain method.
|
|
69
|
+
#
|
|
70
|
+
# A trailing Hash, which Ruby passes for keyword arguments,
|
|
71
|
+
# maps rule names to their expected messages.
|
|
72
|
+
#
|
|
73
|
+
# @param args [Array] The arguments of the chain method
|
|
74
|
+
# @return [Array(Array<Symbol>, Hash{Symbol => Object})] Rule names and expected messages
|
|
75
|
+
def self.arguments_from(args)
|
|
76
|
+
expectations, names = Array(args).flatten.partition { |argument| argument.is_a?(Hash) }
|
|
77
|
+
|
|
78
|
+
[names, expectations.reduce({}, :merge)]
|
|
79
|
+
end
|
|
80
|
+
|
|
28
81
|
# Creates a new must submatcher.
|
|
29
82
|
#
|
|
30
83
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
31
84
|
# @param must_names [Array<Symbol>] Expected validation rule names
|
|
85
|
+
# @param message_expectations [Hash{Symbol => Object}] Expected messages by rule name
|
|
32
86
|
# @return [MustSubmatcher] New submatcher instance
|
|
33
|
-
|
|
87
|
+
# @raise [ArgumentError] If an expected message is of an unsupported kind,
|
|
88
|
+
# or belongs to a rule generated by an option helper
|
|
89
|
+
def initialize(context, must_names, message_expectations = {})
|
|
34
90
|
super(context)
|
|
35
|
-
@must_names = must_names
|
|
91
|
+
@must_names = (must_names + message_expectations.keys).uniq
|
|
92
|
+
@message_expectations = message_expectations.transform_values do |expected|
|
|
93
|
+
Base::MessageExpectation.new(expected)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
message_expectations.each_key { |name| ensure_message_checkable!(name) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Stores the message expected by `.message` chained right after `.must`.
|
|
100
|
+
#
|
|
101
|
+
# `.must(:rule).message(expected)` is the same as `.must(rule: expected)`.
|
|
102
|
+
# Each rule accepts one expected message.
|
|
103
|
+
#
|
|
104
|
+
# @param expected [String, Regexp, Symbol, Object] Expected message, `:default` or an RSpec matcher
|
|
105
|
+
# @return [void]
|
|
106
|
+
# @raise [ArgumentError] If `must` does not name exactly one rule, the rule already has
|
|
107
|
+
# an expected message, or the message cannot be checked
|
|
108
|
+
def expect_message(expected)
|
|
109
|
+
raise ArgumentError, several_rules_error_message unless must_names.one?
|
|
110
|
+
|
|
111
|
+
name = must_names.first
|
|
112
|
+
raise ArgumentError, "Rule `#{name}` already has an expected message" if message_expectations.key?(name)
|
|
113
|
+
|
|
114
|
+
ensure_message_checkable!(name)
|
|
115
|
+
message_expectations[name] = Base::MessageExpectation.new(expected)
|
|
36
116
|
end
|
|
37
117
|
|
|
38
118
|
# Returns description for RSpec output.
|
|
39
119
|
#
|
|
40
120
|
# @return [String] Human-readable description with rule names
|
|
41
121
|
def description
|
|
42
|
-
"must: #{must_names.join(', ')}"
|
|
122
|
+
"must: #{must_names.map { |name| rule_description(name) }.join(', ')}"
|
|
43
123
|
end
|
|
44
124
|
|
|
45
125
|
protected
|
|
@@ -48,31 +128,126 @@ module Servactory
|
|
|
48
128
|
#
|
|
49
129
|
# Filters out dynamic options that are tested by other submatchers.
|
|
50
130
|
#
|
|
51
|
-
# @return [Boolean] True if must rules match (order-independent)
|
|
131
|
+
# @return [Boolean] True if must rules and their messages match (order-independent)
|
|
52
132
|
def passes?
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# NOTE: Dynamic options that are also `must` but tested separately
|
|
57
|
-
attribute_must_keys.delete(:consists_of)
|
|
58
|
-
attribute_must_keys.delete(:schema)
|
|
59
|
-
attribute_must_keys.delete(:be_inclusion)
|
|
60
|
-
attribute_must_keys.delete(:be_target)
|
|
133
|
+
@actual_must_names = actual_must_names
|
|
134
|
+
@message_mismatches = find_message_mismatches
|
|
61
135
|
|
|
62
|
-
|
|
63
|
-
must_names.difference(attribute_must_keys).empty?
|
|
136
|
+
names_match? && @message_mismatches.empty?
|
|
64
137
|
end
|
|
65
138
|
|
|
66
139
|
# Builds the failure message for must validation.
|
|
67
140
|
#
|
|
68
|
-
# @return [String]
|
|
141
|
+
# @return [String] Failure message with expected vs actual rules and messages
|
|
69
142
|
def build_failure_message
|
|
70
|
-
|
|
143
|
+
messages = @message_mismatches.map do |name, mismatch|
|
|
144
|
+
"should return expected message for must rule `#{name}`:\n\n#{mismatch.indent(2)}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
messages.unshift(names_failure_message) unless names_match?
|
|
148
|
+
messages.join("\n")
|
|
71
149
|
end
|
|
72
150
|
|
|
73
151
|
private
|
|
74
152
|
|
|
75
|
-
attr_reader :must_names
|
|
153
|
+
attr_reader :must_names, :message_expectations
|
|
154
|
+
|
|
155
|
+
# Ensures that the message of a rule does not depend on the validated value.
|
|
156
|
+
#
|
|
157
|
+
# @param name [Symbol] Rule name
|
|
158
|
+
# @return [void]
|
|
159
|
+
# @raise [ArgumentError] If an option helper generated the rule
|
|
160
|
+
def ensure_message_checkable!(name)
|
|
161
|
+
return if BUILT_IN_RULE_NAMES.include?(name) || !option_helper_rule?(name)
|
|
162
|
+
|
|
163
|
+
raise ArgumentError,
|
|
164
|
+
"The message of must rule `#{name}` cannot be checked: an option helper generated the rule, " \
|
|
165
|
+
"and its message depends on the validated value. List the rule by name, as in " \
|
|
166
|
+
"`must(:#{name})`, and check the error message with `raise_error`."
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Builds the error message for `.message` after `.must` naming several rules.
|
|
170
|
+
#
|
|
171
|
+
# @return [String] Error message
|
|
172
|
+
def several_rules_error_message
|
|
173
|
+
return "`message` after `must` needs a rule name, e.g. `must(:rule).message(...)`." if must_names.empty?
|
|
174
|
+
|
|
175
|
+
keyed = must_names.map { |name| "#{name}: ..." }.join(", ")
|
|
176
|
+
|
|
177
|
+
"`message` after `must` needs exactly one rule, got #{must_names.size}. " \
|
|
178
|
+
"Use the keyed form to expect a message for each rule, as in `must(#{keyed})`."
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Describes a rule name with its expected message.
|
|
182
|
+
#
|
|
183
|
+
# @param name [Symbol] Rule name
|
|
184
|
+
# @return [String] Rule description
|
|
185
|
+
def rule_description(name)
|
|
186
|
+
expectation = message_expectations[name]
|
|
187
|
+
return name.to_s if expectation.nil?
|
|
188
|
+
|
|
189
|
+
"#{name} with message #{expectation.description}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Returns the attribute's must rules.
|
|
193
|
+
#
|
|
194
|
+
# @return [Hash{Symbol => Hash, Proc}] Rules by name
|
|
195
|
+
def attribute_must
|
|
196
|
+
attribute_data[:must] || {}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Returns the names of the attribute's rules checked by this submatcher.
|
|
200
|
+
#
|
|
201
|
+
# @return [Array<Symbol>] Rule names
|
|
202
|
+
def actual_must_names
|
|
203
|
+
attribute_must.keys - BUILT_IN_RULE_NAMES
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Compares the expected rule names with the attribute's rule names.
|
|
207
|
+
#
|
|
208
|
+
# @return [Boolean] True if both name the same rules
|
|
209
|
+
def names_match?
|
|
210
|
+
@actual_must_names.difference(must_names).empty? &&
|
|
211
|
+
must_names.difference(@actual_must_names).empty?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Builds the failure message for mismatching rule names.
|
|
215
|
+
#
|
|
216
|
+
# @return [String] Failure message with expected vs actual rules
|
|
217
|
+
def names_failure_message
|
|
218
|
+
<<~MESSAGE
|
|
219
|
+
should #{must_names.join(', ')}
|
|
220
|
+
|
|
221
|
+
expected must rules: #{build_list_message(must_names)}
|
|
222
|
+
got must rules: #{build_list_message(@actual_must_names)}
|
|
223
|
+
MESSAGE
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Compares the messages of the attribute's rules with the expected messages.
|
|
227
|
+
#
|
|
228
|
+
# Rules missing from the attribute are reported by the name comparison.
|
|
229
|
+
#
|
|
230
|
+
# @return [Hash{Symbol => String}] Explanations of the mismatches by rule name
|
|
231
|
+
def find_message_mismatches
|
|
232
|
+
message_expectations.filter_map do |name, expectation|
|
|
233
|
+
next unless attribute_must.key?(name)
|
|
234
|
+
|
|
235
|
+
mismatch = expectation.mismatch_for(
|
|
236
|
+
rule_message(attribute_must.fetch(name)),
|
|
237
|
+
default_message: Servactory::Maintenance::Validations::Translator::Must.default_message
|
|
238
|
+
) { |message| call_proc_message(message, :must, code: name) }
|
|
239
|
+
|
|
240
|
+
[name, mismatch] unless mismatch.nil?
|
|
241
|
+
end.to_h
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns the custom message of a rule.
|
|
245
|
+
#
|
|
246
|
+
# @param rule [Hash, Proc] The rule in advanced or simple mode
|
|
247
|
+
# @return [String, Proc, nil] The custom message
|
|
248
|
+
def rule_message(rule)
|
|
249
|
+
rule.is_a?(Hash) ? rule[:message] : nil
|
|
250
|
+
end
|
|
76
251
|
end
|
|
77
252
|
end
|
|
78
253
|
end
|
|
@@ -29,6 +29,9 @@ module Servactory
|
|
|
29
29
|
# Key for the target values within the option
|
|
30
30
|
OPTION_BODY_KEY = :in
|
|
31
31
|
|
|
32
|
+
# @return [Symbol] The name of the target option
|
|
33
|
+
attr_reader :option_name
|
|
34
|
+
|
|
32
35
|
# Creates a new target submatcher.
|
|
33
36
|
#
|
|
34
37
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
@@ -77,8 +80,7 @@ module Servactory
|
|
|
77
80
|
|
|
78
81
|
private
|
|
79
82
|
|
|
80
|
-
attr_reader :
|
|
81
|
-
:values
|
|
83
|
+
attr_reader :values
|
|
82
84
|
|
|
83
85
|
# Formats values for human-readable description.
|
|
84
86
|
#
|
|
@@ -19,6 +19,7 @@ module Servactory
|
|
|
19
19
|
# it { is_expected.to have_service_input(:user_id).type(Integer) }
|
|
20
20
|
# it { is_expected.to have_service_input(:data).types(String, Hash) }
|
|
21
21
|
# it { is_expected.to have_service_internal(:result).type(Array) }
|
|
22
|
+
# it { is_expected.to have_service_input(:user_id).type(Integer).message("User ID must be an Integer") }
|
|
22
23
|
# ```
|
|
23
24
|
#
|
|
24
25
|
# ## Comparison
|
|
@@ -26,6 +27,11 @@ module Servactory
|
|
|
26
27
|
# Types are compared by name, sorted alphabetically. Order of types
|
|
27
28
|
# in the definition doesn't matter - only the set of types must match.
|
|
28
29
|
class TypesSubmatcher < Base::Submatcher
|
|
30
|
+
# Option name in attribute data
|
|
31
|
+
OPTION_NAME = :type
|
|
32
|
+
# Key for the types within the option
|
|
33
|
+
OPTION_BODY_KEY = :is
|
|
34
|
+
|
|
29
35
|
# Creates a new types submatcher.
|
|
30
36
|
#
|
|
31
37
|
# @param context [Base::SubmatcherContext] The submatcher context
|
|
@@ -44,8 +44,12 @@
|
|
|
44
44
|
require_relative "matchers/concerns/attribute_data_access"
|
|
45
45
|
require_relative "matchers/concerns/error_message_builder"
|
|
46
46
|
require_relative "matchers/concerns/value_comparison"
|
|
47
|
+
require_relative "matchers/concerns/option_helper_rules"
|
|
48
|
+
require_relative "matchers/concerns/proc_message"
|
|
49
|
+
require_relative "matchers/concerns/required_message"
|
|
47
50
|
|
|
48
51
|
# Base classes (loaded after concerns)
|
|
52
|
+
require_relative "matchers/base/message_expectation"
|
|
49
53
|
require_relative "matchers/base/submatcher_context"
|
|
50
54
|
require_relative "matchers/base/submatcher"
|
|
51
55
|
require_relative "matchers/base/submatcher_registry"
|
|
@@ -89,6 +93,12 @@ module Servactory
|
|
|
89
93
|
# expect(MyService).to have_service_input(:user_id)
|
|
90
94
|
# .type(Integer)
|
|
91
95
|
# .required
|
|
96
|
+
#
|
|
97
|
+
# @example With messages
|
|
98
|
+
# expect(MyService).to have_service_input(:age)
|
|
99
|
+
# .type(Integer).message("Age must be an Integer")
|
|
100
|
+
# .required(/is required/)
|
|
101
|
+
# .must(:be_adult, be_positive: :default)
|
|
92
102
|
def have_service_input(input_name) # rubocop:disable Naming/PredicatePrefix
|
|
93
103
|
HaveServiceInputMatcher.new(described_class, input_name)
|
|
94
104
|
end
|
|
@@ -101,6 +111,11 @@ module Servactory
|
|
|
101
111
|
# @example
|
|
102
112
|
# expect(MyService).to have_service_internal(:processed_data)
|
|
103
113
|
# .type(Hash)
|
|
114
|
+
#
|
|
115
|
+
# @example With messages
|
|
116
|
+
# expect(MyService).to have_service_internal(:total)
|
|
117
|
+
# .type(Integer).message(:default)
|
|
118
|
+
# .must(:be_positive).message("Total must be positive")
|
|
104
119
|
def have_service_internal(internal_name) # rubocop:disable Naming/PredicatePrefix
|
|
105
120
|
HaveServiceInternalMatcher.new(described_class, internal_name)
|
|
106
121
|
end
|
|
@@ -90,7 +90,7 @@ module Servactory
|
|
|
90
90
|
# @param option_name [Symbol] The option name (default: :consists_of)
|
|
91
91
|
# @param collection_mode_class_names [Servactory::Configuration::CollectionMode::ClassNamesCollection]
|
|
92
92
|
# Valid collection types
|
|
93
|
-
# @return [Servactory::Maintenance::
|
|
93
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
94
94
|
def self.use(option_name = :consists_of, collection_mode_class_names:)
|
|
95
95
|
instance = new(option_name, :type, false)
|
|
96
96
|
instance.assign(collection_mode_class_names)
|
|
@@ -162,7 +162,7 @@ module Servactory
|
|
|
162
162
|
# @param option [WorkOption] Type configuration
|
|
163
163
|
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
164
164
|
def validate_for!(attribute:, values:, option:)
|
|
165
|
-
consists_of_types = Array(option.value)
|
|
165
|
+
consists_of_types = Array(option.value)
|
|
166
166
|
|
|
167
167
|
# Check presence requirements.
|
|
168
168
|
return [false, :required] if fails_presence_validation?(attribute:, values:, consists_of_types:)
|
|
@@ -217,8 +217,7 @@ module Servactory
|
|
|
217
217
|
service_class_name: service.class_name,
|
|
218
218
|
input_name: input.name,
|
|
219
219
|
option_name:,
|
|
220
|
-
|
|
221
|
-
given_type: given_type_for(values: value, option_value:)
|
|
220
|
+
**type_names_for(attribute: input, values: value, option_value:, reason:)
|
|
222
221
|
)
|
|
223
222
|
end
|
|
224
223
|
|
|
@@ -240,8 +239,7 @@ module Servactory
|
|
|
240
239
|
service_class_name: service.class_name,
|
|
241
240
|
internal_name: internal.name,
|
|
242
241
|
option_name:,
|
|
243
|
-
|
|
244
|
-
given_type: given_type_for(values: value, option_value:)
|
|
242
|
+
**type_names_for(attribute: internal, values: value, option_value:, reason:)
|
|
245
243
|
)
|
|
246
244
|
end
|
|
247
245
|
|
|
@@ -263,11 +261,34 @@ module Servactory
|
|
|
263
261
|
service_class_name: service.class_name,
|
|
264
262
|
output_name: output.name,
|
|
265
263
|
option_name:,
|
|
266
|
-
|
|
267
|
-
given_type: given_type_for(values: value, option_value:)
|
|
264
|
+
**type_names_for(attribute: output, values: value, option_value:, reason:)
|
|
268
265
|
)
|
|
269
266
|
end
|
|
270
267
|
|
|
268
|
+
# Builds the expected and given type names for a failure message.
|
|
269
|
+
#
|
|
270
|
+
# For `:wrong_type` the attribute itself is not of a collection type,
|
|
271
|
+
# so the configured collection types are expected and the declared
|
|
272
|
+
# attribute types are given. For other reasons the element types
|
|
273
|
+
# are compared.
|
|
274
|
+
#
|
|
275
|
+
# @param attribute [Object] The attribute being validated
|
|
276
|
+
# @param values [Object] Failed value
|
|
277
|
+
# @param option_value [Object] Expected element types
|
|
278
|
+
# @param reason [Symbol] Failure reason
|
|
279
|
+
# @return [Hash] Hash with :expected_type and :given_type keys
|
|
280
|
+
def type_names_for(attribute:, values:, option_value:, reason:)
|
|
281
|
+
if reason == :wrong_type
|
|
282
|
+
expected_type = @collection_mode_class_names.to_a.join(", ")
|
|
283
|
+
given_type = attribute.types.join(", ")
|
|
284
|
+
else
|
|
285
|
+
expected_type = Array(option_value).uniq.join(", ")
|
|
286
|
+
given_type = given_type_for(values:, option_value:)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
{ expected_type:, given_type: }
|
|
290
|
+
end
|
|
291
|
+
|
|
271
292
|
# Extracts type names of elements that don't match expected types.
|
|
272
293
|
#
|
|
273
294
|
# @param values [Array, nil] Collection elements
|
|
@@ -179,7 +179,7 @@ module Servactory
|
|
|
179
179
|
#
|
|
180
180
|
# @param option_name [Symbol] The option name (default: :format)
|
|
181
181
|
# @param formats [Hash] Custom format definitions to merge with defaults
|
|
182
|
-
# @return [Servactory::Maintenance::
|
|
182
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
183
183
|
def self.use(option_name = :format, formats: {})
|
|
184
184
|
instance = new(option_name)
|
|
185
185
|
instance.assign(formats)
|
|
@@ -232,7 +232,7 @@ module Servactory
|
|
|
232
232
|
# @param internal [Object, nil] Internal attribute if applicable
|
|
233
233
|
# @param output [Object, nil] Output attribute if applicable
|
|
234
234
|
# @return [Boolean, Array] true if valid, or [false, reason]
|
|
235
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
235
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
236
236
|
def common_condition_with(value:, option:, input: nil, internal: nil, output: nil)
|
|
237
237
|
option_value = option.value&.to_sym
|
|
238
238
|
|
|
@@ -269,7 +269,6 @@ module Servactory
|
|
|
269
269
|
# Run validator callback.
|
|
270
270
|
option.properties.fetch(:validator, format_options.fetch(:validator)).call(value:)
|
|
271
271
|
end
|
|
272
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
273
272
|
|
|
274
273
|
########################################################################
|
|
275
274
|
|
|
@@ -87,7 +87,7 @@ module Servactory
|
|
|
87
87
|
# Creates an Inclusion validator instance.
|
|
88
88
|
#
|
|
89
89
|
# @param option_name [Symbol] The option name (default: :inclusion)
|
|
90
|
-
# @return [Servactory::Maintenance::
|
|
90
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
91
91
|
def self.use(option_name = :inclusion)
|
|
92
92
|
instance = new(option_name, :in)
|
|
93
93
|
instance.must(:be_inclusion)
|
|
@@ -97,7 +97,7 @@ module Servactory
|
|
|
97
97
|
# Creates a Max validator instance.
|
|
98
98
|
#
|
|
99
99
|
# @param option_name [Symbol] The option name (default: :max)
|
|
100
|
-
# @return [Servactory::Maintenance::
|
|
100
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
101
101
|
def self.use(option_name = :max)
|
|
102
102
|
new(option_name).must(:be_less_than_or_equal_to)
|
|
103
103
|
end
|
|
@@ -97,7 +97,7 @@ module Servactory
|
|
|
97
97
|
# Creates a Min validator instance.
|
|
98
98
|
#
|
|
99
99
|
# @param option_name [Symbol] The option name (default: :min)
|
|
100
|
-
# @return [Servactory::Maintenance::
|
|
100
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
101
101
|
def self.use(option_name = :min)
|
|
102
102
|
new(option_name).must(:be_greater_than_or_equal_to)
|
|
103
103
|
end
|
|
@@ -93,14 +93,22 @@ module Servactory
|
|
|
93
93
|
# ## Important Notes
|
|
94
94
|
#
|
|
95
95
|
# - Divisor must be a non-zero Numeric
|
|
96
|
-
# -
|
|
96
|
+
# - Integer, Rational and BigDecimal operands are checked exactly
|
|
97
|
+
# - Float operands are accepted when the remainder is within an epsilon
|
|
98
|
+
# tolerance of zero or of the divisor, scaled by the absolute value
|
|
99
|
+
# of the checked value
|
|
97
100
|
# - Returns false for non-numeric values
|
|
98
101
|
# - Provides specific error messages for blank and zero divisors
|
|
99
102
|
class MultipleOf < Must
|
|
103
|
+
# Relative tolerance for Float remainders, scaled by the absolute
|
|
104
|
+
# value of the checked value.
|
|
105
|
+
FLOAT_TOLERANCE = Float::EPSILON * 2
|
|
106
|
+
private_constant :FLOAT_TOLERANCE
|
|
107
|
+
|
|
100
108
|
# Creates a MultipleOf validator instance.
|
|
101
109
|
#
|
|
102
110
|
# @param option_name [Symbol] The option name (default: :multiple_of)
|
|
103
|
-
# @return [Servactory::Maintenance::
|
|
111
|
+
# @return [Servactory::Maintenance::Options::Helper]
|
|
104
112
|
def self.use(option_name = :multiple_of)
|
|
105
113
|
new(option_name).must(:be_multiple_of)
|
|
106
114
|
end
|
|
@@ -148,14 +156,34 @@ module Servactory
|
|
|
148
156
|
return false unless option.value.is_a?(Numeric)
|
|
149
157
|
return false if option.value.zero?
|
|
150
158
|
|
|
151
|
-
|
|
152
|
-
remainder = value % option.value
|
|
153
|
-
remainder.zero? || remainder.abs < Float::EPSILON * [value.abs, option.value.abs].max
|
|
159
|
+
multiple_of?(value, option.value)
|
|
154
160
|
else
|
|
155
161
|
false
|
|
156
162
|
end
|
|
157
163
|
end
|
|
158
164
|
|
|
165
|
+
# Checks whether value is a multiple of divisor.
|
|
166
|
+
#
|
|
167
|
+
# Exact for Integer, Rational and BigDecimal operands. When either
|
|
168
|
+
# operand is a Float, the remainder of the absolute operands counts
|
|
169
|
+
# as a multiple when it is within the tolerance of zero or of the
|
|
170
|
+
# divisor, with the tolerance scaled by the absolute value of the
|
|
171
|
+
# checked value.
|
|
172
|
+
#
|
|
173
|
+
# @param value [Numeric] Value to check
|
|
174
|
+
# @param divisor [Numeric] Non-zero divisor
|
|
175
|
+
# @return [Boolean] true if value is multiple of divisor
|
|
176
|
+
def multiple_of?(value, divisor)
|
|
177
|
+
return (value % divisor).zero? unless value.is_a?(Float) || divisor.is_a?(Float)
|
|
178
|
+
|
|
179
|
+
remainder = value.abs % divisor.abs
|
|
180
|
+
return true if remainder.zero?
|
|
181
|
+
return false unless remainder.finite? && divisor.finite?
|
|
182
|
+
|
|
183
|
+
tolerance = FLOAT_TOLERANCE * value.abs
|
|
184
|
+
remainder <= tolerance || divisor.abs - remainder <= tolerance
|
|
185
|
+
end
|
|
186
|
+
|
|
159
187
|
########################################################################
|
|
160
188
|
|
|
161
189
|
# Generates error message for input validation failure.
|
|
@@ -152,7 +152,7 @@ module Servactory
|
|
|
152
152
|
@body_fallback = body_fallback
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
-
# Creates an
|
|
155
|
+
# Creates an option helper for registration with Servactory.
|
|
156
156
|
#
|
|
157
157
|
# @param name [Symbol] Internal validation name
|
|
158
158
|
# @return [Servactory::Maintenance::Options::Helper]
|
|
@@ -221,7 +221,7 @@ module Servactory
|
|
|
221
221
|
#
|
|
222
222
|
# @param option [WorkOption] Parsed option data
|
|
223
223
|
# @return [Proc] Lambda that generates error message
|
|
224
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
224
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
225
225
|
def must_content_message_with(option)
|
|
226
226
|
is_option_message_present = option.message.present?
|
|
227
227
|
is_option_message_proc = option.message.is_a?(Proc) if is_option_message_present
|
|
@@ -274,7 +274,6 @@ module Servactory
|
|
|
274
274
|
end
|
|
275
275
|
end
|
|
276
276
|
end
|
|
277
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
278
277
|
|
|
279
278
|
########################################################################
|
|
280
279
|
|