better_service 1.0.0 → 1.1.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 +4 -4
- data/LICENSE +13 -0
- data/README.md +256 -18
- data/Rakefile +202 -2
- data/config/locales/better_service.en.yml +37 -0
- data/lib/better_service/concerns/serviceable/messageable.rb +45 -2
- data/lib/better_service/concerns/serviceable/validatable.rb +0 -5
- data/lib/better_service/concerns/serviceable/viewable.rb +0 -16
- data/lib/better_service/presenter.rb +131 -0
- data/lib/better_service/railtie.rb +17 -0
- data/lib/better_service/services/base.rb +78 -21
- data/lib/better_service/services/create_service.rb +3 -0
- data/lib/better_service/services/destroy_service.rb +3 -0
- data/lib/better_service/services/update_service.rb +3 -0
- data/lib/better_service/subscribers/log_subscriber.rb +25 -5
- data/lib/better_service/version.rb +1 -1
- data/lib/better_service/workflows/base.rb +1 -0
- data/lib/better_service/workflows/branch.rb +133 -0
- data/lib/better_service/workflows/branch_dsl.rb +151 -0
- data/lib/better_service/workflows/branch_group.rb +139 -0
- data/lib/better_service/workflows/dsl.rb +46 -0
- data/lib/better_service/workflows/execution.rb +35 -9
- data/lib/better_service/workflows/result_builder.rb +26 -17
- data/lib/better_service.rb +4 -0
- data/lib/generators/better_service/install_generator.rb +38 -0
- data/lib/generators/better_service/locale_generator.rb +54 -0
- data/lib/generators/better_service/presenter_generator.rb +60 -0
- data/lib/generators/better_service/templates/better_service_initializer.rb.tt +90 -0
- data/lib/generators/better_service/templates/locale.en.yml.tt +27 -0
- data/lib/generators/better_service/templates/presenter.rb.tt +53 -0
- data/lib/generators/better_service/templates/presenter_test.rb.tt +46 -0
- data/lib/generators/serviceable/scaffold_generator.rb +9 -0
- data/lib/generators/serviceable/templates/create_service.rb.tt +11 -1
- data/lib/generators/serviceable/templates/destroy_service.rb.tt +11 -1
- data/lib/generators/serviceable/templates/index_service.rb.tt +19 -1
- data/lib/generators/serviceable/templates/show_service.rb.tt +19 -1
- data/lib/generators/serviceable/templates/update_service.rb.tt +11 -1
- data/lib/generators/workflowable/templates/workflow.rb.tt +22 -0
- metadata +16 -4
- data/MIT-LICENSE +0 -20
|
@@ -10,7 +10,6 @@ module BetterService
|
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
12
|
class_attribute :_schema, default: nil
|
|
13
|
-
attr_reader :validation_errors
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
class_methods do
|
|
@@ -23,10 +22,6 @@ module BetterService
|
|
|
23
22
|
end
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def valid?
|
|
27
|
-
@validation_errors.empty?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
25
|
private
|
|
31
26
|
|
|
32
27
|
def validate_params!
|
|
@@ -20,22 +20,6 @@ module BetterService
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
# Override respond to add viewer phase
|
|
24
|
-
def respond(data)
|
|
25
|
-
# Get base result from parent respond
|
|
26
|
-
if self.class._respond_block
|
|
27
|
-
result = instance_exec(data, &self.class._respond_block)
|
|
28
|
-
else
|
|
29
|
-
result = success_result("Operation completed successfully", data)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Add viewer if enabled
|
|
33
|
-
return result unless viewer_enabled?
|
|
34
|
-
|
|
35
|
-
view_config = execute_viewer(data, data, result)
|
|
36
|
-
result.merge(view: view_config)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
23
|
def viewer_enabled?
|
|
40
24
|
self.class._viewer_enabled && self.class._viewer_block.present?
|
|
41
25
|
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
# Presenter - Base class for presenting service data
|
|
5
|
+
#
|
|
6
|
+
# Presenters transform raw model data into view-friendly formats.
|
|
7
|
+
# They are typically used with the Presentable concern's presenter DSL.
|
|
8
|
+
#
|
|
9
|
+
# Example:
|
|
10
|
+
# class ProductPresenter < BetterService::Presenter
|
|
11
|
+
# def as_json(opts = {})
|
|
12
|
+
# {
|
|
13
|
+
# id: object.id,
|
|
14
|
+
# name: object.name,
|
|
15
|
+
# price_formatted: "$#{object.price}",
|
|
16
|
+
# available: object.stock > 0,
|
|
17
|
+
# # Conditional fields based on current user
|
|
18
|
+
# **(admin_fields if current_user&.admin?)
|
|
19
|
+
# }
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# private
|
|
23
|
+
#
|
|
24
|
+
# def admin_fields
|
|
25
|
+
# {
|
|
26
|
+
# cost: object.cost,
|
|
27
|
+
# margin: object.price - object.cost
|
|
28
|
+
# }
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# Usage with services:
|
|
33
|
+
# class Products::IndexService < IndexService
|
|
34
|
+
# presenter ProductPresenter
|
|
35
|
+
#
|
|
36
|
+
# presenter_options do
|
|
37
|
+
# { current_user: user }
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# search_with do
|
|
41
|
+
# { items: Product.all.to_a }
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
class Presenter
|
|
45
|
+
attr_reader :object, :options
|
|
46
|
+
|
|
47
|
+
# Initialize presenter
|
|
48
|
+
#
|
|
49
|
+
# @param object [Object] The object to present (e.g., ActiveRecord model)
|
|
50
|
+
# @param options [Hash] Additional options (e.g., current_user, permissions)
|
|
51
|
+
def initialize(object, **options)
|
|
52
|
+
@object = object
|
|
53
|
+
@options = options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Override in subclass to define JSON representation
|
|
57
|
+
#
|
|
58
|
+
# @param opts [Hash] JSON serialization options
|
|
59
|
+
# @return [Hash] Hash representation of the object
|
|
60
|
+
def as_json(opts = {})
|
|
61
|
+
# Default implementation delegates to object
|
|
62
|
+
if object.respond_to?(:as_json)
|
|
63
|
+
object.as_json(opts)
|
|
64
|
+
else
|
|
65
|
+
{ data: object }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# JSON string representation
|
|
70
|
+
#
|
|
71
|
+
# @param opts [Hash] JSON serialization options
|
|
72
|
+
# @return [String] JSON string
|
|
73
|
+
def to_json(opts = {})
|
|
74
|
+
as_json(opts).to_json
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Hash representation (alias for as_json)
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash] Hash representation
|
|
80
|
+
def to_h
|
|
81
|
+
as_json
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Get current user from options
|
|
87
|
+
#
|
|
88
|
+
# @return [Object, nil] Current user if provided in options
|
|
89
|
+
def current_user
|
|
90
|
+
options[:current_user]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if a field should be included based on options
|
|
94
|
+
#
|
|
95
|
+
# Useful for selective field rendering based on client requests.
|
|
96
|
+
#
|
|
97
|
+
# @param field [Symbol, String] Field name to check
|
|
98
|
+
# @return [Boolean] Whether field should be included
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# # In service:
|
|
102
|
+
# presenter_options do
|
|
103
|
+
# { fields: params[:fields]&.split(',')&.map(&:to_sym) }
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# # In presenter:
|
|
107
|
+
# def as_json(opts = {})
|
|
108
|
+
# {
|
|
109
|
+
# id: object.id,
|
|
110
|
+
# name: object.name,
|
|
111
|
+
# **(expensive_data if include_field?(:details))
|
|
112
|
+
# }
|
|
113
|
+
# end
|
|
114
|
+
def include_field?(field)
|
|
115
|
+
return true unless options[:fields]
|
|
116
|
+
|
|
117
|
+
options[:fields].include?(field.to_sym)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if current user has a specific role/permission
|
|
121
|
+
#
|
|
122
|
+
# @param role [Symbol, String] Role to check
|
|
123
|
+
# @return [Boolean] Whether user has the role
|
|
124
|
+
def user_can?(role)
|
|
125
|
+
return false unless current_user
|
|
126
|
+
return false unless current_user.respond_to?(:has_role?)
|
|
127
|
+
|
|
128
|
+
current_user.has_role?(role)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
if defined?(Rails)
|
|
2
2
|
module BetterService
|
|
3
3
|
class Railtie < ::Rails::Railtie
|
|
4
|
+
# Initialize subscribers after Rails boots
|
|
5
|
+
#
|
|
6
|
+
# This hook runs after all initializers have been executed,
|
|
7
|
+
# ensuring BetterService.configuration is fully loaded.
|
|
8
|
+
config.after_initialize do
|
|
9
|
+
# Attach LogSubscriber if enabled in configuration
|
|
10
|
+
if BetterService.configuration.log_subscriber_enabled
|
|
11
|
+
BetterService::Subscribers::LogSubscriber.attach
|
|
12
|
+
Rails.logger.info "[BetterService] LogSubscriber attached" if Rails.logger
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Attach StatsSubscriber if enabled in configuration
|
|
16
|
+
if BetterService.configuration.stats_subscriber_enabled
|
|
17
|
+
BetterService::Subscribers::StatsSubscriber.attach
|
|
18
|
+
Rails.logger.info "[BetterService] StatsSubscriber attached" if Rails.logger
|
|
19
|
+
end
|
|
20
|
+
end
|
|
4
21
|
end
|
|
5
22
|
end
|
|
6
23
|
end
|
|
@@ -19,6 +19,7 @@ module BetterService
|
|
|
19
19
|
class_attribute :_process_block, default: nil
|
|
20
20
|
class_attribute :_transform_block, default: nil
|
|
21
21
|
class_attribute :_respond_block, default: nil
|
|
22
|
+
class_attribute :_auto_invalidate_cache, default: false
|
|
22
23
|
|
|
23
24
|
include Concerns::Serviceable::Messageable
|
|
24
25
|
include Concerns::Serviceable::Validatable
|
|
@@ -41,11 +42,29 @@ module BetterService
|
|
|
41
42
|
validate_schema_presence!
|
|
42
43
|
@user = user
|
|
43
44
|
@params = safe_params_to_hash(params)
|
|
44
|
-
@validation_errors = {}
|
|
45
45
|
validate_params!
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# DSL methods to define phase blocks
|
|
49
|
+
|
|
50
|
+
# Configure whether this service allows nil user
|
|
51
|
+
#
|
|
52
|
+
# @param value [Boolean] Whether to allow nil user (default: true)
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
# @example Allow nil user
|
|
56
|
+
# class PublicService < BetterService::Services::Base
|
|
57
|
+
# allow_nil_user true
|
|
58
|
+
# end
|
|
59
|
+
#
|
|
60
|
+
# @example Require user (default)
|
|
61
|
+
# class PrivateService < BetterService::Services::Base
|
|
62
|
+
# allow_nil_user false
|
|
63
|
+
# end
|
|
64
|
+
def self.allow_nil_user(value = true)
|
|
65
|
+
self._allow_nil_user = value
|
|
66
|
+
end
|
|
67
|
+
|
|
49
68
|
def self.search_with(&block)
|
|
50
69
|
self._search_block = block
|
|
51
70
|
end
|
|
@@ -62,6 +81,31 @@ module BetterService
|
|
|
62
81
|
self._respond_block = block
|
|
63
82
|
end
|
|
64
83
|
|
|
84
|
+
# Configure automatic cache invalidation after write operations
|
|
85
|
+
#
|
|
86
|
+
# @param enabled [Boolean] Whether to automatically invalidate cache (default: true)
|
|
87
|
+
# @return [void]
|
|
88
|
+
#
|
|
89
|
+
# @example Enable automatic cache invalidation (default for Create/Update/Destroy)
|
|
90
|
+
# class Products::CreateService < CreateService
|
|
91
|
+
# cache_contexts :products, :category_products
|
|
92
|
+
# # Cache is automatically invalidated after successful create
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# @example Disable automatic cache invalidation
|
|
96
|
+
# class Products::CreateService < CreateService
|
|
97
|
+
# auto_invalidate_cache false
|
|
98
|
+
#
|
|
99
|
+
# process_with do |data|
|
|
100
|
+
# product = Product.create!(params)
|
|
101
|
+
# invalidate_cache_for(user) if should_invalidate? # Manual control
|
|
102
|
+
# { resource: product }
|
|
103
|
+
# end
|
|
104
|
+
# end
|
|
105
|
+
def self.auto_invalidate_cache(enabled = true)
|
|
106
|
+
self._auto_invalidate_cache = enabled
|
|
107
|
+
end
|
|
108
|
+
|
|
65
109
|
# Main entry point - executes the 5-phase flow
|
|
66
110
|
def call
|
|
67
111
|
# Validation already raises ValidationError in initialize
|
|
@@ -70,6 +114,12 @@ module BetterService
|
|
|
70
114
|
|
|
71
115
|
data = search
|
|
72
116
|
processed = process(data)
|
|
117
|
+
|
|
118
|
+
# Auto-invalidate cache after write operations if configured
|
|
119
|
+
if should_auto_invalidate_cache?
|
|
120
|
+
invalidate_cache_for(user)
|
|
121
|
+
end
|
|
122
|
+
|
|
73
123
|
transformed = transform(processed)
|
|
74
124
|
result = respond(transformed)
|
|
75
125
|
|
|
@@ -117,7 +167,33 @@ module BetterService
|
|
|
117
167
|
|
|
118
168
|
# Phase 3: Transform - Handled by Presentable concern
|
|
119
169
|
|
|
120
|
-
# Phase 4: Respond -
|
|
170
|
+
# Phase 4: Respond - Format response (override in service types or with respond_with block)
|
|
171
|
+
def respond(data)
|
|
172
|
+
if self.class._respond_block
|
|
173
|
+
instance_exec(data, &self.class._respond_block)
|
|
174
|
+
else
|
|
175
|
+
success_result("Operation completed successfully", data)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Check if cache should be automatically invalidated
|
|
180
|
+
#
|
|
181
|
+
# Auto-invalidation happens when:
|
|
182
|
+
# 1. auto_invalidate_cache is enabled (true)
|
|
183
|
+
# 2. cache_contexts are defined (something to invalidate)
|
|
184
|
+
# 3. Service is a write operation (Create/Update/Destroy)
|
|
185
|
+
#
|
|
186
|
+
# @return [Boolean] Whether cache should be invalidated
|
|
187
|
+
def should_auto_invalidate_cache?
|
|
188
|
+
return false unless self.class._auto_invalidate_cache
|
|
189
|
+
return false unless self.class.respond_to?(:_cache_contexts)
|
|
190
|
+
return false unless self.class._cache_contexts.present?
|
|
191
|
+
|
|
192
|
+
# Only auto-invalidate for write operations
|
|
193
|
+
is_a?(Services::CreateService) ||
|
|
194
|
+
is_a?(Services::UpdateService) ||
|
|
195
|
+
is_a?(Services::DestroyService)
|
|
196
|
+
end
|
|
121
197
|
|
|
122
198
|
# Error handlers for runtime errors
|
|
123
199
|
#
|
|
@@ -222,25 +298,6 @@ module BetterService
|
|
|
222
298
|
}
|
|
223
299
|
end
|
|
224
300
|
|
|
225
|
-
def failure_result(message, errors = {}, code: nil)
|
|
226
|
-
result = {
|
|
227
|
-
success: false,
|
|
228
|
-
error: message,
|
|
229
|
-
errors: errors
|
|
230
|
-
}
|
|
231
|
-
result[:code] = code if code
|
|
232
|
-
result
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def error_result(message, code: nil)
|
|
236
|
-
result = {
|
|
237
|
-
success: false,
|
|
238
|
-
errors: [message]
|
|
239
|
-
}
|
|
240
|
-
result[:code] = code if code
|
|
241
|
-
result
|
|
242
|
-
end
|
|
243
|
-
|
|
244
301
|
# Prepend Instrumentation at the end, after call method is defined
|
|
245
302
|
# This wraps the entire call method (including cache logic from Cacheable)
|
|
246
303
|
prepend Concerns::Instrumentation
|
|
@@ -34,6 +34,9 @@ module BetterService
|
|
|
34
34
|
# Enable database transactions by default for create operations
|
|
35
35
|
with_transaction true
|
|
36
36
|
|
|
37
|
+
# Enable automatic cache invalidation for create operations
|
|
38
|
+
self._auto_invalidate_cache = true
|
|
39
|
+
|
|
37
40
|
# Default empty schema - subclasses MUST override with specific validations
|
|
38
41
|
schema do
|
|
39
42
|
# Override in subclass with required fields
|
|
@@ -31,6 +31,9 @@ module BetterService
|
|
|
31
31
|
# Enable database transactions by default for destroy operations
|
|
32
32
|
with_transaction true
|
|
33
33
|
|
|
34
|
+
# Enable automatic cache invalidation for destroy operations
|
|
35
|
+
self._auto_invalidate_cache = true
|
|
36
|
+
|
|
34
37
|
# Default schema - requires id parameter
|
|
35
38
|
schema do
|
|
36
39
|
required(:id).filled
|
|
@@ -32,6 +32,9 @@ module BetterService
|
|
|
32
32
|
# Enable database transactions by default for update operations
|
|
33
33
|
with_transaction true
|
|
34
34
|
|
|
35
|
+
# Enable automatic cache invalidation for update operations
|
|
36
|
+
self._auto_invalidate_cache = true
|
|
37
|
+
|
|
35
38
|
# Default schema - requires id parameter
|
|
36
39
|
schema do
|
|
37
40
|
required(:id).filled
|
|
@@ -14,6 +14,11 @@ module BetterService
|
|
|
14
14
|
# end
|
|
15
15
|
class LogSubscriber
|
|
16
16
|
class << self
|
|
17
|
+
# Storage for ActiveSupport::Notifications subscriptions
|
|
18
|
+
#
|
|
19
|
+
# @return [Array<ActiveSupport::Notifications::Fanout::Subscriber>]
|
|
20
|
+
attr_reader :subscriptions
|
|
21
|
+
|
|
17
22
|
# Attach the subscriber to ActiveSupport::Notifications
|
|
18
23
|
#
|
|
19
24
|
# This method is called automatically when subscriber is enabled.
|
|
@@ -21,25 +26,40 @@ module BetterService
|
|
|
21
26
|
#
|
|
22
27
|
# @return [void]
|
|
23
28
|
def attach
|
|
29
|
+
@subscriptions ||= []
|
|
24
30
|
subscribe_to_service_events
|
|
25
31
|
subscribe_to_cache_events
|
|
26
32
|
end
|
|
27
33
|
|
|
34
|
+
# Detach the subscriber from ActiveSupport::Notifications
|
|
35
|
+
#
|
|
36
|
+
# Removes all subscriptions. Useful for testing.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
def detach
|
|
40
|
+
if @subscriptions
|
|
41
|
+
@subscriptions.each do |subscription|
|
|
42
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
@subscriptions = []
|
|
46
|
+
end
|
|
47
|
+
|
|
28
48
|
private
|
|
29
49
|
|
|
30
50
|
# Subscribe to service lifecycle events
|
|
31
51
|
#
|
|
32
52
|
# @return [void]
|
|
33
53
|
def subscribe_to_service_events
|
|
34
|
-
ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
|
|
54
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.started") do |name, start, finish, id, payload|
|
|
35
55
|
log_service_started(payload)
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
58
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.completed") do |name, start, finish, id, payload|
|
|
39
59
|
log_service_completed(payload)
|
|
40
60
|
end
|
|
41
61
|
|
|
42
|
-
ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
62
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("service.failed") do |name, start, finish, id, payload|
|
|
43
63
|
log_service_failed(payload)
|
|
44
64
|
end
|
|
45
65
|
end
|
|
@@ -48,11 +68,11 @@ module BetterService
|
|
|
48
68
|
#
|
|
49
69
|
# @return [void]
|
|
50
70
|
def subscribe_to_cache_events
|
|
51
|
-
ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
|
|
71
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("cache.hit") do |name, start, finish, id, payload|
|
|
52
72
|
log_cache_hit(payload)
|
|
53
73
|
end
|
|
54
74
|
|
|
55
|
-
ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
|
|
75
|
+
@subscriptions << ActiveSupport::Notifications.subscribe("cache.miss") do |name, start, finish, id, payload|
|
|
56
76
|
log_cache_miss(payload)
|
|
57
77
|
end
|
|
58
78
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterService
|
|
4
|
+
module Workflows
|
|
5
|
+
# Represents a single conditional branch within a workflow
|
|
6
|
+
#
|
|
7
|
+
# A Branch contains:
|
|
8
|
+
# - A condition (Proc) that determines if the branch should execute
|
|
9
|
+
# - An array of steps to execute if the condition is true
|
|
10
|
+
# - An optional name for identification
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# branch = Branch.new(
|
|
14
|
+
# condition: ->(ctx) { ctx.user.premium? },
|
|
15
|
+
# name: :premium_path
|
|
16
|
+
# )
|
|
17
|
+
# branch.add_step(step1)
|
|
18
|
+
# branch.add_step(step2)
|
|
19
|
+
#
|
|
20
|
+
# if branch.matches?(context)
|
|
21
|
+
# branch.execute(context, user, params)
|
|
22
|
+
# end
|
|
23
|
+
class Branch
|
|
24
|
+
attr_reader :condition, :steps, :name
|
|
25
|
+
|
|
26
|
+
# Creates a new Branch
|
|
27
|
+
#
|
|
28
|
+
# @param condition [Proc, nil] The condition to evaluate (nil for default/otherwise branch)
|
|
29
|
+
# @param name [Symbol, nil] Optional name for the branch
|
|
30
|
+
def initialize(condition: nil, name: nil)
|
|
31
|
+
@condition = condition
|
|
32
|
+
@steps = []
|
|
33
|
+
@name = name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks if this branch's condition matches the given context
|
|
37
|
+
#
|
|
38
|
+
# @param context [Workflowable::Context] The workflow context
|
|
39
|
+
# @return [Boolean] true if condition matches or is nil (default branch)
|
|
40
|
+
def matches?(context)
|
|
41
|
+
return true if condition.nil? # Default branch always matches
|
|
42
|
+
|
|
43
|
+
if condition.is_a?(Proc)
|
|
44
|
+
context.instance_exec(context, &condition)
|
|
45
|
+
else
|
|
46
|
+
condition.call(context)
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Rails.logger.error "Branch condition evaluation failed: #{e.message}"
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Adds a step to this branch
|
|
54
|
+
#
|
|
55
|
+
# @param step [Workflowable::Step] The step to add
|
|
56
|
+
# @return [Array] The updated steps array
|
|
57
|
+
def add_step(step)
|
|
58
|
+
@steps << step
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Executes all steps in this branch
|
|
62
|
+
#
|
|
63
|
+
# @param context [Workflowable::Context] The workflow context
|
|
64
|
+
# @param user [Object] The current user
|
|
65
|
+
# @param params [Hash] The workflow parameters
|
|
66
|
+
# @param branch_decisions [Array, nil] Array to track branch decisions for nested branches
|
|
67
|
+
# @return [Array<Workflowable::Step>] Array of successfully executed steps
|
|
68
|
+
# @raise [Errors::Workflowable::Runtime::StepExecutionError] If a required step fails
|
|
69
|
+
def execute(context, user, params, branch_decisions = nil)
|
|
70
|
+
executed_steps = []
|
|
71
|
+
|
|
72
|
+
@steps.each do |step_or_branch_group|
|
|
73
|
+
# Handle nested BranchGroup
|
|
74
|
+
if step_or_branch_group.is_a?(BranchGroup)
|
|
75
|
+
branch_result = step_or_branch_group.call(context, user, params)
|
|
76
|
+
|
|
77
|
+
# Add executed steps from nested branch
|
|
78
|
+
if branch_result[:executed_steps]
|
|
79
|
+
executed_steps.concat(branch_result[:executed_steps])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Track nested branch decisions
|
|
83
|
+
if branch_decisions && branch_result[:branch_decisions]
|
|
84
|
+
branch_decisions.concat(branch_result[:branch_decisions])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
next
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Handle regular Step
|
|
91
|
+
result = step_or_branch_group.call(context, user, params)
|
|
92
|
+
|
|
93
|
+
# Skip if step was skipped
|
|
94
|
+
next if result[:skipped]
|
|
95
|
+
|
|
96
|
+
# Handle step failure
|
|
97
|
+
if result[:success] == false && !result[:optional_failure]
|
|
98
|
+
raise Errors::Workflowable::Runtime::StepExecutionError.new(
|
|
99
|
+
"Step #{step_or_branch_group.name} failed in branch",
|
|
100
|
+
code: ErrorCodes::STEP_EXECUTION_FAILED,
|
|
101
|
+
context: {
|
|
102
|
+
step: step_or_branch_group.name,
|
|
103
|
+
branch: @name,
|
|
104
|
+
errors: result[:errors]
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Track successfully executed steps
|
|
110
|
+
executed_steps << step_or_branch_group unless result[:optional_failure]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
executed_steps
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns whether this is a default branch (no condition)
|
|
117
|
+
#
|
|
118
|
+
# @return [Boolean]
|
|
119
|
+
def default?
|
|
120
|
+
condition.nil?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns a string representation of this branch
|
|
124
|
+
#
|
|
125
|
+
# @return [String]
|
|
126
|
+
def inspect
|
|
127
|
+
"#<BetterService::Workflows::Branch name=#{@name.inspect} " \
|
|
128
|
+
"condition=#{@condition.present? ? 'present' : 'nil'} " \
|
|
129
|
+
"steps=#{@steps.count}>"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|