better_model 1.0.0 → 1.2.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/README.md +427 -85
- data/lib/better_model/archivable.rb +10 -10
- data/lib/better_model/predicable.rb +39 -52
- data/lib/better_model/sortable.rb +4 -0
- data/lib/better_model/state_transition.rb +106 -0
- data/lib/better_model/stateable/configurator.rb +300 -0
- data/lib/better_model/stateable/errors.rb +45 -0
- data/lib/better_model/stateable/guard.rb +87 -0
- data/lib/better_model/stateable/transition.rb +143 -0
- data/lib/better_model/stateable.rb +354 -0
- data/lib/better_model/traceable.rb +498 -0
- data/lib/better_model/validatable/business_rule_validator.rb +47 -0
- data/lib/better_model/validatable/configurator.rb +245 -0
- data/lib/better_model/validatable/order_validator.rb +77 -0
- data/lib/better_model/validatable.rb +270 -0
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model/version_record.rb +63 -0
- data/lib/better_model.rb +15 -2
- data/lib/generators/better_model/stateable/install_generator.rb +37 -0
- data/lib/generators/better_model/stateable/stateable_generator.rb +50 -0
- data/lib/generators/better_model/stateable/templates/README +38 -0
- data/lib/generators/better_model/stateable/templates/install_migration.rb.tt +20 -0
- data/lib/generators/better_model/stateable/templates/migration.rb.tt +9 -0
- data/lib/generators/better_model/traceable/templates/create_table_migration.rb.tt +28 -0
- data/lib/generators/better_model/traceable/traceable_generator.rb +77 -0
- metadata +22 -3
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Stateable
|
|
5
|
+
# Configurator per il DSL di Stateable
|
|
6
|
+
#
|
|
7
|
+
# Questo configurator permette di definire state machines in modo dichiarativo
|
|
8
|
+
# all'interno del blocco `stateable do...end`.
|
|
9
|
+
#
|
|
10
|
+
# Esempio:
|
|
11
|
+
# stateable do
|
|
12
|
+
# # Definisci stati
|
|
13
|
+
# state :pending, initial: true
|
|
14
|
+
# state :confirmed
|
|
15
|
+
# state :paid
|
|
16
|
+
#
|
|
17
|
+
# # Definisci transizioni
|
|
18
|
+
# transition :confirm, from: :pending, to: :confirmed do
|
|
19
|
+
# guard { items.any? }
|
|
20
|
+
# guard :customer_valid?
|
|
21
|
+
# guard if: :is_ready?
|
|
22
|
+
#
|
|
23
|
+
# validate { errors.add(:base, "Invalid") unless valid_for_confirmation? }
|
|
24
|
+
#
|
|
25
|
+
# before { prepare }
|
|
26
|
+
# after { notify }
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class Configurator
|
|
31
|
+
attr_reader :states, :transitions, :initial_state, :table_name
|
|
32
|
+
|
|
33
|
+
def initialize(model_class)
|
|
34
|
+
@model_class = model_class
|
|
35
|
+
@states = []
|
|
36
|
+
@transitions = {}
|
|
37
|
+
@initial_state = nil
|
|
38
|
+
@table_name = nil
|
|
39
|
+
@current_transition = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Definisce uno stato
|
|
43
|
+
#
|
|
44
|
+
# @param name [Symbol] Nome dello stato
|
|
45
|
+
# @param initial [Boolean] Se questo è lo stato iniziale
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# state :draft, initial: true
|
|
49
|
+
# state :published
|
|
50
|
+
# state :archived
|
|
51
|
+
#
|
|
52
|
+
def state(name, initial: false)
|
|
53
|
+
raise ArgumentError, "State name must be a symbol" unless name.is_a?(Symbol)
|
|
54
|
+
raise ArgumentError, "State #{name} already defined" if @states.include?(name)
|
|
55
|
+
|
|
56
|
+
@states << name
|
|
57
|
+
|
|
58
|
+
if initial
|
|
59
|
+
raise ArgumentError, "Initial state already defined as #{@initial_state}" if @initial_state
|
|
60
|
+
@initial_state = name
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Definisce una transizione
|
|
65
|
+
#
|
|
66
|
+
# @param event [Symbol] Nome dell'evento/transizione
|
|
67
|
+
# @param from [Symbol, Array<Symbol>] Stato/i di partenza
|
|
68
|
+
# @param to [Symbol] Stato di arrivo
|
|
69
|
+
# @yield Blocco per configurare guards, validations, callbacks
|
|
70
|
+
#
|
|
71
|
+
# @example Transizione semplice
|
|
72
|
+
# transition :publish, from: :draft, to: :published
|
|
73
|
+
#
|
|
74
|
+
# @example Con guards e callbacks
|
|
75
|
+
# transition :confirm, from: :pending, to: :confirmed do
|
|
76
|
+
# guard { valid? }
|
|
77
|
+
# guard :ready_to_confirm?
|
|
78
|
+
# before { prepare_confirmation }
|
|
79
|
+
# after { send_email }
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# @example Da multipli stati
|
|
83
|
+
# transition :cancel, from: [:pending, :confirmed, :paid], to: :cancelled
|
|
84
|
+
#
|
|
85
|
+
def transition(event, from:, to:, &block)
|
|
86
|
+
raise ArgumentError, "Event name must be a symbol" unless event.is_a?(Symbol)
|
|
87
|
+
raise ArgumentError, "Transition #{event} already defined" if @transitions.key?(event)
|
|
88
|
+
|
|
89
|
+
# Normalizza from in array
|
|
90
|
+
from_states = Array(from)
|
|
91
|
+
|
|
92
|
+
# Verifica che gli stati esistano
|
|
93
|
+
from_states.each do |state_name|
|
|
94
|
+
unless @states.include?(state_name)
|
|
95
|
+
raise ArgumentError, "Unknown state in from: #{state_name}. Define it with 'state :#{state_name}' first."
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
unless @states.include?(to)
|
|
100
|
+
raise ArgumentError, "Unknown state in to: #{to}. Define it with 'state :#{to}' first."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Inizializza configurazione transizione
|
|
104
|
+
@transitions[event] = {
|
|
105
|
+
from: from_states,
|
|
106
|
+
to: to,
|
|
107
|
+
guards: [],
|
|
108
|
+
validations: [],
|
|
109
|
+
before_callbacks: [],
|
|
110
|
+
after_callbacks: [],
|
|
111
|
+
around_callbacks: []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Se c'è un blocco, configuralo
|
|
115
|
+
if block_given?
|
|
116
|
+
@current_transition = @transitions[event]
|
|
117
|
+
instance_eval(&block)
|
|
118
|
+
@current_transition = nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Definisce un guard per la transizione corrente
|
|
123
|
+
#
|
|
124
|
+
# I guards sono precondizioni che devono essere vere per permettere la transizione.
|
|
125
|
+
#
|
|
126
|
+
# @overload guard(&block)
|
|
127
|
+
# Guard con lambda/proc
|
|
128
|
+
# @yield Blocco da valutare nel contesto dell'istanza
|
|
129
|
+
# @example
|
|
130
|
+
# guard { items.any? && customer.present? }
|
|
131
|
+
#
|
|
132
|
+
# @overload guard(method_name)
|
|
133
|
+
# Guard con metodo
|
|
134
|
+
# @param method_name [Symbol] Nome del metodo da chiamare
|
|
135
|
+
# @example
|
|
136
|
+
# guard :customer_valid?
|
|
137
|
+
#
|
|
138
|
+
# @overload guard(if: predicate)
|
|
139
|
+
# Guard con Statusable predicate
|
|
140
|
+
# @param if [Symbol] Nome del predicate (integrazione Statusable)
|
|
141
|
+
# @example
|
|
142
|
+
# guard if: :is_ready_for_publishing?
|
|
143
|
+
#
|
|
144
|
+
def guard(method_name = nil, if: nil, &block)
|
|
145
|
+
raise StateableError, "guard can only be called inside a transition block" unless @current_transition
|
|
146
|
+
|
|
147
|
+
if block_given?
|
|
148
|
+
@current_transition[:guards] << { type: :block, block: block }
|
|
149
|
+
elsif method_name
|
|
150
|
+
@current_transition[:guards] << { type: :method, method: method_name }
|
|
151
|
+
elsif binding.local_variable_get(:if)
|
|
152
|
+
@current_transition[:guards] << { type: :predicate, predicate: binding.local_variable_get(:if) }
|
|
153
|
+
else
|
|
154
|
+
raise ArgumentError, "guard requires either a block, method name, or if: option"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Definisce una validazione per la transizione corrente
|
|
159
|
+
#
|
|
160
|
+
# Le validazioni sono eseguite dopo i guards e prima dei callbacks.
|
|
161
|
+
# Devono aggiungere errori all'oggetto errors se la validazione fallisce.
|
|
162
|
+
#
|
|
163
|
+
# @yield Blocco da valutare nel contesto dell'istanza
|
|
164
|
+
#
|
|
165
|
+
# @example
|
|
166
|
+
# validate do
|
|
167
|
+
# errors.add(:base, "Stock unavailable") unless stock_available?
|
|
168
|
+
# errors.add(:payment, "required") if payment_method.blank?
|
|
169
|
+
# end
|
|
170
|
+
#
|
|
171
|
+
def validate(&block)
|
|
172
|
+
raise StateableError, "validate can only be called inside a transition block" unless @current_transition
|
|
173
|
+
raise ArgumentError, "validate requires a block" unless block_given?
|
|
174
|
+
|
|
175
|
+
@current_transition[:validations] << block
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Definisce un callback before per la transizione corrente
|
|
179
|
+
#
|
|
180
|
+
# I before callbacks sono eseguiti prima della transizione di stato.
|
|
181
|
+
#
|
|
182
|
+
# @overload before(&block)
|
|
183
|
+
# Before callback con lambda/proc
|
|
184
|
+
# @yield Blocco da eseguire
|
|
185
|
+
# @example
|
|
186
|
+
# before { calculate_total }
|
|
187
|
+
#
|
|
188
|
+
# @overload before(method_name)
|
|
189
|
+
# Before callback con metodo
|
|
190
|
+
# @param method_name [Symbol] Nome del metodo da chiamare
|
|
191
|
+
# @example
|
|
192
|
+
# before :calculate_total
|
|
193
|
+
#
|
|
194
|
+
def before(method_name = nil, &block)
|
|
195
|
+
raise StateableError, "before can only be called inside a transition block" unless @current_transition
|
|
196
|
+
|
|
197
|
+
if block_given?
|
|
198
|
+
@current_transition[:before_callbacks] << { type: :block, block: block }
|
|
199
|
+
elsif method_name
|
|
200
|
+
@current_transition[:before_callbacks] << { type: :method, method: method_name }
|
|
201
|
+
else
|
|
202
|
+
raise ArgumentError, "before requires either a block or method name"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Definisce un callback after per la transizione corrente
|
|
207
|
+
#
|
|
208
|
+
# Gli after callbacks sono eseguiti dopo la transizione di stato.
|
|
209
|
+
#
|
|
210
|
+
# @overload after(&block)
|
|
211
|
+
# After callback con lambda/proc
|
|
212
|
+
# @yield Blocco da eseguire
|
|
213
|
+
# @example
|
|
214
|
+
# after { send_notification }
|
|
215
|
+
#
|
|
216
|
+
# @overload after(method_name)
|
|
217
|
+
# After callback con metodo
|
|
218
|
+
# @param method_name [Symbol] Nome del metodo da chiamare
|
|
219
|
+
# @example
|
|
220
|
+
# after :send_notification
|
|
221
|
+
#
|
|
222
|
+
def after(method_name = nil, &block)
|
|
223
|
+
raise StateableError, "after can only be called inside a transition block" unless @current_transition
|
|
224
|
+
|
|
225
|
+
if block_given?
|
|
226
|
+
@current_transition[:after_callbacks] << { type: :block, block: block }
|
|
227
|
+
elsif method_name
|
|
228
|
+
@current_transition[:after_callbacks] << { type: :method, method: method_name }
|
|
229
|
+
else
|
|
230
|
+
raise ArgumentError, "after requires either a block or method name"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Definisce un callback around per la transizione corrente
|
|
235
|
+
#
|
|
236
|
+
# Gli around callbacks wrappano la transizione di stato.
|
|
237
|
+
# Il blocco riceve un altro blocco che deve chiamare per eseguire la transizione.
|
|
238
|
+
#
|
|
239
|
+
# @yield Blocco da eseguire, riceve un blocco da chiamare
|
|
240
|
+
#
|
|
241
|
+
# @example
|
|
242
|
+
# around do |transition|
|
|
243
|
+
# log_start
|
|
244
|
+
# transition.call
|
|
245
|
+
# log_end
|
|
246
|
+
# end
|
|
247
|
+
#
|
|
248
|
+
def around(&block)
|
|
249
|
+
raise StateableError, "around can only be called inside a transition block" unless @current_transition
|
|
250
|
+
raise ArgumentError, "around requires a block" unless block_given?
|
|
251
|
+
|
|
252
|
+
@current_transition[:around_callbacks] << block
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Specifica il nome della tabella per state transitions
|
|
256
|
+
#
|
|
257
|
+
# @param name [String, Symbol] Nome della tabella
|
|
258
|
+
#
|
|
259
|
+
# @example Default (state_transitions)
|
|
260
|
+
# stateable do
|
|
261
|
+
# # Uses 'state_transitions' table by default
|
|
262
|
+
# end
|
|
263
|
+
#
|
|
264
|
+
# @example Custom table name
|
|
265
|
+
# stateable do
|
|
266
|
+
# transitions_table 'order_transitions'
|
|
267
|
+
# end
|
|
268
|
+
#
|
|
269
|
+
# @example Shared table across models
|
|
270
|
+
# class Order < ApplicationRecord
|
|
271
|
+
# stateable do
|
|
272
|
+
# transitions_table 'transitions'
|
|
273
|
+
# end
|
|
274
|
+
# end
|
|
275
|
+
#
|
|
276
|
+
# class Article < ApplicationRecord
|
|
277
|
+
# stateable do
|
|
278
|
+
# transitions_table 'transitions' # Same table
|
|
279
|
+
# end
|
|
280
|
+
# end
|
|
281
|
+
#
|
|
282
|
+
def transitions_table(name)
|
|
283
|
+
@table_name = name.to_s
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Restituisce la configurazione completa
|
|
287
|
+
#
|
|
288
|
+
# @return [Hash] Configurazione con stati e transizioni
|
|
289
|
+
#
|
|
290
|
+
def to_h
|
|
291
|
+
{
|
|
292
|
+
states: @states,
|
|
293
|
+
transitions: @transitions,
|
|
294
|
+
initial_state: @initial_state,
|
|
295
|
+
table_name: @table_name
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Stateable
|
|
5
|
+
# Base error for all Stateable errors
|
|
6
|
+
class StateableError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when Stateable is not enabled but methods are called
|
|
9
|
+
class NotEnabledError < StateableError
|
|
10
|
+
def initialize(msg = nil)
|
|
11
|
+
super(msg || "Stateable is not enabled. Add 'stateable do...end' to your model.")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Raised when an invalid state is referenced
|
|
16
|
+
class InvalidStateError < StateableError
|
|
17
|
+
def initialize(state)
|
|
18
|
+
super("Invalid state: #{state.inspect}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Raised when trying to transition to an invalid state from current state
|
|
23
|
+
class InvalidTransitionError < StateableError
|
|
24
|
+
def initialize(event, from_state, to_state)
|
|
25
|
+
super("Cannot transition from #{from_state.inspect} to #{to_state.inspect} via #{event.inspect}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Raised when a guard condition fails
|
|
30
|
+
class GuardFailedError < StateableError
|
|
31
|
+
def initialize(event, guard_description = nil)
|
|
32
|
+
msg = "Guard failed for transition #{event.inspect}"
|
|
33
|
+
msg += ": #{guard_description}" if guard_description
|
|
34
|
+
super(msg)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Raised when a transition validation fails
|
|
39
|
+
class ValidationFailedError < StateableError
|
|
40
|
+
def initialize(event, errors)
|
|
41
|
+
super("Validation failed for transition #{event.inspect}: #{errors.full_messages.join(', ')}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Stateable
|
|
5
|
+
# Guard evaluator per Stateable
|
|
6
|
+
#
|
|
7
|
+
# Valuta le guard conditions per determinare se una transizione è permessa.
|
|
8
|
+
# Supporta tre tipi di guards:
|
|
9
|
+
# - Block: lambda/proc valutato nel contesto dell'istanza
|
|
10
|
+
# - Method: metodo chiamato sull'istanza
|
|
11
|
+
# - Predicate: integrazione con Statusable (is_ready?, etc.)
|
|
12
|
+
#
|
|
13
|
+
class Guard
|
|
14
|
+
def initialize(instance, guard_config)
|
|
15
|
+
@instance = instance
|
|
16
|
+
@guard_config = guard_config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Valuta il guard
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true se il guard passa
|
|
22
|
+
# @raise [GuardFailedError] Se il guard fallisce (opzionale, dipende dal contesto)
|
|
23
|
+
#
|
|
24
|
+
def evaluate
|
|
25
|
+
case @guard_config[:type]
|
|
26
|
+
when :block
|
|
27
|
+
evaluate_block
|
|
28
|
+
when :method
|
|
29
|
+
evaluate_method
|
|
30
|
+
when :predicate
|
|
31
|
+
evaluate_predicate
|
|
32
|
+
else
|
|
33
|
+
raise StateableError, "Unknown guard type: #{@guard_config[:type]}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Descrizione del guard per messaggi di errore
|
|
38
|
+
#
|
|
39
|
+
# @return [String] Descrizione human-readable
|
|
40
|
+
#
|
|
41
|
+
def description
|
|
42
|
+
case @guard_config[:type]
|
|
43
|
+
when :block
|
|
44
|
+
"block guard"
|
|
45
|
+
when :method
|
|
46
|
+
"method guard: #{@guard_config[:method]}"
|
|
47
|
+
when :predicate
|
|
48
|
+
"predicate guard: #{@guard_config[:predicate]}"
|
|
49
|
+
else
|
|
50
|
+
"unknown guard"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Valuta un guard block
|
|
57
|
+
def evaluate_block
|
|
58
|
+
block = @guard_config[:block]
|
|
59
|
+
@instance.instance_exec(&block)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Valuta un guard method
|
|
63
|
+
def evaluate_method
|
|
64
|
+
method_name = @guard_config[:method]
|
|
65
|
+
|
|
66
|
+
unless @instance.respond_to?(method_name, true)
|
|
67
|
+
raise NoMethodError, "Guard method '#{method_name}' not found in #{@instance.class.name}. " \
|
|
68
|
+
"Define it in your model: def #{method_name}; ...; end"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@instance.send(method_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Valuta un guard predicate (integrazione Statusable)
|
|
75
|
+
def evaluate_predicate
|
|
76
|
+
predicate_name = @guard_config[:predicate]
|
|
77
|
+
|
|
78
|
+
unless @instance.respond_to?(predicate_name)
|
|
79
|
+
raise NoMethodError, "Guard predicate '#{predicate_name}' not found in #{@instance.class.name}. " \
|
|
80
|
+
"Make sure Statusable is enabled and the predicate is defined: is :ready, -> { ... }"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@instance.send(predicate_name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Stateable
|
|
5
|
+
# Transition executor per Stateable
|
|
6
|
+
#
|
|
7
|
+
# Gestisce l'esecuzione di una transizione di stato, includendo:
|
|
8
|
+
# - Valutazione guards
|
|
9
|
+
# - Esecuzione validazioni
|
|
10
|
+
# - Esecuzione callbacks (before/after/around)
|
|
11
|
+
# - Aggiornamento stato nel database
|
|
12
|
+
# - Creazione record StateTransition per storico
|
|
13
|
+
#
|
|
14
|
+
class Transition
|
|
15
|
+
def initialize(instance, event, config, metadata = {})
|
|
16
|
+
@instance = instance
|
|
17
|
+
@event = event
|
|
18
|
+
@config = config
|
|
19
|
+
@metadata = metadata
|
|
20
|
+
@from_state = instance.state.to_sym
|
|
21
|
+
@to_state = config[:to]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Esegue la transizione
|
|
25
|
+
#
|
|
26
|
+
# @raise [GuardFailedError] Se un guard fallisce
|
|
27
|
+
# @raise [ValidationFailedError] Se una validazione fallisce
|
|
28
|
+
# @raise [ActiveRecord::RecordInvalid] Se il save! fallisce
|
|
29
|
+
# @return [Boolean] true se la transizione ha successo
|
|
30
|
+
#
|
|
31
|
+
def execute!
|
|
32
|
+
# 1. Valuta guards
|
|
33
|
+
evaluate_guards!
|
|
34
|
+
|
|
35
|
+
# 2. Esegui validazioni
|
|
36
|
+
execute_validations!
|
|
37
|
+
|
|
38
|
+
# 3. Wrap in transaction
|
|
39
|
+
@instance.class.transaction do
|
|
40
|
+
# 4. Esegui callbacks around (se presenti)
|
|
41
|
+
if @config[:around_callbacks].any?
|
|
42
|
+
execute_around_callbacks do
|
|
43
|
+
perform_transition!
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
perform_transition!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Valuta tutti i guards
|
|
56
|
+
def evaluate_guards!
|
|
57
|
+
guards = @config[:guards] || []
|
|
58
|
+
|
|
59
|
+
guards.each do |guard_config|
|
|
60
|
+
guard = Guard.new(@instance, guard_config)
|
|
61
|
+
|
|
62
|
+
unless guard.evaluate
|
|
63
|
+
raise GuardFailedError.new(@event, guard.description)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Esegue tutte le validazioni
|
|
69
|
+
def execute_validations!
|
|
70
|
+
validations = @config[:validations] || []
|
|
71
|
+
return if validations.empty?
|
|
72
|
+
|
|
73
|
+
# Clear existing errors per questa transizione
|
|
74
|
+
@instance.errors.clear
|
|
75
|
+
|
|
76
|
+
validations.each do |validation_block|
|
|
77
|
+
@instance.instance_exec(&validation_block)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if @instance.errors.any?
|
|
81
|
+
raise ValidationFailedError.new(@event, @instance.errors)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Esegue i callback around
|
|
86
|
+
def execute_around_callbacks(&block)
|
|
87
|
+
around_callbacks = @config[:around_callbacks] || []
|
|
88
|
+
|
|
89
|
+
if around_callbacks.empty?
|
|
90
|
+
block.call
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Nested around callbacks
|
|
95
|
+
chain = around_callbacks.reverse.reduce(block) do |inner, around_callback|
|
|
96
|
+
proc { @instance.instance_exec(inner, &around_callback) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
chain.call
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Esegue la transizione effettiva
|
|
103
|
+
def perform_transition!
|
|
104
|
+
# 1. Esegui before callbacks
|
|
105
|
+
execute_callbacks(@config[:before_callbacks] || [])
|
|
106
|
+
|
|
107
|
+
# 2. Aggiorna stato
|
|
108
|
+
@instance.state = @to_state.to_s
|
|
109
|
+
|
|
110
|
+
# 3. Salva il record (valida il modello)
|
|
111
|
+
@instance.save!
|
|
112
|
+
|
|
113
|
+
# 4. Crea record StateTransition
|
|
114
|
+
create_state_transition_record
|
|
115
|
+
|
|
116
|
+
# 5. Esegui after callbacks
|
|
117
|
+
execute_callbacks(@config[:after_callbacks] || [])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Esegue una lista di callbacks
|
|
121
|
+
def execute_callbacks(callbacks)
|
|
122
|
+
callbacks.each do |callback_config|
|
|
123
|
+
case callback_config[:type]
|
|
124
|
+
when :block
|
|
125
|
+
@instance.instance_exec(&callback_config[:block])
|
|
126
|
+
when :method
|
|
127
|
+
@instance.send(callback_config[:method])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Crea il record StateTransition per lo storico
|
|
133
|
+
def create_state_transition_record
|
|
134
|
+
@instance.state_transitions.create!(
|
|
135
|
+
event: @event.to_s,
|
|
136
|
+
from_state: @from_state.to_s,
|
|
137
|
+
to_state: @to_state.to_s,
|
|
138
|
+
metadata: @metadata
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|