better_model 2.0.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 +274 -208
- data/lib/better_model/archivable.rb +203 -92
- 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 +142 -131
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +123 -96
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +103 -85
- data/lib/better_model/stateable/guard.rb +41 -21
- data/lib/better_model/stateable/transition.rb +64 -35
- data/lib/better_model/stateable.rb +43 -25
- 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 +54 -177
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -9
- 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 +45 -14
- data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
- data/lib/better_model/schedulable/schedule_builder.rb +0 -269
- data/lib/better_model/schedulable.rb +0 -356
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -45
- 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
- data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
- data/lib/generators/better_model/taggable/templates/README.tt +0 -62
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +0 -21
|
@@ -1,53 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/traceable/traceable_error"
|
|
4
|
+
require_relative "errors/traceable/not_enabled_error"
|
|
5
|
+
require_relative "errors/traceable/configuration_error"
|
|
6
|
+
|
|
7
|
+
# Traceable - Change tracking with audit trail for Rails models.
|
|
4
8
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
9
|
+
# This concern enables automatic tracking of record changes,
|
|
10
|
+
# maintaining a complete history with timestamps, author, and reasoning.
|
|
7
11
|
#
|
|
8
|
-
#
|
|
9
|
-
# # Opzione 1: Generator automatico (raccomandato)
|
|
12
|
+
# @example Quick Setup - Option 1: Automatic Generator (Recommended)
|
|
10
13
|
# rails g better_model:traceable Article --with-reason
|
|
11
14
|
# rails db:migrate
|
|
12
15
|
#
|
|
13
|
-
#
|
|
16
|
+
# @example Quick Setup - Option 2: Using Included Migration
|
|
17
|
+
# # The better_model_versions migration is already in the gem
|
|
14
18
|
# rails db:migrate
|
|
15
19
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
20
|
+
# @note OPT-IN APPROACH
|
|
21
|
+
# Tracking is not enabled automatically. You must explicitly call
|
|
22
|
+
# `traceable do...end` in your model to activate it.
|
|
18
23
|
#
|
|
19
|
-
#
|
|
20
|
-
# - better_model_versions table (
|
|
24
|
+
# @note DATABASE REQUIREMENTS
|
|
25
|
+
# - better_model_versions table (included in gem)
|
|
21
26
|
#
|
|
22
|
-
#
|
|
27
|
+
# @example Basic Model Setup
|
|
23
28
|
# class Article < ApplicationRecord
|
|
24
29
|
# include BetterModel
|
|
25
30
|
#
|
|
26
|
-
# #
|
|
31
|
+
# # Enable traceable (opt-in)
|
|
27
32
|
# traceable do
|
|
28
|
-
# track :status, :title, :published_at #
|
|
33
|
+
# track :status, :title, :published_at # Fields to track
|
|
29
34
|
# end
|
|
30
35
|
# end
|
|
31
36
|
#
|
|
32
|
-
#
|
|
33
|
-
# # Tracking automatico
|
|
37
|
+
# @example Automatic Tracking
|
|
34
38
|
# article.update!(status: "published", updated_by_id: user.id, updated_reason: "Approved")
|
|
35
39
|
#
|
|
36
|
-
#
|
|
37
|
-
# article.versions #
|
|
38
|
-
# article.changes_for(:status) #
|
|
39
|
-
# article.audit_trail #
|
|
40
|
+
# @example Querying Versions
|
|
41
|
+
# article.versions # All versions
|
|
42
|
+
# article.changes_for(:status) # Changes for a field
|
|
43
|
+
# article.audit_trail # Formatted history
|
|
40
44
|
#
|
|
41
|
-
#
|
|
42
|
-
# article.as_of(3.days.ago) #
|
|
45
|
+
# @example Time-Travel
|
|
46
|
+
# article.as_of(3.days.ago) # State at specific date
|
|
43
47
|
#
|
|
44
|
-
#
|
|
45
|
-
# article.rollback_to(version) #
|
|
48
|
+
# @example Rollback
|
|
49
|
+
# article.rollback_to(version) # Restore to previous version
|
|
46
50
|
#
|
|
47
|
-
#
|
|
48
|
-
# Article.changed_by(user.id) #
|
|
49
|
-
# Article.changed_between(start, end) #
|
|
50
|
-
# Article.status_changed_from("draft").to("published") #
|
|
51
|
+
# @example Query Scopes for Changes
|
|
52
|
+
# Article.changed_by(user.id) # Changes by user
|
|
53
|
+
# Article.changed_between(start, end) # Changes in period
|
|
54
|
+
# Article.status_changed_from("draft").to("published") # Specific transitions
|
|
51
55
|
#
|
|
52
56
|
module BetterModel
|
|
53
57
|
module Traceable
|
|
@@ -59,7 +63,7 @@ module BetterModel
|
|
|
59
63
|
included do
|
|
60
64
|
# Validazione ActiveRecord
|
|
61
65
|
unless ancestors.include?(ActiveRecord::Base)
|
|
62
|
-
raise
|
|
66
|
+
raise BetterModel::Errors::Traceable::ConfigurationError, "Invalid configuration"
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
# Configurazione traceable (opt-in)
|
|
@@ -122,16 +126,16 @@ module BetterModel
|
|
|
122
126
|
# Verifica se traceable è attivo
|
|
123
127
|
#
|
|
124
128
|
# @return [Boolean]
|
|
125
|
-
def traceable_enabled?
|
|
126
|
-
traceable_enabled == true
|
|
127
|
-
end
|
|
129
|
+
def traceable_enabled? = traceable_enabled == true
|
|
128
130
|
|
|
129
131
|
# Find records changed by a specific user
|
|
130
132
|
#
|
|
131
133
|
# @param user_id [Integer] User ID
|
|
132
134
|
# @return [ActiveRecord::Relation]
|
|
133
135
|
def changed_by(user_id)
|
|
134
|
-
|
|
136
|
+
unless traceable_enabled?
|
|
137
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
138
|
+
end
|
|
135
139
|
|
|
136
140
|
joins(:versions).where(traceable_table_name => { updated_by_id: user_id }).distinct
|
|
137
141
|
end
|
|
@@ -142,7 +146,9 @@ module BetterModel
|
|
|
142
146
|
# @param end_time [Time, Date] End time
|
|
143
147
|
# @return [ActiveRecord::Relation]
|
|
144
148
|
def changed_between(start_time, end_time)
|
|
145
|
-
|
|
149
|
+
unless traceable_enabled?
|
|
150
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
151
|
+
end
|
|
146
152
|
|
|
147
153
|
joins(:versions).where(traceable_table_name => { created_at: start_time..end_time }).distinct
|
|
148
154
|
end
|
|
@@ -152,7 +158,9 @@ module BetterModel
|
|
|
152
158
|
# @param field [Symbol] Field name
|
|
153
159
|
# @return [ChangeQuery]
|
|
154
160
|
def field_changed(field)
|
|
155
|
-
|
|
161
|
+
unless traceable_enabled?
|
|
162
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
163
|
+
end
|
|
156
164
|
|
|
157
165
|
ChangeQuery.new(self, field)
|
|
158
166
|
end
|
|
@@ -202,7 +210,7 @@ module BetterModel
|
|
|
202
210
|
end
|
|
203
211
|
|
|
204
212
|
# Create new Version class dynamically
|
|
205
|
-
version_class = Class.new(BetterModel::Version) do
|
|
213
|
+
version_class = Class.new(BetterModel::Models::Version) do
|
|
206
214
|
self.table_name = table_name
|
|
207
215
|
end
|
|
208
216
|
|
|
@@ -220,7 +228,9 @@ module BetterModel
|
|
|
220
228
|
# @param field [Symbol] Field name
|
|
221
229
|
# @return [Array<Hash>] Array of changes with :before, :after, :at, :by
|
|
222
230
|
def changes_for(field)
|
|
223
|
-
|
|
231
|
+
unless self.class.traceable_enabled?
|
|
232
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
233
|
+
end
|
|
224
234
|
|
|
225
235
|
versions.select { |v| v.changed?(field) }.map do |version|
|
|
226
236
|
change = version.change_for(field)
|
|
@@ -238,7 +248,9 @@ module BetterModel
|
|
|
238
248
|
#
|
|
239
249
|
# @return [Array<Hash>] Full audit trail
|
|
240
250
|
def audit_trail
|
|
241
|
-
|
|
251
|
+
unless self.class.traceable_enabled?
|
|
252
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
253
|
+
end
|
|
242
254
|
|
|
243
255
|
versions.map do |version|
|
|
244
256
|
{
|
|
@@ -256,7 +268,9 @@ module BetterModel
|
|
|
256
268
|
# @param timestamp [Time, Date] Point in time
|
|
257
269
|
# @return [self] Reconstructed object (not saved)
|
|
258
270
|
def as_of(timestamp)
|
|
259
|
-
|
|
271
|
+
unless self.class.traceable_enabled?
|
|
272
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
273
|
+
end
|
|
260
274
|
|
|
261
275
|
# Get all versions up to timestamp, ordered from oldest to newest
|
|
262
276
|
relevant_versions = versions.where("created_at <= ?", timestamp).order(created_at: :asc)
|
|
@@ -280,12 +294,14 @@ module BetterModel
|
|
|
280
294
|
|
|
281
295
|
# Rollback to a specific version
|
|
282
296
|
#
|
|
283
|
-
# @param version [BetterModel::Version, Integer] Version or version ID
|
|
297
|
+
# @param version [BetterModel::Models::Version, Integer] Version or version ID
|
|
284
298
|
# @param updated_by_id [Integer] User ID performing rollback
|
|
285
299
|
# @param updated_reason [String] Reason for rollback
|
|
286
300
|
# @return [self]
|
|
287
301
|
def rollback_to(version, updated_by_id: nil, updated_reason: nil, allow_sensitive: false)
|
|
288
|
-
|
|
302
|
+
unless self.class.traceable_enabled?
|
|
303
|
+
raise BetterModel::Errors::Traceable::NotEnabledError, "Module is not enabled"
|
|
304
|
+
end
|
|
289
305
|
|
|
290
306
|
version = versions.find(version) if version.is_a?(Integer)
|
|
291
307
|
|
|
@@ -478,14 +494,6 @@ module BetterModel
|
|
|
478
494
|
end
|
|
479
495
|
end
|
|
480
496
|
|
|
481
|
-
# Errori custom
|
|
482
|
-
class TraceableError < StandardError; end
|
|
483
|
-
|
|
484
|
-
class NotEnabledError < TraceableError
|
|
485
|
-
def initialize(msg = nil)
|
|
486
|
-
super(msg || "Traceable is not enabled. Add 'traceable do...end' to your model.")
|
|
487
|
-
end
|
|
488
|
-
end
|
|
489
497
|
|
|
490
498
|
# Configurator per traceable DSL
|
|
491
499
|
class TraceableConfigurator
|
|
@@ -2,221 +2,100 @@
|
|
|
2
2
|
|
|
3
3
|
module BetterModel
|
|
4
4
|
module Validatable
|
|
5
|
-
# Configurator
|
|
5
|
+
# Configurator for Validatable DSL.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# This configurator enables defining validations declaratively
|
|
8
|
+
# within the `validatable do...end` block.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# @example
|
|
11
11
|
# validatable do
|
|
12
|
-
# #
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# # Basic validations
|
|
13
|
+
# check :title, :content, presence: true
|
|
14
|
+
# check :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
15
15
|
#
|
|
16
|
-
# #
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# validate :author_id, presence: true
|
|
20
|
-
# end
|
|
16
|
+
# # Complex validations
|
|
17
|
+
# check_complex :valid_pricing
|
|
18
|
+
# check_complex :stock_check
|
|
21
19
|
#
|
|
22
|
-
# #
|
|
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
|
|
20
|
+
# # Validation groups
|
|
36
21
|
# validation_group :step1, [:email, :password]
|
|
37
22
|
# validation_group :step2, [:first_name, :last_name]
|
|
38
23
|
# end
|
|
39
24
|
#
|
|
25
|
+
# @api private
|
|
40
26
|
class Configurator
|
|
41
27
|
attr_reader :groups
|
|
42
28
|
|
|
29
|
+
# Initialize a new Configurator.
|
|
30
|
+
#
|
|
31
|
+
# @param model_class [Class] Model class being configured
|
|
43
32
|
def initialize(model_class)
|
|
44
33
|
@model_class = model_class
|
|
45
|
-
@
|
|
46
|
-
@order_validations = []
|
|
47
|
-
@business_rules = []
|
|
34
|
+
@complex_validations = []
|
|
48
35
|
@groups = {}
|
|
49
36
|
end
|
|
50
37
|
|
|
51
|
-
#
|
|
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)
|
|
38
|
+
# Define standard validations on fields.
|
|
76
39
|
#
|
|
77
|
-
#
|
|
78
|
-
# @yield Blocco con validazioni da applicare se condizione è vera
|
|
40
|
+
# Delegates to ActiveRecord's validates method.
|
|
79
41
|
#
|
|
80
|
-
# @
|
|
81
|
-
#
|
|
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
|
|
42
|
+
# @param fields [Array<Symbol>] Field names
|
|
43
|
+
# @param options [Hash] Validation options (presence, format, etc.)
|
|
111
44
|
#
|
|
112
45
|
# @example
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
46
|
+
# check :title, :content, presence: true
|
|
47
|
+
# check :email, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
48
|
+
# check :age, numericality: { greater_than: 0 }
|
|
116
49
|
#
|
|
117
|
-
def
|
|
118
|
-
|
|
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
|
|
50
|
+
def check(*fields, **options)
|
|
51
|
+
@model_class.validates(*fields, **options)
|
|
131
52
|
end
|
|
132
53
|
|
|
133
|
-
#
|
|
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
|
|
54
|
+
# Use a registered complex validation.
|
|
173
55
|
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
56
|
+
# Complex validations must be registered first using
|
|
57
|
+
# register_complex_validation in the model.
|
|
176
58
|
#
|
|
177
|
-
# @param
|
|
178
|
-
# @
|
|
59
|
+
# @param name [Symbol] Name of registered complex validation
|
|
60
|
+
# @raise [ArgumentError] If validation is not registered
|
|
179
61
|
#
|
|
180
62
|
# @example
|
|
181
|
-
# #
|
|
182
|
-
#
|
|
183
|
-
#
|
|
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")
|
|
63
|
+
# # In model (registration):
|
|
64
|
+
# register_complex_validation :valid_pricing do
|
|
65
|
+
# if sale_price.present? && sale_price >= price
|
|
66
|
+
# errors.add(:sale_price, "must be less than regular price")
|
|
189
67
|
# end
|
|
190
68
|
# end
|
|
191
69
|
#
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
# end
|
|
70
|
+
# # In configurator (usage):
|
|
71
|
+
# validatable do
|
|
72
|
+
# check_complex :valid_pricing
|
|
196
73
|
# end
|
|
197
74
|
#
|
|
198
|
-
def
|
|
199
|
-
@
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
75
|
+
def check_complex(name)
|
|
76
|
+
unless @model_class.complex_validation?(name)
|
|
77
|
+
raise ArgumentError, "Unknown complex validation: #{name}. Use register_complex_validation to define it first."
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
@complex_validations << name.to_sym
|
|
203
81
|
end
|
|
204
82
|
|
|
205
|
-
#
|
|
83
|
+
# Define a validation group.
|
|
206
84
|
#
|
|
207
|
-
#
|
|
208
|
-
#
|
|
85
|
+
# Groups allow validating only a subset of fields,
|
|
86
|
+
# useful for multi-step forms or partial validations.
|
|
209
87
|
#
|
|
210
|
-
# @param group_name [Symbol]
|
|
211
|
-
# @param fields [Array<Symbol>]
|
|
88
|
+
# @param group_name [Symbol] Group name
|
|
89
|
+
# @param fields [Array<Symbol>] Fields included in group
|
|
90
|
+
# @raise [ArgumentError] If group name is invalid, fields not array, or group already defined
|
|
212
91
|
#
|
|
213
92
|
# @example
|
|
214
93
|
# validation_group :step1, [:email, :password]
|
|
215
94
|
# validation_group :step2, [:first_name, :last_name]
|
|
216
95
|
# validation_group :step3, [:address, :city, :zip_code]
|
|
217
96
|
#
|
|
218
|
-
#
|
|
219
|
-
# user.valid?(:step1) #
|
|
97
|
+
# @example Usage
|
|
98
|
+
# user.valid?(:step1) # Validate only email and password
|
|
220
99
|
# user.errors_for_group(:step1)
|
|
221
100
|
#
|
|
222
101
|
def validation_group(group_name, fields)
|
|
@@ -230,14 +109,12 @@ module BetterModel
|
|
|
230
109
|
}
|
|
231
110
|
end
|
|
232
111
|
|
|
233
|
-
#
|
|
112
|
+
# Return complete configuration.
|
|
234
113
|
#
|
|
235
|
-
# @return [Hash]
|
|
114
|
+
# @return [Hash] Configuration with all validations
|
|
236
115
|
def to_h
|
|
237
116
|
{
|
|
238
|
-
|
|
239
|
-
order_validations: @order_validations,
|
|
240
|
-
business_rules: @business_rules
|
|
117
|
+
complex_validations: @complex_validations
|
|
241
118
|
}
|
|
242
119
|
end
|
|
243
120
|
end
|