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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3237879bac91f8200057cba591a1a2f09ccc761f55fd2ded1f1acb5a156c65f2
4
- data.tar.gz: 6ab53b6128104cac44fdb5ad8ce80ca662a78072724a29c946c9380ad66701f2
3
+ metadata.gz: 43311885d24f9c9001eba91737135c5fa8787cc42e5f5e7beaaf912e2ae177d0
4
+ data.tar.gz: cc80f44ca0c7c5992302175d949ef237d3b0f4d2274c9b1d13bba6398f6dad6c
5
5
  SHA512:
6
- metadata.gz: cf5e0a75c42e6f8c81f76756dd4f292779d1df7e258741bc19877665530a50363d7271c31fcace870d4dc61ac75e7dbd6bf01db3c28857fc29da8ab8e87cbed5
7
- data.tar.gz: 61992d1b052d377e1db14d312ea3eb024998d2f0e36dcb031fd9e900a3bd35b0add348bfbfb6dc687c41b37ce9df85952924cbb5fb7942abd484af107ce87664
6
+ metadata.gz: 77874dd1ad5221c36a5498b74810f1812e978a984f24e35e50dcdfba0019899d4afe742bf431c68a3b5c94e30a3fe1e9c0f7d3e16e16722044ec4f91b2538b57
7
+ data.tar.gz: 11f774a0981cd7bc2ec1f307a63a8d0e3f91dc7431b58e262cd9bb5edc858e9548c8f51ea190e7e50a8b17c88965cdbc6d0bbb36e39e819fef9d7464ffb032de
data/README.md CHANGED
@@ -53,20 +53,20 @@ class Article < ApplicationRecord
53
53
  end
54
54
 
55
55
  # 6. VALIDATABLE - Declarative validation system (opt-in)
56
+ register_complex_validation :published_requirements do
57
+ return unless status == "published"
58
+ errors.add(:published_at, "must be present for published articles") if published_at.blank?
59
+ end
60
+
56
61
  validatable do
57
62
  # Basic validations
58
63
  check :title, :content, presence: true
59
64
 
60
- # Conditional validations
61
- validate_if :is_published? do
62
- check :published_at, presence: true
63
- end
64
-
65
- # Cross-field validations
66
- validate_order :starts_at, :before, :ends_at
65
+ # Conditional validations using Rails options
66
+ check :published_at, presence: true, if: -> { status == "published" }
67
67
 
68
- # Business rules
69
- validate_business_rule :valid_category
68
+ # Complex validations for business rules
69
+ check_complex :published_requirements
70
70
 
71
71
  # Validation groups (multi-step forms)
72
72
  validation_group :step1, [:title, :content]
@@ -335,6 +335,90 @@ rails db:migrate
335
335
  # See model setup instructions after running each generator
336
336
  ```
337
337
 
338
+ ### Repository Generator
339
+
340
+ Create repository classes that implement the Repository Pattern:
341
+
342
+ ```bash
343
+ # Basic usage - creates model repository and ApplicationRepository
344
+ rails g better_model:repository Article
345
+
346
+ # Custom path
347
+ rails g better_model:repository Article --path app/services/repositories
348
+
349
+ # Skip ApplicationRepository creation
350
+ rails g better_model:repository Article --skip-base
351
+
352
+ # With namespace
353
+ rails g better_model:repository Article --namespace Admin
354
+ ```
355
+
356
+ **Generated repository includes:**
357
+ - Repository class inheriting from `ApplicationRepository` (or `BetterModel::Repositable::BaseRepository` with `--skip-base`)
358
+ - Modern Ruby 3 endless method syntax: `def model_class = Article`
359
+ - Commented examples of custom query methods
360
+ - Integration with BetterModel's Searchable, Predicable, and Sortable features
361
+ - Auto-generated documentation of available predicates and sort scopes (if model uses BetterModel)
362
+
363
+ **ApplicationRepository (generated once):**
364
+ - Base class for all repositories in your application
365
+ - Inherits from `BetterModel::Repositable::BaseRepository`
366
+ - Can be customized with application-wide repository behaviors
367
+
368
+ **Example usage:**
369
+
370
+ ```ruby
371
+ # app/repositories/article_repository.rb
372
+ class ArticleRepository < ApplicationRepository
373
+ def model_class = Article
374
+
375
+ def published
376
+ search({ status_eq: "published" })
377
+ end
378
+
379
+ def recent(days: 7)
380
+ search({ created_at_gteq: days.days.ago }, order_scope: { field: :created_at, direction: :desc })
381
+ end
382
+
383
+ def popular(min_views: 100)
384
+ search({ view_count_gteq: min_views }, orders: [:sort_view_count_desc])
385
+ end
386
+
387
+ def search_text(query)
388
+ search({
389
+ or: [
390
+ { title_i_cont: query },
391
+ { content_i_cont: query }
392
+ ]
393
+ })
394
+ end
395
+ end
396
+
397
+ # In your controllers/services
398
+ repo = ArticleRepository.new
399
+ articles = repo.published
400
+ popular = repo.popular(min_views: 200)
401
+ results = repo.search({ status_eq: "published" }, page: 1, per_page: 20)
402
+ article = repo.search({ id_eq: 1 }, limit: 1)
403
+ ```
404
+
405
+ **BaseRepository features:**
406
+ - `search(predicates, page:, per_page:, includes:, joins:, order:, order_scope:, limit:)` - Main search method
407
+ - `find(id)`, `find_by(...)` - Standard ActiveRecord finders
408
+ - `create(attrs)`, `create!(attrs)` - Create records
409
+ - `build(attrs)` - Build new instances
410
+ - `update(id, attrs)` - Update records
411
+ - `delete(id)` - Delete records
412
+ - `where(...)`, `all`, `count`, `exists?` - Basic ActiveRecord methods
413
+
414
+ **Search method parameters:**
415
+ - **predicates**: Hash of BetterModel predicates (e.g., `{ status_eq: "published", view_count_gt: 100 }`)
416
+ - **page/per_page**: Pagination (default: page 1, 20 per page)
417
+ - **includes/joins**: Eager loading and associations
418
+ - **order**: SQL order clause
419
+ - **order_scope**: BetterModel sort scope (e.g., `{ field: :published_at, direction: :desc }`)
420
+ - **limit**: Result limit (Integer for limit, `:default` for pagination, `nil` for all results)
421
+
338
422
  ## 📋 Features Overview
339
423
 
340
424
  BetterModel provides ten powerful concerns that work seamlessly together:
@@ -450,11 +534,10 @@ Define validations declaratively with support for conditional rules, cross-field
450
534
 
451
535
  **🎯 Key Benefits:**
452
536
  - 🎛️ Opt-in activation: only enabled when explicitly configured
453
- - ✨ Declarative DSL for all validation types
454
- - 🔀 Conditional validations: `validate_if` / `validate_unless`
455
- - 🔗 Cross-field validations: `validate_order` for date/number comparisons
456
- - 💼 Business rules: delegate complex logic to custom methods
537
+ - ✨ Declarative DSL with `check` method for basic validations
538
+ - 🔗 Complex validations: reusable validation blocks for cross-field and business logic
457
539
  - 📋 Validation groups: partial validation for multi-step forms
540
+ - 🔀 Conditional validations using Rails `if:` / `unless:` options
458
541
  - 🔒 Thread-safe with immutable configuration
459
542
 
460
543
  **[📖 Full Documentation →](docs/validatable.md)**
@@ -1,41 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Archivable - Sistema di archiviazione dichiarativa per modelli Rails
3
+ require_relative "errors/archivable/archivable_error"
4
+ require_relative "errors/archivable/already_archived_error"
5
+ require_relative "errors/archivable/not_archived_error"
6
+ require_relative "errors/archivable/not_enabled_error"
7
+ require_relative "errors/archivable/configuration_error"
8
+
9
+ # Archivable - Declarative archiving system for Rails models.
4
10
  #
5
- # Questo concern permette di archiviare e ripristinare record utilizzando un DSL
6
- # semplice e dichiarativo, con supporto per predicati e scopes.
11
+ # This concern enables archiving and restoring records using a simple, declarative DSL
12
+ # with support for predicates and scopes.
7
13
  #
8
- # SETUP RAPIDO:
9
- # # Opzione 1: Generator automatico (raccomandato)
14
+ # @example Quick Setup - Option 1: Automatic Generator (Recommended)
10
15
  # rails g better_model:archivable Article --with-tracking
11
16
  # rails db:migrate
12
17
  #
13
- # # Opzione 2: Migration manuale
18
+ # @example Quick Setup - Option 2: Manual Migration
14
19
  # rails g migration AddArchivableToArticles archived_at:datetime archived_by_id:integer archive_reason:string
15
20
  # rails db:migrate
16
21
  #
17
- # APPROCCIO OPT-IN: L'archiviazione non è attiva automaticamente. Devi chiamare
18
- # esplicitamente `archivable do...end` nel tuo modello per attivarla.
22
+ # @note OPT-IN APPROACH
23
+ # Archiving is not enabled automatically. You must explicitly call
24
+ # `archivable do...end` in your model to activate it.
19
25
  #
20
- # APPROCCIO IBRIDO: Usa i predicati esistenti (archived_at_present, archived_at_null, etc.)
21
- # e fornisce alias semantici (archived, not_archived) per una migliore leggibilità.
26
+ # @note HYBRID APPROACH
27
+ # Uses existing predicates (archived_at_present, archived_at_null, etc.)
28
+ # and provides semantic aliases (archived, not_archived) for better readability.
22
29
  #
23
- # REQUISITI DATABASE:
24
- # - archived_at (datetime) - REQUIRED (obbligatorio)
25
- # - archived_by_id (integer) - OPTIONAL (per tracking utente)
26
- # - archive_reason (string) - OPTIONAL (per motivazione)
30
+ # @note DATABASE REQUIREMENTS
31
+ # - archived_at (datetime) - REQUIRED
32
+ # - archived_by_id (integer) - OPTIONAL (for user tracking)
33
+ # - archive_reason (string) - OPTIONAL (for reasoning)
27
34
  #
28
- # Esempio di utilizzo:
35
+ # @example Basic Model Setup
29
36
  # class Article < ApplicationRecord
30
37
  # include BetterModel
31
38
  #
32
- # # Attiva archivable (opt-in)
39
+ # # Enable archivable (opt-in)
33
40
  # archivable do
34
- # skip_archived_by_default true # Opzionale: nascondi archiviati di default
41
+ # skip_archived_by_default true # Optional: hide archived records by default
35
42
  # end
36
43
  # end
37
44
  #
38
- # Utilizzo:
45
+ # @example Instance Methods Usage
39
46
  # article.archive! # Archive the record
40
47
  # article.archive!(by: user) # Archive with user tracking
41
48
  # article.archive!(reason: "Outdated") # Archive with reason
@@ -43,22 +50,22 @@
43
50
  # article.archived? # Check if archived
44
51
  # article.active? # Check if not archived
45
52
  #
46
- # # Scopes semantici
53
+ # @example Semantic Scopes
47
54
  # Article.archived # Find archived records
48
55
  # Article.not_archived # Find active records
49
56
  # Article.archived_only # Bypass default scope
50
57
  #
51
- # # Predicati potenti (generati automaticamente)
58
+ # @example Powerful Predicates (Auto-generated)
52
59
  # Article.archived_at_within(7.days) # Archived in last 7 days
53
60
  # Article.archived_at_today # Archived today
54
61
  # Article.archived_at_between(start, end) # Archived in range
55
62
  #
56
- # # Helper methods
63
+ # @example Helper Methods
57
64
  # Article.archived_today # Alias for archived_at_today
58
65
  # Article.archived_this_week # Alias for archived_at_this_week
59
66
  # Article.archived_recently(7.days) # Alias for archived_at_within
60
67
  #
61
- # # Con Searchable
68
+ # @example Integration with Searchable
62
69
  # Article.search({ archived_at_null: true, status_eq: "published" })
63
70
  #
64
71
  module BetterModel
@@ -66,113 +73,177 @@ module BetterModel
66
73
  extend ActiveSupport::Concern
67
74
 
68
75
  included do
69
- # Validazione ActiveRecord
76
+ # Validate ActiveRecord inheritance
70
77
  unless ancestors.include?(ActiveRecord::Base)
71
- raise ArgumentError, "BetterModel::Archivable can only be included in ActiveRecord models"
78
+ raise BetterModel::Errors::Archivable::ConfigurationError, "Invalid configuration"
72
79
  end
73
80
 
74
- # Configurazione archivable (opt-in)
81
+ # Archivable configuration (opt-in)
75
82
  class_attribute :archivable_enabled, default: false
76
83
  class_attribute :archivable_config, default: {}.freeze
77
84
  end
78
85
 
79
86
  class_methods do
80
- # DSL per attivare e configurare archivable (OPT-IN)
87
+ # DSL to enable and configure archivable (OPT-IN).
88
+ #
89
+ # This method activates archivable functionality on your model. It automatically
90
+ # defines predicates and sorts on archived_at, creates semantic scope aliases,
91
+ # and optionally configures default scoping behavior.
92
+ #
93
+ # @yield [configurator] Optional configuration block
94
+ # @raise [BetterModel::Errors::Archivable::ConfigurationError] If archived_at column doesn't exist
81
95
  #
82
- # @example Attivazione base
96
+ # @example Basic activation
83
97
  # archivable
84
98
  #
85
- # @example Con configurazione
99
+ # @example With configuration
86
100
  # archivable do
87
101
  # skip_archived_by_default true
88
102
  # end
89
103
  def archivable(&block)
90
- # Valida che archived_at esista
104
+ # Validate that archived_at exists
91
105
  unless column_names.include?("archived_at")
92
- raise ArgumentError,
93
- "Archivable requires an 'archived_at' datetime column. " \
94
- "Add it with: rails g migration AddArchivedAtTo#{table_name.classify.pluralize} archived_at:datetime"
106
+ raise BetterModel::Errors::Archivable::ConfigurationError, "Invalid configuration"
95
107
  end
96
108
 
97
- # Attiva archivable
109
+ # Enable archivable
98
110
  self.archivable_enabled = true
99
111
 
100
- # Definisci predicati su archived_at (opt-in!)
112
+ # Define predicates on archived_at (opt-in!)
101
113
  predicates :archived_at unless predicable_field?(:archived_at)
102
114
 
103
- # Definisci anche sort su archived_at (opt-in!)
115
+ # Define sorting on archived_at (opt-in!)
104
116
  sort :archived_at unless sortable_field?(:archived_at)
105
117
 
106
- # Definisci gli scope alias (approccio ibrido)
118
+ # Define semantic scope aliases (hybrid approach)
107
119
  scope :archived, -> { archived_at_present(true) }
108
120
  scope :not_archived, -> { archived_at_null(true) }
109
121
 
110
- # Configura se passato un blocco
122
+ # Configure if block provided
111
123
  if block_given?
112
124
  configurator = ArchivableConfigurator.new(self)
113
125
  configurator.instance_eval(&block)
114
126
  self.archivable_config = configurator.to_h.freeze
115
127
 
116
- # Applica default scope SOLO se configurato
128
+ # Apply default scope ONLY if configured
117
129
  if archivable_config[:skip_archived_by_default]
118
130
  default_scope -> { where(archived_at: nil) }
119
131
  end
120
132
  end
121
133
  end
122
134
 
123
- # Trova SOLO record archiviati, bypassando default scope
135
+ # Find ONLY archived records, bypassing default scope.
124
136
  #
125
137
  # @return [ActiveRecord::Relation]
138
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
139
+ #
140
+ # @example
141
+ # Article.archived_only
126
142
  def archived_only
127
- raise ArchivableNotEnabledError unless archivable_enabled?
143
+ unless archivable_enabled?
144
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
145
+ end
128
146
  unscoped.archived
129
147
  end
130
148
 
131
- # Helper: alias per archived_at_today
149
+ # Find records archived today.
150
+ #
151
+ # Helper alias for archived_at_today predicate.
152
+ #
153
+ # @return [ActiveRecord::Relation]
154
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
155
+ #
156
+ # @example
157
+ # Article.archived_today
132
158
  def archived_today
133
- raise ArchivableNotEnabledError unless archivable_enabled?
159
+ unless archivable_enabled?
160
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
161
+ end
134
162
  archived_at_today
135
163
  end
136
164
 
137
- # Helper: alias per archived_at_this_week
165
+ # Find records archived this week.
166
+ #
167
+ # Helper alias for archived_at_this_week predicate.
168
+ #
169
+ # @return [ActiveRecord::Relation]
170
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
171
+ #
172
+ # @example
173
+ # Article.archived_this_week
138
174
  def archived_this_week
139
- raise ArchivableNotEnabledError unless archivable_enabled?
175
+ unless archivable_enabled?
176
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
177
+ end
140
178
  archived_at_this_week
141
179
  end
142
180
 
143
- # Helper: alias per archived_at_within
181
+ # Find records archived within the specified duration.
144
182
  #
145
- # @param duration [ActiveSupport::Duration] Durata (es: 7.days)
183
+ # Helper alias for archived_at_within predicate.
184
+ #
185
+ # @param duration [ActiveSupport::Duration] Time duration (e.g., 7.days)
146
186
  # @return [ActiveRecord::Relation]
187
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
188
+ #
189
+ # @example Find records archived in the last 7 days
190
+ # Article.archived_recently(7.days)
191
+ #
192
+ # @example Find records archived in the last month
193
+ # Article.archived_recently(1.month)
147
194
  def archived_recently(duration = 7.days)
148
- raise ArchivableNotEnabledError unless archivable_enabled?
195
+ unless archivable_enabled?
196
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
197
+ end
149
198
  archived_at_within(duration)
150
199
  end
151
200
 
152
- # Verifica se archivable è attivo
201
+ # Check if archivable is enabled on this model.
153
202
  #
154
- # @return [Boolean]
155
- def archivable_enabled?
156
- archivable_enabled == true
157
- end
203
+ # @return [Boolean] true if archivable is enabled, false otherwise
204
+ #
205
+ # @example
206
+ # Article.archivable_enabled? # => true
207
+ def archivable_enabled? = archivable_enabled == true
158
208
  end
159
209
 
160
- # Metodi di istanza
210
+ # Instance Methods
161
211
 
162
- # Archivia il record
212
+ # Archive this record.
213
+ #
214
+ # Sets archived_at to current time and optionally tracks the user and reason.
215
+ # Bypasses validations when saving.
216
+ #
217
+ # @param by [Integer, Object, nil] User ID or user object (optional)
218
+ # @param reason [String, nil] Reason for archiving (optional)
219
+ # @return [self] Returns self for method chaining
220
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
221
+ # @raise [BetterModel::Errors::Archivable::AlreadyArchivedError] If already archived
222
+ #
223
+ # @example Basic archiving
224
+ # article.archive!
163
225
  #
164
- # @param by [Integer, Object] ID utente o oggetto user (opzionale)
165
- # @param reason [String] Motivo dell'archiviazione (opzionale)
166
- # @return [self]
167
- # @raise [ArchivableNotEnabledError] se archivable non è attivo
168
- # @raise [AlreadyArchivedError] se già archiviato
226
+ # @example Archive with user tracking
227
+ # article.archive!(by: current_user)
228
+ # article.archive!(by: user.id)
229
+ #
230
+ # @example Archive with reason
231
+ # article.archive!(reason: "Outdated content")
232
+ #
233
+ # @example Archive with both user and reason
234
+ # article.archive!(by: current_user, reason: "Compliance violation")
169
235
  def archive!(by: nil, reason: nil)
170
- raise ArchivableNotEnabledError unless self.class.archivable_enabled?
171
- raise AlreadyArchivedError, "Record is already archived" if archived?
236
+ unless self.class.archivable_enabled?
237
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
238
+ end
239
+
240
+ if archived?
241
+ raise BetterModel::Errors::Archivable::AlreadyArchivedError, "Record is already archived"
242
+ end
172
243
 
173
244
  self.archived_at = Time.current
174
245
 
175
- # Set archived_by_id: accetta sia ID che oggetti con .id
246
+ # Set archived_by_id: accepts both ID and objects with .id
176
247
  if respond_to?(:archived_by_id=) && by.present?
177
248
  self.archived_by_id = by.respond_to?(:id) ? by.id : by
178
249
  end
@@ -183,14 +254,25 @@ module BetterModel
183
254
  self
184
255
  end
185
256
 
186
- # Ripristina record archiviato
257
+ # Restore archived record.
258
+ #
259
+ # Clears archived_at, archived_by_id, and archive_reason.
260
+ # Bypasses validations when saving.
187
261
  #
188
- # @return [self]
189
- # @raise [ArchivableNotEnabledError] se archivable non è attivo
190
- # @raise [NotArchivedError] se non archiviato
262
+ # @return [self] Returns self for method chaining
263
+ # @raise [BetterModel::Errors::Archivable::NotEnabledError] If archivable is not enabled
264
+ # @raise [BetterModel::Errors::Archivable::NotArchivedError] If not archived
265
+ #
266
+ # @example
267
+ # article.restore!
191
268
  def restore!
192
- raise ArchivableNotEnabledError unless self.class.archivable_enabled?
193
- raise NotArchivedError, "Record is not archived" unless archived?
269
+ unless self.class.archivable_enabled?
270
+ raise BetterModel::Errors::Archivable::NotEnabledError, "Module is not enabled"
271
+ end
272
+
273
+ unless archived?
274
+ raise BetterModel::Errors::Archivable::NotArchivedError, "Record is not archived"
275
+ end
194
276
 
195
277
  self.archived_at = nil
196
278
  self.archived_by_id = nil if respond_to?(:archived_by_id=)
@@ -200,26 +282,47 @@ module BetterModel
200
282
  self
201
283
  end
202
284
 
203
- # Verifica se il record è archiviato
285
+ # Check if record is archived.
286
+ #
287
+ # @return [Boolean] true if archived, false otherwise
204
288
  #
205
- # @return [Boolean]
289
+ # @example
290
+ # article.archived? # => true
206
291
  def archived?
207
292
  return false unless self.class.archivable_enabled?
208
293
  archived_at.present?
209
294
  end
210
295
 
211
- # Verifica se il record è attivo (non archiviato)
296
+ # Check if record is active (not archived).
212
297
  #
213
- # @return [Boolean]
214
- def active?
215
- !archived?
216
- end
298
+ # @return [Boolean] true if active, false otherwise
299
+ #
300
+ # @example
301
+ # article.active? # => true
302
+ def active? = !archived?
217
303
 
218
- # Override as_json per includere info archivio
304
+ # Override as_json to include archive information.
305
+ #
306
+ # Optionally includes detailed archive metadata when requested.
219
307
  #
220
- # @param options [Hash] Opzioni as_json
308
+ # @param options [Hash] Options for as_json
221
309
  # @option options [Boolean] :include_archive_info Include archive metadata
222
- # @return [Hash]
310
+ # @return [Hash] JSON representation
311
+ #
312
+ # @example Basic JSON
313
+ # article.as_json
314
+ #
315
+ # @example With archive info
316
+ # article.as_json(include_archive_info: true)
317
+ # # => {
318
+ # # ...,
319
+ # # "archive_info" => {
320
+ # # "archived" => true,
321
+ # # "archived_at" => "2025-01-15T10:30:00Z",
322
+ # # "archived_by_id" => 42,
323
+ # # "archive_reason" => "Outdated"
324
+ # # }
325
+ # # }
223
326
  def as_json(options = {})
224
327
  result = super
225
328
 
@@ -236,21 +339,18 @@ module BetterModel
236
339
  end
237
340
  end
238
341
 
239
- # Errori custom
240
- class ArchivableError < StandardError; end
241
- class AlreadyArchivedError < ArchivableError; end
242
- class NotArchivedError < ArchivableError; end
243
342
 
244
- class ArchivableNotEnabledError < ArchivableError
245
- def initialize(msg = nil)
246
- super(msg || "Archivable is not enabled. Add 'archivable do...end' to your model.")
247
- end
248
- end
249
-
250
- # Configurator per archivable DSL
343
+ # Configurator for archivable DSL.
344
+ #
345
+ # Internal class used to configure archivable behavior through the DSL block.
346
+ #
347
+ # @api private
251
348
  class ArchivableConfigurator
252
349
  attr_reader :config
253
350
 
351
+ # Initialize a new configurator.
352
+ #
353
+ # @param model_class [Class] The model class being configured
254
354
  def initialize(model_class)
255
355
  @model_class = model_class
256
356
  @config = {
@@ -258,13 +358,25 @@ module BetterModel
258
358
  }
259
359
  end
260
360
 
261
- # Configura default scope per nascondere archiviati
361
+ # Configure default scope to hide archived records.
362
+ #
363
+ # When enabled, adds a default scope that filters out archived records.
364
+ # Use Model.unscoped or Model.archived_only to access archived records.
262
365
  #
263
- # @param value [Boolean] true per nascondere archiviati di default
366
+ # @param value [Boolean] true to hide archived records by default
367
+ #
368
+ # @example
369
+ # archivable do
370
+ # skip_archived_by_default true
371
+ # end
264
372
  def skip_archived_by_default(value)
265
373
  @config[:skip_archived_by_default] = !!value
266
374
  end
267
375
 
376
+ # Convert configuration to hash.
377
+ #
378
+ # @return [Hash] Configuration hash
379
+ # @api private
268
380
  def to_h
269
381
  @config
270
382
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "archivable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Archivable
8
+ class AlreadyArchivedError < ArchivableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../better_model_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Archivable
8
+ # Base error class for all Archivable-related errors.
9
+ class ArchivableError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module BetterModel
5
+ module Errors
6
+ module Archivable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "archivable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Archivable
8
+ class NotArchivedError < ArchivableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "archivable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Archivable
8
+ class NotEnabledError < ArchivableError; end
9
+ end
10
+ end
11
+ end