servus 0.1.3 → 0.1.5

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 (139) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/check-docs.md +1 -0
  3. data/.claude/commands/consistency-check.md +1 -0
  4. data/.claude/commands/fine-tooth-comb.md +1 -0
  5. data/.claude/commands/red-green-refactor.md +5 -0
  6. data/.claude/settings.json +15 -0
  7. data/.rubocop.yml +18 -2
  8. data/.yardopts +6 -0
  9. data/CHANGELOG.md +47 -0
  10. data/CLAUDE.md +10 -0
  11. data/IDEAS.md +5 -0
  12. data/READme.md +300 -47
  13. data/Rakefile +33 -0
  14. data/builds/servus-0.1.3.gem +0 -0
  15. data/builds/servus-0.1.4.gem +0 -0
  16. data/builds/servus-0.1.5.gem +0 -0
  17. data/docs/core/1_overview.md +77 -0
  18. data/docs/core/2_architecture.md +120 -0
  19. data/docs/core/3_service_objects.md +121 -0
  20. data/docs/current_focus.md +569 -0
  21. data/docs/features/1_schema_validation.md +119 -0
  22. data/docs/features/2_error_handling.md +121 -0
  23. data/docs/features/3_async_execution.md +81 -0
  24. data/docs/features/4_logging.md +64 -0
  25. data/docs/features/5_event_bus.md +244 -0
  26. data/docs/guides/1_common_patterns.md +90 -0
  27. data/docs/guides/2_migration_guide.md +175 -0
  28. data/docs/integration/1_configuration.md +104 -0
  29. data/docs/integration/2_testing.md +287 -0
  30. data/docs/integration/3_rails_integration.md +99 -0
  31. data/docs/yard/Servus/Base.html +1645 -0
  32. data/docs/yard/Servus/Config.html +582 -0
  33. data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
  34. data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
  35. data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
  36. data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
  37. data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
  38. data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
  39. data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
  40. data/docs/yard/Servus/Extensions/Async.html +141 -0
  41. data/docs/yard/Servus/Extensions.html +117 -0
  42. data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
  43. data/docs/yard/Servus/Generators.html +115 -0
  44. data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
  45. data/docs/yard/Servus/Helpers.html +115 -0
  46. data/docs/yard/Servus/Railtie.html +134 -0
  47. data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
  48. data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
  49. data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
  50. data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
  51. data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
  52. data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
  53. data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
  54. data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
  55. data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
  56. data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
  57. data/docs/yard/Servus/Support/Errors.html +140 -0
  58. data/docs/yard/Servus/Support/Logger.html +856 -0
  59. data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
  60. data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
  61. data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
  62. data/docs/yard/Servus/Support/Rescuer.html +267 -0
  63. data/docs/yard/Servus/Support/Response.html +574 -0
  64. data/docs/yard/Servus/Support/Validator.html +1150 -0
  65. data/docs/yard/Servus/Support.html +119 -0
  66. data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
  67. data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
  68. data/docs/yard/Servus/Testing.html +142 -0
  69. data/docs/yard/Servus.html +343 -0
  70. data/docs/yard/_index.html +535 -0
  71. data/docs/yard/class_list.html +54 -0
  72. data/docs/yard/css/common.css +1 -0
  73. data/docs/yard/css/full_list.css +58 -0
  74. data/docs/yard/css/style.css +503 -0
  75. data/docs/yard/file.1_common_patterns.html +154 -0
  76. data/docs/yard/file.1_configuration.html +115 -0
  77. data/docs/yard/file.1_overview.html +142 -0
  78. data/docs/yard/file.1_schema_validation.html +188 -0
  79. data/docs/yard/file.2_architecture.html +157 -0
  80. data/docs/yard/file.2_error_handling.html +190 -0
  81. data/docs/yard/file.2_migration_guide.html +242 -0
  82. data/docs/yard/file.2_testing.html +227 -0
  83. data/docs/yard/file.3_async_execution.html +145 -0
  84. data/docs/yard/file.3_rails_integration.html +160 -0
  85. data/docs/yard/file.3_service_objects.html +191 -0
  86. data/docs/yard/file.4_logging.html +135 -0
  87. data/docs/yard/file.ErrorHandling.html +190 -0
  88. data/docs/yard/file.READme.html +674 -0
  89. data/docs/yard/file.architecture.html +157 -0
  90. data/docs/yard/file.async_execution.html +145 -0
  91. data/docs/yard/file.common_patterns.html +154 -0
  92. data/docs/yard/file.configuration.html +115 -0
  93. data/docs/yard/file.error_handling.html +190 -0
  94. data/docs/yard/file.logging.html +135 -0
  95. data/docs/yard/file.migration_guide.html +242 -0
  96. data/docs/yard/file.overview.html +142 -0
  97. data/docs/yard/file.rails_integration.html +160 -0
  98. data/docs/yard/file.schema_validation.html +188 -0
  99. data/docs/yard/file.service_objects.html +191 -0
  100. data/docs/yard/file.testing.html +227 -0
  101. data/docs/yard/file_list.html +119 -0
  102. data/docs/yard/frames.html +22 -0
  103. data/docs/yard/index.html +674 -0
  104. data/docs/yard/js/app.js +344 -0
  105. data/docs/yard/js/full_list.js +242 -0
  106. data/docs/yard/js/jquery.js +4 -0
  107. data/docs/yard/method_list.html +542 -0
  108. data/docs/yard/top-level-namespace.html +110 -0
  109. data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
  110. data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
  111. data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
  112. data/lib/generators/servus/service/service_generator.rb +68 -1
  113. data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
  114. data/lib/generators/servus/service/templates/result.json.erb +8 -2
  115. data/lib/generators/servus/service/templates/service.rb.erb +102 -5
  116. data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
  117. data/lib/servus/base.rb +275 -58
  118. data/lib/servus/config.rb +83 -17
  119. data/lib/servus/event_handler.rb +275 -0
  120. data/lib/servus/events/bus.rb +137 -0
  121. data/lib/servus/events/emitter.rb +162 -0
  122. data/lib/servus/events/errors.rb +10 -0
  123. data/lib/servus/extensions/async/call.rb +50 -18
  124. data/lib/servus/extensions/async/errors.rb +23 -3
  125. data/lib/servus/extensions/async/ext.rb +10 -2
  126. data/lib/servus/extensions/async/job.rb +30 -9
  127. data/lib/servus/helpers/controller_helpers.rb +73 -37
  128. data/lib/servus/railtie.rb +16 -0
  129. data/lib/servus/support/errors.rb +135 -45
  130. data/lib/servus/support/rescuer.rb +189 -36
  131. data/lib/servus/support/response.rb +49 -7
  132. data/lib/servus/support/validator.rb +147 -19
  133. data/lib/servus/testing/example_builders.rb +133 -0
  134. data/lib/servus/testing/example_extractor.rb +309 -0
  135. data/lib/servus/testing/matchers.rb +88 -0
  136. data/lib/servus/testing.rb +19 -0
  137. data/lib/servus/version.rb +1 -1
  138. data/lib/servus.rb +6 -0
  139. metadata +135 -19
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servus
4
+ module Generators
5
+ # Rails generator for creating Servus event handlers.
6
+ #
7
+ # Generates an event handler class and spec file.
8
+ #
9
+ # @example Generate an event handler
10
+ # rails g servus:event_handler user_created
11
+ #
12
+ # @example Generated files
13
+ # app/events/user_created_handler.rb
14
+ # spec/app/events/user_created_handler_spec.rb
15
+ #
16
+ # @see https://guides.rubyonrails.org/generators.html
17
+ class EventHandlerGenerator < Rails::Generators::NamedBase
18
+ source_root File.expand_path('templates', __dir__)
19
+
20
+ class_option :no_docs, type: :boolean,
21
+ default: false,
22
+ desc: 'Skip documentation comments in generated files'
23
+
24
+ # Creates the event handler and spec files.
25
+ #
26
+ # @return [void]
27
+ def create_handler_file
28
+ template 'handler.rb.erb', handler_path
29
+ template 'handler_spec.rb.erb', handler_spec_path
30
+ end
31
+
32
+ private
33
+
34
+ # Returns the path for the handler file.
35
+ #
36
+ # @return [String] handler file path
37
+ # @api private
38
+ def handler_path
39
+ File.join(Servus.config.events_dir, "#{file_name}_handler.rb")
40
+ end
41
+
42
+ # Returns the path for the handler spec file.
43
+ #
44
+ # @return [String] spec file path
45
+ # @api private
46
+ def handler_spec_path
47
+ File.join('spec', Servus.config.events_dir, "#{file_name}_handler_spec.rb")
48
+ end
49
+
50
+ # Returns the handler class name.
51
+ #
52
+ # @return [String] handler class name
53
+ # @api private
54
+ def handler_class_name
55
+ "#{class_name}Handler"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%- unless options[:no_docs] -%>
4
+ # Handles the :<%= file_name %> event by invoking configured services.
5
+ #
6
+ # EventHandlers subscribe to a single event and declaratively map it to one or
7
+ # more service invocations. This provides clean separation between event emission
8
+ # (what happened) and event handling (what to do about it).
9
+ #
10
+ # @example Basic usage
11
+ # class <%= handler_class_name %> < Servus::EventHandler
12
+ # handles :<%= file_name %>
13
+ #
14
+ # # Invoke a service when this event fires
15
+ # invoke SendEmail::Service, async: true do |payload|
16
+ # { user_id: payload[:user_id], email: payload[:email] }
17
+ # end
18
+ # end
19
+ #
20
+ # @example Multiple service invocations
21
+ # invoke SendWelcomeEmail::Service, async: true, queue: :mailers do |payload|
22
+ # { user_id: payload[:user_id] }
23
+ # end
24
+ #
25
+ # invoke TrackAnalytics::Service, async: true do |payload|
26
+ # { event: '<%= file_name %>', user_id: payload[:user_id] }
27
+ # end
28
+ #
29
+ # @example Conditional invocation
30
+ # invoke GrantRewards::Service, if: ->(payload) { payload[:premium] } do |payload|
31
+ # { user_id: payload[:user_id] }
32
+ # end
33
+ #
34
+ # @example With payload schema validation
35
+ # schema payload: {
36
+ # type: 'object',
37
+ # required: ['user_id'],
38
+ # properties: {
39
+ # user_id: { type: 'integer' },
40
+ # email: { type: 'string', format: 'email' }
41
+ # }
42
+ # }
43
+ #
44
+ # @example Emit this event from anywhere
45
+ # # From controllers, jobs, rake tasks, etc.
46
+ # <%= handler_class_name %>.emit({ user_id: 123, email: 'user@example.com' })
47
+ #
48
+ # Available options for `invoke`:
49
+ # - async: true - Invoke service asynchronously via ActiveJob
50
+ # - queue: :queue_name - Specify ActiveJob queue (requires async: true)
51
+ # - if: ->(payload) {} - Condition that must be true to invoke
52
+ # - unless: ->(payload) {} - Condition that must be false to invoke
53
+ #
54
+ # @see Servus::EventHandler
55
+ # @see Servus::Events::Bus
56
+ <%- end -%>
57
+ class <%= handler_class_name %> < Servus::EventHandler
58
+ handles :<%= file_name %>
59
+
60
+ <%- unless options[:no_docs] -%>
61
+ # TODO: Define payload schema (optional but recommended)
62
+ # schema payload: {
63
+ # type: 'object',
64
+ # required: ['required_field'],
65
+ # properties: {
66
+ # required_field: { type: 'string' }
67
+ # }
68
+ # }
69
+
70
+ # TODO: Add service invocations
71
+ # invoke YourService, async: true do |payload|
72
+ # {
73
+ # # Map event payload to service arguments
74
+ # argument_name: payload[:field_name]
75
+ # }
76
+ # end
77
+ <%- end -%>
78
+ schema payload: {
79
+ type: 'object',
80
+ description: 'JSON schema for the <%= handler_class_name %> event payload',
81
+ }
82
+
83
+ # invoke ExampleService, async: true do |payload|
84
+ # { example_arg: payload[:example_field] }
85
+ # end
86
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= handler_class_name %> do
6
+ <%- unless options[:no_docs] -%>
7
+ # Test that the handler invokes the correct services with properly mapped arguments.
8
+ #
9
+ # Example test pattern:
10
+ # it 'invokes YourService with correct arguments' do
11
+ # expect(YourService).to receive(:call_async).with(user_id: 123)
12
+ # described_class.handle({ user_id: 123, email: 'test@example.com' })
13
+ # end
14
+ #
15
+ # For testing event emission from controllers/jobs:
16
+ # include Servus::Testing::EventHelpers
17
+ #
18
+ # it 'emits <%= file_name %> event' do
19
+ # servus_expect_event(:<%= file_name %>)
20
+ # .with_payload(hash_including(user_id: 123))
21
+ # .when { YourController.new.create }
22
+ # end
23
+
24
+ <%- end -%>
25
+ let(:payload) do
26
+ {
27
+ # TODO: Add sample payload fields
28
+ # user_id: 123,
29
+ # email: 'test@example.com'
30
+ }
31
+ end
32
+
33
+ <%- unless options[:no_docs] -%>
34
+ # TODO: Add tests for service invocations
35
+ # it 'invokes YourService with mapped arguments' do
36
+ # expect(YourService).to receive(:call_async).with(user_id: payload[:user_id])
37
+ # described_class.handle(payload)
38
+ # end
39
+
40
+ # TODO: Test conditional logic if using :if or :unless
41
+ # it 'skips invocation when condition is false' do
42
+ # expect(YourService).not_to receive(:call_async)
43
+ # described_class.handle(payload.merge(premium: false))
44
+ # end
45
+ <%- else -%>
46
+ # Add your tests here
47
+ <%- end -%>
48
+ end
@@ -2,12 +2,37 @@
2
2
 
3
3
  module Servus
4
4
  module Generators
5
- # Servus Generator
5
+ # Rails generator for creating Servus service objects.
6
+ #
7
+ # Generates a complete service structure including:
8
+ # - Service class file
9
+ # - RSpec test file
10
+ # - JSON schema files for arguments and results
11
+ #
12
+ # @example Generate a service
13
+ # rails g servus:service namespace/do_something_helpful user amount
14
+ #
15
+ # @example Generated files
16
+ # app/services/namespace/do_something_helpful/service.rb
17
+ # spec/services/namespace/do_something_helpful/service_spec.rb
18
+ # app/schemas/services/namespace/do_something_helpful/arguments.json
19
+ # app/schemas/services/namespace/do_something_helpful/result.json
20
+ #
21
+ # @see https://guides.rubyonrails.org/generators.html
6
22
  class ServiceGenerator < Rails::Generators::NamedBase
7
23
  source_root File.expand_path('templates', __dir__)
8
24
 
9
25
  argument :parameters, type: :array, default: [], banner: 'parameter'
10
26
 
27
+ class_option :no_docs, type: :boolean,
28
+ default: false,
29
+ desc: 'Skip documentation comments in generated files'
30
+
31
+ # Creates all service-related files.
32
+ #
33
+ # Generates the service class, spec file, and schema files from templates.
34
+ #
35
+ # @return [void]
11
36
  def create_service_file
12
37
  template 'service.rb.erb', service_path
13
38
  template 'service_spec.rb.erb', service_path_spec
@@ -19,40 +44,82 @@ module Servus
19
44
 
20
45
  private
21
46
 
47
+ # Returns the path for the service file.
48
+ #
49
+ # @return [String] service file path
50
+ # @api private
22
51
  def service_path
23
52
  "app/services/#{file_path}/service.rb"
24
53
  end
25
54
 
55
+ # Returns the path for the service spec file.
56
+ #
57
+ # @return [String] spec file path
58
+ # @api private
26
59
  def service_path_spec
27
60
  "spec/services/#{file_path}/service_spec.rb"
28
61
  end
29
62
 
63
+ # Returns the path for the result schema file.
64
+ #
65
+ # @return [String] result schema path
66
+ # @api private
30
67
  def service_result_schema_path
31
68
  "app/schemas/services/#{file_path}/result.json"
32
69
  end
33
70
 
71
+ # Returns the path for the arguments schema file.
72
+ #
73
+ # @return [String] arguments schema path
74
+ # @api private
34
75
  def service_arguments_shecma_path
35
76
  "app/schemas/services/#{file_path}/arguments.json"
36
77
  end
37
78
 
79
+ # Returns the service class name with ::Service appended.
80
+ #
81
+ # @return [String] service class name
82
+ # @api private
38
83
  def service_class_name
39
84
  "#{class_name}::Service"
40
85
  end
41
86
 
87
+ # Returns the fully-qualified service class name.
88
+ #
89
+ # @return [String] fully-qualified class name
90
+ # @api private
42
91
  def service_full_class_name
43
92
  service_class_name.include?('::') ? service_class_name : "::#{service_class_name}"
44
93
  end
45
94
 
95
+ # Generates the parameter list for the initialize method.
96
+ #
97
+ # @return [String] parameter list with keyword syntax
98
+ # @example
99
+ # parameter_list # => "(user:, amount:)"
100
+ # @api private
46
101
  def parameter_list
47
102
  return '' if parameters.empty?
48
103
 
49
104
  "(#{parameters.map { |param| "#{param}:" }.join(', ')})"
50
105
  end
51
106
 
107
+ # Generates instance variable assignments for initialize method.
108
+ #
109
+ # @return [String] multi-line instance variable assignments
110
+ # @example
111
+ # initialize_params # => "@user = user\n @amount = amount"
112
+ # @api private
52
113
  def initialize_params
53
114
  parameters.map { |param| "@#{param} = #{param}" }.join("\n ")
54
115
  end
55
116
 
117
+ # Generates attr_reader declarations for parameters.
118
+ #
119
+ # @return [String] attr_reader declaration or empty string
120
+ # @example
121
+ # attr_readers # => "attr_reader :user, :amount"
122
+ # @api private
56
123
  def attr_readers
57
124
  return '' if parameters.empty?
58
125
 
@@ -1,15 +1,24 @@
1
1
  {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "<%= service_class_name %> Arguments",
4
+ "description": "JSON Schema for validating <%= service_class_name %> input arguments",
2
5
  "type": "object",
3
6
  "properties": {
4
- <%- parameters.each do |param| %>
5
- "<%= param %>": {
6
- "type": "UNDECLARED_TYPE"
7
- }
8
- <% end %>
7
+ <%- parameters.each_with_index do |param, index| -%>
8
+ "<%= param %>": {
9
+ "type": "string",
10
+ "description": "TODO: Describe the <%= param %> parameter"
11
+ }<%= index < parameters.length - 1 ? ',' : '' %>
12
+ <%- end -%>
13
+ <%- if parameters.empty? -%>
9
14
  },
15
+ <%- else -%>
16
+ },
17
+ <%- end -%>
10
18
  "required": [
11
- <%- parameters.each do |param| %>
12
- "<%= param %>",
13
- <% end %>
14
- ]
15
- }
19
+ <%- parameters.each_with_index do |param, index| -%>
20
+ "<%= param %>"<%= index < parameters.length - 1 ? ',' : '' %>
21
+ <%- end -%>
22
+ ],
23
+ "additionalProperties": false
24
+ }
@@ -1,4 +1,10 @@
1
1
  {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "<%= service_class_name %> Result",
4
+ "description": "JSON Schema for validating <%= service_class_name %> result data",
2
5
  "type": "object",
3
- "properties": {}
4
- }
6
+ "properties": {
7
+ },
8
+ "required": [],
9
+ "additionalProperties": true
10
+ }
@@ -1,12 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%- unless options[:no_docs] -%>
4
+ # Performs <%= file_name.humanize.downcase %> operations.
5
+ #
6
+ # Services encapsulate business logic with automatic validation, logging,
7
+ # benchmarking, and error handling. They return Response objects indicating
8
+ # success or failure.
9
+ #
10
+ # @example Basic usage
11
+ # result = <%= service_full_class_name %>.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: value" }.join(', ') %>)
12
+ #
13
+ # if result.success?
14
+ # result.data # => { ... }
15
+ # else
16
+ # result.error.message # => "Error description"
17
+ # end
18
+ #
19
+ <%- unless parameters.empty? -%>
20
+ # @example With all parameters
21
+ # result = <%= service_full_class_name %>.call(
22
+ <%- parameters.each do |param| -%>
23
+ # <%= param %>: <%= param %>_value<%= param == parameters.last ? '' : ',' %>
24
+ <%- end -%>
25
+ # )
26
+ #
27
+ <%- end -%>
28
+ # @example Async execution (via ActiveJob)
29
+ # <%= service_full_class_name %>.call_async(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: value" }.join(', ') %>)
30
+ #
31
+ # @example With event emission
32
+ # class <%= service_class_name %> < Servus::Base
33
+ # emits :completed, on: :success
34
+ # emits :failed, on: :failure
35
+ # # ...
36
+ # end
37
+ #
38
+ # @see Servus::Base
39
+ # @see Servus::Support::Response
40
+ <%- end -%>
1
41
  module <%= class_name %>
2
- class Service < Servus::ApplicationService
42
+ class Service < Servus::Base
43
+ <%- unless options[:no_docs] -%>
44
+ # TODO: Define argument validation schema (optional but recommended)
45
+ # schema arguments: {
46
+ # type: 'object',
47
+ <%- if parameters.any? -%>
48
+ # required: [<%= parameters.map { |p| "'#{p}'" }.join(', ') %>],
49
+ <%- else -%>
50
+ # required: [],
51
+ <%- end -%>
52
+ # properties: {
53
+ <%- parameters.each do |param| -%>
54
+ # <%= param %>: { type: 'string' }<%= param == parameters.last ? '' : ',' %>
55
+ <%- end -%>
56
+ <%- if parameters.empty? -%>
57
+ # # property_name: { type: 'string' }
58
+ <%- end -%>
59
+ # }
60
+ # }
61
+
62
+ # TODO: Define result validation schema (optional)
63
+ # schema result: {
64
+ # type: 'object',
65
+ # required: [],
66
+ # properties: {
67
+ # # result_field: { type: 'string' }
68
+ # }
69
+ # }
70
+
71
+ <%- end -%>
72
+ <%- unless options[:no_docs] -%>
73
+ # Initializes the service with required parameters.
74
+ #
75
+ <%- parameters.each do |param| -%>
76
+ # @param <%= param %> [Object] TODO: document this parameter
77
+ <%- end -%>
78
+ <%- if parameters.empty? -%>
79
+ # @param args [Hash] service arguments
80
+ <%- end -%>
81
+ <%- end -%>
3
82
  def initialize<%= parameter_list %>
4
- <%= initialize_params %>
83
+ <%= initialize_params.empty? ? '# TODO: Initialize instance variables' : initialize_params %>
5
84
  end
85
+
86
+ <%- unless options[:no_docs] -%>
87
+ # Executes the service logic.
88
+ #
89
+ # @return [Servus::Support::Response] success or failure response
90
+ #
91
+ # @example Returning success
92
+ # success({ user_id: 123, status: 'active' })
93
+ #
94
+ # @example Returning failure
95
+ # failure('Something went wrong')
96
+ #
97
+ # @example Returning typed failure
98
+ # failure('Not found', type: Servus::Support::Errors::NotFoundError)
99
+ <%- end -%>
6
100
  def call
7
- # Implement service logic here
101
+ # TODO: Implement service logic here
8
102
  success({})
9
103
  end
10
- <%= attr_readers %>
104
+
105
+ private
106
+
107
+ <%= attr_readers.empty? ? '# TODO: Add attr_readers for instance variables' : attr_readers %>
11
108
  end
12
- end
109
+ end
@@ -1,9 +1,70 @@
1
- require "rails_helper"
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
2
4
 
3
5
  RSpec.describe <%= service_full_class_name %> do
4
- describe "#call" do
5
- it "does something" do
6
- # Add expectations here
7
- end
6
+ <%- unless options[:no_docs] -%>
7
+ # Test the service by calling it with valid arguments and asserting on the result.
8
+ #
9
+ # Example test patterns:
10
+ #
11
+ # it 'returns success with expected data' do
12
+ # result = described_class.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: #{p}_value" }.join(', ') %>)
13
+ #
14
+ # expect(result).to be_success
15
+ # expect(result.data).to include(expected_key: expected_value)
16
+ # end
17
+ #
18
+ # it 'returns failure when validation fails' do
19
+ # result = described_class.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: invalid_value" }.join(', ') %>)
20
+ #
21
+ # expect(result).to be_failure
22
+ # expect(result.error.message).to eq('Expected error message')
23
+ # end
24
+ #
25
+ # For testing async execution:
26
+ # it 'enqueues the service job' do
27
+ # expect {
28
+ # described_class.call_async(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: #{p}_value" }.join(', ') %>)
29
+ # }.to have_enqueued_job(Servus::ServiceJob)
30
+ # end
31
+ #
32
+ # For testing event emissions:
33
+ # include Servus::Testing::EventHelpers
34
+ #
35
+ # it 'emits expected event on success' do
36
+ # servus_expect_event(:event_name)
37
+ # .with_payload(hash_including(key: value))
38
+ # .when { described_class.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: value" }.join(', ') %>) }
39
+ # end
40
+
41
+ <%- end -%>
42
+ <%- if parameters.any? -%>
43
+ let(:<%= parameters.first %>) { nil } # TODO: Set valid test value
44
+ <%- parameters[1..-1]&.each do |param| -%>
45
+ let(:<%= param %>) { nil } # TODO: Set valid test value
46
+ <%- end -%>
47
+
48
+ <%- end -%>
49
+ describe '#call' do
50
+ <%- unless options[:no_docs] -%>
51
+ # TODO: Add success case test
52
+ # it 'returns success with expected data' do
53
+ # result = described_class.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: #{p}" }.join(', ') %>)
54
+ #
55
+ # expect(result).to be_success
56
+ # expect(result.data).to include(key: value)
57
+ # end
58
+
59
+ # TODO: Add failure case test
60
+ # it 'returns failure when conditions are not met' do
61
+ # result = described_class.call(<%= parameters.empty? ? '' : parameters.map { |p| "#{p}: invalid_#{p}" }.join(', ') %>)
62
+ #
63
+ # expect(result).to be_failure
64
+ # expect(result.error.message).to eq('Expected error')
65
+ # end
66
+ <%- else -%>
67
+ # Add your tests here
68
+ <%- end -%>
8
69
  end
9
- end
70
+ end