servactory 2.16.1 → 3.0.0.rc2

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 (111) 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/test_kit/fake_type.rb +23 -0
  49. data/lib/servactory/test_kit/result.rb +45 -0
  50. data/lib/servactory/test_kit/rspec/helpers/argument_matchers.rb +97 -0
  51. data/lib/servactory/test_kit/rspec/helpers/concerns/error_messages.rb +179 -0
  52. data/lib/servactory/test_kit/rspec/helpers/concerns/service_class_validation.rb +74 -0
  53. data/lib/servactory/test_kit/rspec/helpers/fluent.rb +110 -0
  54. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +149 -0
  55. data/lib/servactory/test_kit/rspec/helpers/legacy.rb +228 -0
  56. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +256 -0
  57. data/lib/servactory/test_kit/rspec/helpers/output_validator.rb +121 -0
  58. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +422 -0
  59. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +129 -0
  60. data/lib/servactory/test_kit/rspec/helpers.rb +51 -84
  61. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +324 -0
  62. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +133 -0
  63. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_context.rb +101 -0
  64. data/lib/servactory/test_kit/rspec/matchers/base/submatcher_registry.rb +205 -0
  65. data/lib/servactory/test_kit/rspec/matchers/concerns/attribute_data_access.rb +100 -0
  66. data/lib/servactory/test_kit/rspec/matchers/concerns/error_message_builder.rb +106 -0
  67. data/lib/servactory/test_kit/rspec/matchers/concerns/value_comparison.rb +97 -0
  68. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +89 -219
  69. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +74 -166
  70. data/lib/servactory/test_kit/rspec/matchers/have_service_output_matcher.rb +238 -0
  71. data/lib/servactory/test_kit/rspec/matchers/result/be_failure_service_matcher.rb +257 -0
  72. data/lib/servactory/test_kit/rspec/matchers/result/be_success_service_matcher.rb +185 -0
  73. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/default_submatcher.rb +81 -0
  74. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/optional_submatcher.rb +62 -0
  75. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +93 -0
  76. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +271 -0
  77. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/consists_of_submatcher.rb +85 -0
  78. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/inclusion_submatcher.rb +120 -0
  79. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +115 -0
  80. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +82 -0
  81. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/schema_submatcher.rb +102 -0
  82. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +125 -0
  83. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +77 -0
  84. data/lib/servactory/test_kit/rspec/matchers.rb +126 -285
  85. data/lib/servactory/test_kit/utils/faker.rb +62 -2
  86. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +166 -0
  87. data/lib/servactory/tool_kit/dynamic_options/format.rb +195 -8
  88. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +219 -17
  89. data/lib/servactory/tool_kit/dynamic_options/max.rb +143 -0
  90. data/lib/servactory/tool_kit/dynamic_options/min.rb +143 -0
  91. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +157 -8
  92. data/lib/servactory/tool_kit/dynamic_options/must.rb +194 -0
  93. data/lib/servactory/tool_kit/dynamic_options/schema.rb +226 -2
  94. data/lib/servactory/tool_kit/dynamic_options/target.rb +248 -0
  95. data/lib/servactory/version.rb +4 -4
  96. data/lib/servactory.rb +6 -0
  97. metadata +57 -48
  98. data/lib/generators/servactory/install_generator.rb +0 -21
  99. data/lib/generators/servactory/rspec_generator.rb +0 -88
  100. data/lib/generators/servactory/service_generator.rb +0 -49
  101. data/lib/servactory/configuration/setup.rb +0 -97
  102. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/consists_of_matcher.rb +0 -68
  103. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/inclusion_matcher.rb +0 -73
  104. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/message_matcher.rb +0 -91
  105. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/must_matcher.rb +0 -72
  106. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/schema_matcher.rb +0 -92
  107. data/lib/servactory/test_kit/rspec/matchers/have_service_attribute_matchers/types_matcher.rb +0 -72
  108. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/default_matcher.rb +0 -69
  109. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/optional_matcher.rb +0 -63
  110. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/required_matcher.rb +0 -81
  111. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matchers/valid_with_matcher.rb +0 -199
@@ -16,17 +16,19 @@ module Servactory
16
16
  end
17
17
  end
18
18
 
19
+ ::Stroma::Registry.register(:configuration, Configuration::DSL)
20
+ ::Stroma::Registry.register(:info, Info::DSL)
21
+ ::Stroma::Registry.register(:context, Context::DSL)
22
+ ::Stroma::Registry.register(:inputs, Inputs::DSL)
23
+ ::Stroma::Registry.register(:internals, Internals::DSL)
24
+ ::Stroma::Registry.register(:outputs, Outputs::DSL)
25
+ ::Stroma::Registry.register(:actions, Actions::DSL)
26
+ ::Stroma::Registry.finalize!
27
+
19
28
  def self.included(base)
20
- base.include(Configuration::DSL)
21
- base.include(Info::DSL)
22
- base.include(Context::DSL)
23
- base.include(Inputs::DSL)
24
- base.include(Internals::DSL)
25
- base.include(Outputs::DSL)
29
+ base.include(::Stroma::DSL)
26
30
 
27
31
  Extensions.registry.each { |extension| base.include(extension) }
28
-
29
- base.include(Actions::DSL)
30
32
  end
31
33
 
32
34
  def self.with_extensions(*extensions)
@@ -74,7 +74,7 @@ module Servactory
74
74
  def raise_errors
75
75
  return if (tmp_errors = errors.not_blank).empty?
76
76
 
77
- raise @context.class.config
77
+ raise @context.config
78
78
  .public_send(:"#{@attribute.system_name}_exception_class")
79
79
  .new(context: @context, message: tmp_errors.first)
80
80
  end
@@ -117,7 +117,7 @@ module Servactory
117
117
  @outputs ||= Outputs.new(
118
118
  outputs: @context.send(:servactory_service_warehouse).outputs,
119
119
  predicate_methods_enabled:
120
- @context.is_a?(Servactory::TestKit::Result) || @context.class.config.predicate_methods_enabled?
120
+ @context.is_a?(Servactory::TestKit::Result) || @context.config.predicate_methods_enabled
121
121
  )
122
122
  end
123
123
 
@@ -126,7 +126,7 @@ module Servactory
126
126
  def rescue_no_method_error_with(exception:) # rubocop:disable Metrics/MethodLength
127
127
  raise exception if @context.blank? || @context.instance_of?(Servactory::TestKit::Result)
128
128
 
129
- raise @context.class.config.failure_class.new(
129
+ raise @context.config.failure_class.new(
130
130
  type: :base,
131
131
  message: @context.send(:servactory_service_info).translate(
132
132
  "common.undefined_method.missing_name",
@@ -2,6 +2,29 @@
2
2
 
3
3
  module Servactory
4
4
  module TestKit
5
+ # Dummy class used as a type mismatch value in tests.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # FakeType is an empty class that doesn't match any expected service
10
+ # input types. It's used by ValidWithSubmatcher to test type validation
11
+ # by providing a value guaranteed to fail type checks.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # Used internally by valid_with tests:
16
+ #
17
+ # ```ruby
18
+ # # In valid_with submatcher
19
+ # prepared_attributes[attribute_name] = Servactory::TestKit::FakeType.new
20
+ # # This triggers type validation failure
21
+ # ```
22
+ #
23
+ # ## Note
24
+ #
25
+ # The class is intentionally empty - its sole purpose is to be a type
26
+ # that doesn't match String, Integer, Hash, Array, or any other type
27
+ # that a service might expect.
5
28
  class FakeType; end # rubocop:disable Lint/EmptyClass
6
29
  end
7
30
  end
@@ -2,7 +2,41 @@
2
2
 
3
3
  module Servactory
4
4
  module TestKit
5
+ # Factory for creating mock Servactory result objects.
6
+ #
7
+ # ## Purpose
8
+ #
9
+ # Provides factory methods for creating success and failure result objects
10
+ # in tests without actually calling a service. Used internally by the
11
+ # service mocking helpers to generate return values.
12
+ #
13
+ # ## Usage
14
+ #
15
+ # ```ruby
16
+ # # Create success result with outputs
17
+ # result = Servactory::TestKit::Result.as_success(
18
+ # service_class: MyService,
19
+ # user: user_object,
20
+ # status: :active
21
+ # )
22
+ #
23
+ # # Create failure result with exception
24
+ # result = Servactory::TestKit::Result.as_failure(
25
+ # service_class: MyService,
26
+ # exception: Servactory::Exceptions::Failure.new(message: "Error")
27
+ # )
28
+ # ```
29
+ #
30
+ # ## Service Class Resolution
31
+ #
32
+ # When `service_class` is provided, uses that service's configured
33
+ # `result_class` for proper result type. Otherwise, uses the default
34
+ # `Servactory::Result` class.
5
35
  class Result
36
+ # Creates a successful mock result with given outputs.
37
+ #
38
+ # @param attributes [Hash] Output attributes and optional :service_class
39
+ # @return [Servactory::Result] Success result with outputs as methods
6
40
  def self.as_success(**attributes)
7
41
  service_class = attributes.delete(:service_class) || self
8
42
 
@@ -15,6 +49,10 @@ module Servactory
15
49
  end
16
50
  end
17
51
 
52
+ # Creates a failed mock result with given exception.
53
+ #
54
+ # @param attributes [Hash] Output attributes, :exception, and optional :service_class
55
+ # @return [Servactory::Result] Failure result with error accessor
18
56
  def self.as_failure(**attributes)
19
57
  service_class = attributes.delete(:service_class) || self
20
58
  exception = attributes.delete(:exception)
@@ -28,6 +66,10 @@ module Servactory
28
66
  end
29
67
  end
30
68
 
69
+ # Initializes result with attribute accessors.
70
+ #
71
+ # @param attributes [Hash] Output name-value pairs
72
+ # @return [Result] New result instance
31
73
  def initialize(attributes = {})
32
74
  attributes.each_pair do |name, value|
33
75
  servactory_service_warehouse.assign_output(name, value)
@@ -38,6 +80,9 @@ module Servactory
38
80
 
39
81
  private
40
82
 
83
+ # Internal warehouse for storing output values.
84
+ #
85
+ # @return [Servactory::Context::Warehouse::Setup] Warehouse instance
41
86
  def servactory_service_warehouse
42
87
  @servactory_service_warehouse ||= Servactory::Context::Warehouse::Setup.new(self)
43
88
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # RSpec argument matchers with service-friendly aliases.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides semantic aliases for RSpec's argument matchers that read
12
+ # more naturally in service testing contexts.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # **Fluent API:**
17
+ #
18
+ # ```ruby
19
+ # allow_service(Service).with(including(amount: 100)).succeeds(result: "ok")
20
+ # allow_service(Service).with(excluding(secret: anything)).succeeds(result: "ok")
21
+ # allow_service(Service).with(any_inputs).succeeds(result: "ok")
22
+ # allow_service(EmptyService).with(no_inputs).succeeds(result: "ok")
23
+ # ```
24
+ #
25
+ # **Legacy API:**
26
+ #
27
+ # ```ruby
28
+ # allow_service_as_success!(Service, with: including(amount: 100)) { { result: "ok" } }
29
+ # allow_service_as_success!(Service, with: excluding(secret: anything)) { { result: "ok" } }
30
+ # allow_service_as_success!(Service, with: any_inputs) { { result: "ok" } }
31
+ # allow_service_as_success!(EmptyService, with: no_inputs) { { result: "ok" } }
32
+ # ```
33
+ module ArgumentMatchers
34
+ # Matches a hash containing specified key-value pairs.
35
+ #
36
+ # Alias for RSpec's `hash_including` with service-friendly naming.
37
+ #
38
+ # @param hash [Hash] Expected key-value pairs
39
+ # @return [RSpec::Mocks::ArgumentMatchers::HashIncludingMatcher]
40
+ #
41
+ # @example Fluent API
42
+ # allow_service(Service).with(including(amount: 100)).succeeds(result: "ok")
43
+ #
44
+ # @example Legacy API
45
+ # allow_service_as_success!(Service, with: including(amount: 100)) { { result: "ok" } }
46
+ def including(hash)
47
+ hash_including(hash)
48
+ end
49
+
50
+ # Matches a hash NOT containing specified key-value pairs.
51
+ #
52
+ # Alias for RSpec's `hash_excluding` with service-friendly naming.
53
+ #
54
+ # @param hash [Hash] Key-value pairs to exclude
55
+ # @return [RSpec::Mocks::ArgumentMatchers::HashExcludingMatcher]
56
+ #
57
+ # @example Fluent API
58
+ # allow_service(Service).with(excluding(secret: anything)).succeeds(result: "ok")
59
+ #
60
+ # @example Legacy API
61
+ # allow_service_as_success!(Service, with: excluding(secret: anything)) { { result: "ok" } }
62
+ def excluding(hash)
63
+ hash_excluding(hash)
64
+ end
65
+
66
+ # Matches any service inputs (wildcard matcher).
67
+ #
68
+ # Useful for "don't care" scenarios where input values don't matter.
69
+ #
70
+ # @return [RSpec::Mocks::ArgumentMatchers::AnyArgMatcher]
71
+ #
72
+ # @example Fluent API
73
+ # allow_service(Service).with(any_inputs).succeeds(result: "ok")
74
+ #
75
+ # @example Legacy API
76
+ # allow_service_as_success!(Service, with: any_inputs) { { result: "ok" } }
77
+ def any_inputs
78
+ anything
79
+ end
80
+
81
+ # Matches no arguments (for services without inputs).
82
+ #
83
+ # @return [RSpec::Mocks::ArgumentMatchers::NoArgsMatcher]
84
+ #
85
+ # @example Fluent API
86
+ # allow_service(EmptyService).with(no_inputs).succeeds(result: "ok")
87
+ #
88
+ # @example Legacy API
89
+ # allow_service_as_success!(EmptyService, with: no_inputs) { { result: "ok" } }
90
+ def no_inputs
91
+ no_args
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ module Concerns
8
+ # Concern providing error message builders for service mock helpers.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # ErrorMessages provides standardized, helpful error messages for common
13
+ # issues in service mocking. Each message includes context about what went
14
+ # wrong, hints for fixing the issue, and code examples.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Include in helper classes that need to report errors:
19
+ #
20
+ # ```ruby
21
+ # class ServiceMockBuilder
22
+ # include Concerns::ErrorMessages
23
+ #
24
+ # def validate!
25
+ # raise ArgumentError, missing_exception_for_failure_message(service_class)
26
+ # end
27
+ # end
28
+ # ```
29
+ #
30
+ # ## Message Categories
31
+ #
32
+ # - Service class validation errors
33
+ # - Block return value errors
34
+ # - Output validation errors
35
+ # - Type mismatch errors
36
+ # - Result type configuration errors
37
+ # - Exception configuration errors
38
+ module ErrorMessages
39
+ private
40
+
41
+ # Builds error message for invalid service class.
42
+ #
43
+ # @param given [Object] The invalid value that was provided
44
+ # @return [String] Error message with hint and example
45
+ def invalid_service_class_message(given)
46
+ <<~MESSAGE.squish
47
+ Invalid service class provided to service mock helper.
48
+ Expected a class responding to `.call` and `.call!`,
49
+ got: #{given.inspect} (#{given.class.name}).
50
+ Hint: Ensure you're passing the service class, not an instance.
51
+ Example: allow_service(MyService).succeeds(result: "value")
52
+ MESSAGE
53
+ end
54
+
55
+ # Builds error message for invalid block return value.
56
+ #
57
+ # @param given [Object] The actual return value
58
+ # @param expected_type [String] Description of expected type
59
+ # @return [String] Error message with example
60
+ def invalid_block_return_message(given, expected_type)
61
+ <<~MESSAGE.squish
62
+ Invalid block return value in service mock helper.
63
+ Expected: #{expected_type},
64
+ got: #{given.class.name}.
65
+ Example for success: allow_service_as_success!(MyService) { { user_id: 123 } }
66
+ MESSAGE
67
+ end
68
+
69
+ # Builds error message for unknown output names.
70
+ #
71
+ # @param service_class [Class] The service class
72
+ # @param unknown_outputs [Array<Symbol>] Outputs not defined in service
73
+ # @param defined_outputs [Array<Symbol>] Valid output names
74
+ # @return [String] Error message with hint
75
+ def unknown_outputs_message(service_class:, unknown_outputs:, defined_outputs:)
76
+ <<~MESSAGE.squish
77
+ Unknown output(s) for #{service_class.name}:
78
+ provided: #{unknown_outputs.map(&:inspect).join(', ')},
79
+ defined: #{defined_outputs.map(&:inspect).join(', ')}.
80
+ Hint: Check that the output names match the service definition.
81
+ MESSAGE
82
+ end
83
+
84
+ # Builds error message for output type mismatch.
85
+ #
86
+ # @param service_class [Class] The service class
87
+ # @param output_name [Symbol] Name of the mismatched output
88
+ # @param expected_types [Array<Class>] Expected type classes
89
+ # @param actual_value [Object] The value with wrong type
90
+ # @return [String] Error message with hint
91
+ def type_mismatch_message(service_class:, output_name:, expected_types:, actual_value:)
92
+ <<~MESSAGE.squish
93
+ Type mismatch for output :#{output_name} in #{service_class.name}.
94
+ Expected: #{expected_types.map(&:name).join(' or ')},
95
+ got: #{actual_value.class.name} (#{actual_value.inspect}).
96
+ Hint: Ensure the mocked value matches the expected type.
97
+ MESSAGE
98
+ end
99
+
100
+ # Builds error message for missing result type.
101
+ #
102
+ # @return [String] Error message with example
103
+ def missing_result_type_message
104
+ <<~MESSAGE.squish
105
+ Result type not specified.
106
+ Use .succeeds() or .fails() to specify the mock result type.
107
+ Example: allow_service(MyService).succeeds(data: "value")
108
+ MESSAGE
109
+ end
110
+
111
+ # Builds error message for failure mock missing exception.
112
+ #
113
+ # @param service_class [Class] The service class
114
+ # @return [String] Error message with example showing full signature
115
+ def missing_exception_for_failure_message(service_class)
116
+ <<~MESSAGE.squish
117
+ Exception is required for failure mock of #{service_class.name}.
118
+ Servactory supports custom exception classes via configuration,
119
+ so you must explicitly specify the exception.
120
+ Example:
121
+ allow_service(#{service_class.name})
122
+ .fails(message: "Error message")
123
+ Full signature: .fails(type: :custom_type, message: "...", meta: { key: :value })
124
+ MESSAGE
125
+ end
126
+
127
+ # Builds error message for wrong exception type.
128
+ #
129
+ # @param service_class [Class] The service class
130
+ # @param expected_class [Class] The configured failure class
131
+ # @param actual_class [Class] The provided exception's class
132
+ # @return [String] Error message with hint and example
133
+ def invalid_exception_type_message(service_class:, expected_class:, actual_class:)
134
+ <<~MESSAGE.squish
135
+ Invalid exception type for failure mock of #{service_class.name}.
136
+ Expected: instance of #{expected_class.name} (configured failure_class),
137
+ got: #{actual_class.name}.
138
+ Hint: Use the service's configured failure class or its subclass.
139
+ Example:
140
+ allow_service(#{service_class.name})
141
+ .fails(#{expected_class.name}, message: "Error message")
142
+ Full signature: .fails(MyException, type: :custom_type, message: "...", meta: { key: :value })
143
+ MESSAGE
144
+ end
145
+
146
+ # Builds error message for unknown input names.
147
+ #
148
+ # @param service_class [Class] The service class
149
+ # @param unknown_inputs [Array<Symbol>] Inputs not defined in service
150
+ # @param defined_inputs [Array<Symbol>] Valid input names
151
+ # @return [String] Error message with hint
152
+ def unknown_inputs_message(service_class:, unknown_inputs:, defined_inputs:)
153
+ <<~MESSAGE.squish
154
+ Unknown input(s) for #{service_class.name}:
155
+ provided: #{unknown_inputs.map(&:inspect).join(', ')},
156
+ defined: #{defined_inputs.map(&:inspect).join(', ')}.
157
+ Hint: Check that the input names match the service definition.
158
+ MESSAGE
159
+ end
160
+
161
+ # Builds error message when no_inputs used but service has required inputs.
162
+ #
163
+ # @param service_class [Class] The service class
164
+ # @param required_inputs [Array<Symbol>] Required input names
165
+ # @return [String] Error message with hint
166
+ def no_inputs_but_required_message(service_class:, required_inputs:)
167
+ <<~MESSAGE.squish
168
+ Service #{service_class.name} has required inputs,
169
+ but no_inputs matcher was used.
170
+ Required inputs: #{required_inputs.map(&:inspect).join(', ')}.
171
+ Hint: Use with({...}) with the required inputs instead of no_inputs.
172
+ MESSAGE
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ module Concerns
8
+ # Concern providing validation for Servactory service classes.
9
+ #
10
+ # ## Purpose
11
+ #
12
+ # ServiceClassValidation ensures that values passed to service mock helpers
13
+ # are valid Servactory service classes. It checks for the required interface
14
+ # methods and provides helpful error messages when validation fails.
15
+ #
16
+ # ## Usage
17
+ #
18
+ # Include in helper classes that accept service classes:
19
+ #
20
+ # ```ruby
21
+ # class ServiceMockBuilder
22
+ # include Concerns::ServiceClassValidation
23
+ #
24
+ # def initialize(service_class)
25
+ # validate_service_class!(service_class)
26
+ # @service_class = service_class
27
+ # end
28
+ # end
29
+ # ```
30
+ #
31
+ # ## Validation Rules
32
+ #
33
+ # A valid service class must:
34
+ # - Be a Class (not instance or module)
35
+ # - Respond to `.call` method
36
+ # - Respond to `.call!` method
37
+ # - Respond to `.info` method (for introspection)
38
+ module ServiceClassValidation
39
+ include ErrorMessages
40
+
41
+ # Error raised when an invalid service class is provided.
42
+ class InvalidServiceClassError < ArgumentError; end
43
+
44
+ private
45
+
46
+ # Validates that the given value is a valid Servactory service class.
47
+ #
48
+ # @param service_class [Object] The value to validate
49
+ # @return [void]
50
+ # @raise [InvalidServiceClassError] If validation fails
51
+ def validate_service_class!(service_class)
52
+ return if valid_service_class?(service_class)
53
+
54
+ raise InvalidServiceClassError, invalid_service_class_message(service_class)
55
+ end
56
+
57
+ # Checks if the given value is a valid Servactory service class.
58
+ #
59
+ # @param service_class [Object] The value to check
60
+ # @return [Boolean] True if valid Servactory service class
61
+ def valid_service_class?(service_class)
62
+ return false unless service_class.is_a?(Class)
63
+ return false unless service_class.respond_to?(:call)
64
+ return false unless service_class.respond_to?(:call!)
65
+ return false unless service_class.respond_to?(:info)
66
+
67
+ true
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module TestKit
5
+ module Rspec
6
+ module Helpers
7
+ # Fluent API for mocking Servactory services in RSpec tests.
8
+ #
9
+ # ## Purpose
10
+ #
11
+ # Provides modern fluent builder interface for configuring service mocks
12
+ # with chainable method calls. This is the recommended API for new tests.
13
+ #
14
+ # ## Usage
15
+ #
16
+ # Include in RSpec configuration via the main Helpers module:
17
+ #
18
+ # ```ruby
19
+ # RSpec.configure do |config|
20
+ # config.include Servactory::TestKit::Rspec::Helpers, type: :service
21
+ # end
22
+ # ```
23
+ #
24
+ # ## Available Methods
25
+ #
26
+ # - `allow_service(ServiceClass)` - mock `.call` method (returns Result)
27
+ # - `allow_service!(ServiceClass)` - mock `.call!` method (raises on failure)
28
+ #
29
+ # ## Examples
30
+ #
31
+ # ```ruby
32
+ # # Mock successful service call with outputs
33
+ # allow_service(PaymentService)
34
+ # .succeeds(transaction_id: "txn_123", status: :completed)
35
+ #
36
+ # # Mock with input matching
37
+ # allow_service(PaymentService)
38
+ # .with(amount: 100)
39
+ # .succeeds(transaction_id: "txn_100")
40
+ #
41
+ # # Mock failure
42
+ # allow_service(PaymentService)
43
+ # .fails(type: :payment_declined, message: "Card declined")
44
+ #
45
+ # # Sequential returns
46
+ # allow_service(PaymentService)
47
+ # .succeeds(status: :pending)
48
+ # .then_succeeds(status: :completed)
49
+ # ```
50
+ #
51
+ # @see ServiceMockBuilder for full fluent API documentation
52
+ module Fluent
53
+ # Start building a service mock with fluent API for .call method.
54
+ #
55
+ # When service fails, returns Result with `.error` attribute.
56
+ #
57
+ # @param service_class [Class] The service class to mock
58
+ # @return [ServiceMockBuilder] Builder for fluent configuration
59
+ #
60
+ # @example Success mock
61
+ # allow_service(PaymentService)
62
+ # .succeeds(transaction_id: "txn_123", status: :completed)
63
+ #
64
+ # @example Success with input matching
65
+ # allow_service(PaymentService)
66
+ # .with(amount: 100)
67
+ # .succeeds(transaction_id: "txn_123")
68
+ #
69
+ # @example Failure mock
70
+ # allow_service(PaymentService)
71
+ # .fails(type: :payment_declined, message: "Card declined")
72
+ #
73
+ # @example Sequential returns
74
+ # allow_service(PaymentService)
75
+ # .succeeds(status: :pending)
76
+ # .then_succeeds(status: :completed)
77
+ # .then_fails(type: :timeout, message: "Request timed out")
78
+ #
79
+ def allow_service(service_class)
80
+ Helpers::ServiceMockBuilder.new(service_class, method_type: :call, rspec_context: self)
81
+ end
82
+
83
+ # Start building a service mock with fluent API for .call! method.
84
+ #
85
+ # When service fails, raises exception.
86
+ #
87
+ # @param service_class [Class] The service class to mock
88
+ # @return [ServiceMockBuilder] Builder for fluent configuration
89
+ #
90
+ # @example Success mock for call!
91
+ # allow_service!(PaymentService)
92
+ # .succeeds(transaction_id: "txn_123", status: :completed)
93
+ #
94
+ # @example Failure mock for call! (raises exception)
95
+ # allow_service!(PaymentService)
96
+ # .fails(type: :payment_declined, message: "Insufficient funds")
97
+ #
98
+ # @example Sequential returns
99
+ # allow_service!(RetryService)
100
+ # .succeeds(status: :pending)
101
+ # .then_fails(type: :timeout, message: "Request timed out")
102
+ #
103
+ def allow_service!(service_class)
104
+ Helpers::ServiceMockBuilder.new(service_class, method_type: :call!, rspec_context: self)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end