better_service 1.0.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +1321 -0
- data/Rakefile +15 -0
- data/lib/better_service/cache_service.rb +310 -0
- data/lib/better_service/concerns/instrumentation.rb +242 -0
- data/lib/better_service/concerns/serviceable/authorizable.rb +106 -0
- data/lib/better_service/concerns/serviceable/cacheable.rb +97 -0
- data/lib/better_service/concerns/serviceable/messageable.rb +30 -0
- data/lib/better_service/concerns/serviceable/presentable.rb +66 -0
- data/lib/better_service/concerns/serviceable/transactional.rb +51 -0
- data/lib/better_service/concerns/serviceable/validatable.rb +58 -0
- data/lib/better_service/concerns/serviceable/viewable.rb +49 -0
- data/lib/better_service/concerns/serviceable.rb +12 -0
- data/lib/better_service/concerns/workflowable/callbacks.rb +116 -0
- data/lib/better_service/concerns/workflowable/context.rb +108 -0
- data/lib/better_service/concerns/workflowable/step.rb +141 -0
- data/lib/better_service/concerns/workflowable.rb +12 -0
- data/lib/better_service/configuration.rb +113 -0
- data/lib/better_service/errors/better_service_error.rb +271 -0
- data/lib/better_service/errors/configuration/configuration_error.rb +21 -0
- data/lib/better_service/errors/configuration/invalid_configuration_error.rb +28 -0
- data/lib/better_service/errors/configuration/invalid_schema_error.rb +28 -0
- data/lib/better_service/errors/configuration/nil_user_error.rb +37 -0
- data/lib/better_service/errors/configuration/schema_required_error.rb +29 -0
- data/lib/better_service/errors/runtime/authorization_error.rb +38 -0
- data/lib/better_service/errors/runtime/database_error.rb +38 -0
- data/lib/better_service/errors/runtime/execution_error.rb +27 -0
- data/lib/better_service/errors/runtime/resource_not_found_error.rb +38 -0
- data/lib/better_service/errors/runtime/runtime_error.rb +22 -0
- data/lib/better_service/errors/runtime/transaction_error.rb +34 -0
- data/lib/better_service/errors/runtime/validation_error.rb +42 -0
- data/lib/better_service/errors/workflowable/configuration/duplicate_step_error.rb +27 -0
- data/lib/better_service/errors/workflowable/configuration/invalid_step_error.rb +12 -0
- data/lib/better_service/errors/workflowable/configuration/step_not_found_error.rb +29 -0
- data/lib/better_service/errors/workflowable/configuration/workflow_configuration_error.rb +24 -0
- data/lib/better_service/errors/workflowable/runtime/rollback_error.rb +46 -0
- data/lib/better_service/errors/workflowable/runtime/step_execution_error.rb +47 -0
- data/lib/better_service/errors/workflowable/runtime/workflow_execution_error.rb +40 -0
- data/lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb +25 -0
- data/lib/better_service/railtie.rb +6 -0
- data/lib/better_service/services/action_service.rb +60 -0
- data/lib/better_service/services/base.rb +249 -0
- data/lib/better_service/services/create_service.rb +60 -0
- data/lib/better_service/services/destroy_service.rb +57 -0
- data/lib/better_service/services/index_service.rb +56 -0
- data/lib/better_service/services/show_service.rb +44 -0
- data/lib/better_service/services/update_service.rb +58 -0
- data/lib/better_service/subscribers/log_subscriber.rb +131 -0
- data/lib/better_service/subscribers/stats_subscriber.rb +208 -0
- data/lib/better_service/version.rb +3 -0
- data/lib/better_service/workflows/base.rb +106 -0
- data/lib/better_service/workflows/dsl.rb +59 -0
- data/lib/better_service/workflows/execution.rb +89 -0
- data/lib/better_service/workflows/result_builder.rb +67 -0
- data/lib/better_service/workflows/rollback_support.rb +44 -0
- data/lib/better_service/workflows/transaction_support.rb +32 -0
- data/lib/better_service.rb +28 -0
- data/lib/generators/serviceable/action_generator.rb +29 -0
- data/lib/generators/serviceable/create_generator.rb +27 -0
- data/lib/generators/serviceable/destroy_generator.rb +27 -0
- data/lib/generators/serviceable/index_generator.rb +27 -0
- data/lib/generators/serviceable/scaffold_generator.rb +70 -0
- data/lib/generators/serviceable/show_generator.rb +27 -0
- data/lib/generators/serviceable/templates/action_service.rb.tt +42 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +33 -0
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +40 -0
- data/lib/generators/serviceable/templates/index_service.rb.tt +54 -0
- data/lib/generators/serviceable/templates/service_test.rb.tt +23 -0
- data/lib/generators/serviceable/templates/show_service.rb.tt +37 -0
- data/lib/generators/serviceable/templates/update_service.rb.tt +50 -0
- data/lib/generators/serviceable/update_generator.rb +27 -0
- data/lib/generators/workflowable/WORKFLOW_README +27 -0
- data/lib/generators/workflowable/templates/workflow.rb.tt +72 -0
- data/lib/generators/workflowable/templates/workflow_test.rb.tt +62 -0
- data/lib/generators/workflowable/workflow_generator.rb +60 -0
- data/lib/tasks/better_service_tasks.rake +4 -0
- metadata +180 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class CreateGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate a Create service for creating new resources"
|
|
11
|
+
|
|
12
|
+
def create_service_file
|
|
13
|
+
template "create_service.rb.tt", File.join("app/services", class_path, "#{file_name}/create_service.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_test_file
|
|
17
|
+
template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/create_service_test.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def service_class_name
|
|
23
|
+
"#{class_name}::CreateService"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class DestroyGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate a Destroy service for deleting resources"
|
|
11
|
+
|
|
12
|
+
def create_service_file
|
|
13
|
+
template "destroy_service.rb.tt", File.join("app/services", class_path, "#{file_name}/destroy_service.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_test_file
|
|
17
|
+
template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/destroy_service_test.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def service_class_name
|
|
23
|
+
"#{class_name}::DestroyService"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class IndexGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate an Index service for listing resources"
|
|
11
|
+
|
|
12
|
+
def create_service_file
|
|
13
|
+
template "index_service.rb.tt", File.join("app/services", class_path, "#{file_name}/index_service.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_test_file
|
|
17
|
+
template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/index_service_test.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def service_class_name
|
|
23
|
+
"#{class_name}::IndexService"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class ScaffoldGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
class_option :skip_index, type: :boolean, default: false, desc: "Skip Index service generation"
|
|
11
|
+
class_option :skip_show, type: :boolean, default: false, desc: "Skip Show service generation"
|
|
12
|
+
class_option :skip_create, type: :boolean, default: false, desc: "Skip Create service generation"
|
|
13
|
+
class_option :skip_update, type: :boolean, default: false, desc: "Skip Update service generation"
|
|
14
|
+
class_option :skip_destroy, type: :boolean, default: false, desc: "Skip Destroy service generation"
|
|
15
|
+
|
|
16
|
+
desc "Generate all CRUD services (Index, Show, Create, Update, Destroy)"
|
|
17
|
+
|
|
18
|
+
def generate_index_service
|
|
19
|
+
return if options[:skip_index]
|
|
20
|
+
|
|
21
|
+
say "Generating Index service...", :green
|
|
22
|
+
generate "serviceable:index", name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate_show_service
|
|
26
|
+
return if options[:skip_show]
|
|
27
|
+
|
|
28
|
+
say "Generating Show service...", :green
|
|
29
|
+
generate "serviceable:show", name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def generate_create_service
|
|
33
|
+
return if options[:skip_create]
|
|
34
|
+
|
|
35
|
+
say "Generating Create service...", :green
|
|
36
|
+
generate "serviceable:create", name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_update_service
|
|
40
|
+
return if options[:skip_update]
|
|
41
|
+
|
|
42
|
+
say "Generating Update service...", :green
|
|
43
|
+
generate "serviceable:update", name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generate_destroy_service
|
|
47
|
+
return if options[:skip_destroy]
|
|
48
|
+
|
|
49
|
+
say "Generating Destroy service...", :green
|
|
50
|
+
generate "serviceable:destroy", name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def show_completion_message
|
|
54
|
+
say "\n" + "=" * 80
|
|
55
|
+
say "Scaffold generation completed! 🎉", :green
|
|
56
|
+
say "=" * 80
|
|
57
|
+
say "\nGenerated services:"
|
|
58
|
+
say " - #{class_name}::IndexService" unless options[:skip_index]
|
|
59
|
+
say " - #{class_name}::ShowService" unless options[:skip_show]
|
|
60
|
+
say " - #{class_name}::CreateService" unless options[:skip_create]
|
|
61
|
+
say " - #{class_name}::UpdateService" unless options[:skip_update]
|
|
62
|
+
say " - #{class_name}::DestroyService" unless options[:skip_destroy]
|
|
63
|
+
say "\nNext steps:"
|
|
64
|
+
say " 1. Review and customize the generated services"
|
|
65
|
+
say " 2. Update schemas with your specific validations"
|
|
66
|
+
say " 3. Run the tests: rails test test/services/#{file_name}\n\n"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class ShowGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate a Show service for displaying a single resource"
|
|
11
|
+
|
|
12
|
+
def create_service_file
|
|
13
|
+
template "show_service.rb.tt", File.join("app/services", class_path, "#{file_name}/show_service.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_test_file
|
|
17
|
+
template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/show_service_test.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def service_class_name
|
|
23
|
+
"#{class_name}::ShowService"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::<%= action_name.camelize %>Service < BetterService::Services::ActionService
|
|
5
|
+
action_name :<%= action_name %>
|
|
6
|
+
|
|
7
|
+
# Schema for validating params
|
|
8
|
+
schema do
|
|
9
|
+
required(:id).filled
|
|
10
|
+
# Add your action-specific params here
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Authorization (optional) - Runs BEFORE search phase
|
|
14
|
+
# Uncomment and customize for your needs:
|
|
15
|
+
#
|
|
16
|
+
# authorize_with do
|
|
17
|
+
# # Example: Check if user can perform this action
|
|
18
|
+
# user.can_<%= action_name %>?
|
|
19
|
+
#
|
|
20
|
+
# # Or use Pundit
|
|
21
|
+
# <%= class_name %>Policy.new(user, <%= class_name %>.find(params[:id])).<%= action_name %>?
|
|
22
|
+
# end
|
|
23
|
+
|
|
24
|
+
# Phase 1: Search - Load the resource
|
|
25
|
+
search_with do
|
|
26
|
+
{ resource: user.<%= plural_table_name %>.find(params[:id]) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Phase 2: Process - Perform the action
|
|
30
|
+
process_with do |data|
|
|
31
|
+
<%= file_name %> = data[:resource]
|
|
32
|
+
# Add your action logic here
|
|
33
|
+
# Example: <%= file_name %>.update!(status: '<%= action_name %>', <%= action_name %>_at: Time.current)
|
|
34
|
+
{ resource: <%= file_name %> }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Phase 4: Respond - Format response (optional override)
|
|
38
|
+
respond_with do |data|
|
|
39
|
+
success_result("<%= human_name %> <%= action_name %> successfully", data)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
<% end -%>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::CreateService < BetterService::Services::CreateService
|
|
5
|
+
# Database transactions are ENABLED by default for create operations
|
|
6
|
+
# The process block will automatically run inside a transaction
|
|
7
|
+
# To disable: with_transaction false
|
|
8
|
+
|
|
9
|
+
# Schema for validating params
|
|
10
|
+
schema do
|
|
11
|
+
# Add your required attributes here
|
|
12
|
+
# Example:
|
|
13
|
+
# required(:title).filled(:string)
|
|
14
|
+
# required(:description).maybe(:string)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Phase 1: Search - Prepare dependencies (optional)
|
|
18
|
+
search_with do
|
|
19
|
+
{}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Phase 2: Process - Create the resource (runs in transaction)
|
|
23
|
+
process_with do |data|
|
|
24
|
+
<%= file_name %> = user.<%= plural_table_name %>.create!(params)
|
|
25
|
+
{ resource: <%= file_name %> }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Phase 4: Respond - Format response (optional override)
|
|
29
|
+
respond_with do |data|
|
|
30
|
+
success_result("<%= human_name %> created successfully", data)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
<% end -%>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::DestroyService < BetterService::Services::DestroyService
|
|
5
|
+
# Database transactions are ENABLED by default for destroy operations
|
|
6
|
+
# The process block will automatically run inside a transaction
|
|
7
|
+
# To disable: with_transaction false
|
|
8
|
+
|
|
9
|
+
# Schema for validating params
|
|
10
|
+
schema do
|
|
11
|
+
required(:id).filled
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Authorization (optional) - Runs BEFORE search phase
|
|
15
|
+
# Uncomment and customize for your needs:
|
|
16
|
+
#
|
|
17
|
+
# authorize_with do
|
|
18
|
+
# # Example: Check ownership or admin role
|
|
19
|
+
# <%= file_name %> = <%= class_name %>.find(params[:id])
|
|
20
|
+
# user.admin? || <%= file_name %>.user_id == user.id
|
|
21
|
+
# end
|
|
22
|
+
|
|
23
|
+
# Phase 1: Search - Load the resource
|
|
24
|
+
search_with do
|
|
25
|
+
{ resource: user.<%= plural_table_name %>.find(params[:id]) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Phase 2: Process - Delete the resource (runs in transaction)
|
|
29
|
+
process_with do |data|
|
|
30
|
+
<%= file_name %> = data[:resource]
|
|
31
|
+
<%= file_name %>.destroy!
|
|
32
|
+
{ resource: <%= file_name %> }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Phase 4: Respond - Format response (optional override)
|
|
36
|
+
respond_with do |data|
|
|
37
|
+
success_result("<%= human_name %> deleted successfully", data)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
<% end -%>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::IndexService < BetterService::Services::IndexService
|
|
5
|
+
# Schema for validating params
|
|
6
|
+
schema do
|
|
7
|
+
optional(:page).filled(:integer, gteq?: 1)
|
|
8
|
+
optional(:per_page).filled(:integer, gteq?: 1, lteq?: 100)
|
|
9
|
+
optional(:search).maybe(:string)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Phase 1: Search - Load raw data
|
|
13
|
+
search_with do
|
|
14
|
+
<%= plural_table_name %> = user.<%= plural_table_name %>
|
|
15
|
+
<%= plural_table_name %> = <%= plural_table_name %>.where("title LIKE ?", "%#{params[:search]}%") if params[:search].present?
|
|
16
|
+
# Add pagination if needed (e.g., with Kaminari or Pagy)
|
|
17
|
+
# <%= plural_table_name %> = <%= plural_table_name %>.page(params[:page]).per(params[:per_page] || 25)
|
|
18
|
+
|
|
19
|
+
{ items: <%= plural_table_name %>.to_a }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Phase 2: Process - Transform and aggregate data
|
|
23
|
+
process_with do |data|
|
|
24
|
+
{
|
|
25
|
+
items: data[:items],
|
|
26
|
+
metadata: {
|
|
27
|
+
stats: {
|
|
28
|
+
total: data[:items].count
|
|
29
|
+
},
|
|
30
|
+
pagination: {
|
|
31
|
+
page: params[:page] || 1,
|
|
32
|
+
per_page: params[:per_page] || 25
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Phase 4: Respond - Format response (optional override)
|
|
39
|
+
respond_with do |data|
|
|
40
|
+
success_result("<%= plural_table_name.titleize %> loaded successfully", data)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Phase 5: Viewer - UI configuration (optional)
|
|
44
|
+
# viewer do |processed, transformed, result|
|
|
45
|
+
# {
|
|
46
|
+
# page_title: "<%= plural_table_name.titleize %>",
|
|
47
|
+
# breadcrumbs: [
|
|
48
|
+
# { label: "Home", url: "/" },
|
|
49
|
+
# { label: "<%= plural_table_name.titleize %>", url: "/<%= plural_table_name %>" }
|
|
50
|
+
# ]
|
|
51
|
+
# }
|
|
52
|
+
# end
|
|
53
|
+
end
|
|
54
|
+
<% end -%>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= service_class_name %>Test < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@user = users(:one) # Assumes you have fixtures
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test "service executes successfully" do
|
|
12
|
+
service = <%= service_class_name %>.new(@user)
|
|
13
|
+
result = service.call
|
|
14
|
+
|
|
15
|
+
assert result[:success]
|
|
16
|
+
assert result.key?(:metadata)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test "service validates params" do
|
|
20
|
+
# Add validation tests based on your schema
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
<% end -%>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::ShowService < BetterService::Services::ShowService
|
|
5
|
+
# Schema for validating params
|
|
6
|
+
schema do
|
|
7
|
+
required(:id).filled
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Phase 1: Search - Load the resource
|
|
11
|
+
search_with do
|
|
12
|
+
{ resource: user.<%= plural_table_name %>.find(params[:id]) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Phase 2: Process - Transform data (optional)
|
|
16
|
+
# process_with do |data|
|
|
17
|
+
# data
|
|
18
|
+
# end
|
|
19
|
+
|
|
20
|
+
# Phase 4: Respond - Format response (optional override)
|
|
21
|
+
respond_with do |data|
|
|
22
|
+
success_result("<%= human_name %> loaded successfully", data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Phase 5: Viewer - UI configuration (optional)
|
|
26
|
+
# viewer do |processed, transformed, result|
|
|
27
|
+
# {
|
|
28
|
+
# page_title: "<%= human_name %> ##{result[:resource].id}",
|
|
29
|
+
# breadcrumbs: [
|
|
30
|
+
# { label: "Home", url: "/" },
|
|
31
|
+
# { label: "<%= plural_table_name.titleize %>", url: "/<%= plural_table_name %>" },
|
|
32
|
+
# { label: "Show", url: "#" }
|
|
33
|
+
# ]
|
|
34
|
+
# }
|
|
35
|
+
# end
|
|
36
|
+
end
|
|
37
|
+
<% end -%>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= class_name %>::UpdateService < BetterService::Services::UpdateService
|
|
5
|
+
# Database transactions are ENABLED by default for update operations
|
|
6
|
+
# The process block will automatically run inside a transaction
|
|
7
|
+
# To disable: with_transaction false
|
|
8
|
+
|
|
9
|
+
# Schema for validating params
|
|
10
|
+
schema do
|
|
11
|
+
required(:id).filled
|
|
12
|
+
# Add your optional attributes here
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Authorization (optional) - Runs BEFORE search phase
|
|
16
|
+
# Uncomment and customize for your needs:
|
|
17
|
+
#
|
|
18
|
+
# authorize_with do
|
|
19
|
+
# # Example 1: Simple role check
|
|
20
|
+
# user.admin?
|
|
21
|
+
#
|
|
22
|
+
# # Example 2: Resource ownership check
|
|
23
|
+
# <%= file_name %> = <%= class_name %>.find(params[:id])
|
|
24
|
+
# <%= file_name %>.user_id == user.id
|
|
25
|
+
#
|
|
26
|
+
# # Example 3: Pundit integration
|
|
27
|
+
# <%= class_name %>Policy.new(user, <%= class_name %>.find(params[:id])).update?
|
|
28
|
+
#
|
|
29
|
+
# # Example 4: CanCanCan integration
|
|
30
|
+
# Ability.new(user).can?(:update, :<%= file_name %>)
|
|
31
|
+
# end
|
|
32
|
+
|
|
33
|
+
# Phase 1: Search - Load the resource
|
|
34
|
+
search_with do
|
|
35
|
+
{ resource: user.<%= plural_table_name %>.find(params[:id]) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Phase 2: Process - Update the resource (runs in transaction)
|
|
39
|
+
process_with do |data|
|
|
40
|
+
<%= file_name %> = data[:resource]
|
|
41
|
+
<%= file_name %>.update!(params.except(:id))
|
|
42
|
+
{ resource: <%= file_name %> }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Phase 4: Respond - Format response (optional override)
|
|
46
|
+
respond_with do |data|
|
|
47
|
+
success_result("<%= human_name %> updated successfully", data)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
<% end -%>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Serviceable
|
|
6
|
+
module Generators
|
|
7
|
+
class UpdateGenerator < Rails::Generators::NamedBase
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Generate an Update service for modifying resources"
|
|
11
|
+
|
|
12
|
+
def create_service_file
|
|
13
|
+
template "update_service.rb.tt", File.join("app/services", class_path, "#{file_name}/update_service.rb")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create_test_file
|
|
17
|
+
template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/update_service_test.rb")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def service_class_name
|
|
23
|
+
"#{class_name}::UpdateService"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
Workflow has been generated!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Define your workflow steps in the generated file
|
|
8
|
+
2. Create the service classes referenced in each step
|
|
9
|
+
3. Implement the input mapping for each step to pass data between services
|
|
10
|
+
4. Add any lifecycle hooks (before_workflow, after_workflow, around_step)
|
|
11
|
+
5. Configure transaction support if needed with: with_transaction true
|
|
12
|
+
|
|
13
|
+
Usage example:
|
|
14
|
+
|
|
15
|
+
result = YourWorkflow.new(current_user, params: { ... }).call
|
|
16
|
+
|
|
17
|
+
if result[:success]
|
|
18
|
+
# Access context data
|
|
19
|
+
resource = result[:context].resource_name
|
|
20
|
+
else
|
|
21
|
+
# Handle errors
|
|
22
|
+
errors = result[:errors]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
For more information, see the BetterService documentation.
|
|
26
|
+
|
|
27
|
+
===============================================================================
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
<% module_namespacing do -%>
|
|
4
|
+
class <%= workflow_class_name %> < BetterService::Workflow
|
|
5
|
+
<% if use_transaction -%>
|
|
6
|
+
# Enable database transactions for the entire workflow
|
|
7
|
+
with_transaction true
|
|
8
|
+
<% else -%>
|
|
9
|
+
# Database transactions are DISABLED by default
|
|
10
|
+
# To enable: with_transaction true
|
|
11
|
+
<% end -%>
|
|
12
|
+
|
|
13
|
+
# Lifecycle hooks (optional)
|
|
14
|
+
# before_workflow :validate_prerequisites
|
|
15
|
+
# after_workflow :cleanup_resources
|
|
16
|
+
# around_step :log_step_execution
|
|
17
|
+
|
|
18
|
+
<% if workflow_steps.any? -%>
|
|
19
|
+
# Define workflow steps
|
|
20
|
+
<% workflow_steps.each do |step_name| -%>
|
|
21
|
+
step :<%= step_name %>,
|
|
22
|
+
with: <%= class_name %>::<%= step_name.to_s.camelize %>Service,
|
|
23
|
+
input: ->(ctx) {
|
|
24
|
+
# Map context data to service params
|
|
25
|
+
# Example: { resource_id: ctx.resource.id, amount: ctx.amount }
|
|
26
|
+
{}
|
|
27
|
+
}
|
|
28
|
+
# Optional configuration:
|
|
29
|
+
# optional: true # Step won't stop workflow if it fails
|
|
30
|
+
# if: ->(ctx) { ctx.some_condition? } # Conditional execution
|
|
31
|
+
# rollback: ->(ctx) { ctx.resource.destroy! } # Rollback logic
|
|
32
|
+
|
|
33
|
+
<% end -%>
|
|
34
|
+
<% else -%>
|
|
35
|
+
# Example step configuration:
|
|
36
|
+
#
|
|
37
|
+
# step :create_resource,
|
|
38
|
+
# with: <%= class_name %>::CreateService,
|
|
39
|
+
# input: ->(ctx) { { name: ctx.resource_name, user_id: ctx.user.id } }
|
|
40
|
+
#
|
|
41
|
+
# step :process_payment,
|
|
42
|
+
# with: Payment::ChargeService,
|
|
43
|
+
# input: ->(ctx) { { amount: ctx.resource.total, payment_method: ctx.payment_method } },
|
|
44
|
+
# rollback: ->(ctx) { Payment::RefundService.new(ctx.user, params: { charge_id: ctx.charge.id }).call }
|
|
45
|
+
#
|
|
46
|
+
# step :send_notification,
|
|
47
|
+
# with: Email::NotificationService,
|
|
48
|
+
# input: ->(ctx) { { resource_id: ctx.resource.id } },
|
|
49
|
+
# optional: true,
|
|
50
|
+
# if: ->(ctx) { ctx.user.notifications_enabled? }
|
|
51
|
+
|
|
52
|
+
<% end -%>
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Example lifecycle hooks:
|
|
56
|
+
#
|
|
57
|
+
# def validate_prerequisites(context)
|
|
58
|
+
# context.fail!("Prerequisites not met") unless some_condition
|
|
59
|
+
# end
|
|
60
|
+
#
|
|
61
|
+
# def cleanup_resources(context)
|
|
62
|
+
# # Clean up only on success
|
|
63
|
+
# context.user.clear_cache! if context.success?
|
|
64
|
+
# end
|
|
65
|
+
#
|
|
66
|
+
# def log_step_execution(step, context)
|
|
67
|
+
# Rails.logger.info "Executing step: #{step.name}"
|
|
68
|
+
# yield # Execute the step
|
|
69
|
+
# Rails.logger.info "Completed step: #{step.name}"
|
|
70
|
+
# end
|
|
71
|
+
end
|
|
72
|
+
<% end -%>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
<% module_namespacing do -%>
|
|
6
|
+
class <%= workflow_class_name %>Test < ActiveSupport::TestCase
|
|
7
|
+
# Setup test data
|
|
8
|
+
setup do
|
|
9
|
+
@user = users(:one) # Adjust based on your fixtures
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
test "workflow executes successfully with valid params" do
|
|
13
|
+
params = {
|
|
14
|
+
# Add your test params here
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
result = <%= workflow_class_name %>.new(@user, params: params).call
|
|
18
|
+
|
|
19
|
+
assert result[:success], "Expected workflow to succeed"
|
|
20
|
+
assert_instance_of BetterService::Workflowable::Context, result[:context]
|
|
21
|
+
assert_equal "<%= workflow_class_name %>", result[:metadata][:workflow]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
test "workflow fails with invalid params" do
|
|
25
|
+
params = {
|
|
26
|
+
# Add invalid params here
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
result = <%= workflow_class_name %>.new(@user, params: params).call
|
|
30
|
+
|
|
31
|
+
assert_not result[:success], "Expected workflow to fail"
|
|
32
|
+
assert result[:errors].present?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test "workflow tracks executed steps" do
|
|
36
|
+
params = {
|
|
37
|
+
# Add your test params here
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
result = <%= workflow_class_name %>.new(@user, params: params).call
|
|
41
|
+
|
|
42
|
+
assert result[:metadata][:steps_executed].is_a?(Array)
|
|
43
|
+
<% if workflow_steps.any? -%>
|
|
44
|
+
# Verify expected steps were executed
|
|
45
|
+
<% workflow_steps.each do |step_name| -%>
|
|
46
|
+
# assert_includes result[:metadata][:steps_executed], :<%= step_name %>
|
|
47
|
+
<% end -%>
|
|
48
|
+
<% end -%>
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
<% if use_transaction -%>
|
|
52
|
+
test "workflow rolls back database changes on failure" do
|
|
53
|
+
# Test that database changes are rolled back when workflow fails
|
|
54
|
+
# Example:
|
|
55
|
+
# assert_no_difference "<%= class_name %>.count" do
|
|
56
|
+
# result = <%= workflow_class_name %>.new(@user, params: invalid_params).call
|
|
57
|
+
# assert_not result[:success]
|
|
58
|
+
# end
|
|
59
|
+
end
|
|
60
|
+
<% end -%>
|
|
61
|
+
end
|
|
62
|
+
<% end -%>
|