servactory 2.16.1 → 3.0.0.rc1
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 +38 -9
- data/config/locales/de.yml +134 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +134 -0
- data/config/locales/fr.yml +134 -0
- data/config/locales/it.yml +134 -0
- data/config/locales/ru.yml +15 -0
- data/lib/generators/README.md +45 -0
- data/lib/generators/servactory/base.rb +82 -0
- data/lib/generators/servactory/extension/USAGE +54 -0
- data/lib/generators/servactory/extension/extension_generator.rb +41 -0
- data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
- data/lib/generators/servactory/install/USAGE +27 -0
- data/lib/generators/servactory/install/install_generator.rb +94 -0
- data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
- data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
- data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
- data/lib/generators/servactory/rspec/USAGE +46 -0
- data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
- data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
- data/lib/generators/servactory/service/USAGE +51 -0
- data/lib/generators/servactory/service/service_generator.rb +56 -0
- data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
- data/lib/servactory/actions/collection.rb +56 -1
- data/lib/servactory/actions/dsl.rb +11 -11
- data/lib/servactory/actions/tools/rules.rb +1 -1
- data/lib/servactory/actions/tools/runner.rb +111 -28
- data/lib/servactory/base.rb +1 -7
- data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
- data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
- data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
- data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/config.rb +36 -0
- data/lib/servactory/configuration/configurable.rb +95 -0
- data/lib/servactory/configuration/dsl.rb +3 -27
- data/lib/servactory/configuration/factory.rb +20 -20
- data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
- data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
- data/lib/servactory/context/warehouse/inputs.rb +2 -2
- 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 +11 -7
- data/lib/servactory/dsl.rb +10 -8
- data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
- data/lib/servactory/result.rb +2 -2
- data/lib/servactory/stroma/dsl.rb +118 -0
- data/lib/servactory/stroma/entry.rb +32 -0
- data/lib/servactory/stroma/exceptions/base.rb +45 -0
- data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
- data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
- data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
- data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
- data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
- data/lib/servactory/stroma/hooks/applier.rb +63 -0
- data/lib/servactory/stroma/hooks/collection.rb +103 -0
- data/lib/servactory/stroma/hooks/factory.rb +80 -0
- data/lib/servactory/stroma/hooks/hook.rb +74 -0
- data/lib/servactory/stroma/registry.rb +94 -0
- data/lib/servactory/stroma/settings/collection.rb +90 -0
- data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
- data/lib/servactory/stroma/settings/setting.rb +113 -0
- data/lib/servactory/stroma/state.rb +59 -0
- data/lib/servactory/test_kit/fake_type.rb +23 -0
- data/lib/servactory/test_kit/result.rb +45 -0
- data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
- data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
- data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
- data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
- data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
- data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
- data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
- data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
- data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
- data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
- data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
- data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
- data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
- data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
- data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
- data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
- data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
- data/lib/servactory/test_kit/utils/faker.rb +62 -2
- data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
- data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
- data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
- data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
- data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
- data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
- data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
- data/lib/servactory/tool_kit/dynamic_options/target.rb +252 -0
- data/lib/servactory/version.rb +4 -4
- data/lib/servactory.rb +4 -0
- metadata +73 -19
- data/lib/generators/servactory/install_generator.rb +0 -21
- data/lib/generators/servactory/rspec_generator.rb +0 -88
- data/lib/generators/servactory/service_generator.rb +0 -49
- data/lib/servactory/configuration/setup.rb +0 -97
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
- data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
- data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating inclusion constraints.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that an attribute has the expected inclusion values.
|
|
14
|
+
# Inclusion restricts attribute values to a specific set of allowed values.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:status).inclusion(%w[active inactive pending]) }
|
|
20
|
+
# it { is_expected.to have_service_input(:priority).inclusion([1, 2, 3]) }
|
|
21
|
+
# ```
|
|
22
|
+
#
|
|
23
|
+
# ## Comparison
|
|
24
|
+
#
|
|
25
|
+
# Uses set difference to compare values - order doesn't matter,
|
|
26
|
+
# only the set of allowed values must match exactly.
|
|
27
|
+
class InclusionSubmatcher < Base::Submatcher
|
|
28
|
+
# Option name in attribute data
|
|
29
|
+
OPTION_NAME = :inclusion
|
|
30
|
+
# Key for the inclusion values within the option
|
|
31
|
+
OPTION_BODY_KEY = :in
|
|
32
|
+
|
|
33
|
+
# Creates a new inclusion submatcher.
|
|
34
|
+
#
|
|
35
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
36
|
+
# @param values [Array] Expected allowed values
|
|
37
|
+
# @return [InclusionSubmatcher] New submatcher instance
|
|
38
|
+
def initialize(context, values)
|
|
39
|
+
super(context)
|
|
40
|
+
@values = values
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns description for RSpec output.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] Human-readable description with values
|
|
46
|
+
def description
|
|
47
|
+
formatted = case values
|
|
48
|
+
when Range then values.inspect
|
|
49
|
+
else values.join(", ")
|
|
50
|
+
end
|
|
51
|
+
"inclusion: #{formatted}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
protected
|
|
55
|
+
|
|
56
|
+
# Checks if the inclusion values match expected values.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean] True if inclusion values match
|
|
59
|
+
def passes?
|
|
60
|
+
return false unless attribute_inclusion.is_a?(Hash)
|
|
61
|
+
return false if attribute_inclusion_in.nil?
|
|
62
|
+
|
|
63
|
+
inclusion_values_match?(attribute_inclusion_in, values)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Builds the failure message for inclusion validation.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] Failure message with expected vs actual values
|
|
69
|
+
def build_failure_message
|
|
70
|
+
<<~MESSAGE
|
|
71
|
+
should include the expected values
|
|
72
|
+
|
|
73
|
+
expected #{values.inspect}
|
|
74
|
+
got #{attribute_inclusion_in.inspect}
|
|
75
|
+
MESSAGE
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
attr_reader :values
|
|
81
|
+
|
|
82
|
+
# Fetches the inclusion option from attribute data.
|
|
83
|
+
#
|
|
84
|
+
# @return [Hash, nil] The inclusion option or nil
|
|
85
|
+
def attribute_inclusion
|
|
86
|
+
@attribute_inclusion ||= attribute_data[OPTION_NAME]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fetches the inclusion values array from the option.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array, Range, nil] The allowed values or nil
|
|
92
|
+
def attribute_inclusion_in
|
|
93
|
+
return @attribute_inclusion_in if defined?(@attribute_inclusion_in)
|
|
94
|
+
|
|
95
|
+
@attribute_inclusion_in = attribute_inclusion&.dig(OPTION_BODY_KEY)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Compares two inclusion values for equality.
|
|
99
|
+
# Supports Range, Array, and mixed types.
|
|
100
|
+
#
|
|
101
|
+
# @param actual [Range, Array] Actual inclusion value
|
|
102
|
+
# @param expected [Range, Array] Expected inclusion value
|
|
103
|
+
# @return [Boolean] True if values are equivalent
|
|
104
|
+
def inclusion_values_match?(actual, expected)
|
|
105
|
+
case [actual.class, expected.class]
|
|
106
|
+
when [Range, Range]
|
|
107
|
+
actual == expected
|
|
108
|
+
when [Array, Array]
|
|
109
|
+
actual.difference(expected).empty? && expected.difference(actual).empty?
|
|
110
|
+
else
|
|
111
|
+
actual.to_s == expected.to_s
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating custom error messages.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that the previous submatcher's option has the expected
|
|
14
|
+
# custom error message. Must be used after another submatcher that
|
|
15
|
+
# defines an option with a message field.
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:email).inclusion(%w[a b]).message("Invalid email") }
|
|
21
|
+
# it { is_expected.to have_service_input(:data).schema({ key: String }).message("Invalid schema") }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Note
|
|
25
|
+
#
|
|
26
|
+
# Requires `requires_last_submatcher: true` - must follow another
|
|
27
|
+
# submatcher. Uses the previous submatcher's OPTION_NAME constant
|
|
28
|
+
# to find the message field.
|
|
29
|
+
class MessageSubmatcher < Base::Submatcher
|
|
30
|
+
# Option name in attribute data (unused - uses last submatcher's)
|
|
31
|
+
OPTION_NAME = :message
|
|
32
|
+
# Key for the message within the option
|
|
33
|
+
OPTION_BODY_KEY = :message
|
|
34
|
+
|
|
35
|
+
# Creates a new message submatcher.
|
|
36
|
+
#
|
|
37
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
38
|
+
# @param custom_message [String] Expected error message
|
|
39
|
+
# @return [MessageSubmatcher] New submatcher instance
|
|
40
|
+
def initialize(context, custom_message)
|
|
41
|
+
super(context)
|
|
42
|
+
@custom_message = custom_message
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Returns description for RSpec output.
|
|
46
|
+
#
|
|
47
|
+
# @return [String] Human-readable description with message
|
|
48
|
+
def description
|
|
49
|
+
"message: #{@attribute_schema_message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Checks if the option's message matches expected message.
|
|
55
|
+
#
|
|
56
|
+
# @return [Boolean] True if messages match
|
|
57
|
+
def passes?
|
|
58
|
+
last_submatcher = context.last_submatcher
|
|
59
|
+
attribute_schema = attribute_data.fetch(last_submatcher.class::OPTION_NAME)
|
|
60
|
+
@attribute_schema_is = attribute_schema.fetch(last_submatcher.class::OPTION_BODY_KEY)
|
|
61
|
+
@attribute_schema_message = attribute_schema.fetch(:message)
|
|
62
|
+
|
|
63
|
+
schema_message_equal?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Builds the failure message for message validation.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] Failure message with expected vs actual message
|
|
69
|
+
def build_failure_message
|
|
70
|
+
return "" if schema_message_equal?
|
|
71
|
+
|
|
72
|
+
<<~MESSAGE
|
|
73
|
+
should return expected message in case of problem:
|
|
74
|
+
|
|
75
|
+
expected #{@attribute_schema_message.inspect}
|
|
76
|
+
got #{custom_message.inspect}
|
|
77
|
+
MESSAGE
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
attr_reader :custom_message
|
|
83
|
+
|
|
84
|
+
# Compares expected and actual messages with type-aware logic.
|
|
85
|
+
#
|
|
86
|
+
# Handles RSpec matchers, Procs, and plain strings.
|
|
87
|
+
#
|
|
88
|
+
# @return [Boolean] True if messages match
|
|
89
|
+
def schema_message_equal? # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
90
|
+
@schema_message_equal ||= begin
|
|
91
|
+
if custom_message.present? && !@attribute_schema_message.nil?
|
|
92
|
+
if custom_message.is_a?(RSpec::Matchers::BuiltIn::BaseMatcher)
|
|
93
|
+
RSpec::Expectations::ValueExpectationTarget
|
|
94
|
+
.new(@attribute_schema_message)
|
|
95
|
+
.to(custom_message)
|
|
96
|
+
true
|
|
97
|
+
elsif @attribute_schema_message.is_a?(Proc)
|
|
98
|
+
@attribute_schema_message.call.casecmp(custom_message).zero?
|
|
99
|
+
else
|
|
100
|
+
@attribute_schema_message.casecmp(custom_message).zero?
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
rescue RSpec::Expectations::ExpectationNotMetError
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating custom `must` validation rules.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that an attribute has the expected custom validation rules
|
|
14
|
+
# defined via the `must` option. Excludes dynamic options that are
|
|
15
|
+
# tested separately (consists_of, schema, be_inclusion, be_target).
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:email).must([:be_valid_email]) }
|
|
21
|
+
# it { is_expected.to have_service_input(:age).must([:be_positive, :be_adult]) }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Comparison
|
|
25
|
+
#
|
|
26
|
+
# Uses set difference to compare validation names - order doesn't matter.
|
|
27
|
+
class MustSubmatcher < Base::Submatcher
|
|
28
|
+
# Creates a new must submatcher.
|
|
29
|
+
#
|
|
30
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
31
|
+
# @param must_names [Array<Symbol>] Expected validation rule names
|
|
32
|
+
# @return [MustSubmatcher] New submatcher instance
|
|
33
|
+
def initialize(context, must_names)
|
|
34
|
+
super(context)
|
|
35
|
+
@must_names = must_names
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns description for RSpec output.
|
|
39
|
+
#
|
|
40
|
+
# @return [String] Human-readable description with rule names
|
|
41
|
+
def description
|
|
42
|
+
"must: #{must_names.join(', ')}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
protected
|
|
46
|
+
|
|
47
|
+
# Checks if the must rules match expected rules.
|
|
48
|
+
#
|
|
49
|
+
# Filters out dynamic options that are tested by other submatchers.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] True if must rules match (order-independent)
|
|
52
|
+
def passes?
|
|
53
|
+
attribute_must = attribute_data.fetch(:must)
|
|
54
|
+
attribute_must_keys = attribute_must.keys.dup
|
|
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)
|
|
61
|
+
|
|
62
|
+
attribute_must_keys.difference(must_names).empty? &&
|
|
63
|
+
must_names.difference(attribute_must_keys).empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Builds the failure message for must validation.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] Simple failure message with expected rules
|
|
69
|
+
def build_failure_message
|
|
70
|
+
"should #{must_names.join(', ')}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
attr_reader :must_names
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating Hash schema definitions.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that a Hash attribute has the expected schema definition.
|
|
14
|
+
# Schemas define the structure and types of keys within a Hash.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:config).schema({ api_key: String }) }
|
|
20
|
+
# it { is_expected.to have_service_input(:user_data).schema({ name: String, age: Integer }) }
|
|
21
|
+
# ```
|
|
22
|
+
#
|
|
23
|
+
# ## Note
|
|
24
|
+
#
|
|
25
|
+
# Requires the `:types` option to be set first (via `.type` chain).
|
|
26
|
+
# Uses RSpec's `match` matcher for schema comparison.
|
|
27
|
+
class SchemaSubmatcher < Base::Submatcher
|
|
28
|
+
# Option name in attribute data
|
|
29
|
+
OPTION_NAME = :schema
|
|
30
|
+
# Key for the schema value within the option
|
|
31
|
+
OPTION_BODY_KEY = :is
|
|
32
|
+
|
|
33
|
+
# Creates a new schema submatcher.
|
|
34
|
+
#
|
|
35
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
36
|
+
# @param schema_data [Hash] Expected schema definition
|
|
37
|
+
# @return [SchemaSubmatcher] New submatcher instance
|
|
38
|
+
def initialize(context, schema_data)
|
|
39
|
+
super(context)
|
|
40
|
+
@schema_data = schema_data
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns description for RSpec output.
|
|
44
|
+
#
|
|
45
|
+
# @return [String] Human-readable description with schema
|
|
46
|
+
def description
|
|
47
|
+
"schema: #{schema_data}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
# Checks if the attribute schema matches expected schema.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean] True if schemas match
|
|
55
|
+
def passes?
|
|
56
|
+
attribute_schema = attribute_data.fetch(OPTION_NAME)
|
|
57
|
+
@attribute_schema_is = attribute_schema.fetch(OPTION_BODY_KEY)
|
|
58
|
+
@attribute_schema_message = attribute_schema.fetch(:message)
|
|
59
|
+
|
|
60
|
+
schema_data_equal?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Builds the failure message for schema validation.
|
|
64
|
+
#
|
|
65
|
+
# @return [String] Failure message with expected vs actual schema
|
|
66
|
+
def build_failure_message
|
|
67
|
+
return "" if schema_data_equal?
|
|
68
|
+
|
|
69
|
+
<<~MESSAGE
|
|
70
|
+
should be schema with corresponding template
|
|
71
|
+
|
|
72
|
+
expected #{@attribute_schema_is.inspect}
|
|
73
|
+
got #{schema_data.inspect}
|
|
74
|
+
MESSAGE
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
attr_reader :schema_data
|
|
80
|
+
|
|
81
|
+
# Compares expected and actual schemas using RSpec's match matcher.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean] True if schemas are equal
|
|
84
|
+
def schema_data_equal?
|
|
85
|
+
@schema_data_equal ||= begin
|
|
86
|
+
matcher_result = RSpec::Expectations::ExpectationHelper
|
|
87
|
+
.with_matcher(
|
|
88
|
+
RSpec::Expectations::PositiveExpectationHandler,
|
|
89
|
+
RSpec::Matchers::BuiltIn::Match.new(schema_data),
|
|
90
|
+
nil
|
|
91
|
+
) { |matcher| matcher.matches?(@attribute_schema_is) }
|
|
92
|
+
|
|
93
|
+
(schema_data.present? && matcher_result) || schema_data.blank?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating target constraints.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that an attribute has the expected target values defined.
|
|
14
|
+
# Targets are similar to inclusions but can be named differently
|
|
15
|
+
# (e.g., :target, :category, :group).
|
|
16
|
+
#
|
|
17
|
+
# ## Usage
|
|
18
|
+
#
|
|
19
|
+
# ```ruby
|
|
20
|
+
# it { is_expected.to have_service_input(:category).target([Category::A, Category::B], name: :category) }
|
|
21
|
+
# it { is_expected.to have_service_input(:type).target(%i[user admin], name: :type) }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Comparison
|
|
25
|
+
#
|
|
26
|
+
# Uses set difference to compare values - order doesn't matter.
|
|
27
|
+
# Supports both single values and arrays.
|
|
28
|
+
class TargetSubmatcher < Base::Submatcher
|
|
29
|
+
# Key for the target values within the option
|
|
30
|
+
OPTION_BODY_KEY = :in
|
|
31
|
+
|
|
32
|
+
# Creates a new target submatcher.
|
|
33
|
+
#
|
|
34
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
35
|
+
# @param option_name [Symbol] The name of the target option
|
|
36
|
+
# @param values [Array] Expected target values
|
|
37
|
+
# @return [TargetSubmatcher] New submatcher instance
|
|
38
|
+
def initialize(context, option_name, values)
|
|
39
|
+
super(context)
|
|
40
|
+
@option_name = option_name
|
|
41
|
+
@values = values
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns description for RSpec output.
|
|
45
|
+
#
|
|
46
|
+
# @return [String] Human-readable description with target values
|
|
47
|
+
def description
|
|
48
|
+
"#{option_name}: #{formatted_values}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
# Checks if the target values match expected values.
|
|
54
|
+
#
|
|
55
|
+
# @return [Boolean] True if target values match (order-independent)
|
|
56
|
+
def passes?
|
|
57
|
+
return false unless attribute_target.is_a?(Hash)
|
|
58
|
+
return false if attribute_target_in.nil?
|
|
59
|
+
|
|
60
|
+
expected = normalize_to_array(values)
|
|
61
|
+
actual = normalize_to_array(attribute_target_in)
|
|
62
|
+
|
|
63
|
+
actual.difference(expected).empty? && expected.difference(actual).empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Builds the failure message for target validation.
|
|
67
|
+
#
|
|
68
|
+
# @return [String] Failure message with expected vs actual values
|
|
69
|
+
def build_failure_message
|
|
70
|
+
<<~MESSAGE
|
|
71
|
+
should include the expected #{option_name} values
|
|
72
|
+
|
|
73
|
+
expected #{values.inspect}
|
|
74
|
+
got #{attribute_target_in.inspect}
|
|
75
|
+
MESSAGE
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
attr_reader :option_name,
|
|
81
|
+
:values
|
|
82
|
+
|
|
83
|
+
# Formats values for human-readable description.
|
|
84
|
+
#
|
|
85
|
+
# @return [String] Comma-separated formatted values
|
|
86
|
+
def formatted_values
|
|
87
|
+
values.map do |value|
|
|
88
|
+
case value
|
|
89
|
+
when nil then "nil"
|
|
90
|
+
when Class then value.name
|
|
91
|
+
else value.to_s
|
|
92
|
+
end
|
|
93
|
+
end.join(", ")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Fetches the target option from attribute data.
|
|
97
|
+
#
|
|
98
|
+
# @return [Hash, nil] The target option or nil
|
|
99
|
+
def attribute_target
|
|
100
|
+
@attribute_target ||= attribute_data[option_name]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Fetches the target values array from the option.
|
|
104
|
+
#
|
|
105
|
+
# @return [Array, Object, nil] The target values or nil
|
|
106
|
+
def attribute_target_in
|
|
107
|
+
return @attribute_target_in if defined?(@attribute_target_in)
|
|
108
|
+
|
|
109
|
+
@attribute_target_in = attribute_target&.dig(OPTION_BODY_KEY)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Normalizes a value to an array for comparison.
|
|
113
|
+
#
|
|
114
|
+
# @param value [Object] Value to normalize
|
|
115
|
+
# @return [Array] Wrapped value or original array
|
|
116
|
+
def normalize_to_array(value)
|
|
117
|
+
value.respond_to?(:difference) ? value : [value]
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Servactory
|
|
4
|
+
module TestKit
|
|
5
|
+
module Rspec
|
|
6
|
+
module Matchers
|
|
7
|
+
module Submatchers
|
|
8
|
+
module Shared
|
|
9
|
+
# Submatcher for validating attribute type definitions.
|
|
10
|
+
#
|
|
11
|
+
# ## Purpose
|
|
12
|
+
#
|
|
13
|
+
# Validates that an attribute (input, internal, or output) has the
|
|
14
|
+
# expected type or types defined. Supports multiple types for union types.
|
|
15
|
+
#
|
|
16
|
+
# ## Usage
|
|
17
|
+
#
|
|
18
|
+
# ```ruby
|
|
19
|
+
# it { is_expected.to have_service_input(:user_id).type(Integer) }
|
|
20
|
+
# it { is_expected.to have_service_input(:data).types(String, Hash) }
|
|
21
|
+
# it { is_expected.to have_service_internal(:result).type(Array) }
|
|
22
|
+
# ```
|
|
23
|
+
#
|
|
24
|
+
# ## Comparison
|
|
25
|
+
#
|
|
26
|
+
# Types are compared by name, sorted alphabetically. Order of types
|
|
27
|
+
# in the definition doesn't matter - only the set of types must match.
|
|
28
|
+
class TypesSubmatcher < Base::Submatcher
|
|
29
|
+
# Creates a new types submatcher.
|
|
30
|
+
#
|
|
31
|
+
# @param context [Base::SubmatcherContext] The submatcher context
|
|
32
|
+
# @param expected_types [Array<Class>] Expected type classes
|
|
33
|
+
# @return [TypesSubmatcher] New submatcher instance
|
|
34
|
+
def initialize(context, expected_types)
|
|
35
|
+
super(context)
|
|
36
|
+
@expected_types = expected_types
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns description for RSpec output.
|
|
40
|
+
#
|
|
41
|
+
# @return [String] Human-readable description with type names
|
|
42
|
+
def description
|
|
43
|
+
"type(s): #{expected_types.map(&:name).join(', ')}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
# Checks if the attribute types match expected types.
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] True if types match (order-independent)
|
|
51
|
+
def passes?
|
|
52
|
+
actual_types = attribute_data.fetch(:types)
|
|
53
|
+
@actual_types = actual_types
|
|
54
|
+
|
|
55
|
+
expected_types.sort_by(&:name) == actual_types.sort_by(&:name)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Builds the failure message for type validation.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] Failure message with expected vs actual types
|
|
61
|
+
def build_failure_message
|
|
62
|
+
<<~MESSAGE.squish
|
|
63
|
+
should have type(s) #{expected_types.map(&:name).join(', ')}
|
|
64
|
+
but got #{@actual_types.map(&:name).join(', ')}
|
|
65
|
+
MESSAGE
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
attr_reader :expected_types
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|