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,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Workflowable
|
|
5
|
+
# Step - Represents a single step in a workflow pipeline
|
|
6
|
+
#
|
|
7
|
+
# Each step wraps a service class and defines how data flows into it,
|
|
8
|
+
# whether it's optional, conditional execution, and rollback behavior.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# step = Step.new(
|
|
12
|
+
# name: :create_order,
|
|
13
|
+
# service_class: Order::CreateService,
|
|
14
|
+
# input: ->(ctx) { { items: ctx.cart_items } },
|
|
15
|
+
# optional: false,
|
|
16
|
+
# condition: ->(ctx) { ctx.cart_items.any? },
|
|
17
|
+
# rollback: ->(ctx) { ctx.order.destroy! }
|
|
18
|
+
# )
|
|
19
|
+
#
|
|
20
|
+
# step.call(context, user, params)
|
|
21
|
+
class Step
|
|
22
|
+
attr_reader :name, :service_class, :input_mapper, :optional, :condition, :rollback_block
|
|
23
|
+
|
|
24
|
+
def initialize(name:, service_class:, input: nil, optional: false, condition: nil, rollback: nil)
|
|
25
|
+
@name = name
|
|
26
|
+
@service_class = service_class
|
|
27
|
+
@input_mapper = input
|
|
28
|
+
@optional = optional
|
|
29
|
+
@condition = condition
|
|
30
|
+
@rollback_block = rollback
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Execute the step
|
|
34
|
+
#
|
|
35
|
+
# @param context [Context] The workflow context
|
|
36
|
+
# @param user [Object] The current user
|
|
37
|
+
# @param params [Hash] Base params for the workflow
|
|
38
|
+
# @return [Hash] Service result
|
|
39
|
+
def call(context, user, base_params = {})
|
|
40
|
+
# Check if step should be skipped due to condition
|
|
41
|
+
if should_skip?(context)
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
skipped: true,
|
|
45
|
+
message: "Step #{name} skipped due to condition"
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Build input params for the service
|
|
50
|
+
service_params = build_params(context, base_params)
|
|
51
|
+
|
|
52
|
+
# Call the service
|
|
53
|
+
result = service_class.new(user, params: service_params).call
|
|
54
|
+
|
|
55
|
+
# Store result in context if successful
|
|
56
|
+
if result[:success]
|
|
57
|
+
store_result_in_context(context, result)
|
|
58
|
+
elsif optional
|
|
59
|
+
# If step is optional and failed, continue but log the failure
|
|
60
|
+
context.add(:"#{name}_error", result[:errors])
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
optional_failure: true,
|
|
64
|
+
message: "Optional step #{name} failed but continuing",
|
|
65
|
+
errors: result[:errors]
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
result
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
# If step is optional, swallow the error and continue
|
|
72
|
+
if optional
|
|
73
|
+
context.add(:"#{name}_error", e.message)
|
|
74
|
+
{
|
|
75
|
+
success: true,
|
|
76
|
+
optional_failure: true,
|
|
77
|
+
message: "Optional step #{name} raised error but continuing",
|
|
78
|
+
error: e.message
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
raise e
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Execute rollback for this step
|
|
86
|
+
#
|
|
87
|
+
# @param context [Context] The workflow context
|
|
88
|
+
# @raise [StandardError] If rollback fails (caught and wrapped by workflow)
|
|
89
|
+
def rollback(context)
|
|
90
|
+
return unless rollback_block
|
|
91
|
+
|
|
92
|
+
if rollback_block.is_a?(Proc)
|
|
93
|
+
context.instance_exec(context, &rollback_block)
|
|
94
|
+
else
|
|
95
|
+
rollback_block.call(context)
|
|
96
|
+
end
|
|
97
|
+
# Note: Exceptions are propagated to workflow which wraps them in RollbackError
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Check if step should be skipped
|
|
103
|
+
def should_skip?(context)
|
|
104
|
+
return false unless condition
|
|
105
|
+
|
|
106
|
+
if condition.is_a?(Proc)
|
|
107
|
+
!context.instance_exec(context, &condition)
|
|
108
|
+
else
|
|
109
|
+
!condition.call(context)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Build params for service from context
|
|
114
|
+
def build_params(context, base_params)
|
|
115
|
+
if input_mapper
|
|
116
|
+
if input_mapper.is_a?(Proc)
|
|
117
|
+
context.instance_exec(context, &input_mapper)
|
|
118
|
+
else
|
|
119
|
+
input_mapper.call(context)
|
|
120
|
+
end
|
|
121
|
+
else
|
|
122
|
+
base_params
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Store successful result data in context
|
|
127
|
+
def store_result_in_context(context, result)
|
|
128
|
+
# Store resource if present
|
|
129
|
+
context.add(name, result[:resource]) if result.key?(:resource)
|
|
130
|
+
|
|
131
|
+
# Store items if present
|
|
132
|
+
context.add(name, result[:items]) if result.key?(:items)
|
|
133
|
+
|
|
134
|
+
# If neither resource nor items, store the whole result
|
|
135
|
+
if !result.key?(:resource) && !result.key?(:items)
|
|
136
|
+
context.add(name, result)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Concerns
|
|
5
|
+
# Workflowable - Namespace for all workflow-related concerns
|
|
6
|
+
#
|
|
7
|
+
# This module groups all concerns that are specific to BetterService workflows,
|
|
8
|
+
# such as lifecycle callbacks, step execution, context management, etc.
|
|
9
|
+
module Workflowable
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
# Configuration - Centralized configuration for BetterService
|
|
5
|
+
#
|
|
6
|
+
# Provides configuration options for various BetterService features.
|
|
7
|
+
#
|
|
8
|
+
# @example Configure in initializer
|
|
9
|
+
# # config/initializers/better_service.rb
|
|
10
|
+
# BetterService.configure do |config|
|
|
11
|
+
# config.instrumentation_enabled = true
|
|
12
|
+
# config.instrumentation_include_args = false
|
|
13
|
+
# config.instrumentation_excluded_services = ["HealthCheckService"]
|
|
14
|
+
# end
|
|
15
|
+
class Configuration
|
|
16
|
+
# Enable/disable instrumentation globally
|
|
17
|
+
#
|
|
18
|
+
# When disabled, no events will be published for any service.
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean] Default: true
|
|
21
|
+
attr_accessor :instrumentation_enabled
|
|
22
|
+
|
|
23
|
+
# Include service arguments in event payloads
|
|
24
|
+
#
|
|
25
|
+
# When enabled, args and kwargs are included in event payloads.
|
|
26
|
+
# Disable if arguments contain sensitive data (passwords, tokens, etc.)
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] Default: true
|
|
29
|
+
attr_accessor :instrumentation_include_args
|
|
30
|
+
|
|
31
|
+
# Include service result in event payloads
|
|
32
|
+
#
|
|
33
|
+
# When enabled, the service result is included in completion events.
|
|
34
|
+
# Disable to reduce payload size or protect sensitive return values.
|
|
35
|
+
#
|
|
36
|
+
# @return [Boolean] Default: false
|
|
37
|
+
attr_accessor :instrumentation_include_result
|
|
38
|
+
|
|
39
|
+
# List of service class names to exclude from instrumentation
|
|
40
|
+
#
|
|
41
|
+
# Services in this list will not publish any events.
|
|
42
|
+
# Useful for high-frequency services that would generate too many events.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<String>] Default: []
|
|
45
|
+
attr_accessor :instrumentation_excluded_services
|
|
46
|
+
|
|
47
|
+
# Enable/disable built-in log subscriber
|
|
48
|
+
#
|
|
49
|
+
# When enabled, all service events are logged to Rails.logger.
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean] Default: false
|
|
52
|
+
attr_accessor :log_subscriber_enabled
|
|
53
|
+
|
|
54
|
+
# Log level for built-in log subscriber
|
|
55
|
+
#
|
|
56
|
+
# Valid values: :debug, :info, :warn, :error
|
|
57
|
+
#
|
|
58
|
+
# @return [Symbol] Default: :info
|
|
59
|
+
attr_accessor :log_subscriber_level
|
|
60
|
+
|
|
61
|
+
# Enable/disable built-in stats subscriber
|
|
62
|
+
#
|
|
63
|
+
# When enabled, statistics are collected for all service executions.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean] Default: false
|
|
66
|
+
attr_accessor :stats_subscriber_enabled
|
|
67
|
+
|
|
68
|
+
def initialize
|
|
69
|
+
# Instrumentation defaults
|
|
70
|
+
@instrumentation_enabled = true
|
|
71
|
+
@instrumentation_include_args = true
|
|
72
|
+
@instrumentation_include_result = false
|
|
73
|
+
@instrumentation_excluded_services = []
|
|
74
|
+
|
|
75
|
+
# Built-in subscribers defaults
|
|
76
|
+
@log_subscriber_enabled = false
|
|
77
|
+
@log_subscriber_level = :info
|
|
78
|
+
@stats_subscriber_enabled = false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class << self
|
|
83
|
+
# Get the global configuration object
|
|
84
|
+
#
|
|
85
|
+
# @return [BetterService::Configuration]
|
|
86
|
+
def configuration
|
|
87
|
+
@configuration ||= Configuration.new
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Configure BetterService
|
|
91
|
+
#
|
|
92
|
+
# @yield [Configuration] Configuration object
|
|
93
|
+
# @return [void]
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# BetterService.configure do |config|
|
|
97
|
+
# config.instrumentation_enabled = true
|
|
98
|
+
# config.log_subscriber_enabled = true
|
|
99
|
+
# end
|
|
100
|
+
def configure
|
|
101
|
+
yield(configuration)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Reset configuration to defaults
|
|
105
|
+
#
|
|
106
|
+
# Useful for testing.
|
|
107
|
+
#
|
|
108
|
+
# @return [void]
|
|
109
|
+
def reset_configuration!
|
|
110
|
+
@configuration = Configuration.new
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
# Base exception class for all BetterService errors
|
|
5
|
+
#
|
|
6
|
+
# This class provides rich error information including error codes, context,
|
|
7
|
+
# original errors, and structured metadata for debugging and logging.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# raise BetterServiceError.new(
|
|
11
|
+
# "Something went wrong",
|
|
12
|
+
# code: :custom_error,
|
|
13
|
+
# context: { user_id: 123, action: "create" }
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example With original error
|
|
17
|
+
# begin
|
|
18
|
+
# dangerous_operation
|
|
19
|
+
# rescue StandardError => e
|
|
20
|
+
# raise BetterServiceError.new(
|
|
21
|
+
# "Operation failed",
|
|
22
|
+
# code: :operation_failed,
|
|
23
|
+
# original_error: e,
|
|
24
|
+
# context: { operation: "dangerous_operation" }
|
|
25
|
+
# )
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# @example Accessing error information
|
|
29
|
+
# begin
|
|
30
|
+
# service.call
|
|
31
|
+
# rescue BetterServiceError => e
|
|
32
|
+
# logger.error e.to_h
|
|
33
|
+
# # => {
|
|
34
|
+
# # error_class: "BetterService::Errors::Runtime::ValidationError",
|
|
35
|
+
# # message: "Validation failed",
|
|
36
|
+
# # code: :validation_failed,
|
|
37
|
+
# # timestamp: "2025-11-09T10:30:00Z",
|
|
38
|
+
# # context: { service: "UserService", validation_errors: {...} },
|
|
39
|
+
# # original_error: nil,
|
|
40
|
+
# # backtrace: [...]
|
|
41
|
+
# # }
|
|
42
|
+
# end
|
|
43
|
+
class BetterServiceError < StandardError
|
|
44
|
+
# @return [Symbol, nil] Error code for programmatic handling
|
|
45
|
+
attr_reader :code
|
|
46
|
+
|
|
47
|
+
# @return [Exception, nil] Original exception that caused this error
|
|
48
|
+
attr_reader :original_error
|
|
49
|
+
|
|
50
|
+
# @return [Hash] Additional context about the error
|
|
51
|
+
attr_reader :context
|
|
52
|
+
|
|
53
|
+
# @return [Time] When the error was raised
|
|
54
|
+
attr_reader :timestamp
|
|
55
|
+
|
|
56
|
+
# Initialize a new BetterService error
|
|
57
|
+
#
|
|
58
|
+
# @param message [String, nil] Human-readable error message
|
|
59
|
+
# @param code [Symbol, nil] Error code for programmatic handling
|
|
60
|
+
# @param original_error [Exception, nil] Original exception that caused this error
|
|
61
|
+
# @param context [Hash] Additional context about the error (service name, params, etc.)
|
|
62
|
+
def initialize(message = nil, code: nil, original_error: nil, context: {})
|
|
63
|
+
super(message)
|
|
64
|
+
@code = code
|
|
65
|
+
@original_error = original_error
|
|
66
|
+
@context = context || {}
|
|
67
|
+
@timestamp = Time.current
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Convert error to a structured hash
|
|
71
|
+
#
|
|
72
|
+
# @return [Hash] Hash representation with all error information
|
|
73
|
+
def to_h
|
|
74
|
+
{
|
|
75
|
+
error_class: self.class.name,
|
|
76
|
+
message: message,
|
|
77
|
+
code: code,
|
|
78
|
+
timestamp: timestamp.iso8601,
|
|
79
|
+
context: context,
|
|
80
|
+
original_error: original_error_info,
|
|
81
|
+
backtrace: backtrace&.first(10) || []
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get detailed error message with context
|
|
86
|
+
#
|
|
87
|
+
# @return [String] Detailed message including context
|
|
88
|
+
def detailed_message
|
|
89
|
+
parts = [ message ]
|
|
90
|
+
parts << "Code: #{code}" if code
|
|
91
|
+
parts << "Context: #{context.inspect}" if context.any?
|
|
92
|
+
parts << "Original: #{original_error.class.name}: #{original_error.message}" if original_error
|
|
93
|
+
parts.join(" | ")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Enhanced inspect for debugging
|
|
97
|
+
#
|
|
98
|
+
# @return [String] Detailed string representation
|
|
99
|
+
def inspect
|
|
100
|
+
"#<#{self.class.name}: #{detailed_message}>"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Override backtrace to include original error's backtrace
|
|
104
|
+
#
|
|
105
|
+
# @return [Array<String>] Combined backtrace
|
|
106
|
+
def backtrace
|
|
107
|
+
trace = super || []
|
|
108
|
+
|
|
109
|
+
if original_error && original_error.backtrace
|
|
110
|
+
trace + [ "--- Original Error Backtrace ---" ] + original_error.backtrace
|
|
111
|
+
else
|
|
112
|
+
trace
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Get original error information as hash
|
|
119
|
+
#
|
|
120
|
+
# @return [Hash, nil] Original error details or nil
|
|
121
|
+
def original_error_info
|
|
122
|
+
return nil unless original_error
|
|
123
|
+
|
|
124
|
+
{
|
|
125
|
+
class: original_error.class.name,
|
|
126
|
+
message: original_error.message,
|
|
127
|
+
backtrace: original_error.backtrace&.first(5) || []
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Standard error codes used in BetterService responses
|
|
133
|
+
#
|
|
134
|
+
# These codes are used for both hash responses and exception codes,
|
|
135
|
+
# providing consistent error identification across the system.
|
|
136
|
+
#
|
|
137
|
+
# @example Using error codes in exceptions
|
|
138
|
+
# raise Errors::Runtime::ValidationError.new(
|
|
139
|
+
# "Validation failed",
|
|
140
|
+
# code: ErrorCodes::VALIDATION_FAILED,
|
|
141
|
+
# context: { validation_errors: {...} }
|
|
142
|
+
# )
|
|
143
|
+
#
|
|
144
|
+
# @example Handling errors by code
|
|
145
|
+
# begin
|
|
146
|
+
# service.call
|
|
147
|
+
# rescue BetterServiceError => e
|
|
148
|
+
# case e.code
|
|
149
|
+
# when ErrorCodes::VALIDATION_FAILED
|
|
150
|
+
# render json: { errors: e.context[:validation_errors] }, status: :unprocessable_entity
|
|
151
|
+
# when ErrorCodes::UNAUTHORIZED
|
|
152
|
+
# render json: { error: e.message }, status: :forbidden
|
|
153
|
+
# end
|
|
154
|
+
# end
|
|
155
|
+
module ErrorCodes
|
|
156
|
+
# ============================================
|
|
157
|
+
# BUSINESS LOGIC ERROR CODES
|
|
158
|
+
# ============================================
|
|
159
|
+
|
|
160
|
+
# Validation failed - input parameters are invalid
|
|
161
|
+
#
|
|
162
|
+
# Used when Dry::Schema validation fails.
|
|
163
|
+
# The error will include validation details in context.
|
|
164
|
+
#
|
|
165
|
+
# @example
|
|
166
|
+
# raise Errors::Runtime::ValidationError.new(
|
|
167
|
+
# "Validation failed",
|
|
168
|
+
# code: :validation_failed,
|
|
169
|
+
# context: {
|
|
170
|
+
# validation_errors: { email: ["is invalid"], age: ["must be greater than 18"] }
|
|
171
|
+
# }
|
|
172
|
+
# )
|
|
173
|
+
VALIDATION_FAILED = :validation_failed
|
|
174
|
+
|
|
175
|
+
# Authorization failed - user doesn't have permission
|
|
176
|
+
#
|
|
177
|
+
# Used when the authorize_with block returns false.
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# raise Errors::Runtime::AuthorizationError.new(
|
|
181
|
+
# "Not authorized to perform this action",
|
|
182
|
+
# code: :unauthorized,
|
|
183
|
+
# context: { user_id: 123, action: "destroy" }
|
|
184
|
+
# )
|
|
185
|
+
UNAUTHORIZED = :unauthorized
|
|
186
|
+
|
|
187
|
+
# ============================================
|
|
188
|
+
# PROGRAMMING ERROR CODES
|
|
189
|
+
# ============================================
|
|
190
|
+
|
|
191
|
+
# Schema is missing or invalid
|
|
192
|
+
SCHEMA_REQUIRED = :schema_required
|
|
193
|
+
|
|
194
|
+
# Service configuration is invalid
|
|
195
|
+
CONFIGURATION_ERROR = :configuration_error
|
|
196
|
+
|
|
197
|
+
# ============================================
|
|
198
|
+
# RUNTIME ERROR CODES
|
|
199
|
+
# ============================================
|
|
200
|
+
|
|
201
|
+
# Unexpected error during service execution
|
|
202
|
+
EXECUTION_ERROR = :execution_error
|
|
203
|
+
|
|
204
|
+
# Required resource was not found
|
|
205
|
+
RESOURCE_NOT_FOUND = :resource_not_found
|
|
206
|
+
|
|
207
|
+
# Database transaction failed
|
|
208
|
+
TRANSACTION_ERROR = :transaction_error
|
|
209
|
+
|
|
210
|
+
# Database operation failed (validation errors, save failures)
|
|
211
|
+
DATABASE_ERROR = :database_error
|
|
212
|
+
|
|
213
|
+
# Workflow execution failed
|
|
214
|
+
WORKFLOW_FAILED = :workflow_failed
|
|
215
|
+
|
|
216
|
+
# Workflow step failed
|
|
217
|
+
STEP_FAILED = :step_failed
|
|
218
|
+
|
|
219
|
+
# Workflow rollback failed
|
|
220
|
+
ROLLBACK_FAILED = :rollback_failed
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Require all error classes
|
|
225
|
+
require_relative "configuration/configuration_error"
|
|
226
|
+
require_relative "configuration/schema_required_error"
|
|
227
|
+
require_relative "configuration/invalid_schema_error"
|
|
228
|
+
require_relative "configuration/invalid_configuration_error"
|
|
229
|
+
require_relative "configuration/nil_user_error"
|
|
230
|
+
|
|
231
|
+
require_relative "runtime/runtime_error"
|
|
232
|
+
require_relative "runtime/execution_error"
|
|
233
|
+
require_relative "runtime/transaction_error"
|
|
234
|
+
require_relative "runtime/resource_not_found_error"
|
|
235
|
+
require_relative "runtime/database_error"
|
|
236
|
+
require_relative "runtime/validation_error"
|
|
237
|
+
require_relative "runtime/authorization_error"
|
|
238
|
+
|
|
239
|
+
require_relative "workflowable/configuration/workflow_configuration_error"
|
|
240
|
+
require_relative "workflowable/configuration/step_not_found_error"
|
|
241
|
+
require_relative "workflowable/configuration/invalid_step_error"
|
|
242
|
+
require_relative "workflowable/configuration/duplicate_step_error"
|
|
243
|
+
|
|
244
|
+
require_relative "workflowable/runtime/workflow_runtime_error"
|
|
245
|
+
require_relative "workflowable/runtime/workflow_execution_error"
|
|
246
|
+
require_relative "workflowable/runtime/step_execution_error"
|
|
247
|
+
require_relative "workflowable/runtime/rollback_error"
|
|
248
|
+
|
|
249
|
+
# Namespace for all BetterService errors
|
|
250
|
+
module BetterService
|
|
251
|
+
module Errors
|
|
252
|
+
# Namespace for configuration/programming errors
|
|
253
|
+
module Configuration
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Namespace for runtime errors
|
|
257
|
+
module Runtime
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Namespace for workflow-related errors
|
|
261
|
+
module Workflowable
|
|
262
|
+
# Namespace for workflow configuration errors
|
|
263
|
+
module Configuration
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Namespace for workflow runtime errors
|
|
267
|
+
module Runtime
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Errors
|
|
5
|
+
module Configuration
|
|
6
|
+
# Base class for all configuration/programming errors in BetterService
|
|
7
|
+
#
|
|
8
|
+
# Configuration errors are raised when a service is incorrectly configured or used.
|
|
9
|
+
# These are programming errors that should be fixed during development.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# raise BetterService::Errors::Configuration::ConfigurationError.new(
|
|
13
|
+
# "Invalid service configuration",
|
|
14
|
+
# code: :configuration_error,
|
|
15
|
+
# context: { service: "MyService", issue: "missing required config" }
|
|
16
|
+
# )
|
|
17
|
+
class ConfigurationError < BetterServiceError
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Errors
|
|
5
|
+
module Configuration
|
|
6
|
+
# Raised when a service has invalid configuration settings
|
|
7
|
+
#
|
|
8
|
+
# This error is raised when configuration options are invalid or conflicting,
|
|
9
|
+
# such as invalid cache settings, presenter configurations, or workflow definitions.
|
|
10
|
+
#
|
|
11
|
+
# @example Invalid cache configuration
|
|
12
|
+
# class MyService < BetterService::Services::Base
|
|
13
|
+
# config do
|
|
14
|
+
# cache enabled: true, expires_in: "invalid" # Should be integer
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Invalid workflow step
|
|
19
|
+
# class MyWorkflow < BetterService::Workflow
|
|
20
|
+
# step :invalid,
|
|
21
|
+
# with: nil, # Missing service class
|
|
22
|
+
# input: -> (ctx) { ctx.data }
|
|
23
|
+
# end
|
|
24
|
+
class InvalidConfigurationError < ConfigurationError
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Errors
|
|
5
|
+
module Configuration
|
|
6
|
+
# Raised when a service schema definition is invalid
|
|
7
|
+
#
|
|
8
|
+
# This error is raised when the Dry::Schema definition contains syntax errors
|
|
9
|
+
# or invalid validation rules.
|
|
10
|
+
#
|
|
11
|
+
# @example Invalid schema definition
|
|
12
|
+
# class MyService < BetterService::Services::Base
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:email).filled(:invalid_type) # Invalid type
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @example Schema with syntax error
|
|
19
|
+
# class MyService < BetterService::Services::Base
|
|
20
|
+
# schema do
|
|
21
|
+
# required(:name) # Missing predicate
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
class InvalidSchemaError < ConfigurationError
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Errors
|
|
5
|
+
module Configuration
|
|
6
|
+
# Raised when a service is initialized with nil user when user is required
|
|
7
|
+
#
|
|
8
|
+
# By default, all BetterService services require a user object. This error is raised
|
|
9
|
+
# during initialization if user is nil and `allow_nil_user` is not configured.
|
|
10
|
+
#
|
|
11
|
+
# @example Service called with nil user (will raise error)
|
|
12
|
+
# class MyService < BetterService::Services::Base
|
|
13
|
+
# schema do
|
|
14
|
+
# required(:name).filled(:string)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# MyService.new(nil, params: { name: "test" })
|
|
19
|
+
# # => raises NilUserError
|
|
20
|
+
#
|
|
21
|
+
# @example Allowing nil user
|
|
22
|
+
# class MyService < BetterService::Services::Base
|
|
23
|
+
# config do
|
|
24
|
+
# allow_nil_user true
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# schema do
|
|
28
|
+
# required(:name).filled(:string)
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# MyService.new(nil, params: { name: "test" }) # OK
|
|
33
|
+
class NilUserError < ConfigurationError
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Errors
|
|
5
|
+
module Configuration
|
|
6
|
+
# Raised when a service is missing a required schema definition
|
|
7
|
+
#
|
|
8
|
+
# All BetterService services must define a schema block for parameter validation.
|
|
9
|
+
# This error is raised during service initialization if no schema is defined.
|
|
10
|
+
#
|
|
11
|
+
# @example Service without schema (will raise error)
|
|
12
|
+
# class MyService < BetterService::Services::Base
|
|
13
|
+
# # Missing: schema do ... end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# MyService.new(user, params: {})
|
|
17
|
+
# # => raises SchemaRequiredError
|
|
18
|
+
#
|
|
19
|
+
# @example Correct usage with schema
|
|
20
|
+
# class MyService < BetterService::Services::Base
|
|
21
|
+
# schema do
|
|
22
|
+
# required(:name).filled(:string)
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
class SchemaRequiredError < ConfigurationError
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|