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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Errors
5
+ # Root error class for all BetterModel errors.
6
+ # All module-specific errors inherit from this class.
7
+ class BetterModelError < StandardError; end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Errors
5
+ module Permissible
6
+ class ConfigurationError < ArgumentError; end
7
+ end
8
+ end
9
+ 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 Permissible
8
+ # Base error class for all Permissible-related errors.
9
+ class PermissibleError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Errors
5
+ module Predicable
6
+ class ConfigurationError < ArgumentError; end
7
+ end
8
+ end
9
+ 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 Predicable
8
+ # Base error class for all Predicable-related errors.
9
+ class PredicableError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Errors
5
+ module Searchable
6
+ class ConfigurationError < ArgumentError; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "searchable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Searchable
8
+ class InvalidOrderError < SearchableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "searchable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Searchable
8
+ class InvalidPaginationError < SearchableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "searchable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Searchable
8
+ class InvalidPredicateError < SearchableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "searchable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Searchable
8
+ class InvalidSecurityError < SearchableError; 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 Searchable
8
+ # Base error class for all Searchable-related errors.
9
+ class SearchableError < 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 Sortable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ 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 Sortable
8
+ # Base error class for all Sortable-related errors.
9
+ class SortableError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stateable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Stateable
8
+ class CheckFailedError < StateableError; end
9
+
10
+ # Alias for backward compatibility
11
+ GuardFailedError = CheckFailedError
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ module BetterModel
5
+ module Errors
6
+ module Stateable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stateable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Stateable
8
+ class InvalidStateError < StateableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stateable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Stateable
8
+ class InvalidTransitionError < StateableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stateable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Stateable
8
+ class NotEnabledError < StateableError; 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 Stateable
8
+ # Base error class for all Stateable-related errors.
9
+ class StateableError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stateable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Stateable
8
+ class ValidationFailedError < StateableError; end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Errors
5
+ module Statusable
6
+ class ConfigurationError < ArgumentError; end
7
+ end
8
+ end
9
+ 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 Statusable
8
+ # Base error class for all Statusable-related errors.
9
+ class StatusableError < 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 Taggable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ 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 Taggable
8
+ # Base error class for all Taggable-related errors.
9
+ class TaggableError < 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 Traceable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "traceable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Traceable
8
+ class NotEnabledError < TraceableError; 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 Traceable
8
+ # Base error class for all Traceable-related errors.
9
+ class TraceableError < 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 Validatable
7
+ class ConfigurationError < ArgumentError; end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "validatable_error"
4
+
5
+ module BetterModel
6
+ module Errors
7
+ module Validatable
8
+ class NotEnabledError < ValidatableError; 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 Validatable
8
+ # Base error class for all Validatable-related errors.
9
+ class ValidatableError < BetterModelError
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Models
5
+ # StateTransition - Base ActiveRecord model for state transition history.
6
+ #
7
+ # This is an abstract model. Concrete classes are generated dynamically
8
+ # for each table (state_transitions, order_transitions, etc.).
9
+ #
10
+ # @note Table Schema
11
+ # t.string :transitionable_type, null: false
12
+ # t.integer :transitionable_id, null: false
13
+ # t.string :event, null: false
14
+ # t.string :from_state, null: false
15
+ # t.string :to_state, null: false
16
+ # t.json :metadata
17
+ # t.datetime :created_at, null: false
18
+ #
19
+ # @example Usage
20
+ # # All transitions for a model
21
+ # order.state_transitions
22
+ #
23
+ # @example Global queries (via dynamic classes)
24
+ # BetterModel::Models::StateTransitions.for_model(Order)
25
+ # BetterModel::Models::OrderTransitions.by_event(:confirm)
26
+ #
27
+ class StateTransition < ActiveRecord::Base
28
+ # Default table name (can be overridden by dynamic subclasses)
29
+ self.table_name = "state_transitions"
30
+
31
+ # Polymorphic association
32
+ belongs_to :transitionable, polymorphic: true
33
+
34
+ # Validations
35
+ validates :event, :from_state, :to_state, presence: true
36
+
37
+ # Scopes
38
+
39
+ # Scope for specific model.
40
+ #
41
+ # @param model_class [Class] Model class
42
+ # @return [ActiveRecord::Relation]
43
+ #
44
+ # @example
45
+ # StateTransition.for_model(Order)
46
+ scope :for_model, ->(model_class) {
47
+ where(transitionable_type: model_class.name)
48
+ }
49
+
50
+ # Scope for specific event.
51
+ #
52
+ # @param event [Symbol, String] Event name
53
+ # @return [ActiveRecord::Relation]
54
+ #
55
+ # @example
56
+ # StateTransition.by_event(:confirm)
57
+ scope :by_event, ->(event) {
58
+ where(event: event.to_s)
59
+ }
60
+
61
+ # Scope for source state.
62
+ #
63
+ # @param state [Symbol, String] Source state
64
+ # @return [ActiveRecord::Relation]
65
+ #
66
+ # @example
67
+ # StateTransition.from_state(:pending)
68
+ scope :from_state, ->(state) {
69
+ where(from_state: state.to_s)
70
+ }
71
+
72
+ # Scope for destination state.
73
+ #
74
+ # @param state [Symbol, String] Destination state
75
+ # @return [ActiveRecord::Relation]
76
+ #
77
+ # @example
78
+ # StateTransition.to_state(:confirmed)
79
+ scope :to_state, ->(state) {
80
+ where(to_state: state.to_s)
81
+ }
82
+
83
+ # Scope for recent transitions.
84
+ #
85
+ # @param duration [ActiveSupport::Duration] Time duration (e.g., 7.days)
86
+ # @return [ActiveRecord::Relation]
87
+ #
88
+ # @example
89
+ # StateTransition.recent(7.days)
90
+ scope :recent, ->(duration = 7.days) {
91
+ where("created_at >= ?", duration.ago)
92
+ }
93
+
94
+ # Scope for transitions in a time period.
95
+ #
96
+ # @param start_time [Time, Date] Period start
97
+ # @param end_time [Time, Date] Period end
98
+ # @return [ActiveRecord::Relation]
99
+ #
100
+ # @example
101
+ # StateTransition.between(1.week.ago, Time.current)
102
+ scope :between, ->(start_time, end_time) {
103
+ where(created_at: start_time..end_time)
104
+ }
105
+
106
+ # Instance Methods
107
+
108
+ # Formatted description of the transition.
109
+ #
110
+ # @return [String] Human-readable transition description
111
+ #
112
+ # @example
113
+ # transition.description # => "Order#123: pending -> confirmed (confirm)"
114
+ def description
115
+ "#{transitionable_type}##{transitionable_id}: #{from_state} -> #{to_state} (#{event})"
116
+ end
117
+
118
+ # Alias for backward compatibility
119
+ alias_method :to_s, :description
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterModel
4
+ module Models
5
+ # Version model for tracking changes
6
+ # This is the base AR model for version history
7
+ # Actual table_name is set dynamically in subclasses
8
+ class Version < ActiveRecord::Base
9
+ self.abstract_class = true
10
+
11
+ # Polymorphic association to the tracked model
12
+ belongs_to :item, polymorphic: true, optional: true
13
+
14
+ # Optional: belongs_to user who made the change
15
+ # belongs_to :updated_by, class_name: "User", optional: true
16
+
17
+ # Serialize object_changes as JSON
18
+ # Rails handles this automatically for json/jsonb columns
19
+
20
+ # Validations
21
+ validates :item_type, :event, presence: true
22
+ validates :event, inclusion: { in: %w[created updated destroyed] }
23
+
24
+ # Scopes
25
+ scope :for_item, ->(item) { where(item_type: item.class.name, item_id: item.id) }
26
+ scope :created_events, -> { where(event: "created") }
27
+ scope :updated_events, -> { where(event: "updated") }
28
+ scope :destroyed_events, -> { where(event: "destroyed") }
29
+ scope :by_user, ->(user_id) { where(updated_by_id: user_id) }
30
+ scope :between, ->(start_time, end_time) { where(created_at: start_time..end_time) }
31
+ scope :recent, ->(limit = 10) { order(created_at: :desc).limit(limit) }
32
+
33
+ # Get the change for a specific field
34
+ #
35
+ # @param field_name [Symbol, String] Field name
36
+ # @return [Hash, nil] Hash with :before and :after keys
37
+ def change_for(field_name)
38
+ return nil unless object_changes
39
+
40
+ field = field_name.to_s
41
+ return nil unless object_changes.key?(field)
42
+
43
+ {
44
+ before: object_changes[field][0],
45
+ after: object_changes[field][1]
46
+ }
47
+ end
48
+
49
+ # Check if a specific field changed in this version
50
+ # This method overrides ActiveRecord's changed? to accept a field_name parameter
51
+ #
52
+ # @param field_name [Symbol, String, nil] Field name (if nil, calls ActiveRecord's changed?)
53
+ # @return [Boolean]
54
+ def changed?(field_name = nil)
55
+ return super() if field_name.nil?
56
+
57
+ object_changes&.key?(field_name.to_s) || false
58
+ end
59
+
60
+ # Get list of changed fields
61
+ #
62
+ # @return [Array<String>]
63
+ def changed_fields
64
+ object_changes&.keys || []
65
+ end
66
+ end
67
+ end
68
+ end