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.
- checksums.yaml +4 -4
- data/.claude/commands/check-docs.md +1 -0
- data/.claude/commands/consistency-check.md +1 -0
- data/.claude/commands/fine-tooth-comb.md +1 -0
- data/.claude/commands/red-green-refactor.md +5 -0
- data/.claude/settings.json +15 -0
- data/.rubocop.yml +18 -2
- data/.yardopts +6 -0
- data/CHANGELOG.md +47 -0
- data/CLAUDE.md +10 -0
- data/IDEAS.md +5 -0
- data/READme.md +300 -47
- data/Rakefile +33 -0
- data/builds/servus-0.1.3.gem +0 -0
- data/builds/servus-0.1.4.gem +0 -0
- data/builds/servus-0.1.5.gem +0 -0
- data/docs/core/1_overview.md +77 -0
- data/docs/core/2_architecture.md +120 -0
- data/docs/core/3_service_objects.md +121 -0
- data/docs/current_focus.md +569 -0
- data/docs/features/1_schema_validation.md +119 -0
- data/docs/features/2_error_handling.md +121 -0
- data/docs/features/3_async_execution.md +81 -0
- data/docs/features/4_logging.md +64 -0
- data/docs/features/5_event_bus.md +244 -0
- data/docs/guides/1_common_patterns.md +90 -0
- data/docs/guides/2_migration_guide.md +175 -0
- data/docs/integration/1_configuration.md +104 -0
- data/docs/integration/2_testing.md +287 -0
- data/docs/integration/3_rails_integration.md +99 -0
- data/docs/yard/Servus/Base.html +1645 -0
- data/docs/yard/Servus/Config.html +582 -0
- data/docs/yard/Servus/Extensions/Async/Call.html +400 -0
- data/docs/yard/Servus/Extensions/Async/Errors/AsyncError.html +140 -0
- data/docs/yard/Servus/Extensions/Async/Errors/JobEnqueueError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors/ServiceNotFoundError.html +154 -0
- data/docs/yard/Servus/Extensions/Async/Errors.html +128 -0
- data/docs/yard/Servus/Extensions/Async/Ext.html +119 -0
- data/docs/yard/Servus/Extensions/Async/Job.html +310 -0
- data/docs/yard/Servus/Extensions/Async.html +141 -0
- data/docs/yard/Servus/Extensions.html +117 -0
- data/docs/yard/Servus/Generators/ServiceGenerator.html +261 -0
- data/docs/yard/Servus/Generators.html +115 -0
- data/docs/yard/Servus/Helpers/ControllerHelpers.html +457 -0
- data/docs/yard/Servus/Helpers.html +115 -0
- data/docs/yard/Servus/Railtie.html +134 -0
- data/docs/yard/Servus/Support/Errors/AuthenticationError.html +287 -0
- data/docs/yard/Servus/Support/Errors/BadRequestError.html +283 -0
- data/docs/yard/Servus/Support/Errors/ForbiddenError.html +284 -0
- data/docs/yard/Servus/Support/Errors/InternalServerError.html +283 -0
- data/docs/yard/Servus/Support/Errors/NotFoundError.html +284 -0
- data/docs/yard/Servus/Support/Errors/ServiceError.html +489 -0
- data/docs/yard/Servus/Support/Errors/ServiceUnavailableError.html +290 -0
- data/docs/yard/Servus/Support/Errors/UnauthorizedError.html +200 -0
- data/docs/yard/Servus/Support/Errors/UnprocessableEntityError.html +288 -0
- data/docs/yard/Servus/Support/Errors/ValidationError.html +200 -0
- data/docs/yard/Servus/Support/Errors.html +140 -0
- data/docs/yard/Servus/Support/Logger.html +856 -0
- data/docs/yard/Servus/Support/Rescuer/BlockContext.html +585 -0
- data/docs/yard/Servus/Support/Rescuer/CallOverride.html +257 -0
- data/docs/yard/Servus/Support/Rescuer/ClassMethods.html +343 -0
- data/docs/yard/Servus/Support/Rescuer.html +267 -0
- data/docs/yard/Servus/Support/Response.html +574 -0
- data/docs/yard/Servus/Support/Validator.html +1150 -0
- data/docs/yard/Servus/Support.html +119 -0
- data/docs/yard/Servus/Testing/ExampleBuilders.html +523 -0
- data/docs/yard/Servus/Testing/ExampleExtractor.html +578 -0
- data/docs/yard/Servus/Testing.html +142 -0
- data/docs/yard/Servus.html +343 -0
- data/docs/yard/_index.html +535 -0
- data/docs/yard/class_list.html +54 -0
- data/docs/yard/css/common.css +1 -0
- data/docs/yard/css/full_list.css +58 -0
- data/docs/yard/css/style.css +503 -0
- data/docs/yard/file.1_common_patterns.html +154 -0
- data/docs/yard/file.1_configuration.html +115 -0
- data/docs/yard/file.1_overview.html +142 -0
- data/docs/yard/file.1_schema_validation.html +188 -0
- data/docs/yard/file.2_architecture.html +157 -0
- data/docs/yard/file.2_error_handling.html +190 -0
- data/docs/yard/file.2_migration_guide.html +242 -0
- data/docs/yard/file.2_testing.html +227 -0
- data/docs/yard/file.3_async_execution.html +145 -0
- data/docs/yard/file.3_rails_integration.html +160 -0
- data/docs/yard/file.3_service_objects.html +191 -0
- data/docs/yard/file.4_logging.html +135 -0
- data/docs/yard/file.ErrorHandling.html +190 -0
- data/docs/yard/file.READme.html +674 -0
- data/docs/yard/file.architecture.html +157 -0
- data/docs/yard/file.async_execution.html +145 -0
- data/docs/yard/file.common_patterns.html +154 -0
- data/docs/yard/file.configuration.html +115 -0
- data/docs/yard/file.error_handling.html +190 -0
- data/docs/yard/file.logging.html +135 -0
- data/docs/yard/file.migration_guide.html +242 -0
- data/docs/yard/file.overview.html +142 -0
- data/docs/yard/file.rails_integration.html +160 -0
- data/docs/yard/file.schema_validation.html +188 -0
- data/docs/yard/file.service_objects.html +191 -0
- data/docs/yard/file.testing.html +227 -0
- data/docs/yard/file_list.html +119 -0
- data/docs/yard/frames.html +22 -0
- data/docs/yard/index.html +674 -0
- data/docs/yard/js/app.js +344 -0
- data/docs/yard/js/full_list.js +242 -0
- data/docs/yard/js/jquery.js +4 -0
- data/docs/yard/method_list.html +542 -0
- data/docs/yard/top-level-namespace.html +110 -0
- data/lib/generators/servus/event_handler/event_handler_generator.rb +59 -0
- data/lib/generators/servus/event_handler/templates/handler.rb.erb +86 -0
- data/lib/generators/servus/event_handler/templates/handler_spec.rb.erb +48 -0
- data/lib/generators/servus/service/service_generator.rb +68 -1
- data/lib/generators/servus/service/templates/arguments.json.erb +19 -10
- data/lib/generators/servus/service/templates/result.json.erb +8 -2
- data/lib/generators/servus/service/templates/service.rb.erb +102 -5
- data/lib/generators/servus/service/templates/service_spec.rb.erb +67 -6
- data/lib/servus/base.rb +275 -58
- data/lib/servus/config.rb +83 -17
- data/lib/servus/event_handler.rb +275 -0
- data/lib/servus/events/bus.rb +137 -0
- data/lib/servus/events/emitter.rb +162 -0
- data/lib/servus/events/errors.rb +10 -0
- data/lib/servus/extensions/async/call.rb +50 -18
- data/lib/servus/extensions/async/errors.rb +23 -3
- data/lib/servus/extensions/async/ext.rb +10 -2
- data/lib/servus/extensions/async/job.rb +30 -9
- data/lib/servus/helpers/controller_helpers.rb +73 -37
- data/lib/servus/railtie.rb +16 -0
- data/lib/servus/support/errors.rb +135 -45
- data/lib/servus/support/rescuer.rb +189 -36
- data/lib/servus/support/response.rb +49 -7
- data/lib/servus/support/validator.rb +147 -19
- data/lib/servus/testing/example_builders.rb +133 -0
- data/lib/servus/testing/example_extractor.rb +309 -0
- data/lib/servus/testing/matchers.rb +88 -0
- data/lib/servus/testing.rb +19 -0
- data/lib/servus/version.rb +1 -1
- data/lib/servus.rb +6 -0
- 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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails_helper'
|
|
2
4
|
|
|
3
5
|
RSpec.describe <%= service_full_class_name %> do
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|