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
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module BetterModel
6
+ module Generators
7
+ # Generator for creating repository classes that implement the Repository Pattern.
8
+ #
9
+ # This generator creates a repository class for a given model, integrating seamlessly
10
+ # with BetterModel's Searchable, Predicable, and Sortable concerns.
11
+ #
12
+ # @example Generate a repository for Article model
13
+ # rails generate better_model:repository Article
14
+ #
15
+ # @example Generate with custom path
16
+ # rails generate better_model:repository Article --path app/services/repositories
17
+ #
18
+ # @example Skip ApplicationRepository creation
19
+ # rails generate better_model:repository Article --skip-base
20
+ #
21
+ class RepositoryGenerator < Rails::Generators::NamedBase
22
+ source_root File.expand_path("templates", __dir__)
23
+
24
+ class_option :path, type: :string, default: "app/repositories",
25
+ desc: "Directory where the repository will be created"
26
+ class_option :skip_base, type: :boolean, default: false,
27
+ desc: "Skip creating ApplicationRepository if it doesn't exist"
28
+ class_option :namespace, type: :string, default: nil,
29
+ desc: "Namespace for the repository class"
30
+
31
+ # Create the ApplicationRepository base class if it doesn't exist
32
+ def create_application_repository
33
+ return if options[:skip_base]
34
+ return if File.exist?(File.join(destination_root, application_repository_path))
35
+
36
+ template "application_repository.rb.tt", application_repository_path
37
+ say "Created ApplicationRepository at #{application_repository_path}", :green
38
+ end
39
+
40
+ # Create the model-specific repository class
41
+ def create_repository_file
42
+ template "repository.rb.tt", repository_path
43
+ say "Created #{repository_class_name} at #{repository_path}", :green
44
+ end
45
+
46
+ # Display usage instructions
47
+ def show_instructions
48
+ say "\nRepository created successfully!", :green
49
+ say "\nUsage example:", :yellow
50
+ say " repo = #{repository_class_name}.new", :white
51
+ say " results = repo.search({ #{example_predicate} })", :white
52
+ say " record = repo.search({ id_eq: 1 }, limit: 1)", :white
53
+ say " all = repo.search({}, limit: nil)", :white
54
+ say "\nAdd custom methods to #{repository_path}", :yellow
55
+
56
+ if model_has_better_model_features?
57
+ say "\nYour model has BetterModel features enabled:", :green
58
+ display_available_features
59
+ else
60
+ say "\nTip: Include BetterModel in your #{class_name} model to unlock:", :yellow
61
+ say " - Predicable: Auto-generated filter scopes", :white
62
+ say " - Sortable: Auto-generated sort scopes", :white
63
+ say " - Searchable: Unified search interface", :white
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ def repository_path
70
+ File.join(options[:path], "#{file_name}_repository.rb")
71
+ end
72
+
73
+ def application_repository_path
74
+ File.join(options[:path], "application_repository.rb")
75
+ end
76
+
77
+ def repository_class_name
78
+ if options[:namespace]
79
+ "#{options[:namespace]}::#{class_name}Repository"
80
+ else
81
+ "#{class_name}Repository"
82
+ end
83
+ end
84
+
85
+ def base_repository_class
86
+ if options[:skip_base]
87
+ "BetterModel::Repositable::BaseRepository"
88
+ else
89
+ "ApplicationRepository"
90
+ end
91
+ end
92
+
93
+ def example_predicate
94
+ if model_class_exists? && model_class.column_names.include?("name")
95
+ "name_cont: 'search'"
96
+ elsif model_class_exists? && model_class.column_names.include?("title")
97
+ "title_cont: 'search'"
98
+ elsif model_class_exists? && model_class.column_names.include?("status")
99
+ "status_eq: 'active'"
100
+ else
101
+ "id_eq: 1"
102
+ end
103
+ end
104
+
105
+ def model_class_exists?
106
+ return false unless Object.const_defined?(class_name)
107
+ klass = class_name.constantize
108
+ klass < ActiveRecord::Base && klass.table_exists?
109
+ rescue NameError, ActiveRecord::StatementInvalid, ActiveRecord::NoDatabaseError
110
+ false
111
+ end
112
+
113
+ def model_class
114
+ class_name.constantize if model_class_exists?
115
+ end
116
+
117
+ def model_has_better_model_features?
118
+ return false unless model_class_exists?
119
+ model_class.respond_to?(:predicable_fields) ||
120
+ model_class.respond_to?(:sortable_fields) ||
121
+ model_class.respond_to?(:searchable_fields)
122
+ end
123
+
124
+ def display_available_features
125
+ return unless model_class_exists?
126
+
127
+ if model_class.respond_to?(:predicable_fields) && model_class.predicable_fields.any?
128
+ say " • Predicable fields: #{model_class.predicable_fields.to_a.join(', ')}", :white
129
+ end
130
+
131
+ if model_class.respond_to?(:sortable_fields) && model_class.sortable_fields.any?
132
+ say " • Sortable fields: #{model_class.sortable_fields.to_a.join(', ')}", :white
133
+ end
134
+
135
+ if model_class.respond_to?(:searchable_fields) && model_class.searchable_fields.any?
136
+ say " • Searchable fields: #{model_class.searchable_fields.to_a.join(', ')}", :white
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base repository class for the application.
4
+ #
5
+ # Inherits from BetterModel::Repositable::BaseRepository and can be customized with
6
+ # application-wide repository behaviors.
7
+ #
8
+ # @example Add custom methods available to all repositories
9
+ # class ApplicationRepository < BetterModel::Repositable::BaseRepository
10
+ # def find_active(id)
11
+ # search({ id_eq: id, status_eq: "active" }, limit: 1)
12
+ # end
13
+ #
14
+ # def paginated_search(filters, page: 1)
15
+ # search(filters, page: page, per_page: 25)
16
+ # end
17
+ # end
18
+ #
19
+ class ApplicationRepository < BetterModel::Repositable::BaseRepository
20
+ # Add application-wide repository methods here
21
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% if options[:namespace] -%>
4
+ module <%= options[:namespace] %>
5
+ class <%= class_name %>Repository < <%= base_repository_class %>
6
+ def model_class = <%= class_name %>
7
+
8
+ # Add your custom query methods here
9
+ #
10
+ # Example methods:
11
+ # def active
12
+ # search({ status_eq: "active" })
13
+ # end
14
+ #
15
+ # def recent(days: 7)
16
+ # search({ created_at_gteq: days.days.ago }, order_scope: { field: :created_at, direction: :desc })
17
+ # end
18
+ #
19
+ # def find_with_details(id)
20
+ # search({ id_eq: id }, includes: [:associated_records], limit: 1)
21
+ # end
22
+ <% if model_class_exists? && model_class.respond_to?(:predicable_fields) && model_class.predicable_fields.any? -%>
23
+
24
+ # Available predicates for <%= class_name %>:
25
+ <% model_class.predicable_fields.each do |field| -%>
26
+ # <%= field %>: <%= model_class.searchable_predicates_for(field).map { |p| ":#{p}" }.join(", ") %>
27
+ <% end -%>
28
+ <% end -%>
29
+ <% if model_class_exists? && model_class.respond_to?(:sortable_fields) && model_class.sortable_fields.any? -%>
30
+
31
+ # Available sort scopes for <%= class_name %>:
32
+ <% model_class.sortable_fields.each do |field| -%>
33
+ # <%= field %>: <%= model_class.searchable_sorts_for(field).join(", ") %>
34
+ <% end -%>
35
+ <% end -%>
36
+ end
37
+ end
38
+ <% else -%>
39
+ class <%= class_name %>Repository < <%= base_repository_class %>
40
+ def model_class = <%= class_name %>
41
+
42
+ # Add your custom query methods here
43
+ #
44
+ # Example methods:
45
+ # def active
46
+ # search({ status_eq: "active" })
47
+ # end
48
+ #
49
+ # def recent(days: 7)
50
+ # search({ created_at_gteq: days.days.ago }, order_scope: { field: :created_at, direction: :desc })
51
+ # end
52
+ #
53
+ # def find_with_details(id)
54
+ # search({ id_eq: id }, includes: [:associated_records], limit: 1)
55
+ # end
56
+ <% if model_class_exists? && model_class.respond_to?(:predicable_fields) && model_class.predicable_fields.any? -%>
57
+
58
+ # Available predicates for <%= class_name %>:
59
+ <% model_class.predicable_fields.each do |field| -%>
60
+ # <%= field %>: <%= model_class.searchable_predicates_for(field).map { |p| ":#{p}" }.join(", ") %>
61
+ <% end -%>
62
+ <% end -%>
63
+ <% if model_class_exists? && model_class.respond_to?(:sortable_fields) && model_class.sortable_fields.any? -%>
64
+
65
+ # Available sort scopes for <%= class_name %>:
66
+ <% model_class.sortable_fields.each do |field| -%>
67
+ # <%= field %>: <%= model_class.searchable_sorts_for(field).join(", ") %>
68
+ <% end -%>
69
+ <% end -%>
70
+ end
71
+ <% end -%>
@@ -20,7 +20,7 @@ Next steps:
20
20
 
21
21
  # Define transitions
22
22
  transition :confirm, from: :<%= initial_state_value %>, to: :confirmed do
23
- guard { valid? }
23
+ check { valid? }
24
24
  before { prepare_confirmation }
25
25
  after { send_notification }
26
26
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - alessiobussolari
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-05 00:00:00.000000000 Z
11
+ date: 2025-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,28 +43,65 @@ files:
43
43
  - Rakefile
44
44
  - lib/better_model.rb
45
45
  - lib/better_model/archivable.rb
46
+ - lib/better_model/errors/archivable/already_archived_error.rb
47
+ - lib/better_model/errors/archivable/archivable_error.rb
48
+ - lib/better_model/errors/archivable/configuration_error.rb
49
+ - lib/better_model/errors/archivable/not_archived_error.rb
50
+ - lib/better_model/errors/archivable/not_enabled_error.rb
51
+ - lib/better_model/errors/better_model_error.rb
52
+ - lib/better_model/errors/permissible/configuration_error.rb
53
+ - lib/better_model/errors/permissible/permissible_error.rb
54
+ - lib/better_model/errors/predicable/configuration_error.rb
55
+ - lib/better_model/errors/predicable/predicable_error.rb
56
+ - lib/better_model/errors/searchable/configuration_error.rb
57
+ - lib/better_model/errors/searchable/invalid_order_error.rb
58
+ - lib/better_model/errors/searchable/invalid_pagination_error.rb
59
+ - lib/better_model/errors/searchable/invalid_predicate_error.rb
60
+ - lib/better_model/errors/searchable/invalid_security_error.rb
61
+ - lib/better_model/errors/searchable/searchable_error.rb
62
+ - lib/better_model/errors/sortable/configuration_error.rb
63
+ - lib/better_model/errors/sortable/sortable_error.rb
64
+ - lib/better_model/errors/stateable/check_failed_error.rb
65
+ - lib/better_model/errors/stateable/configuration_error.rb
66
+ - lib/better_model/errors/stateable/invalid_state_error.rb
67
+ - lib/better_model/errors/stateable/invalid_transition_error.rb
68
+ - lib/better_model/errors/stateable/not_enabled_error.rb
69
+ - lib/better_model/errors/stateable/stateable_error.rb
70
+ - lib/better_model/errors/stateable/validation_failed_error.rb
71
+ - lib/better_model/errors/statusable/configuration_error.rb
72
+ - lib/better_model/errors/statusable/statusable_error.rb
73
+ - lib/better_model/errors/taggable/configuration_error.rb
74
+ - lib/better_model/errors/taggable/taggable_error.rb
75
+ - lib/better_model/errors/traceable/configuration_error.rb
76
+ - lib/better_model/errors/traceable/not_enabled_error.rb
77
+ - lib/better_model/errors/traceable/traceable_error.rb
78
+ - lib/better_model/errors/validatable/configuration_error.rb
79
+ - lib/better_model/errors/validatable/not_enabled_error.rb
80
+ - lib/better_model/errors/validatable/validatable_error.rb
81
+ - lib/better_model/models/state_transition.rb
82
+ - lib/better_model/models/version.rb
46
83
  - lib/better_model/permissible.rb
47
84
  - lib/better_model/predicable.rb
48
85
  - lib/better_model/railtie.rb
86
+ - lib/better_model/repositable.rb
87
+ - lib/better_model/repositable/base_repository.rb
49
88
  - lib/better_model/searchable.rb
50
89
  - lib/better_model/sortable.rb
51
- - lib/better_model/state_transition.rb
52
90
  - lib/better_model/stateable.rb
53
91
  - lib/better_model/stateable/configurator.rb
54
- - lib/better_model/stateable/errors.rb
55
92
  - lib/better_model/stateable/guard.rb
56
93
  - lib/better_model/stateable/transition.rb
57
94
  - lib/better_model/statusable.rb
58
95
  - lib/better_model/taggable.rb
59
96
  - lib/better_model/traceable.rb
60
97
  - lib/better_model/validatable.rb
61
- - lib/better_model/validatable/business_rule_validator.rb
62
98
  - lib/better_model/validatable/configurator.rb
63
- - lib/better_model/validatable/order_validator.rb
64
99
  - lib/better_model/version.rb
65
- - lib/better_model/version_record.rb
66
100
  - lib/generators/better_model/archivable/archivable_generator.rb
67
101
  - lib/generators/better_model/archivable/templates/migration.rb.tt
102
+ - lib/generators/better_model/repository/repository_generator.rb
103
+ - lib/generators/better_model/repository/templates/application_repository.rb.tt
104
+ - lib/generators/better_model/repository/templates/repository.rb.tt
68
105
  - lib/generators/better_model/stateable/install_generator.rb
69
106
  - lib/generators/better_model/stateable/stateable_generator.rb
70
107
  - lib/generators/better_model/stateable/templates/README
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BetterModel
4
- # StateTransition - Base ActiveRecord model for state transition history
5
- #
6
- # Questo è un modello abstract. Le classi concrete vengono generate dinamicamente
7
- # per ogni tabella (state_transitions, order_transitions, etc.).
8
- #
9
- # Schema della tabella:
10
- # t.string :transitionable_type, null: false
11
- # t.integer :transitionable_id, null: false
12
- # t.string :event, null: false
13
- # t.string :from_state, null: false
14
- # t.string :to_state, null: false
15
- # t.json :metadata
16
- # t.datetime :created_at, null: false
17
- #
18
- # Utilizzo:
19
- # # Tutte le transizioni di un modello
20
- # order.state_transitions
21
- #
22
- # # Query globali (tramite classi dinamiche)
23
- # BetterModel::StateTransitions.for_model(Order)
24
- # BetterModel::OrderTransitions.by_event(:confirm)
25
- #
26
- class StateTransition < ActiveRecord::Base
27
- # Default table name (can be overridden by dynamic subclasses)
28
- self.table_name = "state_transitions"
29
-
30
- # Polymorphic association
31
- belongs_to :transitionable, polymorphic: true
32
-
33
- # Validations
34
- validates :event, :from_state, :to_state, presence: true
35
-
36
- # Scopes
37
-
38
- # Scope per modello specifico
39
- #
40
- # @param model_class [Class] Classe del modello
41
- # @return [ActiveRecord::Relation]
42
- #
43
- scope :for_model, ->(model_class) {
44
- where(transitionable_type: model_class.name)
45
- }
46
-
47
- # Scope per evento specifico
48
- #
49
- # @param event [Symbol, String] Nome dell'evento
50
- # @return [ActiveRecord::Relation]
51
- #
52
- scope :by_event, ->(event) {
53
- where(event: event.to_s)
54
- }
55
-
56
- # Scope per stato di partenza
57
- #
58
- # @param state [Symbol, String] Stato di partenza
59
- # @return [ActiveRecord::Relation]
60
- #
61
- scope :from_state, ->(state) {
62
- where(from_state: state.to_s)
63
- }
64
-
65
- # Scope per stato di arrivo
66
- #
67
- # @param state [Symbol, String] Stato di arrivo
68
- # @return [ActiveRecord::Relation]
69
- #
70
- scope :to_state, ->(state) {
71
- where(to_state: state.to_s)
72
- }
73
-
74
- # Scope per transizioni recenti
75
- #
76
- # @param duration [ActiveSupport::Duration] Durata (es. 7.days)
77
- # @return [ActiveRecord::Relation]
78
- #
79
- scope :recent, ->(duration = 7.days) {
80
- where("created_at >= ?", duration.ago)
81
- }
82
-
83
- # Scope per transizioni in un periodo
84
- #
85
- # @param start_time [Time, Date] Inizio periodo
86
- # @param end_time [Time, Date] Fine periodo
87
- # @return [ActiveRecord::Relation]
88
- #
89
- scope :between, ->(start_time, end_time) {
90
- where(created_at: start_time..end_time)
91
- }
92
-
93
- # Metodi di istanza
94
-
95
- # Formatted description della transizione
96
- #
97
- # @return [String]
98
- #
99
- def description
100
- "#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
101
- end
102
-
103
- # Alias per retrocompatibilità
104
- alias_method :to_s, :description
105
- end
106
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BetterModel
4
- module Stateable
5
- # Base error for all Stateable errors
6
- class StateableError < StandardError; end
7
-
8
- # Raised when Stateable is not enabled but methods are called
9
- class NotEnabledError < StateableError
10
- def initialize(msg = nil)
11
- super(msg || "Stateable is not enabled. Add 'stateable do...end' to your model.")
12
- end
13
- end
14
-
15
- # Raised when an invalid state is referenced
16
- class InvalidStateError < StateableError
17
- def initialize(state)
18
- super("Invalid state: #{state.inspect}")
19
- end
20
- end
21
-
22
- # Raised when trying to transition to an invalid state from current state
23
- class InvalidTransitionError < StateableError
24
- def initialize(event, from_state, to_state)
25
- super("Cannot transition from #{from_state.inspect} to #{to_state.inspect} via #{event.inspect}")
26
- end
27
- end
28
-
29
- # Raised when a check condition fails
30
- class CheckFailedError < StateableError
31
- def initialize(event, check_description = nil)
32
- msg = "Check failed for transition #{event.inspect}"
33
- msg += ": #{check_description}" if check_description
34
- super(msg)
35
- end
36
- end
37
-
38
- # Alias for backwards compatibility
39
- GuardFailedError = CheckFailedError
40
-
41
- # Raised when a transition validation fails
42
- class ValidationFailedError < StateableError
43
- def initialize(event, errors)
44
- super("Validation failed for transition #{event.inspect}: #{errors.full_messages.join(', ')}")
45
- end
46
- end
47
- end
48
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module BetterModel
4
- module Validatable
5
- # Validator per business rules custom
6
- #
7
- # Permette di eseguire metodi custom come validatori, delegando la logica
8
- # di validazione complessa a metodi del modello.
9
- #
10
- # Il metodo della business rule deve aggiungere errori tramite `errors.add`
11
- # se la validazione fallisce.
12
- #
13
- # Esempio:
14
- # validates_with BusinessRuleValidator, rule_name: :valid_category
15
- #
16
- # # Nel modello:
17
- # def valid_category
18
- # unless Category.exists?(id: category_id)
19
- # errors.add(:category_id, "must be a valid category")
20
- # end
21
- # end
22
- #
23
- class BusinessRuleValidator < ActiveModel::Validator
24
- def initialize(options)
25
- super
26
-
27
- @rule_name = options[:rule_name]
28
-
29
- unless @rule_name
30
- raise ArgumentError, "BusinessRuleValidator requires :rule_name option"
31
- end
32
- end
33
-
34
- def validate(record)
35
- # Verifica che il metodo esista
36
- unless record.respond_to?(@rule_name, true)
37
- raise NoMethodError, "Business rule method '#{@rule_name}' not found in #{record.class.name}. " \
38
- "Define it in your model: def #{@rule_name}; ...; end"
39
- end
40
-
41
- # Esegui il metodo della business rule
42
- # Il metodo stesso è responsabile di aggiungere errori tramite errors.add
43
- record.send(@rule_name)
44
- end
45
- end
46
- end
47
- end
@@ -1,77 +0,0 @@
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
@@ -1,66 +0,0 @@
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 in this version
49
- # This method overrides ActiveRecord's changed? to accept a field_name parameter
50
- #
51
- # @param field_name [Symbol, String, nil] Field name (if nil, calls ActiveRecord's changed?)
52
- # @return [Boolean]
53
- def changed?(field_name = nil)
54
- return super() if field_name.nil?
55
-
56
- object_changes&.key?(field_name.to_s) || false
57
- end
58
-
59
- # Get list of changed fields
60
- #
61
- # @return [Array<String>]
62
- def changed_fields
63
- object_changes&.keys || []
64
- end
65
- end
66
- end