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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -9
  3. data/config/locales/de.yml +134 -0
  4. data/config/locales/en.yml +15 -0
  5. data/config/locales/es.yml +134 -0
  6. data/config/locales/fr.yml +134 -0
  7. data/config/locales/it.yml +134 -0
  8. data/config/locales/ru.yml +15 -0
  9. data/lib/generators/README.md +45 -0
  10. data/lib/generators/servactory/base.rb +82 -0
  11. data/lib/generators/servactory/extension/USAGE +54 -0
  12. data/lib/generators/servactory/extension/extension_generator.rb +41 -0
  13. data/lib/generators/servactory/extension/templates/extension.rb.tt +62 -0
  14. data/lib/generators/servactory/install/USAGE +27 -0
  15. data/lib/generators/servactory/install/install_generator.rb +94 -0
  16. data/lib/generators/servactory/{templates/services/application_service/base.rb → install/templates/application_service/base.rb.tt} +29 -19
  17. data/lib/generators/servactory/{templates/services/application_service/exceptions.rb → install/templates/application_service/exceptions.rb.tt} +1 -1
  18. data/lib/generators/servactory/{templates/services/application_service/result.rb → install/templates/application_service/result.rb.tt} +1 -1
  19. data/lib/generators/servactory/rspec/USAGE +46 -0
  20. data/lib/generators/servactory/rspec/rspec_generator.rb +95 -0
  21. data/lib/generators/servactory/rspec/templates/service_spec.rb.tt +58 -0
  22. data/lib/generators/servactory/service/USAGE +51 -0
  23. data/lib/generators/servactory/service/service_generator.rb +56 -0
  24. data/lib/generators/servactory/service/templates/service.rb.tt +22 -0
  25. data/lib/servactory/actions/collection.rb +56 -1
  26. data/lib/servactory/actions/dsl.rb +11 -11
  27. data/lib/servactory/actions/tools/rules.rb +1 -1
  28. data/lib/servactory/actions/tools/runner.rb +111 -28
  29. data/lib/servactory/base.rb +1 -7
  30. data/lib/servactory/configuration/actions/aliases/collection.rb +5 -0
  31. data/lib/servactory/configuration/actions/rescue_handlers/collection.rb +5 -0
  32. data/lib/servactory/configuration/actions/shortcuts/collection.rb +5 -0
  33. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +5 -0
  34. data/lib/servactory/configuration/config.rb +36 -0
  35. data/lib/servactory/configuration/configurable.rb +95 -0
  36. data/lib/servactory/configuration/dsl.rb +3 -27
  37. data/lib/servactory/configuration/factory.rb +20 -20
  38. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +5 -0
  39. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +5 -0
  40. data/lib/servactory/context/warehouse/inputs.rb +2 -2
  41. data/lib/servactory/context/workspace/inputs.rb +2 -2
  42. data/lib/servactory/context/workspace/internals.rb +2 -2
  43. data/lib/servactory/context/workspace/outputs.rb +2 -2
  44. data/lib/servactory/context/workspace.rb +11 -7
  45. data/lib/servactory/dsl.rb +10 -8
  46. data/lib/servactory/maintenance/attributes/tools/validation.rb +1 -1
  47. data/lib/servactory/result.rb +2 -2
  48. data/lib/servactory/stroma/dsl.rb +118 -0
  49. data/lib/servactory/stroma/entry.rb +32 -0
  50. data/lib/servactory/stroma/exceptions/base.rb +45 -0
  51. data/lib/servactory/stroma/exceptions/invalid_hook_type.rb +29 -0
  52. data/lib/servactory/stroma/exceptions/key_already_registered.rb +32 -0
  53. data/lib/servactory/stroma/exceptions/registry_frozen.rb +33 -0
  54. data/lib/servactory/stroma/exceptions/registry_not_finalized.rb +33 -0
  55. data/lib/servactory/stroma/exceptions/unknown_hook_target.rb +39 -0
  56. data/lib/servactory/stroma/hooks/applier.rb +63 -0
  57. data/lib/servactory/stroma/hooks/collection.rb +103 -0
  58. data/lib/servactory/stroma/hooks/factory.rb +80 -0
  59. data/lib/servactory/stroma/hooks/hook.rb +74 -0
  60. data/lib/servactory/stroma/registry.rb +94 -0
  61. data/lib/servactory/stroma/settings/collection.rb +90 -0
  62. data/lib/servactory/stroma/settings/registry_settings.rb +88 -0
  63. data/lib/servactory/stroma/settings/setting.rb +113 -0
  64. data/lib/servactory/stroma/state.rb +59 -0
  65. data/lib/servactory/test_kit/fake_type.rb +23 -0
  66. data/lib/servactory/test_kit/result.rb +45 -0
  67. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  69. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  70. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  71. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  72. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  73. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  74. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  75. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  76. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  77. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  78. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  79. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  80. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  81. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  82. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  83. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  84. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  85. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  86. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  87. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  88. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  89. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  90. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  91. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  92. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  93. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  94. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  95. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  96. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  97. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  98. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  99. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  100. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  101. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  102. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  103. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  104. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  105. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  106. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  107. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  108. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  109. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  110. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  111. data/lib/servactory/tool_kit/dynamic_options/target.rb +252 -0
  112. data/lib/servactory/version.rb +4 -4
  113. data/lib/servactory.rb +4 -0
  114. metadata +73 -19
  115. data/lib/generators/servactory/install_generator.rb +0 -21
  116. data/lib/generators/servactory/rspec_generator.rb +0 -88
  117. data/lib/generators/servactory/service_generator.rb +0 -49
  118. data/lib/servactory/configuration/setup.rb +0 -97
  119. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  120. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  121. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  122. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  123. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  124. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  125. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  126. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  127. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  128. 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