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.
- checksums.yaml +4 -4
- data/README.md +96 -13
- data/lib/better_model/archivable.rb +203 -91
- 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 +114 -63
- data/lib/better_model/repositable/base_repository.rb +232 -0
- data/lib/better_model/repositable.rb +32 -0
- data/lib/better_model/searchable.rb +92 -92
- data/lib/better_model/sortable.rb +137 -41
- data/lib/better_model/stateable/configurator.rb +71 -53
- data/lib/better_model/stateable/guard.rb +35 -15
- data/lib/better_model/stateable/transition.rb +59 -30
- data/lib/better_model/stateable.rb +33 -15
- 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 +49 -172
- data/lib/better_model/validatable.rb +88 -113
- data/lib/better_model/version.rb +1 -1
- data/lib/better_model.rb +42 -5
- 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 +44 -7
- data/lib/better_model/state_transition.rb +0 -106
- data/lib/better_model/stateable/errors.rb +0 -48
- 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
|
@@ -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 -%>
|
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:
|
|
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-
|
|
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
|