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,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Subscribers
5
+ # StatsSubscriber - Built-in subscriber that collects service statistics
6
+ #
7
+ # This subscriber tracks execution metrics for all services:
8
+ # - Total executions
9
+ # - Success/failure counts
10
+ # - Average duration
11
+ # - Cache hit rate
12
+ #
13
+ # @example Enable in initializer
14
+ # BetterService.configure do |config|
15
+ # config.stats_subscriber_enabled = true
16
+ # end
17
+ #
18
+ # @example Access statistics
19
+ # BetterService::Subscribers::StatsSubscriber.stats
20
+ # # => {
21
+ # # "ProductsIndexService" => {
22
+ # # executions: 150,
23
+ # # successes: 148,
24
+ # # failures: 2,
25
+ # # total_duration: 4500.0,
26
+ # # avg_duration: 30.0,
27
+ # # cache_hits: 120,
28
+ # # cache_misses: 30
29
+ # # }
30
+ # # }
31
+ class StatsSubscriber
32
+ class << self
33
+ # Storage for service statistics
34
+ #
35
+ # @return [Hash] Statistics hash
36
+ attr_reader :stats
37
+
38
+ # Storage for ActiveSupport::Notifications subscriptions
39
+ #
40
+ # @return [Array<ActiveSupport::Notifications::Fanout::Subscriber>]
41
+ attr_reader :subscriptions
42
+
43
+ # Attach the subscriber to ActiveSupport::Notifications
44
+ #
45
+ # This method is called automatically when subscriber is enabled.
46
+ #
47
+ # @return [void]
48
+ def attach
49
+ reset!
50
+ @subscriptions ||= []
51
+ subscribe_to_service_events
52
+ subscribe_to_cache_events
53
+ end
54
+
55
+ # Reset all statistics
56
+ #
57
+ # Useful for testing or periodic reset in production.
58
+ #
59
+ # @return [void]
60
+ def reset!
61
+ # Unsubscribe from all existing subscriptions
62
+ if @subscriptions
63
+ @subscriptions.each do |subscription|
64
+ ActiveSupport::Notifications.unsubscribe(subscription)
65
+ end
66
+ end
67
+ @subscriptions = []
68
+ @stats = {}
69
+ end
70
+
71
+ # Get statistics for a specific service
72
+ #
73
+ # @param service_name [String] Name of service class
74
+ # @return [Hash, nil] Service statistics or nil if not found
75
+ def stats_for(service_name)
76
+ @stats[service_name]
77
+ end
78
+
79
+ # Get statistics summary across all services
80
+ #
81
+ # @return [Hash] Aggregated statistics
82
+ def summary
83
+ total_executions = @stats.values.sum { |s| s[:executions] }
84
+ total_successes = @stats.values.sum { |s| s[:successes] }
85
+ total_failures = @stats.values.sum { |s| s[:failures] }
86
+ total_duration = @stats.values.sum { |s| s[:total_duration] }
87
+ total_cache_hits = @stats.values.sum { |s| s[:cache_hits] }
88
+ total_cache_misses = @stats.values.sum { |s| s[:cache_misses] }
89
+
90
+ {
91
+ total_services: @stats.keys.size,
92
+ total_executions: total_executions,
93
+ total_successes: total_successes,
94
+ total_failures: total_failures,
95
+ success_rate: total_executions > 0 ? (total_successes.to_f / total_executions * 100).round(2) : 0,
96
+ avg_duration: total_executions > 0 ? (total_duration / total_executions).round(2) : 0,
97
+ cache_hit_rate: (total_cache_hits + total_cache_misses) > 0 ? (total_cache_hits.to_f / (total_cache_hits + total_cache_misses) * 100).round(2) : 0
98
+ }
99
+ end
100
+
101
+ private
102
+
103
+ # Subscribe to service lifecycle events
104
+ #
105
+ # @return [void]
106
+ def subscribe_to_service_events
107
+ @subscriptions << ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
108
+ record_completion(payload)
109
+ end
110
+
111
+ @subscriptions << ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
112
+ record_failure(payload)
113
+ end
114
+ end
115
+
116
+ # Subscribe to cache events
117
+ #
118
+ # @return [void]
119
+ def subscribe_to_cache_events
120
+ @subscriptions << ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
121
+ record_cache_hit(payload)
122
+ end
123
+
124
+ @subscriptions << ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
125
+ record_cache_miss(payload)
126
+ end
127
+ end
128
+
129
+ # Record service completion
130
+ #
131
+ # @param payload [Hash] Event payload
132
+ # @return [void]
133
+ def record_completion(payload)
134
+ service_name = payload[:service_name]
135
+ ensure_service_stats(service_name)
136
+
137
+ @stats[service_name][:executions] += 1
138
+ @stats[service_name][:successes] += 1
139
+ @stats[service_name][:total_duration] += payload[:duration]
140
+ @stats[service_name][:avg_duration] = (@stats[service_name][:total_duration] / @stats[service_name][:executions]).round(2)
141
+
142
+ # Record cache hit if present
143
+ if payload[:cache_hit]
144
+ @stats[service_name][:cache_hits] += 1
145
+ end
146
+ end
147
+
148
+ # Record service failure
149
+ #
150
+ # @param payload [Hash] Event payload
151
+ # @return [void]
152
+ def record_failure(payload)
153
+ service_name = payload[:service_name]
154
+ ensure_service_stats(service_name)
155
+
156
+ @stats[service_name][:executions] += 1
157
+ @stats[service_name][:failures] += 1
158
+ @stats[service_name][:total_duration] += payload[:duration]
159
+ @stats[service_name][:avg_duration] = (@stats[service_name][:total_duration] / @stats[service_name][:executions]).round(2)
160
+
161
+ # Track error types
162
+ error_class = payload[:error_class]
163
+ @stats[service_name][:errors][error_class] ||= 0
164
+ @stats[service_name][:errors][error_class] += 1
165
+ end
166
+
167
+ # Record cache hit
168
+ #
169
+ # @param payload [Hash] Event payload
170
+ # @return [void]
171
+ def record_cache_hit(payload)
172
+ service_name = payload[:service_name]
173
+ ensure_service_stats(service_name)
174
+
175
+ @stats[service_name][:cache_hits] += 1
176
+ end
177
+
178
+ # Record cache miss
179
+ #
180
+ # @param payload [Hash] Event payload
181
+ # @return [void]
182
+ def record_cache_miss(payload)
183
+ service_name = payload[:service_name]
184
+ ensure_service_stats(service_name)
185
+
186
+ @stats[service_name][:cache_misses] += 1
187
+ end
188
+
189
+ # Ensure service has stats entry
190
+ #
191
+ # @param service_name [String] Name of service class
192
+ # @return [void]
193
+ def ensure_service_stats(service_name)
194
+ @stats[service_name] ||= {
195
+ executions: 0,
196
+ successes: 0,
197
+ failures: 0,
198
+ total_duration: 0.0,
199
+ avg_duration: 0.0,
200
+ cache_hits: 0,
201
+ cache_misses: 0,
202
+ errors: {}
203
+ }
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,3 @@
1
+ module BetterService
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ # Workflow - Base class for composing multiple services into a pipeline
5
+ #
6
+ # Workflows allow you to chain multiple services together with explicit
7
+ # data mapping, conditional execution, rollback support, and lifecycle hooks.
8
+ #
9
+ # Example:
10
+ # class OrderPurchaseWorkflow < BetterService::Workflow
11
+ # with_transaction true
12
+ #
13
+ # before_workflow :validate_cart
14
+ # after_workflow :clear_cart
15
+ #
16
+ # step :create_order,
17
+ # with: Order::CreateService,
18
+ # input: ->(ctx) { { items: ctx.cart_items, total: ctx.total } }
19
+ #
20
+ # step :charge_payment,
21
+ # with: Payment::ChargeService,
22
+ # input: ->(ctx) { { amount: ctx.order.total } },
23
+ # rollback: ->(ctx) { Payment::RefundService.new(ctx.user, params: { charge_id: ctx.charge.id }).call }
24
+ #
25
+ # step :send_email,
26
+ # with: Email::ConfirmationService,
27
+ # input: ->(ctx) { { order_id: ctx.order.id } },
28
+ # optional: true,
29
+ # if: ->(ctx) { ctx.user.notifications_enabled? }
30
+ #
31
+ # private
32
+ #
33
+ # def validate_cart(context)
34
+ # context.fail!("Cart is empty") if context.cart_items.empty?
35
+ # end
36
+ #
37
+ # def clear_cart(context)
38
+ # context.user.clear_cart! if context.success?
39
+ # end
40
+ # end
41
+ #
42
+ # # Usage:
43
+ # result = OrderPurchaseWorkflow.new(current_user, params: { cart_items: [...] }).call
44
+ # if result[:success]
45
+ # order = result[:context].order
46
+ # else
47
+ # errors = result[:errors]
48
+ # end
49
+ module Workflows
50
+ class Base
51
+ include Concerns::Workflowable::Callbacks
52
+ include DSL
53
+ include TransactionSupport
54
+ include Execution
55
+ include RollbackSupport
56
+ include ResultBuilder
57
+
58
+ attr_reader :user, :params, :context
59
+
60
+ # Initialize a new workflow
61
+ #
62
+ # @param user [Object] The current user executing the workflow
63
+ # @param params [Hash] Parameters for the workflow
64
+ def initialize(user, params: {})
65
+ @user = user
66
+ @params = params
67
+ @context = Workflowable::Context.new(user, **params)
68
+ @executed_steps = []
69
+ @start_time = nil
70
+ @end_time = nil
71
+ end
72
+
73
+ # Main entry point - executes the workflow
74
+ #
75
+ # Runs before_workflow callbacks, executes the workflow (with or without
76
+ # transaction), and runs after_workflow callbacks. Tracks timing and
77
+ # ensures callbacks run even if execution fails.
78
+ #
79
+ # @return [Hash] Result hash with success status, context, and metadata
80
+ def call
81
+ @start_time = Time.current
82
+ @context.called!
83
+
84
+ # Run before_workflow callbacks
85
+ run_before_workflow_callbacks(@context)
86
+
87
+ # If callbacks failed the context, return early
88
+ if @context.failure?
89
+ @end_time = Time.current
90
+ return build_failure_result
91
+ end
92
+
93
+ # Execute workflow with or without transaction
94
+ if self.class._use_transaction
95
+ execute_with_transaction
96
+ else
97
+ execute_workflow
98
+ end
99
+ ensure
100
+ @end_time ||= Time.current
101
+ # Always run after_workflow callbacks
102
+ run_after_workflow_callbacks(@context) if @context.called?
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Workflows
5
+ # DSL - Provides class-level DSL methods for defining workflow steps
6
+ #
7
+ # This module adds the `step` and `with_transaction` class methods that
8
+ # allow declarative workflow definition using a clean DSL syntax.
9
+ module DSL
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class_attribute :_steps, default: []
14
+ class_attribute :_use_transaction, default: false
15
+ end
16
+
17
+ class_methods do
18
+ # DSL method to define a step in the workflow
19
+ #
20
+ # @param name [Symbol] Name of the step
21
+ # @param with [Class] Service class to execute
22
+ # @param input [Proc] Lambda to map context data to service params
23
+ # @param optional [Boolean] Whether step failure should stop the workflow
24
+ # @param if [Proc] Condition to determine if step should execute
25
+ # @param rollback [Proc] Block to execute if rollback is needed
26
+ #
27
+ # @example Define a workflow step
28
+ # step :create_order,
29
+ # with: Order::CreateService,
30
+ # input: ->(ctx) { { items: ctx.cart_items } },
31
+ # rollback: ->(ctx) { ctx.order.destroy! }
32
+ def step(name, with:, input: nil, optional: false, if: nil, rollback: nil)
33
+ step = Workflowable::Step.new(
34
+ name: name,
35
+ service_class: with,
36
+ input: input,
37
+ optional: optional,
38
+ condition: binding.local_variable_get(:if), # Use binding to get the 'if' keyword param
39
+ rollback: rollback
40
+ )
41
+
42
+ self._steps += [ step ]
43
+ end
44
+
45
+ # Enable or disable database transactions for the entire workflow
46
+ #
47
+ # @param enabled [Boolean] Whether to use transactions
48
+ #
49
+ # @example Enable transactions
50
+ # class MyWorkflow < BetterService::Workflow
51
+ # with_transaction true
52
+ # end
53
+ def with_transaction(enabled)
54
+ self._use_transaction = enabled
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Workflows
5
+ # Execution - Core workflow execution engine
6
+ #
7
+ # This module handles the sequential execution of workflow steps,
8
+ # error handling, and step tracking.
9
+ module Execution
10
+ private
11
+
12
+ # Execute workflow steps sequentially
13
+ #
14
+ # Iterates through all defined steps, executing each with around_step callbacks.
15
+ # Tracks executed and skipped steps. Handles step failures by rolling back
16
+ # previously executed steps and raising appropriate errors.
17
+ #
18
+ # @return [Hash] Success or failure result
19
+ # @raise [Errors::Workflowable::Runtime::StepExecutionError] If a step fails
20
+ # @raise [Errors::Workflowable::Runtime::WorkflowExecutionError] If workflow execution fails
21
+ def execute_workflow
22
+ steps_executed = []
23
+ steps_skipped = []
24
+
25
+ self.class._steps.each do |step|
26
+ # Execute step with around_step callbacks
27
+ result = nil
28
+ run_around_step_callbacks(step, @context) do
29
+ result = step.call(@context, @user, @params)
30
+ end
31
+
32
+ # Track skipped steps
33
+ if result[:skipped]
34
+ steps_skipped << step.name
35
+ next
36
+ end
37
+
38
+ # If step failed and it's not optional, stop and rollback
39
+ if result[:success] == false && !result[:optional_failure]
40
+ # With Pure Exception Pattern, all failures raise exceptions
41
+ rollback_steps
42
+
43
+ raise Errors::Workflowable::Runtime::StepExecutionError.new(
44
+ "Step #{step.name} failed: #{result[:error] || result[:message]}",
45
+ code: ErrorCodes::STEP_FAILED,
46
+ context: {
47
+ workflow: self.class.name,
48
+ step: step.name,
49
+ steps_executed: steps_executed,
50
+ errors: result[:errors] || {}
51
+ }
52
+ )
53
+ end
54
+
55
+ # Track successful execution
56
+ @executed_steps << step
57
+ steps_executed << step.name
58
+ end
59
+
60
+ # All steps succeeded
61
+ @end_time = Time.current
62
+ build_success_result(
63
+ steps_executed: steps_executed,
64
+ steps_skipped: steps_skipped
65
+ )
66
+ rescue Errors::Workflowable::Runtime::StepExecutionError
67
+ # Step error already raised, just re-raise
68
+ raise
69
+ rescue StandardError => e
70
+ # Unexpected error during workflow execution
71
+ rollback_steps
72
+
73
+ Rails.logger.error "Workflow error: #{e.message}" if defined?(Rails)
74
+ Rails.logger.error e.backtrace.join("\n") if defined?(Rails)
75
+
76
+ raise Errors::Workflowable::Runtime::WorkflowExecutionError.new(
77
+ "Workflow execution failed: #{e.message}",
78
+ code: ErrorCodes::WORKFLOW_FAILED,
79
+ original_error: e,
80
+ context: {
81
+ workflow: self.class.name,
82
+ steps_executed: steps_executed,
83
+ steps_skipped: steps_skipped
84
+ }
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Workflows
5
+ # ResultBuilder - Handles construction of success and failure result hashes
6
+ #
7
+ # This module provides methods for building consistent result structures
8
+ # for workflow execution, including metadata tracking and duration measurement.
9
+ module ResultBuilder
10
+ private
11
+
12
+ # Build success result
13
+ #
14
+ # @param steps_executed [Array<Symbol>] Names of steps that were executed
15
+ # @param steps_skipped [Array<Symbol>] Names of steps that were skipped
16
+ # @return [Hash] Success result with context and metadata
17
+ def build_success_result(steps_executed: [], steps_skipped: [])
18
+ {
19
+ success: true,
20
+ message: "Workflow completed successfully",
21
+ context: @context,
22
+ metadata: {
23
+ workflow: self.class.name,
24
+ steps_executed: steps_executed,
25
+ steps_skipped: steps_skipped,
26
+ duration_ms: duration_ms
27
+ }
28
+ }
29
+ end
30
+
31
+ # Build failure result
32
+ #
33
+ # @param message [String, nil] Error message
34
+ # @param errors [Hash] Error details
35
+ # @param failed_step [Symbol, nil] Name of the step that failed
36
+ # @param steps_executed [Array<Symbol>] Names of steps that were executed before failure
37
+ # @param steps_skipped [Array<Symbol>] Names of steps that were skipped
38
+ # @return [Hash] Failure result with error details and metadata
39
+ def build_failure_result(message: nil, errors: {}, failed_step: nil, steps_executed: [], steps_skipped: [])
40
+ result = {
41
+ success: false,
42
+ error: message || @context.errors[:message] || "Workflow failed",
43
+ errors: errors.any? ? errors : @context.errors,
44
+ context: @context,
45
+ metadata: {
46
+ workflow: self.class.name,
47
+ failed_step: failed_step,
48
+ steps_executed: steps_executed,
49
+ steps_skipped: steps_skipped,
50
+ duration_ms: duration_ms
51
+ }
52
+ }
53
+
54
+ result[:metadata].delete(:failed_step) if failed_step.nil?
55
+ result
56
+ end
57
+
58
+ # Calculate duration in milliseconds
59
+ #
60
+ # @return [Float, nil] Duration in milliseconds or nil if not available
61
+ def duration_ms
62
+ return nil unless @start_time && @end_time
63
+ (((@end_time - @start_time) * 1000).round(2))
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Workflows
5
+ # RollbackSupport - Handles rollback of executed steps when workflow fails
6
+ #
7
+ # This module provides the rollback mechanism that executes step rollback
8
+ # blocks in reverse order when a workflow execution fails.
9
+ module RollbackSupport
10
+ private
11
+
12
+ # Rollback all executed steps in reverse order
13
+ #
14
+ # Iterates through executed steps in reverse and calls their rollback method.
15
+ # If any rollback fails, raises a RollbackError with context about which
16
+ # step failed and what steps were executed.
17
+ #
18
+ # @raise [Errors::Workflowable::Runtime::RollbackError] If any rollback fails
19
+ # @return [void]
20
+ def rollback_steps
21
+ @executed_steps.reverse_each do |step|
22
+ begin
23
+ step.rollback(@context)
24
+ rescue StandardError => e
25
+ # Rollback failure is serious - raise exception
26
+ Rails.logger.error "Rollback failed for step #{step.name}: #{e.message}" if defined?(Rails)
27
+ Rails.logger.error e.backtrace.join("\n") if defined?(Rails)
28
+
29
+ raise Errors::Workflowable::Runtime::RollbackError.new(
30
+ "Rollback failed for step #{step.name}: #{e.message}",
31
+ code: ErrorCodes::ROLLBACK_FAILED,
32
+ original_error: e,
33
+ context: {
34
+ workflow: self.class.name,
35
+ step: step.name,
36
+ executed_steps: @executed_steps.map(&:name)
37
+ }
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterService
4
+ module Workflows
5
+ # TransactionSupport - Handles database transaction wrapping for workflows
6
+ #
7
+ # This module provides the ability to execute workflows within a database
8
+ # transaction, with automatic rollback on failure.
9
+ module TransactionSupport
10
+ private
11
+
12
+ # Execute workflow with transaction wrapper
13
+ #
14
+ # Wraps the workflow execution in an ActiveRecord transaction. If the
15
+ # workflow fails (returns success: false), triggers a rollback.
16
+ #
17
+ # @return [Hash] Result from execute_workflow
18
+ def execute_with_transaction
19
+ result = nil
20
+ ActiveRecord::Base.transaction do
21
+ result = execute_workflow
22
+ # If workflow failed, raise to trigger rollback
23
+ raise ActiveRecord::Rollback if result[:success] == false
24
+ end
25
+ result
26
+ rescue ActiveRecord::Rollback
27
+ # Rollback was triggered, result already contains failure
28
+ result
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ require "better_service/version"
2
+ require "better_service/railtie"
3
+ require "better_service/configuration"
4
+ require "better_service/errors/better_service_error"
5
+ require "better_service/cache_service"
6
+ require "better_service/concerns/instrumentation"
7
+ require "better_service/subscribers/log_subscriber"
8
+ require "better_service/subscribers/stats_subscriber"
9
+ require "better_service/services/base"
10
+ require "better_service/services/index_service"
11
+ require "better_service/services/show_service"
12
+ require "better_service/services/create_service"
13
+ require "better_service/services/update_service"
14
+ require "better_service/services/destroy_service"
15
+ require "better_service/services/action_service"
16
+ require "better_service/concerns/workflowable/context"
17
+ require "better_service/concerns/workflowable/step"
18
+ require "better_service/concerns/workflowable/callbacks"
19
+ require "better_service/workflows/result_builder"
20
+ require "better_service/workflows/rollback_support"
21
+ require "better_service/workflows/transaction_support"
22
+ require "better_service/workflows/dsl"
23
+ require "better_service/workflows/execution"
24
+ require "better_service/workflows/base"
25
+
26
+ module BetterService
27
+ # Your code goes here...
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Serviceable
6
+ module Generators
7
+ class ActionGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ argument :action_name, type: :string, desc: "The action name (e.g., accept, reject, publish)"
11
+
12
+ desc "Generate an Action service for custom state transitions"
13
+
14
+ def create_service_file
15
+ template "action_service.rb.tt", File.join("app/services", class_path, "#{file_name}/#{action_name}_service.rb")
16
+ end
17
+
18
+ def create_test_file
19
+ template "service_test.rb.tt", File.join("test/services", class_path, "#{file_name}/#{action_name}_service_test.rb")
20
+ end
21
+
22
+ private
23
+
24
+ def service_class_name
25
+ "#{class_name}::#{action_name.camelize}Service"
26
+ end
27
+ end
28
+ end
29
+ end