better_model 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +274 -208
- data/lib/better_model/archivable.rb +203 -92
- data/lib/better_model/errors/archivable/already_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/archivable_error.rb +13 -0
- data/lib/better_model/errors/archivable/configuration_error.rb +10 -0
- data/lib/better_model/errors/archivable/not_archived_error.rb +11 -0
- data/lib/better_model/errors/archivable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/better_model_error.rb +9 -0
- data/lib/better_model/errors/permissible/configuration_error.rb +9 -0
- data/lib/better_model/errors/permissible/permissible_error.rb +13 -0
- data/lib/better_model/errors/predicable/configuration_error.rb +9 -0
- data/lib/better_model/errors/predicable/predicable_error.rb +13 -0
- data/lib/better_model/errors/searchable/configuration_error.rb +9 -0
- data/lib/better_model/errors/searchable/invalid_order_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_pagination_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_predicate_error.rb +11 -0
- data/lib/better_model/errors/searchable/invalid_security_error.rb +11 -0
- data/lib/better_model/errors/searchable/searchable_error.rb +13 -0
- data/lib/better_model/errors/sortable/configuration_error.rb +10 -0
- data/lib/better_model/errors/sortable/sortable_error.rb +13 -0
- data/lib/better_model/errors/stateable/check_failed_error.rb +14 -0
- data/lib/better_model/errors/stateable/configuration_error.rb +10 -0
- data/lib/better_model/errors/stateable/invalid_state_error.rb +11 -0
- data/lib/better_model/errors/stateable/invalid_transition_error.rb +11 -0
- data/lib/better_model/errors/stateable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/stateable/stateable_error.rb +13 -0
- data/lib/better_model/errors/stateable/validation_failed_error.rb +11 -0
- data/lib/better_model/errors/statusable/configuration_error.rb +9 -0
- data/lib/better_model/errors/statusable/statusable_error.rb +13 -0
- data/lib/better_model/errors/taggable/configuration_error.rb +10 -0
- data/lib/better_model/errors/taggable/taggable_error.rb +13 -0
- data/lib/better_model/errors/traceable/configuration_error.rb +10 -0
- data/lib/better_model/errors/traceable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/traceable/traceable_error.rb +13 -0
- data/lib/better_model/errors/validatable/configuration_error.rb +10 -0
- data/lib/better_model/errors/validatable/not_enabled_error.rb +11 -0
- data/lib/better_model/errors/validatable/validatable_error.rb +13 -0
- data/lib/better_model/models/state_transition.rb +122 -0
- data/lib/better_model/models/version.rb +68 -0
- data/lib/better_model/permissible.rb +103 -52
- data/lib/better_model/predicable.rb +142 -131
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +123 -96
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +103 -85
- data/lib/better_model/stateable/guard.rb +41 -21
- data/lib/better_model/stateable/transition.rb +64 -35
- data/lib/better_model/stateable.rb +43 -25
- data/lib/better_model/statusable.rb +84 -52
- data/lib/better_model/taggable.rb +120 -75
- data/lib/better_model/traceable.rb +56 -48
- data/lib/better_model/validatable/configurator.rb +54 -177
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -9
- data/lib/generators/better_model/repository/repository_generator.rb +141 -0
- data/lib/generators/better_model/repository/templates/application_repository.rb.tt +21 -0
- data/lib/generators/better_model/repository/templates/repository.rb.tt +71 -0
- data/lib/generators/better_model/stateable/templates/README +1 -1
- metadata +45 -14
- data/lib/better_model/schedulable/occurrence_calculator.rb +0 -1034
- data/lib/better_model/schedulable/schedule_builder.rb +0 -269
- data/lib/better_model/schedulable.rb +0 -356
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -45
- data/lib/better_model/validatable/business_rule_validator.rb +0 -47
- data/lib/better_model/validatable/order_validator.rb +0 -77
- data/lib/better_model/version_record.rb +0 -66
- data/lib/generators/better_model/taggable/taggable_generator.rb +0 -129
- data/lib/generators/better_model/taggable/templates/README.tt +0 -62
- data/lib/generators/better_model/taggable/templates/migration.rb.tt +0 -21
|
@@ -1,56 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "errors/validatable/validatable_error"
|
|
4
|
+
require_relative "errors/validatable/not_enabled_error"
|
|
5
|
+
require_relative "errors/validatable/configuration_error"
|
|
6
|
+
|
|
7
|
+
# Validatable - Declarative validation system for Rails models.
|
|
4
8
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
9
|
+
# This concern enables defining validations declaratively and readably,
|
|
10
|
+
# with support for conditional validations, groups, cross-field, and business rules.
|
|
7
11
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
12
|
+
# @note OPT-IN APPROACH
|
|
13
|
+
# Declarative validations are not enabled automatically.
|
|
14
|
+
# You must explicitly call `validatable do...end` in your model.
|
|
10
15
|
#
|
|
11
|
-
#
|
|
16
|
+
# @example Basic Usage
|
|
12
17
|
# class Article < ApplicationRecord
|
|
13
18
|
# include BetterModel
|
|
14
19
|
#
|
|
15
|
-
# # Status (
|
|
20
|
+
# # Status (from Statusable)
|
|
16
21
|
# is :draft, -> { status == "draft" }
|
|
17
22
|
# is :published, -> { status == "published" }
|
|
18
23
|
#
|
|
19
|
-
# #
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
24
|
+
# # Register complex validations
|
|
25
|
+
# register_complex_validation :valid_date_range do
|
|
26
|
+
# return if starts_at.blank? || ends_at.blank?
|
|
27
|
+
# errors.add(:starts_at, "must be before end date") if starts_at >= ends_at
|
|
28
|
+
# end
|
|
23
29
|
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# end
|
|
30
|
+
# # Enable validatable (opt-in)
|
|
31
|
+
# validatable do
|
|
32
|
+
# # Basic validations
|
|
33
|
+
# check :title, :content, presence: true
|
|
29
34
|
#
|
|
30
|
-
# #
|
|
31
|
-
#
|
|
35
|
+
# # Conditional validations (using Rails options)
|
|
36
|
+
# check :published_at, presence: true, if: -> { status == "published" }
|
|
37
|
+
# check :author_id, presence: true, if: :is_published?
|
|
32
38
|
#
|
|
33
|
-
# #
|
|
34
|
-
#
|
|
39
|
+
# # Complex validations for cross-field and business logic
|
|
40
|
+
# check_complex :valid_date_range
|
|
35
41
|
#
|
|
36
|
-
# #
|
|
42
|
+
# # Validation groups
|
|
37
43
|
# validation_group :step1, [:email, :password]
|
|
38
44
|
# validation_group :step2, [:first_name, :last_name]
|
|
39
45
|
# end
|
|
40
46
|
# end
|
|
41
47
|
#
|
|
42
|
-
#
|
|
43
|
-
# article.valid? #
|
|
44
|
-
# article.valid?(:step1) #
|
|
48
|
+
# @example Validation Usage
|
|
49
|
+
# article.valid? # All validations
|
|
50
|
+
# article.valid?(:step1) # Only step1 group
|
|
45
51
|
#
|
|
46
52
|
module BetterModel
|
|
47
53
|
module Validatable
|
|
48
54
|
extend ActiveSupport::Concern
|
|
49
55
|
|
|
50
56
|
included do
|
|
51
|
-
#
|
|
57
|
+
# Validate ActiveRecord inheritance
|
|
52
58
|
unless ancestors.include?(ActiveRecord::Base)
|
|
53
|
-
raise
|
|
59
|
+
raise BetterModel::Errors::Validatable::ConfigurationError, "Invalid configuration"
|
|
54
60
|
end
|
|
55
61
|
|
|
56
62
|
# Configurazione validatable (opt-in)
|
|
@@ -58,6 +64,8 @@ module BetterModel
|
|
|
58
64
|
class_attribute :validatable_config, default: {}.freeze
|
|
59
65
|
class_attribute :validatable_groups, default: {}.freeze
|
|
60
66
|
class_attribute :_validatable_setup_done, default: false
|
|
67
|
+
# Registry dei complex validations custom
|
|
68
|
+
class_attribute :complex_validations_registry, default: {}.freeze
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
class_methods do
|
|
@@ -65,14 +73,12 @@ module BetterModel
|
|
|
65
73
|
#
|
|
66
74
|
# @example Attivazione base
|
|
67
75
|
# validatable do
|
|
68
|
-
#
|
|
76
|
+
# check :title, presence: true
|
|
69
77
|
# end
|
|
70
78
|
#
|
|
71
79
|
# @example Con validazioni condizionali
|
|
72
80
|
# validatable do
|
|
73
|
-
#
|
|
74
|
-
# validate :published_at, presence: true
|
|
75
|
-
# end
|
|
81
|
+
# check :published_at, presence: true, if: :is_published?
|
|
76
82
|
# end
|
|
77
83
|
#
|
|
78
84
|
def validatable(&block)
|
|
@@ -99,98 +105,71 @@ module BetterModel
|
|
|
99
105
|
# Verifica se validatable è attivo
|
|
100
106
|
#
|
|
101
107
|
# @return [Boolean]
|
|
102
|
-
def validatable_enabled?
|
|
103
|
-
|
|
108
|
+
def validatable_enabled? = validatable_enabled == true
|
|
109
|
+
|
|
110
|
+
# Registra una validazione complessa custom
|
|
111
|
+
#
|
|
112
|
+
# Permette di definire validazioni complesse riutilizzabili che possono combinare
|
|
113
|
+
# più campi o utilizzare logica custom non coperta dalle validazioni standard.
|
|
114
|
+
#
|
|
115
|
+
# @param name [Symbol] il nome della validazione
|
|
116
|
+
# @param block [Proc] il blocco di validazione che verrà eseguito nel contesto dell'istanza
|
|
117
|
+
#
|
|
118
|
+
# @example Validazione complessa base
|
|
119
|
+
# register_complex_validation :valid_pricing do
|
|
120
|
+
# if sale_price.present? && sale_price >= price
|
|
121
|
+
# errors.add(:sale_price, "must be less than regular price")
|
|
122
|
+
# end
|
|
123
|
+
# end
|
|
124
|
+
#
|
|
125
|
+
# @example Con logica multi-campo
|
|
126
|
+
# register_complex_validation :valid_dates do
|
|
127
|
+
# if starts_at.present? && ends_at.present? && starts_at >= ends_at
|
|
128
|
+
# errors.add(:ends_at, "must be after start date")
|
|
129
|
+
# end
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
def register_complex_validation(name, &block)
|
|
133
|
+
unless block_given?
|
|
134
|
+
raise BetterModel::Errors::Validatable::ConfigurationError, "Invalid configuration"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Registra nel registry
|
|
138
|
+
self.complex_validations_registry = complex_validations_registry.merge(name.to_sym => block).freeze
|
|
104
139
|
end
|
|
105
140
|
|
|
141
|
+
# Verifica se una validazione complessa è stata registrata
|
|
142
|
+
#
|
|
143
|
+
# @param name [Symbol] il nome della validazione
|
|
144
|
+
# @return [Boolean]
|
|
145
|
+
def complex_validation?(name) = complex_validations_registry.key?(name.to_sym)
|
|
146
|
+
|
|
106
147
|
private
|
|
107
148
|
|
|
108
149
|
# Applica le configurazioni di validazione al modello
|
|
109
150
|
def apply_validatable_config
|
|
110
151
|
return unless validatable_config.present?
|
|
111
152
|
|
|
112
|
-
# Apply
|
|
113
|
-
validatable_config[:
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Apply order validations
|
|
118
|
-
validatable_config[:order_validations]&.each do |order_val|
|
|
119
|
-
apply_order_validation(order_val)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
# Apply business rules
|
|
123
|
-
validatable_config[:business_rules]&.each do |rule|
|
|
124
|
-
apply_business_rule(rule)
|
|
153
|
+
# Apply complex validations
|
|
154
|
+
validatable_config[:complex_validations]&.each do |name|
|
|
155
|
+
apply_complex_validation(name)
|
|
125
156
|
end
|
|
126
157
|
end
|
|
127
158
|
|
|
128
|
-
# Applica una validazione
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
validations = conditional[:validations]
|
|
159
|
+
# Applica una validazione complessa
|
|
160
|
+
def apply_complex_validation(name)
|
|
161
|
+
block = complex_validations_registry[name]
|
|
162
|
+
return unless block
|
|
133
163
|
|
|
134
|
-
#
|
|
164
|
+
# Crea un validator custom per questa validazione complessa
|
|
135
165
|
validate do
|
|
136
|
-
|
|
137
|
-
send(condition)
|
|
138
|
-
elsif condition.is_a?(Proc)
|
|
139
|
-
instance_exec(&condition)
|
|
140
|
-
else
|
|
141
|
-
raise ArgumentError, "Condition must be a Symbol or Proc"
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
condition_met = !condition_met if negate
|
|
145
|
-
|
|
146
|
-
if condition_met
|
|
147
|
-
validations.each do |validation|
|
|
148
|
-
apply_validation_in_context(validation)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
166
|
+
instance_eval(&block)
|
|
151
167
|
end
|
|
152
168
|
end
|
|
153
|
-
|
|
154
|
-
# Applica una validazione order (cross-field)
|
|
155
|
-
def apply_order_validation(order_val)
|
|
156
|
-
validates_with BetterModel::Validatable::OrderValidator,
|
|
157
|
-
attributes: [ order_val[:first_field] ],
|
|
158
|
-
second_field: order_val[:second_field],
|
|
159
|
-
comparator: order_val[:comparator],
|
|
160
|
-
**order_val[:options]
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Applica una business rule
|
|
164
|
-
def apply_business_rule(rule)
|
|
165
|
-
validates_with BetterModel::Validatable::BusinessRuleValidator,
|
|
166
|
-
rule_name: rule[:name],
|
|
167
|
-
**rule[:options]
|
|
168
|
-
end
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
# Metodi di istanza
|
|
172
172
|
|
|
173
|
-
# Apply a validation in the context of the current instance
|
|
174
|
-
def apply_validation_in_context(validation)
|
|
175
|
-
fields = validation[:fields]
|
|
176
|
-
options = validation[:options]
|
|
177
|
-
|
|
178
|
-
fields.each do |field|
|
|
179
|
-
options.each do |validator_type, validator_options|
|
|
180
|
-
# Prepare validator options
|
|
181
|
-
# If validator_options is true, convert to empty hash
|
|
182
|
-
# If it's a hash, use as-is
|
|
183
|
-
opts = validator_options.is_a?(Hash) ? validator_options : {}
|
|
184
|
-
|
|
185
|
-
validator = ActiveModel::Validations.const_get("#{validator_type.to_s.camelize}Validator").new(
|
|
186
|
-
attributes: [ field ],
|
|
187
|
-
**opts
|
|
188
|
-
)
|
|
189
|
-
validator.validate(self)
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
173
|
# Override valid? per supportare validation groups
|
|
195
174
|
#
|
|
196
175
|
# @param context [Symbol, nil] Context o gruppo di validazione
|
|
@@ -210,7 +189,9 @@ module BetterModel
|
|
|
210
189
|
# @param group_name [Symbol] Nome del gruppo
|
|
211
190
|
# @return [Boolean]
|
|
212
191
|
def validate_group(group_name)
|
|
213
|
-
|
|
192
|
+
unless self.class.validatable_enabled?
|
|
193
|
+
raise BetterModel::Errors::Validatable::NotEnabledError, "Module is not enabled"
|
|
194
|
+
end
|
|
214
195
|
|
|
215
196
|
group = self.class.validatable_groups[group_name]
|
|
216
197
|
return false unless group
|
|
@@ -231,7 +212,9 @@ module BetterModel
|
|
|
231
212
|
# @param group_name [Symbol] Nome del gruppo
|
|
232
213
|
# @return [ActiveModel::Errors]
|
|
233
214
|
def errors_for_group(group_name)
|
|
234
|
-
|
|
215
|
+
unless self.class.validatable_enabled?
|
|
216
|
+
raise BetterModel::Errors::Validatable::NotEnabledError, "Module is not enabled"
|
|
217
|
+
end
|
|
235
218
|
|
|
236
219
|
group = self.class.validatable_groups[group_name]
|
|
237
220
|
return errors unless group
|
|
@@ -259,12 +242,4 @@ module BetterModel
|
|
|
259
242
|
end
|
|
260
243
|
end
|
|
261
244
|
|
|
262
|
-
# Errori custom
|
|
263
|
-
class ValidatableError < StandardError; end
|
|
264
|
-
|
|
265
|
-
class ValidatableNotEnabledError < ValidatableError
|
|
266
|
-
def initialize(msg = nil)
|
|
267
|
-
super(msg || "Validatable is not enabled. Add 'validatable do...end' to your model.")
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
245
|
end
|
data/lib/better_model/version.rb
CHANGED
data/lib/better_model.rb
CHANGED
|
@@ -1,27 +1,61 @@
|
|
|
1
1
|
require "better_model/version"
|
|
2
2
|
require "better_model/railtie"
|
|
3
|
+
|
|
4
|
+
# Load all error classes first
|
|
5
|
+
require "better_model/errors/better_model_error"
|
|
6
|
+
require "better_model/errors/archivable/archivable_error"
|
|
7
|
+
require "better_model/errors/archivable/already_archived_error"
|
|
8
|
+
require "better_model/errors/archivable/not_archived_error"
|
|
9
|
+
require "better_model/errors/archivable/not_enabled_error"
|
|
10
|
+
require "better_model/errors/validatable/validatable_error"
|
|
11
|
+
require "better_model/errors/validatable/not_enabled_error"
|
|
12
|
+
require "better_model/errors/traceable/traceable_error"
|
|
13
|
+
require "better_model/errors/traceable/not_enabled_error"
|
|
14
|
+
require "better_model/errors/searchable/searchable_error"
|
|
15
|
+
require "better_model/errors/searchable/invalid_predicate_error"
|
|
16
|
+
require "better_model/errors/searchable/invalid_order_error"
|
|
17
|
+
require "better_model/errors/searchable/invalid_pagination_error"
|
|
18
|
+
require "better_model/errors/searchable/invalid_security_error"
|
|
19
|
+
require "better_model/errors/stateable/stateable_error"
|
|
20
|
+
require "better_model/errors/stateable/not_enabled_error"
|
|
21
|
+
require "better_model/errors/stateable/invalid_state_error"
|
|
22
|
+
require "better_model/errors/stateable/invalid_transition_error"
|
|
23
|
+
require "better_model/errors/stateable/check_failed_error"
|
|
24
|
+
require "better_model/errors/stateable/validation_failed_error"
|
|
25
|
+
require "better_model/errors/stateable/configuration_error"
|
|
26
|
+
require "better_model/errors/archivable/configuration_error"
|
|
27
|
+
require "better_model/errors/validatable/configuration_error"
|
|
28
|
+
require "better_model/errors/traceable/configuration_error"
|
|
29
|
+
require "better_model/errors/searchable/configuration_error"
|
|
30
|
+
require "better_model/errors/predicable/predicable_error"
|
|
31
|
+
require "better_model/errors/predicable/configuration_error"
|
|
32
|
+
require "better_model/errors/sortable/sortable_error"
|
|
33
|
+
require "better_model/errors/sortable/configuration_error"
|
|
34
|
+
require "better_model/errors/statusable/statusable_error"
|
|
35
|
+
require "better_model/errors/statusable/configuration_error"
|
|
36
|
+
require "better_model/errors/permissible/permissible_error"
|
|
37
|
+
require "better_model/errors/permissible/configuration_error"
|
|
38
|
+
require "better_model/errors/taggable/taggable_error"
|
|
39
|
+
require "better_model/errors/taggable/configuration_error"
|
|
40
|
+
|
|
41
|
+
# Load modules
|
|
3
42
|
require "better_model/statusable"
|
|
4
43
|
require "better_model/permissible"
|
|
5
44
|
require "better_model/sortable"
|
|
6
45
|
require "better_model/predicable"
|
|
7
46
|
require "better_model/searchable"
|
|
8
47
|
require "better_model/archivable"
|
|
9
|
-
require "better_model/
|
|
48
|
+
require "better_model/models/version"
|
|
10
49
|
require "better_model/traceable"
|
|
11
50
|
require "better_model/validatable"
|
|
12
51
|
require "better_model/validatable/configurator"
|
|
13
|
-
require "better_model/
|
|
14
|
-
require "better_model/validatable/business_rule_validator"
|
|
15
|
-
require "better_model/state_transition"
|
|
52
|
+
require "better_model/models/state_transition"
|
|
16
53
|
require "better_model/stateable"
|
|
17
54
|
require "better_model/stateable/configurator"
|
|
18
|
-
require "better_model/stateable/errors"
|
|
19
55
|
require "better_model/stateable/guard"
|
|
20
56
|
require "better_model/stateable/transition"
|
|
21
57
|
require "better_model/taggable"
|
|
22
|
-
require "better_model/
|
|
23
|
-
require "better_model/schedulable/schedule_builder"
|
|
24
|
-
require "better_model/schedulable/occurrence_calculator"
|
|
58
|
+
require "better_model/repositable"
|
|
25
59
|
|
|
26
60
|
module BetterModel
|
|
27
61
|
extend ActiveSupport::Concern
|
|
@@ -38,6 +72,5 @@ module BetterModel
|
|
|
38
72
|
include BetterModel::Validatable
|
|
39
73
|
include BetterModel::Stateable
|
|
40
74
|
include BetterModel::Taggable
|
|
41
|
-
include BetterModel::Schedulable
|
|
42
75
|
end
|
|
43
76
|
end
|
|
@@ -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 -%>
|