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,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Validatable
|
|
5
|
+
# Configurator per il DSL di Validatable
|
|
6
|
+
#
|
|
7
|
+
# Questo configurator permette di definire validazioni in modo dichiarativo
|
|
8
|
+
# all'interno del blocco `validatable do...end`.
|
|
9
|
+
#
|
|
10
|
+
# Esempio:
|
|
11
|
+
# validatable do
|
|
12
|
+
# # Validazioni base
|
|
13
|
+
# validate :title, :content, presence: true
|
|
14
|
+
# validate :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
15
|
+
#
|
|
16
|
+
# # Validazioni condizionali
|
|
17
|
+
# validate_if :is_published? do
|
|
18
|
+
# validate :published_at, presence: true
|
|
19
|
+
# validate :author_id, presence: true
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# # Validazioni condizionali negate
|
|
23
|
+
# validate_unless :is_draft? do
|
|
24
|
+
# validate :reviewer_id, presence: true
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# # Cross-field validations
|
|
28
|
+
# validate_order :starts_at, :before, :ends_at
|
|
29
|
+
# validate_order :min_price, :lteq, :max_price
|
|
30
|
+
#
|
|
31
|
+
# # Business rules
|
|
32
|
+
# validate_business_rule :valid_category
|
|
33
|
+
# validate_business_rule :author_has_permission, on: :create
|
|
34
|
+
#
|
|
35
|
+
# # Gruppi di validazioni
|
|
36
|
+
# validation_group :step1, [:email, :password]
|
|
37
|
+
# validation_group :step2, [:first_name, :last_name]
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
class Configurator
|
|
41
|
+
attr_reader :groups
|
|
42
|
+
|
|
43
|
+
def initialize(model_class)
|
|
44
|
+
@model_class = model_class
|
|
45
|
+
@conditional_validations = []
|
|
46
|
+
@order_validations = []
|
|
47
|
+
@business_rules = []
|
|
48
|
+
@groups = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Definisce validazioni standard sui campi
|
|
52
|
+
#
|
|
53
|
+
# @param fields [Array<Symbol>] Nomi dei campi
|
|
54
|
+
# @param options [Hash] Opzioni di validazione (presence, format, etc.)
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# validate :title, :content, presence: true
|
|
58
|
+
# validate :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
59
|
+
# validate :age, numericality: { greater_than: 0 }
|
|
60
|
+
#
|
|
61
|
+
def validate(*fields, **options)
|
|
62
|
+
# Se siamo dentro un blocco condizionale, aggiungi alla condizione corrente
|
|
63
|
+
if @current_conditional
|
|
64
|
+
@current_conditional[:validations] << {
|
|
65
|
+
fields: fields,
|
|
66
|
+
options: options
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
# Altrimenti applica direttamente alla classe
|
|
70
|
+
# Questo viene fatto subito, non in apply_validatable_config
|
|
71
|
+
@model_class.validates(*fields, **options)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Validazioni condizionali (se condizione è vera)
|
|
76
|
+
#
|
|
77
|
+
# @param condition [Symbol, Proc] Condizione da verificare
|
|
78
|
+
# @yield Blocco con validazioni da applicare se condizione è vera
|
|
79
|
+
#
|
|
80
|
+
# @example Con simbolo (metodo)
|
|
81
|
+
# validate_if :is_published? do
|
|
82
|
+
# validate :published_at, presence: true
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# @example Con lambda
|
|
86
|
+
# validate_if -> { status == "published" } do
|
|
87
|
+
# validate :published_at, presence: true
|
|
88
|
+
# end
|
|
89
|
+
#
|
|
90
|
+
def validate_if(condition, &block)
|
|
91
|
+
raise ArgumentError, "validate_if requires a block" unless block_given?
|
|
92
|
+
|
|
93
|
+
conditional = {
|
|
94
|
+
condition: condition,
|
|
95
|
+
negate: false,
|
|
96
|
+
validations: []
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Set current conditional per catturare le validate dentro il blocco
|
|
100
|
+
@current_conditional = conditional
|
|
101
|
+
instance_eval(&block)
|
|
102
|
+
@current_conditional = nil
|
|
103
|
+
|
|
104
|
+
@conditional_validations << conditional
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validazioni condizionali negate (se condizione è falsa)
|
|
108
|
+
#
|
|
109
|
+
# @param condition [Symbol, Proc] Condizione da verificare
|
|
110
|
+
# @yield Blocco con validazioni da applicare se condizione è falsa
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# validate_unless :is_draft? do
|
|
114
|
+
# validate :reviewer_id, presence: true
|
|
115
|
+
# end
|
|
116
|
+
#
|
|
117
|
+
def validate_unless(condition, &block)
|
|
118
|
+
raise ArgumentError, "validate_unless requires a block" unless block_given?
|
|
119
|
+
|
|
120
|
+
conditional = {
|
|
121
|
+
condition: condition,
|
|
122
|
+
negate: true,
|
|
123
|
+
validations: []
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@current_conditional = conditional
|
|
127
|
+
instance_eval(&block)
|
|
128
|
+
@current_conditional = nil
|
|
129
|
+
|
|
130
|
+
@conditional_validations << conditional
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Validazione di ordine tra campi (cross-field)
|
|
134
|
+
#
|
|
135
|
+
# Verifica che un campo sia in una relazione d'ordine rispetto ad un altro.
|
|
136
|
+
#
|
|
137
|
+
# @param first_field [Symbol] Primo campo
|
|
138
|
+
# @param comparator [Symbol] Comparatore (:before, :after, :lteq, :gteq)
|
|
139
|
+
# @param second_field [Symbol] Secondo campo
|
|
140
|
+
# @param options [Hash] Opzioni aggiuntive (on, if, unless, message)
|
|
141
|
+
#
|
|
142
|
+
# @example Date validation
|
|
143
|
+
# validate_order :starts_at, :before, :ends_at
|
|
144
|
+
# validate_order :starts_at, :before, :ends_at, message: "must be before end date"
|
|
145
|
+
#
|
|
146
|
+
# @example Numeric validation
|
|
147
|
+
# validate_order :min_price, :lteq, :max_price
|
|
148
|
+
# validate_order :discount, :lteq, :price, on: :create
|
|
149
|
+
#
|
|
150
|
+
# Comparatori supportati:
|
|
151
|
+
# - :before - first < second (date/time)
|
|
152
|
+
# - :after - first > second (date/time)
|
|
153
|
+
# - :lteq - first <= second (numeric)
|
|
154
|
+
# - :gteq - first >= second (numeric)
|
|
155
|
+
# - :lt - first < second (numeric)
|
|
156
|
+
# - :gt - first > second (numeric)
|
|
157
|
+
#
|
|
158
|
+
def validate_order(first_field, comparator, second_field, **options)
|
|
159
|
+
valid_comparators = %i[before after lteq gteq lt gt]
|
|
160
|
+
unless valid_comparators.include?(comparator)
|
|
161
|
+
raise ArgumentError, "Invalid comparator: #{comparator}. Valid: #{valid_comparators.join(', ')}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
@order_validations << {
|
|
165
|
+
first_field: first_field,
|
|
166
|
+
comparator: comparator,
|
|
167
|
+
second_field: second_field,
|
|
168
|
+
options: options
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Definisce una business rule custom
|
|
173
|
+
#
|
|
174
|
+
# Le business rules sono metodi custom che implementano logica di validazione
|
|
175
|
+
# complessa che non può essere espressa con validatori standard.
|
|
176
|
+
#
|
|
177
|
+
# @param rule_name [Symbol] Nome del metodo che implementa la rule
|
|
178
|
+
# @param options [Hash] Opzioni (on, if, unless)
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# # Nel configurator:
|
|
182
|
+
# validate_business_rule :valid_category
|
|
183
|
+
# validate_business_rule :author_has_permission, on: :create
|
|
184
|
+
#
|
|
185
|
+
# # Nel modello (implementazione):
|
|
186
|
+
# def valid_category
|
|
187
|
+
# unless Category.exists?(id: category_id)
|
|
188
|
+
# errors.add(:category_id, "must be a valid category")
|
|
189
|
+
# end
|
|
190
|
+
# end
|
|
191
|
+
#
|
|
192
|
+
# def author_has_permission
|
|
193
|
+
# unless author&.can_create_articles?
|
|
194
|
+
# errors.add(:author_id, "does not have permission")
|
|
195
|
+
# end
|
|
196
|
+
# end
|
|
197
|
+
#
|
|
198
|
+
def validate_business_rule(rule_name, **options)
|
|
199
|
+
@business_rules << {
|
|
200
|
+
name: rule_name,
|
|
201
|
+
options: options
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Definisce un gruppo di validazioni
|
|
206
|
+
#
|
|
207
|
+
# I gruppi permettono di validare solo un sottoinsieme di campi,
|
|
208
|
+
# utile per form multi-step o validazioni parziali.
|
|
209
|
+
#
|
|
210
|
+
# @param group_name [Symbol] Nome del gruppo
|
|
211
|
+
# @param fields [Array<Symbol>] Campi inclusi nel gruppo
|
|
212
|
+
#
|
|
213
|
+
# @example
|
|
214
|
+
# validation_group :step1, [:email, :password]
|
|
215
|
+
# validation_group :step2, [:first_name, :last_name]
|
|
216
|
+
# validation_group :step3, [:address, :city, :zip_code]
|
|
217
|
+
#
|
|
218
|
+
# # Utilizzo:
|
|
219
|
+
# user.valid?(:step1) # Valida solo email e password
|
|
220
|
+
# user.errors_for_group(:step1)
|
|
221
|
+
#
|
|
222
|
+
def validation_group(group_name, fields)
|
|
223
|
+
raise ArgumentError, "Group name must be a symbol" unless group_name.is_a?(Symbol)
|
|
224
|
+
raise ArgumentError, "Fields must be an array" unless fields.is_a?(Array)
|
|
225
|
+
raise ArgumentError, "Group already defined: #{group_name}" if @groups.key?(group_name)
|
|
226
|
+
|
|
227
|
+
@groups[group_name] = {
|
|
228
|
+
name: group_name,
|
|
229
|
+
fields: fields
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Restituisce la configurazione completa
|
|
234
|
+
#
|
|
235
|
+
# @return [Hash] Configurazione con tutte le validazioni
|
|
236
|
+
def to_h
|
|
237
|
+
{
|
|
238
|
+
conditional_validations: @conditional_validations,
|
|
239
|
+
order_validations: @order_validations,
|
|
240
|
+
business_rules: @business_rules
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
module Validatable
|
|
5
|
+
# Validator per validazioni di ordine tra campi (cross-field)
|
|
6
|
+
#
|
|
7
|
+
# Verifica che un campo sia in una relazione d'ordine rispetto ad un altro campo.
|
|
8
|
+
# Supporta date/time (before/after) e numeri (lteq/gteq/lt/gt).
|
|
9
|
+
#
|
|
10
|
+
# Esempio:
|
|
11
|
+
# validates_with OrderValidator,
|
|
12
|
+
# attributes: [:starts_at],
|
|
13
|
+
# second_field: :ends_at,
|
|
14
|
+
# comparator: :before
|
|
15
|
+
#
|
|
16
|
+
class OrderValidator < ActiveModel::EachValidator
|
|
17
|
+
COMPARATORS = {
|
|
18
|
+
before: :<,
|
|
19
|
+
after: :>,
|
|
20
|
+
lteq: :<=,
|
|
21
|
+
gteq: :>=,
|
|
22
|
+
lt: :<,
|
|
23
|
+
gt: :>
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(options)
|
|
27
|
+
super
|
|
28
|
+
|
|
29
|
+
@second_field = options[:second_field]
|
|
30
|
+
@comparator = options[:comparator]
|
|
31
|
+
|
|
32
|
+
unless @second_field
|
|
33
|
+
raise ArgumentError, "OrderValidator requires :second_field option"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
unless COMPARATORS.key?(@comparator)
|
|
37
|
+
raise ArgumentError, "Invalid comparator: #{@comparator}. Valid: #{COMPARATORS.keys.join(', ')}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_each(record, attribute, value)
|
|
42
|
+
second_value = record.send(@second_field)
|
|
43
|
+
|
|
44
|
+
# Skip validation if either field is nil (use presence validation for that)
|
|
45
|
+
return if value.nil? || second_value.nil?
|
|
46
|
+
|
|
47
|
+
# Get the comparison operator
|
|
48
|
+
operator = COMPARATORS[@comparator]
|
|
49
|
+
|
|
50
|
+
# Perform comparison
|
|
51
|
+
unless value.send(operator, second_value)
|
|
52
|
+
record.errors.add(attribute, error_message(attribute, @comparator, @second_field))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def error_message(first_field, comparator, second_field)
|
|
59
|
+
# Messaggi user-friendly basati sul comparatore
|
|
60
|
+
case comparator
|
|
61
|
+
when :before
|
|
62
|
+
"must be before #{second_field.to_s.humanize.downcase}"
|
|
63
|
+
when :after
|
|
64
|
+
"must be after #{second_field.to_s.humanize.downcase}"
|
|
65
|
+
when :lteq
|
|
66
|
+
"must be less than or equal to #{second_field.to_s.humanize.downcase}"
|
|
67
|
+
when :gteq
|
|
68
|
+
"must be greater than or equal to #{second_field.to_s.humanize.downcase}"
|
|
69
|
+
when :lt
|
|
70
|
+
"must be less than #{second_field.to_s.humanize.downcase}"
|
|
71
|
+
when :gt
|
|
72
|
+
"must be greater than #{second_field.to_s.humanize.downcase}"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Validatable - Sistema di validazioni dichiarativo per modelli Rails
|
|
4
|
+
#
|
|
5
|
+
# Questo concern permette di definire validazioni in modo dichiarativo e leggibile,
|
|
6
|
+
# con supporto per validazioni condizionali, gruppi, cross-field e business rules.
|
|
7
|
+
#
|
|
8
|
+
# APPROCCIO OPT-IN: Le validazioni dichiarative non sono attive automaticamente.
|
|
9
|
+
# Devi chiamare esplicitamente `validatable do...end` nel tuo modello.
|
|
10
|
+
#
|
|
11
|
+
# Esempio di utilizzo:
|
|
12
|
+
# class Article < ApplicationRecord
|
|
13
|
+
# include BetterModel
|
|
14
|
+
#
|
|
15
|
+
# # Status (già esistente in Statusable)
|
|
16
|
+
# is :draft, -> { status == "draft" }
|
|
17
|
+
# is :published, -> { status == "published" }
|
|
18
|
+
#
|
|
19
|
+
# # Attiva validatable (opt-in)
|
|
20
|
+
# validatable do
|
|
21
|
+
# # Validazioni base
|
|
22
|
+
# validate :title, :content, presence: true
|
|
23
|
+
#
|
|
24
|
+
# # Validazioni condizionali
|
|
25
|
+
# validate_if :is_published? do
|
|
26
|
+
# validate :published_at, presence: true
|
|
27
|
+
# validate :author_id, presence: true
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # Cross-field validations
|
|
31
|
+
# validate_order :starts_at, :before, :ends_at
|
|
32
|
+
#
|
|
33
|
+
# # Business rules
|
|
34
|
+
# validate_business_rule :valid_category
|
|
35
|
+
#
|
|
36
|
+
# # Gruppi di validazioni
|
|
37
|
+
# validation_group :step1, [:email, :password]
|
|
38
|
+
# validation_group :step2, [:first_name, :last_name]
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# Utilizzo:
|
|
43
|
+
# article.valid? # Tutte le validazioni
|
|
44
|
+
# article.valid?(:step1) # Solo gruppo step1
|
|
45
|
+
#
|
|
46
|
+
module BetterModel
|
|
47
|
+
module Validatable
|
|
48
|
+
extend ActiveSupport::Concern
|
|
49
|
+
|
|
50
|
+
included do
|
|
51
|
+
# Validazione ActiveRecord
|
|
52
|
+
unless ancestors.include?(ActiveRecord::Base)
|
|
53
|
+
raise ArgumentError, "BetterModel::Validatable can only be included in ActiveRecord models"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Configurazione validatable (opt-in)
|
|
57
|
+
class_attribute :validatable_enabled, default: false
|
|
58
|
+
class_attribute :validatable_config, default: {}.freeze
|
|
59
|
+
class_attribute :validatable_groups, default: {}.freeze
|
|
60
|
+
class_attribute :_validatable_setup_done, default: false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class_methods do
|
|
64
|
+
# DSL per attivare e configurare validatable (OPT-IN)
|
|
65
|
+
#
|
|
66
|
+
# @example Attivazione base
|
|
67
|
+
# validatable do
|
|
68
|
+
# validate :title, presence: true
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @example Con validazioni condizionali
|
|
72
|
+
# validatable do
|
|
73
|
+
# validate_if :is_published? do
|
|
74
|
+
# validate :published_at, presence: true
|
|
75
|
+
# end
|
|
76
|
+
# end
|
|
77
|
+
#
|
|
78
|
+
def validatable(&block)
|
|
79
|
+
# Attiva validatable
|
|
80
|
+
self.validatable_enabled = true
|
|
81
|
+
|
|
82
|
+
# Configura se passato un blocco
|
|
83
|
+
if block_given?
|
|
84
|
+
configurator = Configurator.new(self)
|
|
85
|
+
configurator.instance_eval(&block)
|
|
86
|
+
self.validatable_config = configurator.to_h.freeze
|
|
87
|
+
self.validatable_groups = configurator.groups.freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Setup validators only once
|
|
91
|
+
return if self._validatable_setup_done
|
|
92
|
+
|
|
93
|
+
self._validatable_setup_done = true
|
|
94
|
+
|
|
95
|
+
# Apply validators from configuration
|
|
96
|
+
apply_validatable_config
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Verifica se validatable è attivo
|
|
100
|
+
#
|
|
101
|
+
# @return [Boolean]
|
|
102
|
+
def validatable_enabled?
|
|
103
|
+
validatable_enabled == true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Applica le configurazioni di validazione al modello
|
|
109
|
+
def apply_validatable_config
|
|
110
|
+
return unless validatable_config.present?
|
|
111
|
+
|
|
112
|
+
# Apply conditional validations
|
|
113
|
+
validatable_config[:conditional_validations]&.each do |conditional|
|
|
114
|
+
apply_conditional_validation(conditional)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Apply order validations
|
|
118
|
+
validatable_config[:order_validations]&.each do |order_val|
|
|
119
|
+
apply_order_validation(order_val)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Apply business rules
|
|
123
|
+
validatable_config[:business_rules]&.each do |rule|
|
|
124
|
+
apply_business_rule(rule)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Applica una validazione condizionale
|
|
129
|
+
def apply_conditional_validation(conditional)
|
|
130
|
+
condition = conditional[:condition]
|
|
131
|
+
negate = conditional[:negate]
|
|
132
|
+
validations = conditional[:validations]
|
|
133
|
+
|
|
134
|
+
# Create a custom validator for this conditional block
|
|
135
|
+
validate do
|
|
136
|
+
condition_met = if condition.is_a?(Symbol)
|
|
137
|
+
send(condition)
|
|
138
|
+
elsif condition.is_a?(Proc)
|
|
139
|
+
instance_exec(&condition)
|
|
140
|
+
else
|
|
141
|
+
raise ArgumentError, "Condition must be a Symbol or Proc"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
condition_met = !condition_met if negate
|
|
145
|
+
|
|
146
|
+
if condition_met
|
|
147
|
+
validations.each do |validation|
|
|
148
|
+
apply_validation_in_context(validation)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Applica una validazione order (cross-field)
|
|
155
|
+
def apply_order_validation(order_val)
|
|
156
|
+
validates_with BetterModel::Validatable::OrderValidator,
|
|
157
|
+
attributes: [ order_val[:first_field] ],
|
|
158
|
+
second_field: order_val[:second_field],
|
|
159
|
+
comparator: order_val[:comparator],
|
|
160
|
+
**order_val[:options]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Applica una business rule
|
|
164
|
+
def apply_business_rule(rule)
|
|
165
|
+
validates_with BetterModel::Validatable::BusinessRuleValidator,
|
|
166
|
+
rule_name: rule[:name],
|
|
167
|
+
**rule[:options]
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Metodi di istanza
|
|
172
|
+
|
|
173
|
+
# Apply a validation in the context of the current instance
|
|
174
|
+
def apply_validation_in_context(validation)
|
|
175
|
+
fields = validation[:fields]
|
|
176
|
+
options = validation[:options]
|
|
177
|
+
|
|
178
|
+
fields.each do |field|
|
|
179
|
+
options.each do |validator_type, validator_options|
|
|
180
|
+
# Prepare validator options
|
|
181
|
+
# If validator_options is true, convert to empty hash
|
|
182
|
+
# If it's a hash, use as-is
|
|
183
|
+
opts = validator_options.is_a?(Hash) ? validator_options : {}
|
|
184
|
+
|
|
185
|
+
validator = ActiveModel::Validations.const_get("#{validator_type.to_s.camelize}Validator").new(
|
|
186
|
+
attributes: [ field ],
|
|
187
|
+
**opts
|
|
188
|
+
)
|
|
189
|
+
validator.validate(self)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Override valid? per supportare validation groups
|
|
195
|
+
#
|
|
196
|
+
# @param context [Symbol, nil] Context o gruppo di validazione
|
|
197
|
+
# @return [Boolean]
|
|
198
|
+
def valid?(context = nil)
|
|
199
|
+
if context && self.class.validatable_groups.key?(context)
|
|
200
|
+
# Valida solo il gruppo specificato
|
|
201
|
+
validate_group(context)
|
|
202
|
+
else
|
|
203
|
+
# Validazione standard Rails
|
|
204
|
+
super(context)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Valida solo un gruppo specifico
|
|
209
|
+
#
|
|
210
|
+
# @param group_name [Symbol] Nome del gruppo
|
|
211
|
+
# @return [Boolean]
|
|
212
|
+
def validate_group(group_name)
|
|
213
|
+
raise ValidatableNotEnabledError unless self.class.validatable_enabled?
|
|
214
|
+
|
|
215
|
+
group = self.class.validatable_groups[group_name]
|
|
216
|
+
return false unless group
|
|
217
|
+
|
|
218
|
+
# Clear existing errors
|
|
219
|
+
errors.clear
|
|
220
|
+
|
|
221
|
+
# Run validations only for fields in this group
|
|
222
|
+
group[:fields].each do |field|
|
|
223
|
+
run_validations_for_field(field)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
errors.empty?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Ottieni gli errori per un gruppo specifico
|
|
230
|
+
#
|
|
231
|
+
# @param group_name [Symbol] Nome del gruppo
|
|
232
|
+
# @return [ActiveModel::Errors]
|
|
233
|
+
def errors_for_group(group_name)
|
|
234
|
+
raise ValidatableNotEnabledError unless self.class.validatable_enabled?
|
|
235
|
+
|
|
236
|
+
group = self.class.validatable_groups[group_name]
|
|
237
|
+
return errors unless group
|
|
238
|
+
|
|
239
|
+
# Filter errors to only include fields in this group
|
|
240
|
+
filtered_errors = ActiveModel::Errors.new(self)
|
|
241
|
+
group[:fields].each do |field|
|
|
242
|
+
errors[field].each do |error|
|
|
243
|
+
filtered_errors.add(field, error)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
filtered_errors
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
private
|
|
251
|
+
|
|
252
|
+
# Run validations for a specific field
|
|
253
|
+
def run_validations_for_field(field)
|
|
254
|
+
# This is a simplified version - Rails validations are complex
|
|
255
|
+
# We'll leverage Rails' built-in validation framework
|
|
256
|
+
self.class.validators_on(field).each do |validator|
|
|
257
|
+
validator.validate(self)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Errori custom
|
|
263
|
+
class ValidatableError < StandardError; end
|
|
264
|
+
|
|
265
|
+
class ValidatableNotEnabledError < ValidatableError
|
|
266
|
+
def initialize(msg = nil)
|
|
267
|
+
super(msg || "Validatable is not enabled. Add 'validatable do...end' to your model.")
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
data/lib/better_model/version.rb
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterModel
|
|
4
|
+
# Version model for tracking changes
|
|
5
|
+
# This is the base AR model for version history
|
|
6
|
+
# Actual table_name is set dynamically in subclasses
|
|
7
|
+
class Version < ActiveRecord::Base
|
|
8
|
+
self.abstract_class = true
|
|
9
|
+
|
|
10
|
+
# Polymorphic association to the tracked model
|
|
11
|
+
belongs_to :item, polymorphic: true, optional: true
|
|
12
|
+
|
|
13
|
+
# Optional: belongs_to user who made the change
|
|
14
|
+
# belongs_to :updated_by, class_name: "User", optional: true
|
|
15
|
+
|
|
16
|
+
# Serialize object_changes as JSON
|
|
17
|
+
# Rails handles this automatically for json/jsonb columns
|
|
18
|
+
|
|
19
|
+
# Validations
|
|
20
|
+
validates :item_type, :event, presence: true
|
|
21
|
+
validates :event, inclusion: { in: %w[created updated destroyed] }
|
|
22
|
+
|
|
23
|
+
# Scopes
|
|
24
|
+
scope :for_item, ->(item) { where(item_type: item.class.name, item_id: item.id) }
|
|
25
|
+
scope :created_events, -> { where(event: "created") }
|
|
26
|
+
scope :updated_events, -> { where(event: "updated") }
|
|
27
|
+
scope :destroyed_events, -> { where(event: "destroyed") }
|
|
28
|
+
scope :by_user, ->(user_id) { where(updated_by_id: user_id) }
|
|
29
|
+
scope :between, ->(start_time, end_time) { where(created_at: start_time..end_time) }
|
|
30
|
+
scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
|
|
31
|
+
|
|
32
|
+
# Get the change for a specific field
|
|
33
|
+
#
|
|
34
|
+
# @param field_name [Symbol, String] Field name
|
|
35
|
+
# @return [Hash, nil] Hash with :before and :after keys
|
|
36
|
+
def change_for(field_name)
|
|
37
|
+
return nil unless object_changes
|
|
38
|
+
|
|
39
|
+
field = field_name.to_s
|
|
40
|
+
return nil unless object_changes.key?(field)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
before: object_changes[field][0],
|
|
44
|
+
after: object_changes[field][1]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if a specific field changed
|
|
49
|
+
#
|
|
50
|
+
# @param field_name [Symbol, String] Field name
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def changed?(field_name)
|
|
53
|
+
object_changes&.key?(field_name.to_s) || false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get list of changed fields
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<String>]
|
|
59
|
+
def changed_fields
|
|
60
|
+
object_changes&.keys || []
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/better_model.rb
CHANGED
|
@@ -6,6 +6,18 @@ require "better_model/sortable"
|
|
|
6
6
|
require "better_model/predicable"
|
|
7
7
|
require "better_model/searchable"
|
|
8
8
|
require "better_model/archivable"
|
|
9
|
+
require "better_model/version_record"
|
|
10
|
+
require "better_model/traceable"
|
|
11
|
+
require "better_model/validatable"
|
|
12
|
+
require "better_model/validatable/configurator"
|
|
13
|
+
require "better_model/validatable/order_validator"
|
|
14
|
+
require "better_model/validatable/business_rule_validator"
|
|
15
|
+
require "better_model/state_transition"
|
|
16
|
+
require "better_model/stateable"
|
|
17
|
+
require "better_model/stateable/configurator"
|
|
18
|
+
require "better_model/stateable/errors"
|
|
19
|
+
require "better_model/stateable/guard"
|
|
20
|
+
require "better_model/stateable/transition"
|
|
9
21
|
|
|
10
22
|
module BetterModel
|
|
11
23
|
extend ActiveSupport::Concern
|
|
@@ -18,7 +30,8 @@ module BetterModel
|
|
|
18
30
|
include BetterModel::Predicable
|
|
19
31
|
include BetterModel::Searchable
|
|
20
32
|
include BetterModel::Archivable
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
include BetterModel::Traceable
|
|
34
|
+
include BetterModel::Validatable
|
|
35
|
+
include BetterModel::Stateable
|
|
23
36
|
end
|
|
24
37
|
end
|