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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +96 -13
  3. data/lib/better_model/archivable.rb +203 -91
  4. data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
  5. data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
  6. data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
  7. data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
  8. data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
  9. data/lib/better_model/errors/better_model_error.rb +9 -0
  10. data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
  11. data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
  12. data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
  13. data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
  14. data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
  15. data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
  16. data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
  17. data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
  18. data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
  19. data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
  20. data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
  21. data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
  22. data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
  23. data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
  24. data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
  25. data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
  26. data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
  27. data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
  28. data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
  29. data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
  30. data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
  31. data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
  32. data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
  33. data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
  34. data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
  35. data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
  36. data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
  37. data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
  38. data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
  39. data/lib/better_model/models/state_transition.rb +122 -0
  40. data/lib/better_model/models/version.rb +68 -0
  41. data/lib/better_model/permissible.rb +103 -52
  42. data/lib/better_model/predicable.rb +114 -63
  43. data/lib/better_model/repositable/base_repository.rb +232 -0
  44. data/lib/better_model/repositable.rb +32 -0
  45. data/lib/better_model/searchable.rb +92 -92
  46. data/lib/better_model/sortable.rb +137 -41
  47. data/lib/better_model/stateable/configurator.rb +71 -53
  48. data/lib/better_model/stateable/guard.rb +35 -15
  49. data/lib/better_model/stateable/transition.rb +59 -30
  50. data/lib/better_model/stateable.rb +33 -15
  51. data/lib/better_model/statusable.rb +84 -52
  52. data/lib/better_model/taggable.rb +120 -75
  53. data/lib/better_model/traceable.rb +56 -48
  54. data/lib/better_model/validatable/configurator.rb +49 -172
  55. data/lib/better_model/validatable.rb +88 -113
  56. data/lib/better_model/version.rb +1 -1
  57. data/lib/better_model.rb +42 -5
  58. data/lib/generators/better_model/repository/repository_generator.rb +141 -0
  59. data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
  60. data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
  61. data/lib/generators/better_model/stateable/templates/README +1 -1
  62. metadata +44 -7
  63. data/lib/better_model/state_transition.rb +0 -106
  64. data/lib/better_model/stateable/errors.rb +0 -48
  65. data/lib/better_model/validatable/business_rule_validator.rb +0 -47
  66. data/lib/better_model/validatable/order_validator.rb +0 -77
  67. data/lib/better_model/version_record.rb +0 -66
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Taggable - Sistema di gestione tag dichiarativo per modelli Rails
3
+ require_relative "errors/taggable/taggable_error"
4
+ require_relative "errors/taggable/configuration_error"
5
+
6
+ # Taggable - Declarative tag management system for Rails models.
4
7
  #
5
- # Questo concern permette di gestire tag multipli sui modelli utilizzando array PostgreSQL
6
- # con normalizzazione, validazione e statistiche. La ricerca è delegata a Predicable.
8
+ # This concern enables managing multiple tags on models using PostgreSQL arrays
9
+ # with normalization, validation, and statistics. Search is delegated to Predicable.
7
10
  #
8
- # Esempio di utilizzo:
11
+ # @example Basic Usage
9
12
  # class Article < ApplicationRecord
10
13
  # include BetterModel
11
14
  #
@@ -16,18 +19,18 @@
16
19
  # end
17
20
  # end
18
21
  #
19
- # Utilizzo:
20
- # article.tag_with("ruby", "rails") # Aggiungi tag
21
- # article.untag("rails") # Rimuovi tag
22
- # article.tag_list = "ruby, rails, tutorial" # Da stringa CSV
22
+ # @example Managing Tags
23
+ # article.tag_with("ruby", "rails") # Add tags
24
+ # article.untag("rails") # Remove tags
25
+ # article.tag_list = "ruby, rails, tutorial" # From CSV string
23
26
  # article.tagged_with?("ruby") # => true
24
27
  #
25
- # Ricerca (delegata a Predicable):
28
+ # @example Searching (Delegated to Predicable)
26
29
  # Article.tags_contains("ruby") # Predicable
27
30
  # Article.tags_overlaps(["ruby", "python"]) # Predicable
28
31
  # Article.search(tags_contains: "ruby") # Searchable + Predicable
29
32
  #
30
- # Statistiche:
33
+ # @example Statistics
31
34
  # Article.tag_counts # => {"ruby" => 45, "rails" => 38}
32
35
  # Article.popular_tags(limit: 10) # => [["ruby", 45], ["rails", 38], ...]
33
36
  #
@@ -35,7 +38,11 @@ module BetterModel
35
38
  module Taggable
36
39
  extend ActiveSupport::Concern
37
40
 
38
- # Configurazione Taggable
41
+ # Taggable Configuration.
42
+ #
43
+ # Internal configuration class for the Taggable DSL.
44
+ #
45
+ # @api private
39
46
  class Configuration
40
47
  attr_reader :validates_minimum, :validates_maximum, :allowed_tags, :forbidden_tags
41
48
 
@@ -91,19 +98,22 @@ module BetterModel
91
98
  end
92
99
 
93
100
  included do
94
- # Valida che sia incluso solo in modelli ActiveRecord
101
+ # Validate ActiveRecord inheritance
95
102
  unless ancestors.include?(ActiveRecord::Base)
96
- raise ArgumentError, "BetterModel::Taggable can only be included in ActiveRecord models"
103
+ raise BetterModel::Errors::Taggable::ConfigurationError, "Invalid configuration"
97
104
  end
98
105
 
99
- # Configurazione Taggable per questa classe
106
+ # Taggable configuration for this class
100
107
  class_attribute :taggable_config, default: nil
101
108
  end
102
109
 
103
110
  class_methods do
104
- # DSL per configurare Taggable
111
+ # DSL to configure Taggable.
105
112
  #
106
- # Esempio:
113
+ # @yield [config] Configuration block
114
+ # @raise [BetterModel::Errors::Taggable::ConfigurationError] If already configured or field doesn't exist
115
+ #
116
+ # @example
107
117
  # taggable do
108
118
  # tag_field :tags
109
119
  # normalize true
@@ -114,39 +124,41 @@ module BetterModel
114
124
  # validates_tags minimum: 1, maximum: 10, allowed_tags: ["ruby", "rails"]
115
125
  # end
116
126
  def taggable(&block)
117
- # Previeni configurazione multipla
127
+ # Prevent multiple configuration
118
128
  if taggable_config.present?
119
- raise ArgumentError, "Taggable already configured for #{name}"
129
+ raise BetterModel::Errors::Taggable::ConfigurationError, "Invalid configuration"
120
130
  end
121
131
 
122
- # Crea configurazione
132
+ # Create configuration
123
133
  config = Configuration.new
124
134
  config.instance_eval(&block) if block_given?
125
135
 
126
- # Valida che il campo esista
136
+ # Validate that field exists
127
137
  tag_field_name = config.tag_field.to_s
128
138
  unless column_names.include?(tag_field_name)
129
- raise ArgumentError, "Tag field #{config.tag_field} does not exist in #{table_name}"
139
+ raise BetterModel::Errors::Taggable::ConfigurationError, "Invalid configuration"
130
140
  end
131
141
 
132
- # Salva configurazione (frozen per thread-safety)
142
+ # Save configuration (frozen for thread-safety)
133
143
  self.taggable_config = config.freeze
134
144
 
135
- # Auto-registra predicates per ricerca (delegato a Predicable)
145
+ # Auto-register predicates for search (delegated to Predicable)
136
146
  predicates config.tag_field if respond_to?(:predicates)
137
147
 
138
- # Registra validazioni se configurate
148
+ # Register validations if configured
139
149
  setup_validations(config) if config.validates_minimum || config.validates_maximum ||
140
150
  config.allowed_tags || config.forbidden_tags
141
151
  end
142
152
 
143
153
  # ============================================================================
144
- # CLASS METHODS - Statistiche
154
+ # CLASS METHODS - Statistics
145
155
  # ============================================================================
146
156
 
147
- # Restituisce un hash con il conteggio di ciascun tag
157
+ # Returns a hash with the count of each tag.
158
+ #
159
+ # @return [Hash{String => Integer}] Tag counts
148
160
  #
149
- # Esempio:
161
+ # @example
150
162
  # Article.tag_counts # => {"ruby" => 45, "rails" => 38, "tutorial" => 12}
151
163
  def tag_counts
152
164
  return {} unless taggable_config
@@ -163,9 +175,12 @@ module BetterModel
163
175
  counts
164
176
  end
165
177
 
166
- # Restituisce i tag più popolari con il loro conteggio
178
+ # Returns the most popular tags with their counts.
167
179
  #
168
- # Esempio:
180
+ # @param limit [Integer] Maximum number of tags to return
181
+ # @return [Array<Array(String, Integer)>] Tag-count pairs sorted by count
182
+ #
183
+ # @example
169
184
  # Article.popular_tags(limit: 10)
170
185
  # # => [["ruby", 45], ["rails", 38], ["tutorial", 12]]
171
186
  def popular_tags(limit: 10)
@@ -176,9 +191,13 @@ module BetterModel
176
191
  .first(limit)
177
192
  end
178
193
 
179
- # Restituisce i tag che appaiono insieme al tag specificato
194
+ # Returns tags that appear together with the specified tag.
195
+ #
196
+ # @param tag [String] Tag to find related tags for
197
+ # @param limit [Integer] Maximum number of related tags to return
198
+ # @return [Array<String>] Related tags sorted by frequency
180
199
  #
181
- # Esempio:
200
+ # @example
182
201
  # Article.related_tags("ruby", limit: 10)
183
202
  # # => ["rails", "gem", "activerecord"]
184
203
  def related_tags(tag, limit: 10)
@@ -187,25 +206,25 @@ module BetterModel
187
206
  field = taggable_config.tag_field
188
207
  related_counts = Hash.new(0)
189
208
 
190
- # Normalizza il tag query
209
+ # Normalize query tag
191
210
  config = taggable_config
192
211
  normalized_tag = tag.to_s
193
212
  normalized_tag = normalized_tag.strip if config.strip
194
213
  normalized_tag = normalized_tag.downcase if config.normalize
195
214
 
196
- # Trova record che contengono il tag
215
+ # Find records containing the tag
197
216
  find_each do |record|
198
217
  tags = record.public_send(field) || []
199
218
  next unless tags.include?(normalized_tag)
200
219
 
201
- # Conta gli altri tag che appaiono insieme
220
+ # Count other tags that appear together
202
221
  tags.each do |other_tag|
203
222
  next if other_tag == normalized_tag
204
223
  related_counts[other_tag] += 1
205
224
  end
206
225
  end
207
226
 
208
- # Restituisci ordinati per frequenza
227
+ # Return sorted by frequency
209
228
  related_counts
210
229
  .sort_by { |_tag, count| -count }
211
230
  .first(limit)
@@ -214,11 +233,14 @@ module BetterModel
214
233
 
215
234
  private
216
235
 
217
- # Setup delle validazioni ActiveRecord
236
+ # Setup ActiveRecord validations.
237
+ #
238
+ # @param config [Configuration] Taggable configuration
239
+ # @api private
218
240
  def setup_validations(config)
219
241
  field = config.tag_field
220
242
 
221
- # Validazione minimum
243
+ # Minimum validation
222
244
  if config.validates_minimum
223
245
  min = config.validates_minimum
224
246
  validate do
@@ -229,7 +251,7 @@ module BetterModel
229
251
  end
230
252
  end
231
253
 
232
- # Validazione maximum
254
+ # Maximum validation
233
255
  if config.validates_maximum
234
256
  max = config.validates_maximum
235
257
  validate do
@@ -240,7 +262,7 @@ module BetterModel
240
262
  end
241
263
  end
242
264
 
243
- # Validazione whitelist
265
+ # Whitelist validation
244
266
  if config.allowed_tags
245
267
  allowed = config.allowed_tags
246
268
  validate do
@@ -252,7 +274,7 @@ module BetterModel
252
274
  end
253
275
  end
254
276
 
255
- # Validazione blacklist
277
+ # Blacklist validation
256
278
  if config.forbidden_tags
257
279
  forbidden = config.forbidden_tags
258
280
  validate do
@@ -267,12 +289,15 @@ module BetterModel
267
289
  end
268
290
 
269
291
  # ============================================================================
270
- # INSTANCE METHODS - Gestione Tag
292
+ # INSTANCE METHODS - Tag Management
271
293
  # ============================================================================
272
294
 
273
- # Aggiunge uno o più tag al record
295
+ # Add one or more tags to the record.
296
+ #
297
+ # @param new_tags [Array<String>] Tags to add
298
+ # @return [void]
274
299
  #
275
- # Esempio:
300
+ # @example
276
301
  # article.tag_with("ruby")
277
302
  # article.tag_with("ruby", "rails", "tutorial")
278
303
  def tag_with(*new_tags)
@@ -281,21 +306,24 @@ module BetterModel
281
306
  config = self.class.taggable_config
282
307
  field = config.tag_field
283
308
 
284
- # Inizializza array se nil
309
+ # Initialize array if nil
285
310
  current_tags = public_send(field) || []
286
311
 
287
- # Normalizza e aggiungi tag (evita duplicati con |)
312
+ # Normalize and add tags (avoid duplicates with |)
288
313
  normalized_tags = new_tags.flatten.map { |tag| normalize_tag(tag) }.compact
289
314
  updated_tags = (current_tags | normalized_tags)
290
315
 
291
- # Aggiorna il campo
316
+ # Update field
292
317
  public_send("#{field}=", updated_tags)
293
318
  save if persisted?
294
319
  end
295
320
 
296
- # Rimuove uno o più tag dal record
321
+ # Remove one or more tags from the record.
297
322
  #
298
- # Esempio:
323
+ # @param tags_to_remove [Array<String>] Tags to remove
324
+ # @return [void]
325
+ #
326
+ # @example
299
327
  # article.untag("tutorial")
300
328
  # article.untag("ruby", "rails")
301
329
  def untag(*tags_to_remove)
@@ -304,23 +332,26 @@ module BetterModel
304
332
  config = self.class.taggable_config
305
333
  field = config.tag_field
306
334
 
307
- # Ottieni tag attuali
335
+ # Get current tags
308
336
  current_tags = public_send(field) || []
309
337
 
310
- # Normalizza tag da rimuovere
338
+ # Normalize tags to remove
311
339
  normalized_tags = tags_to_remove.flatten.map { |tag| normalize_tag(tag) }.compact
312
340
 
313
- # Rimuovi tag
341
+ # Remove tags
314
342
  updated_tags = current_tags - normalized_tags
315
343
 
316
- # Aggiorna il campo
344
+ # Update field
317
345
  public_send("#{field}=", updated_tags)
318
346
  save if persisted?
319
347
  end
320
348
 
321
- # Sostituisce tutti i tag esistenti con nuovi tag
349
+ # Replace all existing tags with new tags.
350
+ #
351
+ # @param new_tags [Array<String>] New tags to set
352
+ # @return [void]
322
353
  #
323
- # Esempio:
354
+ # @example
324
355
  # article.retag("python", "django")
325
356
  def retag(*new_tags)
326
357
  return unless taggable_enabled?
@@ -328,17 +359,20 @@ module BetterModel
328
359
  config = self.class.taggable_config
329
360
  field = config.tag_field
330
361
 
331
- # Normalizza nuovi tag
362
+ # Normalize new tags
332
363
  normalized_tags = new_tags.flatten.map { |tag| normalize_tag(tag) }.compact.uniq
333
364
 
334
- # Sostituisci tutti i tag
365
+ # Replace all tags
335
366
  public_send("#{field}=", normalized_tags)
336
367
  save if persisted?
337
368
  end
338
369
 
339
- # Verifica se il record ha un determinato tag
370
+ # Check if record has a specific tag.
340
371
  #
341
- # Esempio:
372
+ # @param tag [String] Tag to check
373
+ # @return [Boolean] true if record has the tag
374
+ #
375
+ # @example
342
376
  # article.tagged_with?("ruby") # => true/false
343
377
  def tagged_with?(tag)
344
378
  return false unless taggable_enabled?
@@ -356,9 +390,11 @@ module BetterModel
356
390
  # TAG LIST (CSV Interface)
357
391
  # ============================================================================
358
392
 
359
- # Restituisce i tag come stringa separata da delimitatore
393
+ # Returns tags as a delimited string.
394
+ #
395
+ # @return [String] Tags joined by delimiter
360
396
  #
361
- # Esempio:
397
+ # @example
362
398
  # article.tag_list # => "ruby, rails, tutorial"
363
399
  def tag_list
364
400
  return "" unless taggable_enabled?
@@ -369,14 +405,17 @@ module BetterModel
369
405
 
370
406
  current_tags = public_send(field) || []
371
407
 
372
- # Aggiungi spazio dopo virgola per leggibilità (solo se delimiter è virgola)
408
+ # Add space after comma for readability (only if delimiter is comma)
373
409
  separator = delimiter == "," ? "#{delimiter} " : delimiter
374
410
  current_tags.join(separator)
375
411
  end
376
412
 
377
- # Imposta i tag da una stringa separata da delimitatore
413
+ # Set tags from a delimited string.
378
414
  #
379
- # Esempio:
415
+ # @param tag_string [String] Delimited tag string
416
+ # @return [void]
417
+ #
418
+ # @example
380
419
  # article.tag_list = "ruby, rails, tutorial"
381
420
  def tag_list=(tag_string)
382
421
  return unless taggable_enabled?
@@ -401,25 +440,26 @@ module BetterModel
401
440
  # JSON SERIALIZATION
402
441
  # ============================================================================
403
442
 
404
- # Override as_json per includere informazioni tag
443
+ # Override as_json to include tag information.
405
444
  #
406
- # Opzioni:
407
- # include_tag_list: true # Includi tag_list come string
408
- # include_tag_stats: true # Includi statistiche tag
445
+ # @param options [Hash] Options for as_json
446
+ # @option options [Boolean] :include_tag_list Include tag_list as string
447
+ # @option options [Boolean] :include_tag_stats Include tag statistics
448
+ # @return [Hash] JSON representation
409
449
  #
410
- # Esempio:
450
+ # @example
411
451
  # article.as_json(include_tag_list: true, include_tag_stats: true)
412
452
  def as_json(options = {})
413
453
  json = super(options)
414
454
 
415
455
  return json unless taggable_enabled?
416
456
 
417
- # Aggiungi tag_list se richiesto
457
+ # Add tag_list if requested
418
458
  if options[:include_tag_list]
419
459
  json["tag_list"] = tag_list
420
460
  end
421
461
 
422
- # Aggiungi statistiche tag se richiesto
462
+ # Add tag statistics if requested
423
463
  if options[:include_tag_stats]
424
464
  config = self.class.taggable_config
425
465
  field = config.tag_field
@@ -436,12 +476,17 @@ module BetterModel
436
476
 
437
477
  private
438
478
 
439
- # Verifica se Taggable è abilitato per questa classe
440
- def taggable_enabled?
441
- self.class.taggable_config.present?
442
- end
479
+ # Check if Taggable is enabled for this class.
480
+ #
481
+ # @return [Boolean] true if enabled
482
+ # @api private
483
+ def taggable_enabled? = self.class.taggable_config.present?
443
484
 
444
- # Normalizza un tag secondo la configurazione
485
+ # Normalize a tag according to configuration.
486
+ #
487
+ # @param tag [String] Tag to normalize
488
+ # @return [String, nil] Normalized tag or nil if invalid
489
+ # @api private
445
490
  def normalize_tag(tag)
446
491
  return nil if tag.blank?
447
492
 
@@ -1,53 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Traceable - Change tracking con audit trail per modelli Rails
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
- # Questo concern permette di tracciare automaticamente i cambiamenti ai record,
6
- # mantenendo uno storico completo con timestamp, autore e motivazione.
9
+ # This concern enables automatic tracking of record changes,
10
+ # maintaining a complete history with timestamps, author, and reasoning.
7
11
  #
8
- # SETUP RAPIDO:
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
- # # Opzione 2: La migration better_model_versions è già nel gem
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
- # APPROCCIO OPT-IN: Il tracking non è attivo automaticamente. Devi chiamare
17
- # esplicitamente `traceable do...end` nel tuo modello per attivarlo.
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
- # REQUISITI DATABASE:
20
- # - better_model_versions table (inclusa nel gem)
24
+ # @note DATABASE REQUIREMENTS
25
+ # - better_model_versions table (included in gem)
21
26
  #
22
- # Esempio di utilizzo:
27
+ # @example Basic Model Setup
23
28
  # class Article < ApplicationRecord
24
29
  # include BetterModel
25
30
  #
26
- # # Attiva traceable (opt-in)
31
+ # # Enable traceable (opt-in)
27
32
  # traceable do
28
- # track :status, :title, :published_at # Campi da tracciare
33
+ # track :status, :title, :published_at # Fields to track
29
34
  # end
30
35
  # end
31
36
  #
32
- # Utilizzo:
33
- # # Tracking automatico
37
+ # @example Automatic Tracking
34
38
  # article.update!(status: "published", updated_by_id: user.id, updated_reason: "Approved")
35
39
  #
36
- # # Query versioni
37
- # article.versions # Tutte le versioni
38
- # article.changes_for(:status) # Cambiamenti per un campo
39
- # article.audit_trail # Storico formattato
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
- # # Time-travel
42
- # article.as_of(3.days.ago) # Stato a una data specifica
45
+ # @example Time-Travel
46
+ # article.as_of(3.days.ago) # State at specific date
43
47
  #
44
- # # Rollback
45
- # article.rollback_to(version) # Ripristina a versione precedente
48
+ # @example Rollback
49
+ # article.rollback_to(version) # Restore to previous version
46
50
  #
47
- # # Scopes per query su cambiamenti
48
- # Article.changed_by(user.id) # Modifiche di un utente
49
- # Article.changed_between(start, end) # Modifiche in un periodo
50
- # Article.status_changed_from("draft").to("published") # Transizioni specifiche
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 ArgumentError, "BetterModel::Traceable can only be included in ActiveRecord models"
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
- raise NotEnabledError unless traceable_enabled?
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
- raise NotEnabledError unless traceable_enabled?
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
- raise NotEnabledError unless traceable_enabled?
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
- raise NotEnabledError unless self.class.traceable_enabled?
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
- raise NotEnabledError unless self.class.traceable_enabled?
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
- raise NotEnabledError unless self.class.traceable_enabled?
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
- raise NotEnabledError unless self.class.traceable_enabled?
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