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.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +7 -2
- data/config/locales/ja.yml +134 -0
- data/lib/generators/README.md +2 -2
- 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/config.rb +16 -0
- data/lib/servactory/configuration/configurable.rb +7 -4
- data/lib/servactory/configuration/factory.rb +30 -3
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -1
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +156 -29
- 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/inputs/tools/validation.rb +10 -18
- 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 +71 -30
- data/lib/servactory/maintenance/options/option.rb +3 -12
- data/lib/servactory/maintenance/options/registrar.rb +3 -13
- data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
- data/lib/servactory/maintenance/validations/performer.rb +12 -23
- 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 -125
- 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 +185 -50
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +45 -10
- 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 +34 -11
- data/lib/servactory/tool_kit/dynamic_options/format.rb +3 -4
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/max.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/min.rb +3 -3
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +35 -7
- data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +40 -25
- data/lib/servactory/tool_kit/dynamic_options/target.rb +4 -4
- data/lib/servactory/utils.rb +13 -3
- data/lib/servactory/version.rb +1 -1
- data/lib/servactory.rb +3 -4
- metadata +20 -4
|
@@ -28,18 +28,16 @@ module Servactory
|
|
|
28
28
|
# ## Performance
|
|
29
29
|
#
|
|
30
30
|
# The collection uses memoization for frequently accessed data:
|
|
31
|
-
# - `
|
|
32
|
-
# - `options_for_checks` - cached hash for validation pipeline
|
|
31
|
+
# - `validations_for_checks` - cached tuples for validation pipeline
|
|
33
32
|
# - `options_index` - cached hash for O(1) lookups by name
|
|
34
33
|
#
|
|
35
34
|
class Collection
|
|
36
35
|
extend Forwardable
|
|
37
36
|
|
|
38
37
|
def_delegators :@collection,
|
|
39
|
-
:<<,
|
|
40
38
|
:filter,
|
|
41
|
-
:each_with_object,
|
|
42
|
-
:map,
|
|
39
|
+
:each, :each_with_object,
|
|
40
|
+
:map,
|
|
43
41
|
:size,
|
|
44
42
|
:empty?
|
|
45
43
|
|
|
@@ -50,6 +48,26 @@ module Servactory
|
|
|
50
48
|
@collection = Set.new
|
|
51
49
|
end
|
|
52
50
|
|
|
51
|
+
# Duplicates the collection, resetting memoized caches.
|
|
52
|
+
#
|
|
53
|
+
# @param original [Collection] the collection being duplicated
|
|
54
|
+
# @return [void]
|
|
55
|
+
def initialize_dup(original)
|
|
56
|
+
super
|
|
57
|
+
@collection = original.instance_variable_get(:@collection).dup
|
|
58
|
+
reset_memoized_caches
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Adds an option to the collection, invalidating memoized caches.
|
|
62
|
+
#
|
|
63
|
+
# @param option [Option] the option to add
|
|
64
|
+
# @return [Collection] self
|
|
65
|
+
def <<(option)
|
|
66
|
+
@collection << option
|
|
67
|
+
reset_memoized_caches
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
53
71
|
# Returns all option names in the collection.
|
|
54
72
|
#
|
|
55
73
|
# @return [Array<Symbol>] list of option names
|
|
@@ -57,32 +75,51 @@ module Servactory
|
|
|
57
75
|
map(&:name)
|
|
58
76
|
end
|
|
59
77
|
|
|
60
|
-
#
|
|
78
|
+
# @deprecated Use {#validations_for_checks} instead.
|
|
61
79
|
#
|
|
62
80
|
# @return [Array<Class>] deduplicated list of validation classes
|
|
63
81
|
def validation_classes
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.uniq
|
|
82
|
+
warn "[DEPRECATION] Servactory::Maintenance::Options::Collection#validation_classes is deprecated. " \
|
|
83
|
+
"Use #validations_for_checks instead."
|
|
84
|
+
validations_for_checks.map(&:last).uniq
|
|
68
85
|
end
|
|
69
86
|
|
|
70
|
-
#
|
|
87
|
+
# @deprecated Use {#validations_for_checks} instead.
|
|
71
88
|
#
|
|
72
89
|
# @return [Hash{Symbol => Object}] option names mapped to normalized bodies
|
|
73
90
|
def options_for_checks
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
warn "[DEPRECATION] Servactory::Maintenance::Options::Collection#options_for_checks is deprecated. " \
|
|
92
|
+
"Use #validations_for_checks instead."
|
|
93
|
+
check_options_by_key
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns options that need validation checks as an array of tuples.
|
|
97
|
+
# Each tuple contains [check_key, check_options, validation_class],
|
|
98
|
+
# enabling direct dispatch without nested iteration.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<Array(Symbol, Object, Class)>] tuples for direct validation dispatch
|
|
101
|
+
def validations_for_checks
|
|
102
|
+
@validations_for_checks ||= filter(&:need_for_checks?).filter_map do |option|
|
|
103
|
+
next if option.validation_class.nil?
|
|
104
|
+
|
|
105
|
+
[option.name, extract_normalized_body_from(option:), option.validation_class]
|
|
76
106
|
end
|
|
77
107
|
end
|
|
78
108
|
|
|
79
109
|
# Returns the first conflict code found among options.
|
|
80
110
|
#
|
|
81
|
-
# @return [
|
|
111
|
+
# @return [Symbol, nil] conflict code or nil if no conflicts
|
|
82
112
|
def defined_conflict_code
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
|
|
113
|
+
each do |option|
|
|
114
|
+
next unless option.define_conflicts
|
|
115
|
+
|
|
116
|
+
option.define_conflicts.each do |conflict|
|
|
117
|
+
code = conflict.content.call
|
|
118
|
+
return code if code.present?
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
nil
|
|
86
123
|
end
|
|
87
124
|
|
|
88
125
|
# Finds an option by its name using indexed lookup.
|
|
@@ -95,13 +132,27 @@ module Servactory
|
|
|
95
132
|
|
|
96
133
|
private
|
|
97
134
|
|
|
135
|
+
# Builds the result of the deprecated `options_for_checks` methods
|
|
136
|
+
# without emitting a deprecation warning.
|
|
137
|
+
#
|
|
138
|
+
# @return [Hash{Symbol => Object}] option names mapped to normalized bodies
|
|
139
|
+
def check_options_by_key
|
|
140
|
+
validations_for_checks.to_h { |check_key, check_options, _| [check_key, check_options] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Resets memoized data derived from the collection contents.
|
|
144
|
+
#
|
|
145
|
+
# @return [void]
|
|
146
|
+
def reset_memoized_caches
|
|
147
|
+
@validations_for_checks = nil
|
|
148
|
+
@options_index = nil
|
|
149
|
+
end
|
|
150
|
+
|
|
98
151
|
# Builds and caches a hash index for O(1) option lookups.
|
|
99
152
|
#
|
|
100
153
|
# @return [Hash{Symbol => Option}] option names mapped to Option instances
|
|
101
154
|
def options_index
|
|
102
|
-
@options_index ||=
|
|
103
|
-
index[option.name] = option
|
|
104
|
-
end
|
|
155
|
+
@options_index ||= @collection.to_h { |option| [option.name, option] }
|
|
105
156
|
end
|
|
106
157
|
|
|
107
158
|
# Extracts the normalized body value from an option.
|
|
@@ -111,16 +162,6 @@ module Servactory
|
|
|
111
162
|
def extract_normalized_body_from(option:)
|
|
112
163
|
option.value
|
|
113
164
|
end
|
|
114
|
-
|
|
115
|
-
# Resolves conflict codes from an option's define_conflicts.
|
|
116
|
-
#
|
|
117
|
-
# @param option [Option] the option to check for conflicts
|
|
118
|
-
# @return [Array<Object>] array of conflict codes (may contain nils/blanks)
|
|
119
|
-
def resolve_conflicts_from(option:)
|
|
120
|
-
return [] unless option.define_conflicts
|
|
121
|
-
|
|
122
|
-
option.define_conflicts.map { |conflict| conflict.content.call }
|
|
123
|
-
end
|
|
124
165
|
end
|
|
125
166
|
end
|
|
126
167
|
end
|
|
@@ -17,7 +17,7 @@ module Servactory
|
|
|
17
17
|
:body_key,
|
|
18
18
|
:return_value_on_access
|
|
19
19
|
|
|
20
|
-
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
20
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists
|
|
21
21
|
def initialize(
|
|
22
22
|
name:,
|
|
23
23
|
attribute:,
|
|
@@ -54,7 +54,6 @@ module Servactory
|
|
|
54
54
|
|
|
55
55
|
apply_dynamic_methods_to(attribute:)
|
|
56
56
|
end
|
|
57
|
-
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
58
57
|
|
|
59
58
|
def value
|
|
60
59
|
return body unless body.is_a?(Hash)
|
|
@@ -72,7 +71,7 @@ module Servactory
|
|
|
72
71
|
|
|
73
72
|
private
|
|
74
73
|
|
|
75
|
-
# rubocop:disable Metrics/MethodLength
|
|
74
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
76
75
|
def construct_body(
|
|
77
76
|
original_value:,
|
|
78
77
|
options:,
|
|
@@ -81,9 +80,7 @@ module Servactory
|
|
|
81
80
|
body_fallback:,
|
|
82
81
|
detect_advanced_mode:
|
|
83
82
|
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
raw_value = options.fetch(@name, body_fallback)
|
|
83
|
+
raw_value = original_value.nil? ? options.fetch(@name, body_fallback) : original_value
|
|
87
84
|
|
|
88
85
|
result = if detect_advanced_mode
|
|
89
86
|
use_advanced_mode(
|
|
@@ -98,12 +95,6 @@ module Servactory
|
|
|
98
95
|
|
|
99
96
|
apply_normalizer(result)
|
|
100
97
|
end
|
|
101
|
-
# rubocop:enable Metrics/MethodLength
|
|
102
|
-
|
|
103
|
-
def wrap_and_normalize(value)
|
|
104
|
-
result = wrap_value(value)
|
|
105
|
-
apply_normalizer(result)
|
|
106
|
-
end
|
|
107
98
|
|
|
108
99
|
def apply_normalizer(body)
|
|
109
100
|
return body unless @normalizer && body.is_a?(Hash)
|
|
@@ -78,9 +78,10 @@ module Servactory
|
|
|
78
78
|
create_option(
|
|
79
79
|
name: :types,
|
|
80
80
|
validation_class:,
|
|
81
|
-
original_value:
|
|
81
|
+
original_value: @options[:type],
|
|
82
82
|
need_for_checks: true,
|
|
83
83
|
body_key: :is,
|
|
84
|
+
body_value: nil,
|
|
84
85
|
body_fallback: nil,
|
|
85
86
|
detect_advanced_mode: true,
|
|
86
87
|
return_value_on_access: true,
|
|
@@ -98,7 +99,7 @@ module Servactory
|
|
|
98
99
|
content: ->(option:) { !option[:is].nil? }
|
|
99
100
|
)
|
|
100
101
|
],
|
|
101
|
-
need_for_checks:
|
|
102
|
+
need_for_checks: false,
|
|
102
103
|
body_key: :is,
|
|
103
104
|
body_fallback: nil,
|
|
104
105
|
detect_advanced_mode: false,
|
|
@@ -142,17 +143,6 @@ module Servactory
|
|
|
142
143
|
)
|
|
143
144
|
end
|
|
144
145
|
|
|
145
|
-
def extract_types_value
|
|
146
|
-
type_option = @options[:type]
|
|
147
|
-
return nil if type_option.nil?
|
|
148
|
-
|
|
149
|
-
# Advanced Mode: type: { is: String, message: "..." }
|
|
150
|
-
return nil if type_option.is_a?(Hash) && type_option.key?(:is)
|
|
151
|
-
|
|
152
|
-
# Simple Mode: type: String or type: [String, Integer]
|
|
153
|
-
Array(type_option).uniq
|
|
154
|
-
end
|
|
155
|
-
|
|
156
146
|
########################################################################
|
|
157
147
|
|
|
158
148
|
def required_define_methods
|
|
@@ -21,11 +21,19 @@ module Servactory
|
|
|
21
21
|
# input :id, type: Integer
|
|
22
22
|
# input :data, type: [Hash, Array] # union type
|
|
23
23
|
# input :status, type: String, required: false
|
|
24
|
+
# input :email, type: { is: String, message: "Email must be a String" }
|
|
24
25
|
#
|
|
25
26
|
# internal :result, type: CustomType
|
|
26
27
|
# output :response, type: Hash
|
|
27
28
|
# end
|
|
28
29
|
# ```
|
|
30
|
+
#
|
|
31
|
+
# ## Custom Message
|
|
32
|
+
#
|
|
33
|
+
# Advanced mode `type: { is:, message: }` replaces the default error
|
|
34
|
+
# message for every type failure of the attribute. A Proc message
|
|
35
|
+
# receives `service:`, `input:`/`internal:`/`output:`, `value:`,
|
|
36
|
+
# `expected_type:` and `given_type:`.
|
|
29
37
|
class Type
|
|
30
38
|
extend Concerns::ErrorBuilder
|
|
31
39
|
|
|
@@ -51,7 +59,7 @@ module Servactory
|
|
|
51
59
|
|
|
52
60
|
return if error_data.nil?
|
|
53
61
|
|
|
54
|
-
build_error_message(error_data)
|
|
62
|
+
build_error_message(attribute:, error_data:)
|
|
55
63
|
end
|
|
56
64
|
|
|
57
65
|
# Determines if type validation should run for given attribute.
|
|
@@ -67,7 +75,7 @@ module Servactory
|
|
|
67
75
|
# @param value [Object] Current value (for optional input check)
|
|
68
76
|
# @param check_key [Symbol] Current validation check key
|
|
69
77
|
# @return [Boolean] true if type validation should run
|
|
70
|
-
# rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
78
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
71
79
|
def self.should_be_checked_for?(attribute, value, check_key)
|
|
72
80
|
check_key == :types && (
|
|
73
81
|
(
|
|
@@ -81,7 +89,6 @@ module Servactory
|
|
|
81
89
|
) || attribute.internal? || attribute.output?
|
|
82
90
|
)
|
|
83
91
|
end
|
|
84
|
-
# rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
85
92
|
private_class_method :should_be_checked_for?
|
|
86
93
|
|
|
87
94
|
# Computes prepared value with default substitution for optional inputs.
|
|
@@ -100,10 +107,21 @@ module Servactory
|
|
|
100
107
|
|
|
101
108
|
# Builds error message from validation error data.
|
|
102
109
|
#
|
|
110
|
+
# Uses the custom message from `type: { is:, message: }` if provided,
|
|
111
|
+
# otherwise falls back to the default message.
|
|
112
|
+
#
|
|
113
|
+
# @param attribute [Inputs::Input, Internals::Internal, Outputs::Output] Failed attribute
|
|
103
114
|
# @param error_data [Hash] Error data from TypeValidator.validate
|
|
104
115
|
# @return [String] Processed error message
|
|
105
|
-
def self.build_error_message(error_data)
|
|
106
|
-
|
|
116
|
+
def self.build_error_message(attribute:, error_data:)
|
|
117
|
+
message = attribute.collection_of_options.find_by(name: :types).body[:message]
|
|
118
|
+
return process_message(error_data[:message], **error_data) if message.blank?
|
|
119
|
+
|
|
120
|
+
process_message(
|
|
121
|
+
message,
|
|
122
|
+
**attribute.typed_actor_kwargs,
|
|
123
|
+
**error_data.slice(:service, :value, :expected_type, :given_type)
|
|
124
|
+
)
|
|
107
125
|
end
|
|
108
126
|
private_class_method :build_error_message
|
|
109
127
|
end
|
|
@@ -19,14 +19,11 @@ module Servactory
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
|
-
def process(context:, attribute:, value:)
|
|
23
|
-
attribute.
|
|
22
|
+
def process(context:, attribute:, value:)
|
|
23
|
+
attribute.collection_of_options.validations_for_checks.each do |check_key, check_options, validation_class|
|
|
24
24
|
error = process_option(
|
|
25
|
-
context:,
|
|
26
|
-
|
|
27
|
-
value:,
|
|
28
|
-
check_key:,
|
|
29
|
-
check_options:
|
|
25
|
+
context:, attribute:, value:,
|
|
26
|
+
check_key:, check_options:, validation_class:
|
|
30
27
|
)
|
|
31
28
|
return error if error.present?
|
|
32
29
|
end
|
|
@@ -34,22 +31,14 @@ module Servactory
|
|
|
34
31
|
nil
|
|
35
32
|
end
|
|
36
33
|
|
|
37
|
-
def process_option(context:, attribute:, value:, check_key:, check_options:)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
value:,
|
|
46
|
-
check_key:,
|
|
47
|
-
check_options:
|
|
48
|
-
)
|
|
49
|
-
return error_message if error_message.present?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
nil
|
|
34
|
+
def process_option(context:, attribute:, value:, check_key:, check_options:, validation_class:)
|
|
35
|
+
validation_class.check(
|
|
36
|
+
context:,
|
|
37
|
+
attribute:,
|
|
38
|
+
value:,
|
|
39
|
+
check_key:,
|
|
40
|
+
check_options:
|
|
41
|
+
)
|
|
53
42
|
end
|
|
54
43
|
end
|
|
55
44
|
end
|
|
@@ -17,12 +17,18 @@ module Servactory
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def output(name, *helpers, **options)
|
|
20
|
-
|
|
20
|
+
output = Output.new(
|
|
21
21
|
name,
|
|
22
22
|
*helpers,
|
|
23
23
|
option_helpers: config.output_option_helpers,
|
|
24
24
|
**options
|
|
25
25
|
)
|
|
26
|
+
|
|
27
|
+
if output.types.empty?
|
|
28
|
+
raise ArgumentError, "[#{self.name}] Output attribute `#{name}` must have the `type` option"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
collection_of_outputs << output
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
def collection_of_outputs
|
data/lib/servactory/result.rb
CHANGED
|
@@ -41,7 +41,7 @@ module Servactory
|
|
|
41
41
|
# ```ruby
|
|
42
42
|
# result.active? # Equivalent to Utils.query_attribute(result.active)
|
|
43
43
|
# ```
|
|
44
|
-
class Result
|
|
44
|
+
class Result # rubocop:disable Metrics/ClassLength
|
|
45
45
|
# Internal container for service output values.
|
|
46
46
|
#
|
|
47
47
|
# Provides dynamic method access to output values.
|
|
@@ -120,6 +120,10 @@ module Servactory
|
|
|
120
120
|
|
|
121
121
|
private_constant :Outputs
|
|
122
122
|
|
|
123
|
+
# Result state keys exposed by {#deconstruct_keys}.
|
|
124
|
+
STATE_KEYS = %i[success failure error].freeze
|
|
125
|
+
private_constant :STATE_KEYS
|
|
126
|
+
|
|
123
127
|
############################################################################
|
|
124
128
|
|
|
125
129
|
# Creates a success result for the given context.
|
|
@@ -198,20 +202,27 @@ module Servactory
|
|
|
198
202
|
|
|
199
203
|
# Converts outputs to hash.
|
|
200
204
|
#
|
|
201
|
-
#
|
|
205
|
+
# Returns only outputs that were assigned during the call, including
|
|
206
|
+
# outputs explicitly assigned nil. Declared outputs that were never
|
|
207
|
+
# assigned are absent. Excludes predicate methods from the result hash.
|
|
202
208
|
#
|
|
203
|
-
# @return [Hash<Symbol, Object>]
|
|
209
|
+
# @return [Hash<Symbol, Object>] Assigned output name-value pairs
|
|
204
210
|
#
|
|
205
211
|
# @example
|
|
206
|
-
# result.to_h # => { user: #<User>, token:
|
|
212
|
+
# result.to_h # => { user: #<User>, token: nil }
|
|
207
213
|
def to_h
|
|
208
|
-
|
|
214
|
+
hash = {}
|
|
215
|
+
outputs.send(:output_names).each { |key| hash[key] = outputs.public_send(key) }
|
|
216
|
+
hash
|
|
209
217
|
end
|
|
210
218
|
|
|
211
219
|
# Pattern matching support.
|
|
212
220
|
#
|
|
213
221
|
# Returns hash of result state and output values for use with case/in.
|
|
214
|
-
#
|
|
222
|
+
# Output keys match {#to_h}: only assigned outputs are present.
|
|
223
|
+
# State keys (:success, :failure, :error) take priority over output names:
|
|
224
|
+
# outputs with these names are not included, and :error is present only
|
|
225
|
+
# for failures.
|
|
215
226
|
#
|
|
216
227
|
# @param keys [Array<Symbol>, nil] Keys to include, or nil for all
|
|
217
228
|
# @return [Hash<Symbol, Object>] Hash of state and outputs for pattern matching
|
|
@@ -234,6 +245,8 @@ module Servactory
|
|
|
234
245
|
available[:error] = error if failure?
|
|
235
246
|
|
|
236
247
|
outputs.send(:output_names).each do |name|
|
|
248
|
+
next if STATE_KEYS.include?(name)
|
|
249
|
+
|
|
237
250
|
available[name] = outputs.public_send(name)
|
|
238
251
|
end
|
|
239
252
|
|
|
@@ -347,7 +360,7 @@ module Servactory
|
|
|
347
360
|
Outputs.new(
|
|
348
361
|
outputs: build_outputs_hash,
|
|
349
362
|
predicate_methods_enabled:
|
|
350
|
-
@context.is_a?(Servactory::TestKit::Result) || @context.config.predicate_methods_enabled
|
|
363
|
+
@context.is_a?(Servactory::TestKit::Result) || @context.class.config.predicate_methods_enabled
|
|
351
364
|
)
|
|
352
365
|
end
|
|
353
366
|
|
|
@@ -374,7 +387,7 @@ module Servactory
|
|
|
374
387
|
def rescue_no_method_error_with(exception:) # rubocop:disable Metrics/MethodLength
|
|
375
388
|
raise exception if @context.blank? || @context.instance_of?(Servactory::TestKit::Result)
|
|
376
389
|
|
|
377
|
-
raise @context.config.failure_class.new(
|
|
390
|
+
raise @context.class.config.failure_class.new(
|
|
378
391
|
type: :base,
|
|
379
392
|
message: @context.send(:servactory_service_info).translate(
|
|
380
393
|
"common.undefined_method.missing_name",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Helpers
|
|
7
|
+
# Validates the exception of a mock configuration.
|
|
8
|
+
#
|
|
9
|
+
# ## Purpose
|
|
10
|
+
#
|
|
11
|
+
# Ensures that failure mocks have an exception and that the exception
|
|
12
|
+
# type matches the service configuration. Called automatically by
|
|
13
|
+
# MockExecutor for every config before stubbing.
|
|
14
|
+
#
|
|
15
|
+
# ## Usage
|
|
16
|
+
#
|
|
17
|
+
# ```ruby
|
|
18
|
+
# ExceptionValidator.validate!(config)
|
|
19
|
+
# ```
|
|
20
|
+
#
|
|
21
|
+
# ## Validation Strategy
|
|
22
|
+
#
|
|
23
|
+
# - `.call` - accepts any Servactory::Exceptions::Failure subclass
|
|
24
|
+
# - `.call!` - requires the service's configured failure_class or its subclass
|
|
25
|
+
class ExceptionValidator
|
|
26
|
+
include Concerns::ErrorMessages
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Validates a config and raises on failure.
|
|
30
|
+
#
|
|
31
|
+
# @param config [ServiceMockConfig] The config to validate
|
|
32
|
+
# @return [void]
|
|
33
|
+
# @raise [ArgumentError] If config is invalid
|
|
34
|
+
def validate!(config)
|
|
35
|
+
new(config).validate!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Creates a new validator instance.
|
|
40
|
+
#
|
|
41
|
+
# @param config [ServiceMockConfig] The config to validate
|
|
42
|
+
# @return [ExceptionValidator] New validator
|
|
43
|
+
def initialize(config)
|
|
44
|
+
@config = config
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validates a single configuration.
|
|
48
|
+
#
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [ArgumentError] If config is invalid
|
|
51
|
+
def validate!
|
|
52
|
+
validate_failure_has_exception!
|
|
53
|
+
validate_exception_type!
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Validates that failure configs have an exception.
|
|
59
|
+
#
|
|
60
|
+
# @return [void]
|
|
61
|
+
# @raise [ArgumentError] If failure config is missing exception
|
|
62
|
+
def validate_failure_has_exception!
|
|
63
|
+
return unless @config.failure? && @config.exception.nil?
|
|
64
|
+
|
|
65
|
+
raise ArgumentError, missing_exception_for_failure_message(@config.service_class)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Validates that exception is the correct type for the service.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
# @raise [ArgumentError] If exception type is wrong
|
|
72
|
+
def validate_exception_type!
|
|
73
|
+
return if @config.exception.nil?
|
|
74
|
+
return if valid_exception_type?
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, invalid_exception_type_message(
|
|
77
|
+
service_class: @config.service_class,
|
|
78
|
+
expected_class: failure_class,
|
|
79
|
+
actual_class: @config.exception.class
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Checks if exception is the correct type.
|
|
84
|
+
#
|
|
85
|
+
# Uses different validation strategies based on method type:
|
|
86
|
+
# - For `.call` (non-bang): Relaxed validation - accepts any Servactory::Exceptions::Failure subclass
|
|
87
|
+
# because the exception is wrapped in Result and never raised, so type doesn't matter.
|
|
88
|
+
# - For `.call!` (bang): Strict validation - requires the service's configured failure_class
|
|
89
|
+
# because the exception IS raised and type matters for rescue clauses.
|
|
90
|
+
#
|
|
91
|
+
# @return [Boolean] True if exception is valid type
|
|
92
|
+
def valid_exception_type?
|
|
93
|
+
if @config.bang_method?
|
|
94
|
+
# Strict validation for call! - exception will be raised
|
|
95
|
+
@config.exception.is_a?(failure_class)
|
|
96
|
+
else
|
|
97
|
+
# Relaxed validation for call - exception is only wrapped in Result
|
|
98
|
+
@config.exception.is_a?(Servactory::Exceptions::Failure)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns the expected failure class for the service.
|
|
103
|
+
#
|
|
104
|
+
# @return [Class] The service's failure class
|
|
105
|
+
def failure_class
|
|
106
|
+
@config.service_class.config.failure_class
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -133,7 +133,9 @@ module Servactory
|
|
|
133
133
|
# @raise [ValidationError] If service has required inputs
|
|
134
134
|
# @return [void]
|
|
135
135
|
def validate_service_has_no_required_inputs!
|
|
136
|
-
required_inputs = @service_class.info.inputs.
|
|
136
|
+
required_inputs = @service_class.info.inputs.filter_map do |input_name, input|
|
|
137
|
+
input_name if input.fetch(:actor).required?
|
|
138
|
+
end
|
|
137
139
|
|
|
138
140
|
return if required_inputs.empty?
|
|
139
141
|
|