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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1321 -0
  4. data/Rakefile +15 -0
  5. data/lib/better_service/cache_service.rb +310 -0
  6. data/lib/better_service/concerns/instrumentation.rb +242 -0
  7. data/lib/better_service/concerns/serviceable/authorizable.rb +106 -0
  8. data/lib/better_service/concerns/serviceable/cacheable.rb +97 -0
  9. data/lib/better_service/concerns/serviceable/messageable.rb +30 -0
  10. data/lib/better_service/concerns/serviceable/presentable.rb +66 -0
  11. data/lib/better_service/concerns/serviceable/transactional.rb +51 -0
  12. data/lib/better_service/concerns/serviceable/validatable.rb +58 -0
  13. data/lib/better_service/concerns/serviceable/viewable.rb +49 -0
  14. data/lib/better_service/concerns/serviceable.rb +12 -0
  15. data/lib/better_service/concerns/workflowable/callbacks.rb +116 -0
  16. data/lib/better_service/concerns/workflowable/context.rb +108 -0
  17. data/lib/better_service/concerns/workflowable/step.rb +141 -0
  18. data/lib/better_service/concerns/workflowable.rb +12 -0
  19. data/lib/better_service/configuration.rb +113 -0
  20. data/lib/better_service/errors/better_service_error.rb +271 -0
  21. data/lib/better_service/errors/configuration/configuration_error.rb +21 -0
  22. data/lib/better_service/errors/configuration/invalid_configuration_error.rb +28 -0
  23. data/lib/better_service/errors/configuration/invalid_schema_error.rb +28 -0
  24. data/lib/better_service/errors/configuration/nil_user_error.rb +37 -0
  25. data/lib/better_service/errors/configuration/schema_required_error.rb +29 -0
  26. data/lib/better_service/errors/runtime/authorization_error.rb +38 -0
  27. data/lib/better_service/errors/runtime/database_error.rb +38 -0
  28. data/lib/better_service/errors/runtime/execution_error.rb +27 -0
  29. data/lib/better_service/errors/runtime/resource_not_found_error.rb +38 -0
  30. data/lib/better_service/errors/runtime/runtime_error.rb +22 -0
  31. data/lib/better_service/errors/runtime/transaction_error.rb +34 -0
  32. data/lib/better_service/errors/runtime/validation_error.rb +42 -0
  33. data/lib/better_service/errors/workflowable/configuration/duplicate_step_error.rb +27 -0
  34. data/lib/better_service/errors/workflowable/configuration/invalid_step_error.rb +12 -0
  35. data/lib/better_service/errors/workflowable/configuration/step_not_found_error.rb +29 -0
  36. data/lib/better_service/errors/workflowable/configuration/workflow_configuration_error.rb +24 -0
  37. data/lib/better_service/errors/workflowable/runtime/rollback_error.rb +46 -0
  38. data/lib/better_service/errors/workflowable/runtime/step_execution_error.rb +47 -0
  39. data/lib/better_service/errors/workflowable/runtime/workflow_execution_error.rb +40 -0
  40. data/lib/better_service/errors/workflowable/runtime/workflow_runtime_error.rb +25 -0
  41. data/lib/better_service/railtie.rb +6 -0
  42. data/lib/better_service/services/action_service.rb +60 -0
  43. data/lib/better_service/services/base.rb +249 -0
  44. data/lib/better_service/services/create_service.rb +60 -0
  45. data/lib/better_service/services/destroy_service.rb +57 -0
  46. data/lib/better_service/services/index_service.rb +56 -0
  47. data/lib/better_service/services/show_service.rb +44 -0
  48. data/lib/better_service/services/update_service.rb +58 -0
  49. data/lib/better_service/subscribers/log_subscriber.rb +131 -0
  50. data/lib/better_service/subscribers/stats_subscriber.rb +208 -0
  51. data/lib/better_service/version.rb +3 -0
  52. data/lib/better_service/workflows/base.rb +106 -0
  53. data/lib/better_service/workflows/dsl.rb +59 -0
  54. data/lib/better_service/workflows/execution.rb +89 -0
  55. data/lib/better_service/workflows/result_builder.rb +67 -0
  56. data/lib/better_service/workflows/rollback_support.rb +44 -0
  57. data/lib/better_service/workflows/transaction_support.rb +32 -0
  58. data/lib/better_service.rb +28 -0
  59. data/lib/generators/serviceable/action_generator.rb +29 -0
  60. data/lib/generators/serviceable/create_generator.rb +27 -0
  61. data/lib/generators/serviceable/destroy_generator.rb +27 -0
  62. data/lib/generators/serviceable/index_generator.rb +27 -0
  63. data/lib/generators/serviceable/scaffold_generator.rb +70 -0
  64. data/lib/generators/serviceable/show_generator.rb +27 -0
  65. data/lib/generators/serviceable/templates/action_service.rb.tt +42 -0
  66. data/lib/generators/serviceable/templates/create_service.rb.tt +33 -0
  67. data/lib/generators/serviceable/templates/destroy_service.rb.tt +40 -0
  68. data/lib/generators/serviceable/templates/index_service.rb.tt +54 -0
  69. data/lib/generators/serviceable/templates/service_test.rb.tt +23 -0
  70. data/lib/generators/serviceable/templates/show_service.rb.tt +37 -0
  71. data/lib/generators/serviceable/templates/update_service.rb.tt +50 -0
  72. data/lib/generators/serviceable/update_generator.rb +27 -0
  73. data/lib/generators/workflowable/WORKFLOW_README +27 -0
  74. data/lib/generators/workflowable/templates/workflow.rb.tt +72 -0
  75. data/lib/generators/workflowable/templates/workflow_test.rb.tt +62 -0
  76. data/lib/generators/workflowable/workflow_generator.rb +60 -0
  77. data/lib/tasks/better_service_tasks.rake +4 -0
  78. 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 -%>