better_model 2.1.0 → 3.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 +4 -4
- data/README.md +96 -13
- data/lib/better_model/archivable.rb +203 -91
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +114 -63
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +92 -92
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +71 -53
- data/lib/better_model/stateable/guard.rb +35 -15
- data/lib/better_model/stateable/transition.rb +59 -30
- data/lib/better_model/stateable.rb +33 -15
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +49 -172
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -5
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +44 -7
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -48
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
|
@@ -2,25 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterModel
|
|
4
4
|
module Stateable
|
|
5
|
-
# Check evaluator
|
|
5
|
+
# Check evaluator for Stateable transitions.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# - Block: lambda/proc
|
|
10
|
-
# - Method:
|
|
11
|
-
# - Predicate:
|
|
7
|
+
# Evaluates check conditions to determine if a transition is allowed.
|
|
8
|
+
# Supports three types of checks:
|
|
9
|
+
# - Block: lambda/proc evaluated in instance context
|
|
10
|
+
# - Method: method called on instance
|
|
11
|
+
# - Predicate: integration with Statusable (is_ready?, etc.)
|
|
12
12
|
#
|
|
13
|
+
# @api private
|
|
13
14
|
class Guard
|
|
15
|
+
# Initialize a new Guard.
|
|
16
|
+
#
|
|
17
|
+
# @param instance [Object] Model instance
|
|
18
|
+
# @param guard_config [Hash] Guard configuration hash
|
|
14
19
|
def initialize(instance, guard_config)
|
|
15
20
|
@instance = instance
|
|
16
21
|
@guard_config = guard_config
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
#
|
|
24
|
+
# Evaluate the check.
|
|
20
25
|
#
|
|
21
|
-
# @return [Boolean] true
|
|
22
|
-
# @raise [CheckFailedError]
|
|
26
|
+
# @return [Boolean] true if check passes
|
|
27
|
+
# @raise [BetterModel::Errors::Stateable::CheckFailedError] If check fails (optional, context-dependent)
|
|
23
28
|
#
|
|
29
|
+
# @example
|
|
30
|
+
# guard.evaluate # => true
|
|
24
31
|
def evaluate
|
|
25
32
|
case @guard_config[:type]
|
|
26
33
|
when :block
|
|
@@ -30,14 +37,16 @@ module BetterModel
|
|
|
30
37
|
when :predicate
|
|
31
38
|
evaluate_predicate
|
|
32
39
|
else
|
|
33
|
-
raise StateableError, "Unknown check type: #{@guard_config[:type]}"
|
|
40
|
+
raise BetterModel::Errors::Stateable::StateableError, "Unknown check type: #{@guard_config[:type]}"
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
|
|
37
|
-
#
|
|
44
|
+
# Description of check for error messages.
|
|
38
45
|
#
|
|
39
|
-
# @return [String]
|
|
46
|
+
# @return [String] Human-readable description
|
|
40
47
|
#
|
|
48
|
+
# @example
|
|
49
|
+
# guard.description # => "method check: customer_valid?"
|
|
41
50
|
def description
|
|
42
51
|
case @guard_config[:type]
|
|
43
52
|
when :block
|
|
@@ -53,13 +62,20 @@ module BetterModel
|
|
|
53
62
|
|
|
54
63
|
private
|
|
55
64
|
|
|
56
|
-
#
|
|
65
|
+
# Evaluate a block check.
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean] Result of block evaluation
|
|
68
|
+
# @api private
|
|
57
69
|
def evaluate_block
|
|
58
70
|
block = @guard_config[:block]
|
|
59
71
|
@instance.instance_exec(&block)
|
|
60
72
|
end
|
|
61
73
|
|
|
62
|
-
#
|
|
74
|
+
# Evaluate a method check.
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] Result of method call
|
|
77
|
+
# @raise [NoMethodError] If method not found
|
|
78
|
+
# @api private
|
|
63
79
|
def evaluate_method
|
|
64
80
|
method_name = @guard_config[:method]
|
|
65
81
|
|
|
@@ -71,7 +87,11 @@ module BetterModel
|
|
|
71
87
|
@instance.send(method_name)
|
|
72
88
|
end
|
|
73
89
|
|
|
74
|
-
#
|
|
90
|
+
# Evaluate a predicate check (Statusable integration).
|
|
91
|
+
#
|
|
92
|
+
# @return [Boolean] Result of predicate call
|
|
93
|
+
# @raise [NoMethodError] If predicate not found
|
|
94
|
+
# @api private
|
|
75
95
|
def evaluate_predicate
|
|
76
96
|
predicate_name = @guard_config[:predicate]
|
|
77
97
|
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../errors/stateable/check_failed_error"
|
|
4
|
+
require_relative "../errors/stateable/validation_failed_error"
|
|
5
|
+
|
|
3
6
|
module BetterModel
|
|
4
7
|
module Stateable
|
|
5
|
-
# Transition executor
|
|
8
|
+
# Transition executor for Stateable.
|
|
6
9
|
#
|
|
7
|
-
#
|
|
8
|
-
# -
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
11
|
-
# -
|
|
12
|
-
# -
|
|
10
|
+
# Handles the execution of a state transition, including:
|
|
11
|
+
# - Check evaluation
|
|
12
|
+
# - Validation execution
|
|
13
|
+
# - Callback execution (before_transition/after_transition/around)
|
|
14
|
+
# - State update in database
|
|
15
|
+
# - StateTransition record creation for history
|
|
13
16
|
#
|
|
17
|
+
# @api private
|
|
14
18
|
class Transition
|
|
19
|
+
# Initialize a new Transition.
|
|
20
|
+
#
|
|
21
|
+
# @param instance [Object] Model instance
|
|
22
|
+
# @param event [Symbol] Transition event name
|
|
23
|
+
# @param config [Hash] Transition configuration
|
|
24
|
+
# @param metadata [Hash] Additional metadata for transition
|
|
15
25
|
def initialize(instance, event, config, metadata = {})
|
|
16
26
|
@instance = instance
|
|
17
27
|
@event = event
|
|
@@ -21,23 +31,25 @@ module BetterModel
|
|
|
21
31
|
@to_state = config[:to]
|
|
22
32
|
end
|
|
23
33
|
|
|
24
|
-
#
|
|
34
|
+
# Execute the transition.
|
|
25
35
|
#
|
|
26
|
-
# @raise [CheckFailedError]
|
|
27
|
-
# @raise [ValidationFailedError]
|
|
28
|
-
# @raise [ActiveRecord::RecordInvalid]
|
|
29
|
-
# @return [Boolean] true
|
|
36
|
+
# @raise [BetterModel::Errors::Stateable::CheckFailedError] If a check fails
|
|
37
|
+
# @raise [BetterModel::Errors::Stateable::ValidationFailedError] If a validation fails
|
|
38
|
+
# @raise [ActiveRecord::RecordInvalid] If save! fails
|
|
39
|
+
# @return [Boolean] true if transition succeeds
|
|
30
40
|
#
|
|
41
|
+
# @example
|
|
42
|
+
# transition.execute! # => true
|
|
31
43
|
def execute!
|
|
32
|
-
# 1.
|
|
44
|
+
# 1. Evaluate checks
|
|
33
45
|
evaluate_checks!
|
|
34
46
|
|
|
35
|
-
# 2.
|
|
47
|
+
# 2. Execute validations
|
|
36
48
|
execute_validations!
|
|
37
49
|
|
|
38
50
|
# 3. Wrap in transaction
|
|
39
51
|
@instance.class.transaction do
|
|
40
|
-
# 4.
|
|
52
|
+
# 4. Execute around callbacks (if present)
|
|
41
53
|
if @config[:around_callbacks].any?
|
|
42
54
|
execute_around_callbacks do
|
|
43
55
|
perform_transition!
|
|
@@ -52,25 +64,31 @@ module BetterModel
|
|
|
52
64
|
|
|
53
65
|
private
|
|
54
66
|
|
|
55
|
-
#
|
|
67
|
+
# Evaluate all checks.
|
|
68
|
+
#
|
|
69
|
+
# @raise [BetterModel::Errors::Stateable::CheckFailedError] If any check fails
|
|
70
|
+
# @api private
|
|
56
71
|
def evaluate_checks!
|
|
57
|
-
checks = @config[:guards] || [] #
|
|
72
|
+
checks = @config[:guards] || [] # Keep :guards for internal compatibility
|
|
58
73
|
|
|
59
74
|
checks.each do |check_config|
|
|
60
75
|
check = Guard.new(@instance, check_config) # Guard class handles the logic
|
|
61
76
|
|
|
62
77
|
unless check.evaluate
|
|
63
|
-
raise CheckFailedError
|
|
78
|
+
raise BetterModel::Errors::Stateable::CheckFailedError, "Check failed for transition #{@event}"
|
|
64
79
|
end
|
|
65
80
|
end
|
|
66
81
|
end
|
|
67
82
|
|
|
68
|
-
#
|
|
83
|
+
# Execute all validations.
|
|
84
|
+
#
|
|
85
|
+
# @raise [BetterModel::Errors::Stateable::ValidationFailedError] If validations fail
|
|
86
|
+
# @api private
|
|
69
87
|
def execute_validations!
|
|
70
88
|
validations = @config[:validations] || []
|
|
71
89
|
return if validations.empty?
|
|
72
90
|
|
|
73
|
-
# Clear existing errors
|
|
91
|
+
# Clear existing errors for this transition
|
|
74
92
|
@instance.errors.clear
|
|
75
93
|
|
|
76
94
|
validations.each do |validation_block|
|
|
@@ -78,11 +96,15 @@ module BetterModel
|
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
if @instance.errors.any?
|
|
81
|
-
|
|
99
|
+
error_messages = @instance.errors.full_messages.join(", ")
|
|
100
|
+
raise BetterModel::Errors::Stateable::ValidationFailedError, "Validation failed for transition #{@event}: #{error_messages}"
|
|
82
101
|
end
|
|
83
102
|
end
|
|
84
103
|
|
|
85
|
-
#
|
|
104
|
+
# Execute around callbacks.
|
|
105
|
+
#
|
|
106
|
+
# @yield Block to wrap with around callbacks
|
|
107
|
+
# @api private
|
|
86
108
|
def execute_around_callbacks(&block)
|
|
87
109
|
around_callbacks = @config[:around_callbacks] || []
|
|
88
110
|
|
|
@@ -99,25 +121,30 @@ module BetterModel
|
|
|
99
121
|
chain.call
|
|
100
122
|
end
|
|
101
123
|
|
|
102
|
-
#
|
|
124
|
+
# Perform the actual transition.
|
|
125
|
+
#
|
|
126
|
+
# @api private
|
|
103
127
|
def perform_transition!
|
|
104
|
-
# 1.
|
|
128
|
+
# 1. Execute before_transition callbacks
|
|
105
129
|
execute_callbacks(@config[:before_callbacks] || [])
|
|
106
130
|
|
|
107
|
-
# 2.
|
|
131
|
+
# 2. Update state
|
|
108
132
|
@instance.state = @to_state.to_s
|
|
109
133
|
|
|
110
|
-
# 3.
|
|
134
|
+
# 3. Save record (validates model)
|
|
111
135
|
@instance.save!
|
|
112
136
|
|
|
113
|
-
# 4.
|
|
137
|
+
# 4. Create StateTransition record
|
|
114
138
|
create_state_transition_record
|
|
115
139
|
|
|
116
|
-
# 5.
|
|
140
|
+
# 5. Execute after_transition callbacks
|
|
117
141
|
execute_callbacks(@config[:after_callbacks] || [])
|
|
118
142
|
end
|
|
119
143
|
|
|
120
|
-
#
|
|
144
|
+
# Execute a list of callbacks.
|
|
145
|
+
#
|
|
146
|
+
# @param callbacks [Array<Hash>] Callback configurations
|
|
147
|
+
# @api private
|
|
121
148
|
def execute_callbacks(callbacks)
|
|
122
149
|
callbacks.each do |callback_config|
|
|
123
150
|
case callback_config[:type]
|
|
@@ -129,7 +156,9 @@ module BetterModel
|
|
|
129
156
|
end
|
|
130
157
|
end
|
|
131
158
|
|
|
132
|
-
#
|
|
159
|
+
# Create StateTransition record for history.
|
|
160
|
+
#
|
|
161
|
+
# @api private
|
|
133
162
|
def create_state_transition_record
|
|
134
163
|
@instance.state_transitions.create!(
|
|
135
164
|
event: @event.to_s,
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "errors/stateable/stateable_error"
|
|
4
|
+
require_relative "errors/stateable/not_enabled_error"
|
|
5
|
+
require_relative "errors/stateable/invalid_state_error"
|
|
6
|
+
require_relative "errors/stateable/invalid_transition_error"
|
|
7
|
+
require_relative "errors/stateable/check_failed_error"
|
|
8
|
+
require_relative "errors/stateable/validation_failed_error"
|
|
9
|
+
require_relative "errors/stateable/configuration_error"
|
|
10
|
+
|
|
3
11
|
# Stateable - Declarative State Machine per modelli Rails
|
|
4
12
|
#
|
|
5
13
|
# Questo concern permette di definire state machines dichiarative con:
|
|
@@ -96,7 +104,7 @@ module BetterModel
|
|
|
96
104
|
included do
|
|
97
105
|
# Validazione ActiveRecord
|
|
98
106
|
unless ancestors.include?(ActiveRecord::Base)
|
|
99
|
-
raise
|
|
107
|
+
raise BetterModel::Errors::Stateable::ConfigurationError, "Invalid configuration"
|
|
100
108
|
end
|
|
101
109
|
|
|
102
110
|
# Configurazione stateable (opt-in)
|
|
@@ -171,9 +179,7 @@ module BetterModel
|
|
|
171
179
|
# Verifica se stateable è attivo
|
|
172
180
|
#
|
|
173
181
|
# @return [Boolean]
|
|
174
|
-
def stateable_enabled?
|
|
175
|
-
stateable_enabled == true
|
|
176
|
-
end
|
|
182
|
+
def stateable_enabled? = stateable_enabled == true
|
|
177
183
|
|
|
178
184
|
private
|
|
179
185
|
|
|
@@ -214,7 +220,7 @@ module BetterModel
|
|
|
214
220
|
end
|
|
215
221
|
|
|
216
222
|
# Create new StateTransition class dynamically
|
|
217
|
-
transition_class = Class.new(BetterModel::StateTransition) do
|
|
223
|
+
transition_class = Class.new(BetterModel::Models::StateTransition) do
|
|
218
224
|
self.table_name = table_name
|
|
219
225
|
end
|
|
220
226
|
|
|
@@ -236,8 +242,11 @@ module BetterModel
|
|
|
236
242
|
# Metodi per ogni transizione: confirm!, can_confirm?, etc.
|
|
237
243
|
stateable_transitions.each do |event_name, transition_config|
|
|
238
244
|
# event! - esegue transizione (raise se fallisce)
|
|
239
|
-
|
|
240
|
-
|
|
245
|
+
# Accepts both positional hash and keyword arguments for flexibility
|
|
246
|
+
define_method "#{event_name}!" do |metadata = {}, **kwargs|
|
|
247
|
+
# Convert positional hash to keyword args if provided
|
|
248
|
+
combined_metadata = metadata.merge(kwargs)
|
|
249
|
+
transition_to!(event_name, **combined_metadata)
|
|
241
250
|
end
|
|
242
251
|
|
|
243
252
|
# can_event? - controlla se transizione è possibile
|
|
@@ -269,23 +278,27 @@ module BetterModel
|
|
|
269
278
|
#
|
|
270
279
|
# @param event [Symbol] Nome della transizione
|
|
271
280
|
# @param metadata [Hash] Metadata opzionale da salvare nella StateTransition
|
|
272
|
-
# @raise [InvalidTransitionError] Se la transizione non è valida
|
|
273
|
-
# @raise [CheckFailedError] Se un check fallisce
|
|
274
|
-
# @raise [ValidationFailedError] Se una validazione fallisce
|
|
281
|
+
# @raise [BetterModel::Errors::Stateable::InvalidTransitionError] Se la transizione non è valida
|
|
282
|
+
# @raise [BetterModel::Errors::Stateable::CheckFailedError] Se un check fallisce
|
|
283
|
+
# @raise [BetterModel::Errors::Stateable::ValidationFailedError] Se una validazione fallisce
|
|
275
284
|
# @return [Boolean] true se la transizione ha successo
|
|
276
285
|
#
|
|
277
286
|
def transition_to!(event, **metadata)
|
|
278
|
-
|
|
287
|
+
unless self.class.stateable_enabled?
|
|
288
|
+
raise BetterModel::Errors::Stateable::NotEnabledError, "Module is not enabled"
|
|
289
|
+
end
|
|
279
290
|
|
|
280
291
|
transition_config = self.class.stateable_transitions[event.to_sym]
|
|
281
|
-
|
|
292
|
+
unless transition_config
|
|
293
|
+
raise BetterModel::Errors::Stateable::ConfigurationError, "Unknown transition: #{event}"
|
|
294
|
+
end
|
|
282
295
|
|
|
283
296
|
current_state = state.to_sym
|
|
284
297
|
|
|
285
298
|
# Verifica che from_state sia valido
|
|
286
299
|
from_states = Array(transition_config[:from])
|
|
287
300
|
unless from_states.include?(current_state)
|
|
288
|
-
raise InvalidTransitionError
|
|
301
|
+
raise BetterModel::Errors::Stateable::InvalidTransitionError, "Cannot transition from #{current_state} to #{transition_config[:to]} via #{event}"
|
|
289
302
|
end
|
|
290
303
|
|
|
291
304
|
# Esegui la transizione usando Transition executor
|
|
@@ -322,7 +335,9 @@ module BetterModel
|
|
|
322
335
|
# @return [Array<Hash>] Array di transizioni con :event, :from, :to, :at, :metadata
|
|
323
336
|
#
|
|
324
337
|
def transition_history
|
|
325
|
-
|
|
338
|
+
unless self.class.stateable_enabled?
|
|
339
|
+
raise BetterModel::Errors::Stateable::NotEnabledError, "Module is not enabled"
|
|
340
|
+
end
|
|
326
341
|
|
|
327
342
|
state_transitions.map do |transition|
|
|
328
343
|
{
|
|
@@ -345,7 +360,10 @@ module BetterModel
|
|
|
345
360
|
result = super
|
|
346
361
|
|
|
347
362
|
if options[:include_transition_history] && self.class.stateable_enabled?
|
|
348
|
-
|
|
363
|
+
# Convert symbol keys to string keys for JSON compatibility
|
|
364
|
+
result["transition_history"] = transition_history.map do |item|
|
|
365
|
+
item.transform_keys(&:to_s)
|
|
366
|
+
end
|
|
349
367
|
end
|
|
350
368
|
|
|
351
369
|
result
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/statusable/statusable_error"
|
|
4
|
+
require_relative "errors/statusable/configuration_error"
|
|
5
|
+
|
|
6
|
+
# Statusable - Declarative status system for Rails models.
|
|
4
7
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
8
|
+
# This concern enables defining statuses on models using a simple, declarative DSL
|
|
9
|
+
# similar to the Enrichable pattern but for statuses.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
11
|
+
# @example Basic Usage
|
|
9
12
|
# class Communications::Consult < ApplicationRecord
|
|
10
13
|
# include BetterModel::Statusable
|
|
11
14
|
#
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
# is :ready_to_start, -> { scheduled? && scheduled_at <= Time.current }
|
|
18
21
|
# end
|
|
19
22
|
#
|
|
20
|
-
#
|
|
23
|
+
# @example Checking Statuses
|
|
21
24
|
# consult.is?(:pending) # => true/false
|
|
22
25
|
# consult.is_pending? # => true/false
|
|
23
26
|
# consult.is_active_session? # => true/false
|
|
@@ -29,58 +32,79 @@ module BetterModel
|
|
|
29
32
|
extend ActiveSupport::Concern
|
|
30
33
|
|
|
31
34
|
included do
|
|
32
|
-
# Registry
|
|
35
|
+
# Registry of statuses defined for this class
|
|
33
36
|
class_attribute :is_definitions
|
|
34
37
|
self.is_definitions = {}
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
class_methods do
|
|
38
|
-
# DSL
|
|
41
|
+
# DSL to define statuses.
|
|
42
|
+
#
|
|
43
|
+
# Defines a status check that can be evaluated against model instances.
|
|
44
|
+
# Automatically creates a convenience method is_<status_name>? for each status.
|
|
39
45
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
46
|
+
# @param status_name [Symbol, String] Status identifier (e.g., :pending, :active)
|
|
47
|
+
# @param condition_proc [Proc, nil] Lambda or proc that defines the condition
|
|
48
|
+
# @yield Alternative to condition_proc parameter
|
|
49
|
+
# @raise [BetterModel::Errors::Statusable::ConfigurationError] If parameters are invalid
|
|
44
50
|
#
|
|
45
|
-
#
|
|
51
|
+
# @example With lambda parameter
|
|
46
52
|
# is :pending, -> { status == 'initialized' }
|
|
47
|
-
#
|
|
53
|
+
#
|
|
54
|
+
# @example With block
|
|
55
|
+
# is :expired do
|
|
56
|
+
# expires_at.present? && expires_at <= Time.current
|
|
57
|
+
# end
|
|
58
|
+
#
|
|
59
|
+
# @example Complex condition
|
|
48
60
|
# is :ready do
|
|
49
61
|
# scheduled_at.present? && scheduled_at <= Time.current
|
|
50
62
|
# end
|
|
51
63
|
def is(status_name, condition_proc = nil, &block)
|
|
52
|
-
#
|
|
53
|
-
|
|
64
|
+
# Validate parameters before converting
|
|
65
|
+
if status_name.blank?
|
|
66
|
+
raise BetterModel::Errors::Statusable::ConfigurationError, "Status name cannot be blank"
|
|
67
|
+
end
|
|
54
68
|
|
|
55
69
|
status_name = status_name.to_sym
|
|
56
70
|
condition = condition_proc || block
|
|
57
|
-
raise ArgumentError, "Condition proc or block is required" unless condition
|
|
58
|
-
raise ArgumentError, "Condition must respond to call" unless condition.respond_to?(:call)
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
unless condition
|
|
73
|
+
raise BetterModel::Errors::Statusable::ConfigurationError, "Condition proc or block is required"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
unless condition.respond_to?(:call)
|
|
77
|
+
raise BetterModel::Errors::Statusable::ConfigurationError, "Condition must respond to call"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Register status in registry
|
|
61
81
|
self.is_definitions = is_definitions.merge(status_name => condition.freeze).freeze
|
|
62
82
|
|
|
63
|
-
#
|
|
83
|
+
# Generate dynamic method is_#{status_name}?
|
|
64
84
|
define_is_method(status_name)
|
|
65
85
|
end
|
|
66
86
|
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
# List all statuses defined for this class.
|
|
88
|
+
#
|
|
89
|
+
# @return [Array<Symbol>] Array of defined status names
|
|
90
|
+
def defined_statuses = is_definitions.keys
|
|
71
91
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
92
|
+
# Check if a status is defined.
|
|
93
|
+
#
|
|
94
|
+
# @param status_name [Symbol, String] Status name to check
|
|
95
|
+
# @return [Boolean] true if status is defined
|
|
96
|
+
def status_defined?(status_name) = is_definitions.key?(status_name.to_sym)
|
|
76
97
|
|
|
77
98
|
private
|
|
78
99
|
|
|
79
|
-
#
|
|
100
|
+
# Generate dynamic method is_#{status_name}? for each defined status.
|
|
101
|
+
#
|
|
102
|
+
# @param status_name [Symbol] Status name
|
|
103
|
+
# @api private
|
|
80
104
|
def define_is_method(status_name)
|
|
81
105
|
method_name = "is_#{status_name}?"
|
|
82
106
|
|
|
83
|
-
#
|
|
107
|
+
# Avoid redefining methods if they already exist
|
|
84
108
|
return if method_defined?(method_name)
|
|
85
109
|
|
|
86
110
|
define_method(method_name) do
|
|
@@ -89,35 +113,33 @@ module BetterModel
|
|
|
89
113
|
end
|
|
90
114
|
end
|
|
91
115
|
|
|
92
|
-
#
|
|
116
|
+
# Generic method to check if a status is active.
|
|
93
117
|
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
118
|
+
# Evaluates the status condition in the context of the model instance.
|
|
119
|
+
# Returns false if status is not defined (secure by default).
|
|
96
120
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
# - false se lo stato non è attivo o non è definito
|
|
121
|
+
# @param status_name [Symbol, String] Status name to check
|
|
122
|
+
# @return [Boolean] true if status is active, false otherwise
|
|
100
123
|
#
|
|
101
|
-
#
|
|
102
|
-
# consult.is?(:pending)
|
|
124
|
+
# @example
|
|
125
|
+
# consult.is?(:pending) # => true
|
|
103
126
|
def is?(status_name)
|
|
104
127
|
status_name = status_name.to_sym
|
|
105
128
|
condition = self.class.is_definitions[status_name]
|
|
106
129
|
|
|
107
|
-
#
|
|
130
|
+
# If status is not defined, return false (secure by default)
|
|
108
131
|
return false unless condition
|
|
109
132
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
133
|
+
# Evaluate condition in context of model instance
|
|
134
|
+
# Errors propagate naturally - fail fast
|
|
112
135
|
instance_exec(&condition)
|
|
113
136
|
end
|
|
114
137
|
|
|
115
|
-
#
|
|
138
|
+
# Returns all available statuses for this instance with their values.
|
|
116
139
|
#
|
|
117
|
-
#
|
|
118
|
-
# - Hash con chiavi simbolo (stati) e valori booleani (attivi/inattivi)
|
|
140
|
+
# @return [Hash{Symbol => Boolean}] Hash with status names and their active state
|
|
119
141
|
#
|
|
120
|
-
#
|
|
142
|
+
# @example
|
|
121
143
|
# consult.statuses
|
|
122
144
|
# # => { pending: true, active: false, expired: false, scheduled: true }
|
|
123
145
|
def statuses
|
|
@@ -126,26 +148,36 @@ module BetterModel
|
|
|
126
148
|
end
|
|
127
149
|
end
|
|
128
150
|
|
|
129
|
-
#
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
# Check if instance has at least one active status.
|
|
152
|
+
#
|
|
153
|
+
# @return [Boolean] true if any status is active
|
|
154
|
+
def has_any_status? = statuses.values.any?
|
|
133
155
|
|
|
134
|
-
#
|
|
156
|
+
# Check if instance has all specified statuses active.
|
|
157
|
+
#
|
|
158
|
+
# @param status_names [Array<Symbol>] Status names to check
|
|
159
|
+
# @return [Boolean] true if all statuses are active
|
|
135
160
|
def has_all_statuses?(status_names)
|
|
136
161
|
Array(status_names).all? { |status_name| is?(status_name) }
|
|
137
162
|
end
|
|
138
163
|
|
|
139
|
-
#
|
|
164
|
+
# Filter a list of statuses returning only active ones.
|
|
165
|
+
#
|
|
166
|
+
# @param status_names [Array<Symbol>] Status names to filter
|
|
167
|
+
# @return [Array<Symbol>] Active statuses
|
|
140
168
|
def active_statuses(status_names)
|
|
141
169
|
Array(status_names).select { |status_name| is?(status_name) }
|
|
142
170
|
end
|
|
143
171
|
|
|
144
|
-
# Override
|
|
172
|
+
# Override as_json to automatically include statuses if requested.
|
|
173
|
+
#
|
|
174
|
+
# @param options [Hash] Options for as_json
|
|
175
|
+
# @option options [Boolean] :include_statuses Include statuses in JSON output
|
|
176
|
+
# @return [Hash] JSON representation
|
|
145
177
|
def as_json(options = {})
|
|
146
178
|
result = super
|
|
147
179
|
|
|
148
|
-
# Include
|
|
180
|
+
# Include statuses if explicitly requested, converting symbol keys to strings
|
|
149
181
|
result["statuses"] = statuses.transform_keys(&:to_s) if options[:include_statuses]
|
|
150
182
|
|
|
151
183
|
result
|