rails_console_pro 0.1.3 → 0.1.4
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/.rspec_status +259 -232
- data/CHANGELOG.md +3 -0
- data/QUICK_START.md +9 -0
- data/README.md +27 -0
- data/docs/MODEL_INTROSPECTION.md +371 -0
- data/docs/QUERY_BUILDER.md +385 -0
- data/lib/rails_console_pro/commands/compare_command.rb +151 -0
- data/lib/rails_console_pro/commands/introspect_command.rb +220 -0
- data/lib/rails_console_pro/commands/query_builder_command.rb +43 -0
- data/lib/rails_console_pro/commands.rb +15 -0
- data/lib/rails_console_pro/compare_result.rb +81 -0
- data/lib/rails_console_pro/configuration.rb +12 -0
- data/lib/rails_console_pro/format_exporter.rb +24 -0
- data/lib/rails_console_pro/global_methods.rb +12 -0
- data/lib/rails_console_pro/initializer.rb +18 -1
- data/lib/rails_console_pro/introspect_result.rb +101 -0
- data/lib/rails_console_pro/printers/compare_printer.rb +138 -0
- data/lib/rails_console_pro/printers/introspect_printer.rb +282 -0
- data/lib/rails_console_pro/printers/query_builder_printer.rb +81 -0
- data/lib/rails_console_pro/query_builder.rb +197 -0
- data/lib/rails_console_pro/query_builder_result.rb +66 -0
- data/lib/rails_console_pro/serializers/compare_serializer.rb +66 -0
- data/lib/rails_console_pro/serializers/introspect_serializer.rb +99 -0
- data/lib/rails_console_pro/serializers/query_builder_serializer.rb +35 -0
- data/lib/rails_console_pro/services/introspection_collector.rb +420 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +1 -0
- data/lib/rails_console_pro/snippets.rb +1 -0
- data/lib/rails_console_pro/version.rb +1 -1
- metadata +17 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object for query builder results
|
|
5
|
+
class QueryBuilderResult
|
|
6
|
+
attr_reader :relation, :sql, :explain_result, :statistics, :model_class
|
|
7
|
+
|
|
8
|
+
def initialize(relation:, sql:, explain_result: nil, statistics: {}, model_class: nil)
|
|
9
|
+
@relation = relation
|
|
10
|
+
@sql = sql
|
|
11
|
+
@explain_result = explain_result
|
|
12
|
+
@statistics = statistics
|
|
13
|
+
@model_class = model_class || (relation.respond_to?(:klass) ? relation.klass : nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def analyze
|
|
17
|
+
return self if explain_result
|
|
18
|
+
return self if sql.nil? # Can't analyze if SQL generation failed
|
|
19
|
+
|
|
20
|
+
explain_cmd = Commands::ExplainCommand.new
|
|
21
|
+
@explain_result = explain_cmd.execute(relation)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute
|
|
26
|
+
return nil if sql.nil?
|
|
27
|
+
relation.load
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_a
|
|
31
|
+
return [] if sql.nil?
|
|
32
|
+
relation.to_a
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def count
|
|
36
|
+
return 0 if sql.nil?
|
|
37
|
+
relation.count
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exists?
|
|
41
|
+
return false if sql.nil?
|
|
42
|
+
relation.exists?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Export to JSON
|
|
46
|
+
def to_json(pretty: true)
|
|
47
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Export to YAML
|
|
51
|
+
def to_yaml
|
|
52
|
+
FormatExporter.to_yaml(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Export to HTML
|
|
56
|
+
def to_html(style: :default)
|
|
57
|
+
FormatExporter.to_html(self, title: "Query Builder: #{model_class.name}", style: style)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Export to file
|
|
61
|
+
def export_to_file(file_path, format: nil)
|
|
62
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for CompareResult objects
|
|
6
|
+
class CompareSerializer < BaseSerializer
|
|
7
|
+
def serialize(compare_result)
|
|
8
|
+
result = {
|
|
9
|
+
timestamp: compare_result.timestamp&.iso8601,
|
|
10
|
+
total_strategies: compare_result.comparisons.size,
|
|
11
|
+
fastest: compare_result.fastest_name,
|
|
12
|
+
slowest: compare_result.slowest_name,
|
|
13
|
+
performance_ratio: compare_result.performance_ratio,
|
|
14
|
+
has_errors: compare_result.has_errors?,
|
|
15
|
+
error_count: compare_result.error_count,
|
|
16
|
+
total_queries: compare_result.total_queries,
|
|
17
|
+
comparisons: serialize_comparisons(compare_result.comparisons)
|
|
18
|
+
}
|
|
19
|
+
result[:winner] = serialize_comparison(compare_result.winner) if compare_result.winner
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def serialize_comparisons(comparisons)
|
|
26
|
+
Array(comparisons).map { |c| serialize_comparison(c) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def serialize_comparison(comparison)
|
|
30
|
+
return nil unless comparison
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
name: comparison.name,
|
|
34
|
+
duration_ms: comparison.duration_ms,
|
|
35
|
+
query_count: comparison.query_count,
|
|
36
|
+
memory_usage_kb: comparison.memory_usage_kb,
|
|
37
|
+
error: serialize_error(comparison.error),
|
|
38
|
+
sql_queries: serialize_sql_queries(comparison.sql_queries),
|
|
39
|
+
result: serialize_data(comparison.result)
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def serialize_sql_queries(queries)
|
|
44
|
+
Array(queries).map do |query|
|
|
45
|
+
{
|
|
46
|
+
sql: query[:sql],
|
|
47
|
+
duration_ms: query[:duration_ms],
|
|
48
|
+
name: query[:name],
|
|
49
|
+
cached: query[:cached] || false
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serialize_error(error)
|
|
55
|
+
return nil unless error
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
class: error.class.name,
|
|
59
|
+
message: error.message,
|
|
60
|
+
backtrace: Array(error.backtrace).first(10)
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for introspection results
|
|
6
|
+
class IntrospectSerializer < BaseSerializer
|
|
7
|
+
def serialize(result)
|
|
8
|
+
{
|
|
9
|
+
'type' => 'introspection',
|
|
10
|
+
'model' => result.model.name,
|
|
11
|
+
'callbacks' => serialize_callbacks(result.callbacks),
|
|
12
|
+
'enums' => serialize_enums(result.enums),
|
|
13
|
+
'concerns' => serialize_concerns(result.concerns),
|
|
14
|
+
'scopes' => serialize_scopes(result.scopes),
|
|
15
|
+
'validations' => serialize_validations(result.validations),
|
|
16
|
+
'lifecycle_hooks' => serialize_data(result.lifecycle_hooks),
|
|
17
|
+
'timestamp' => result.timestamp.iso8601,
|
|
18
|
+
'has_callbacks' => result.has_callbacks?,
|
|
19
|
+
'has_enums' => result.has_enums?,
|
|
20
|
+
'has_concerns' => result.has_concerns?,
|
|
21
|
+
'has_scopes' => result.has_scopes?,
|
|
22
|
+
'has_validations' => result.has_validations?
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def serialize_callbacks(callbacks)
|
|
29
|
+
callbacks.transform_values do |chain|
|
|
30
|
+
chain.map do |callback|
|
|
31
|
+
{
|
|
32
|
+
'name' => callback[:name].to_s,
|
|
33
|
+
'kind' => callback[:kind].to_s,
|
|
34
|
+
'if' => serialize_condition(callback[:if]),
|
|
35
|
+
'unless' => serialize_condition(callback[:unless])
|
|
36
|
+
}.compact
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serialize_enums(enums)
|
|
42
|
+
enums.transform_values do |data|
|
|
43
|
+
{
|
|
44
|
+
'mapping' => data[:mapping],
|
|
45
|
+
'values' => data[:values],
|
|
46
|
+
'type' => data[:type].to_s
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def serialize_concerns(concerns)
|
|
52
|
+
concerns.map do |concern|
|
|
53
|
+
{
|
|
54
|
+
'name' => concern[:name],
|
|
55
|
+
'type' => concern[:type].to_s,
|
|
56
|
+
'location' => serialize_location(concern[:location])
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def serialize_scopes(scopes)
|
|
62
|
+
scopes.transform_keys(&:to_s).transform_values do |data|
|
|
63
|
+
{
|
|
64
|
+
'sql' => data[:sql],
|
|
65
|
+
'values' => serialize_data(data[:values]),
|
|
66
|
+
'conditions' => data[:conditions]
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def serialize_validations(validations)
|
|
72
|
+
validations.transform_keys(&:to_s).transform_values do |validators|
|
|
73
|
+
validators.map do |validator|
|
|
74
|
+
{
|
|
75
|
+
'type' => validator[:type],
|
|
76
|
+
'attributes' => validator[:attributes].map(&:to_s),
|
|
77
|
+
'options' => serialize_data(validator[:options]),
|
|
78
|
+
'conditions' => serialize_data(validator[:conditions])
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize_condition(condition)
|
|
85
|
+
return nil if condition.nil? || condition.empty?
|
|
86
|
+
condition.map(&:to_s)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def serialize_location(location)
|
|
90
|
+
return nil if location.nil?
|
|
91
|
+
{
|
|
92
|
+
'file' => location[:file],
|
|
93
|
+
'line' => location[:line]
|
|
94
|
+
}
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Serializers
|
|
5
|
+
# Serializer for QueryBuilderResult objects
|
|
6
|
+
class QueryBuilderSerializer < BaseSerializer
|
|
7
|
+
def serialize(query_builder_result)
|
|
8
|
+
{
|
|
9
|
+
model_class: query_builder_result.model_class.name,
|
|
10
|
+
sql: query_builder_result.sql,
|
|
11
|
+
statistics: query_builder_result.statistics,
|
|
12
|
+
explain_result: serialize_explain(query_builder_result.explain_result)
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def serialize_explain(explain_result)
|
|
19
|
+
return nil unless explain_result
|
|
20
|
+
|
|
21
|
+
if defined?(Serializers::ExplainSerializer)
|
|
22
|
+
Serializers::ExplainSerializer.serialize(explain_result, exporter)
|
|
23
|
+
else
|
|
24
|
+
{
|
|
25
|
+
sql: explain_result.sql,
|
|
26
|
+
execution_time: explain_result.execution_time,
|
|
27
|
+
indexes_used: explain_result.indexes_used,
|
|
28
|
+
recommendations: explain_result.recommendations
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Services
|
|
5
|
+
# Service to collect model introspection data
|
|
6
|
+
class IntrospectionCollector
|
|
7
|
+
attr_reader :model_class
|
|
8
|
+
|
|
9
|
+
def initialize(model_class)
|
|
10
|
+
@model_class = model_class
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Collect all introspection data
|
|
14
|
+
def collect
|
|
15
|
+
{
|
|
16
|
+
callbacks: collect_callbacks,
|
|
17
|
+
enums: collect_enums,
|
|
18
|
+
concerns: collect_concerns,
|
|
19
|
+
scopes: collect_scopes,
|
|
20
|
+
validations: collect_validations,
|
|
21
|
+
lifecycle_hooks: collect_lifecycle_hooks
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Collect all callbacks with their order
|
|
26
|
+
def collect_callbacks
|
|
27
|
+
return {} unless model_class.respond_to?(:_commit_callbacks)
|
|
28
|
+
|
|
29
|
+
callback_types = [
|
|
30
|
+
:before_validation, :after_validation,
|
|
31
|
+
:before_save, :around_save, :after_save,
|
|
32
|
+
:before_create, :around_create, :after_create,
|
|
33
|
+
:before_update, :around_update, :after_update,
|
|
34
|
+
:before_destroy, :around_destroy, :after_destroy,
|
|
35
|
+
:after_commit, :after_rollback,
|
|
36
|
+
:after_find, :after_initialize,
|
|
37
|
+
:after_touch
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
callbacks = {}
|
|
41
|
+
callback_types.each do |type|
|
|
42
|
+
chain = get_callback_chain(type)
|
|
43
|
+
next if chain.empty?
|
|
44
|
+
|
|
45
|
+
callbacks[type] = chain
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
callbacks
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Collect all enums
|
|
52
|
+
def collect_enums
|
|
53
|
+
return {} unless model_class.respond_to?(:defined_enums)
|
|
54
|
+
|
|
55
|
+
model_class.defined_enums.transform_values do |mapping|
|
|
56
|
+
{
|
|
57
|
+
mapping: mapping,
|
|
58
|
+
values: mapping.keys,
|
|
59
|
+
type: detect_enum_type(mapping)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
rescue => e
|
|
63
|
+
{}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Collect all concerns and modules
|
|
67
|
+
def collect_concerns
|
|
68
|
+
return [] unless model_class.respond_to?(:ancestors)
|
|
69
|
+
|
|
70
|
+
concerns = []
|
|
71
|
+
model_class.ancestors.each do |ancestor|
|
|
72
|
+
next if ancestor == model_class
|
|
73
|
+
next if [ActiveRecord::Base, Object, BasicObject, Kernel].include?(ancestor)
|
|
74
|
+
next if ancestor.name.nil? || ancestor.name.empty?
|
|
75
|
+
next if ancestor.name.start_with?('ActiveRecord::', 'ActiveSupport::')
|
|
76
|
+
|
|
77
|
+
# Check if it's a concern or module
|
|
78
|
+
is_concern = ancestor.respond_to?(:included_modules) &&
|
|
79
|
+
ancestor.included_modules.include?(ActiveSupport::Concern)
|
|
80
|
+
|
|
81
|
+
concerns << {
|
|
82
|
+
name: ancestor.name,
|
|
83
|
+
type: is_concern ? :concern : (ancestor.is_a?(Class) ? :class : :module),
|
|
84
|
+
location: source_location_for(ancestor)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
concerns.uniq { |c| c[:name] }
|
|
89
|
+
rescue => e
|
|
90
|
+
[]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Collect all scopes with their SQL
|
|
94
|
+
def collect_scopes
|
|
95
|
+
return {} unless model_class.respond_to?(:scope_attributes?)
|
|
96
|
+
|
|
97
|
+
scopes = {}
|
|
98
|
+
|
|
99
|
+
# Get all singleton methods that might be scopes
|
|
100
|
+
scope_methods = model_class.methods(false) - ActiveRecord::Base.methods(false)
|
|
101
|
+
|
|
102
|
+
scope_methods.each do |method_name|
|
|
103
|
+
next if method_name.to_s.start_with?('_')
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
# Try to call the scope and get its SQL
|
|
107
|
+
scope_result = model_class.public_send(method_name)
|
|
108
|
+
|
|
109
|
+
if scope_result.is_a?(ActiveRecord::Relation)
|
|
110
|
+
scopes[method_name] = {
|
|
111
|
+
sql: scope_result.to_sql,
|
|
112
|
+
values: extract_scope_values(scope_result),
|
|
113
|
+
conditions: extract_scope_conditions(scope_result)
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
rescue ArgumentError, NameError, NoMethodError
|
|
117
|
+
# Skip if it requires arguments or is not a scope
|
|
118
|
+
next
|
|
119
|
+
rescue => e
|
|
120
|
+
# Skip problematic scopes
|
|
121
|
+
next
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
scopes
|
|
126
|
+
rescue => e
|
|
127
|
+
{}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Collect all validations
|
|
131
|
+
def collect_validations
|
|
132
|
+
return [] unless model_class.respond_to?(:validators)
|
|
133
|
+
|
|
134
|
+
validations = []
|
|
135
|
+
|
|
136
|
+
model_class.validators.each do |validator|
|
|
137
|
+
attributes = validator.attributes rescue [:unknown]
|
|
138
|
+
|
|
139
|
+
validations << {
|
|
140
|
+
type: validator.class.name.demodulize,
|
|
141
|
+
attributes: attributes,
|
|
142
|
+
options: extract_validator_options(validator),
|
|
143
|
+
conditions: extract_validator_conditions(validator)
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Group by attribute for better organization
|
|
148
|
+
validations.group_by { |v| v[:attributes].first }
|
|
149
|
+
rescue => e
|
|
150
|
+
[]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Collect lifecycle hooks summary
|
|
154
|
+
def collect_lifecycle_hooks
|
|
155
|
+
{
|
|
156
|
+
callbacks_count: count_callbacks,
|
|
157
|
+
validations_count: count_validations,
|
|
158
|
+
has_observers: has_observers?,
|
|
159
|
+
has_state_machine: has_state_machine?
|
|
160
|
+
}
|
|
161
|
+
rescue => e
|
|
162
|
+
{}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Find where a method is defined
|
|
166
|
+
def method_source_location(method_name)
|
|
167
|
+
return nil unless model_class.respond_to?(method_name)
|
|
168
|
+
|
|
169
|
+
method = if model_class.respond_to?(method_name)
|
|
170
|
+
model_class.method(method_name)
|
|
171
|
+
else
|
|
172
|
+
return nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
location = method.source_location
|
|
176
|
+
return nil unless location
|
|
177
|
+
|
|
178
|
+
{
|
|
179
|
+
file: location[0],
|
|
180
|
+
line: location[1],
|
|
181
|
+
owner: method.owner.name,
|
|
182
|
+
type: determine_method_type(method.owner)
|
|
183
|
+
}
|
|
184
|
+
rescue => e
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
# Get callback chain for a specific type
|
|
191
|
+
def get_callback_chain(type)
|
|
192
|
+
chain_method = "_#{type}_callbacks"
|
|
193
|
+
return [] unless model_class.respond_to?(chain_method)
|
|
194
|
+
|
|
195
|
+
callback_chain = model_class.send(chain_method)
|
|
196
|
+
return [] unless callback_chain.respond_to?(:each)
|
|
197
|
+
|
|
198
|
+
callbacks = []
|
|
199
|
+
callback_chain.each do |callback|
|
|
200
|
+
next unless callback.respond_to?(:filter)
|
|
201
|
+
|
|
202
|
+
filter_name = extract_callback_name(callback)
|
|
203
|
+
next if filter_name.to_s.empty?
|
|
204
|
+
|
|
205
|
+
callback_kind = begin
|
|
206
|
+
callback.kind
|
|
207
|
+
rescue
|
|
208
|
+
:unknown
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
callbacks << {
|
|
212
|
+
name: filter_name,
|
|
213
|
+
kind: callback_kind,
|
|
214
|
+
if: extract_callback_condition(callback, :if),
|
|
215
|
+
unless: extract_callback_condition(callback, :unless)
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
callbacks
|
|
220
|
+
rescue => e
|
|
221
|
+
[]
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Extract callback name
|
|
225
|
+
def extract_callback_name(callback)
|
|
226
|
+
filter = callback.filter
|
|
227
|
+
|
|
228
|
+
case filter
|
|
229
|
+
when Symbol, String
|
|
230
|
+
filter
|
|
231
|
+
when Proc
|
|
232
|
+
"<Proc>"
|
|
233
|
+
else
|
|
234
|
+
if filter.respond_to?(:name)
|
|
235
|
+
filter.name
|
|
236
|
+
else
|
|
237
|
+
filter.class.name
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
rescue => e
|
|
241
|
+
:unknown
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Extract callback condition
|
|
245
|
+
def extract_callback_condition(callback, type)
|
|
246
|
+
return nil unless callback.respond_to?(type)
|
|
247
|
+
|
|
248
|
+
conditions = callback.send(type)
|
|
249
|
+
return nil if conditions.empty?
|
|
250
|
+
|
|
251
|
+
conditions.map do |condition|
|
|
252
|
+
case condition
|
|
253
|
+
when Symbol, String
|
|
254
|
+
condition
|
|
255
|
+
when Proc
|
|
256
|
+
"<Proc>"
|
|
257
|
+
else
|
|
258
|
+
condition.class.name
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
rescue => e
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Detect enum type (integer or string)
|
|
266
|
+
def detect_enum_type(mapping)
|
|
267
|
+
return :integer if mapping.values.first.is_a?(Integer)
|
|
268
|
+
return :string if mapping.values.first.is_a?(String)
|
|
269
|
+
:unknown
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Extract scope values
|
|
273
|
+
def extract_scope_values(scope)
|
|
274
|
+
return {} unless scope.respond_to?(:values)
|
|
275
|
+
|
|
276
|
+
values = scope.values
|
|
277
|
+
{
|
|
278
|
+
where: values[:where]&.to_s || nil,
|
|
279
|
+
order: values[:order]&.to_s || nil,
|
|
280
|
+
limit: values[:limit],
|
|
281
|
+
offset: values[:offset],
|
|
282
|
+
includes: values[:includes]&.to_s || nil,
|
|
283
|
+
joins: values[:joins]&.to_s || nil
|
|
284
|
+
}.compact
|
|
285
|
+
rescue => e
|
|
286
|
+
{}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Extract scope conditions
|
|
290
|
+
def extract_scope_conditions(scope)
|
|
291
|
+
return [] unless scope.respond_to?(:where_clause)
|
|
292
|
+
|
|
293
|
+
predicates = scope.where_clause.send(:predicates) rescue []
|
|
294
|
+
predicates.map(&:to_s).compact
|
|
295
|
+
rescue => e
|
|
296
|
+
[]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Extract validator options
|
|
300
|
+
def extract_validator_options(validator)
|
|
301
|
+
options = {}
|
|
302
|
+
|
|
303
|
+
# Common validation options
|
|
304
|
+
[:allow_nil, :allow_blank, :on, :strict, :message].each do |opt|
|
|
305
|
+
options[opt] = validator.options[opt] if validator.options.key?(opt)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Type-specific options
|
|
309
|
+
case validator
|
|
310
|
+
when ActiveModel::Validations::LengthValidator
|
|
311
|
+
[:minimum, :maximum, :in, :within, :is].each do |opt|
|
|
312
|
+
options[opt] = validator.options[opt] if validator.options.key?(opt)
|
|
313
|
+
end
|
|
314
|
+
when ActiveModel::Validations::NumericalityValidator
|
|
315
|
+
[:greater_than, :greater_than_or_equal_to, :less_than, :less_than_or_equal_to,
|
|
316
|
+
:equal_to, :odd, :even, :only_integer].each do |opt|
|
|
317
|
+
options[opt] = validator.options[opt] if validator.options.key?(opt)
|
|
318
|
+
end
|
|
319
|
+
when ActiveRecord::Validations::UniquenessValidator
|
|
320
|
+
options[:scope] = validator.options[:scope] if validator.options.key?(:scope)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
options
|
|
324
|
+
rescue => e
|
|
325
|
+
{}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Extract validator conditions
|
|
329
|
+
def extract_validator_conditions(validator)
|
|
330
|
+
conditions = {}
|
|
331
|
+
|
|
332
|
+
[:if, :unless].each do |cond|
|
|
333
|
+
next unless validator.options.key?(cond)
|
|
334
|
+
|
|
335
|
+
value = validator.options[cond]
|
|
336
|
+
conditions[cond] = case value
|
|
337
|
+
when Symbol, String
|
|
338
|
+
value
|
|
339
|
+
when Proc
|
|
340
|
+
"<Proc>"
|
|
341
|
+
else
|
|
342
|
+
value.class.name
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
conditions
|
|
347
|
+
rescue => e
|
|
348
|
+
{}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Count all callbacks
|
|
352
|
+
def count_callbacks
|
|
353
|
+
collect_callbacks.values.flatten.count
|
|
354
|
+
rescue => e
|
|
355
|
+
0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Count all validations
|
|
359
|
+
def count_validations
|
|
360
|
+
model_class.validators.count
|
|
361
|
+
rescue => e
|
|
362
|
+
0
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Check if model has observers
|
|
366
|
+
def has_observers?
|
|
367
|
+
# In modern Rails, observers are deprecated
|
|
368
|
+
# This checks for any observer-like patterns
|
|
369
|
+
return false unless defined?(ActiveRecord::Observer)
|
|
370
|
+
|
|
371
|
+
ActiveRecord::Observer.descendants.any? { |obs| obs.observed_classes.include?(model_class) }
|
|
372
|
+
rescue => e
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Check if model has state machine
|
|
377
|
+
def has_state_machine?
|
|
378
|
+
# Check for common state machine gems
|
|
379
|
+
model_class.respond_to?(:state_machines) || # state_machines gem
|
|
380
|
+
model_class.respond_to?(:aasm_states) || # aasm gem
|
|
381
|
+
model_class.respond_to?(:workflow_spec) # workflow gem
|
|
382
|
+
rescue => e
|
|
383
|
+
false
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Get source location for a module/class
|
|
387
|
+
def source_location_for(klass)
|
|
388
|
+
return nil if klass.name.nil?
|
|
389
|
+
|
|
390
|
+
# Try to find where it's defined
|
|
391
|
+
methods = klass.instance_methods(false)
|
|
392
|
+
return nil if methods.empty?
|
|
393
|
+
|
|
394
|
+
method = klass.instance_method(methods.first)
|
|
395
|
+
location = method.source_location
|
|
396
|
+
|
|
397
|
+
return nil unless location
|
|
398
|
+
|
|
399
|
+
{
|
|
400
|
+
file: location[0],
|
|
401
|
+
line: location[1]
|
|
402
|
+
}
|
|
403
|
+
rescue => e
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Determine method type (model, concern, gem, etc.)
|
|
408
|
+
def determine_method_type(owner)
|
|
409
|
+
return :model if owner == model_class
|
|
410
|
+
return :concern if owner.name&.end_with?('Concern')
|
|
411
|
+
return :gem if owner.name&.start_with?('ActiveRecord::', 'ActiveSupport::')
|
|
412
|
+
return :parent if owner < ActiveRecord::Base && owner != ActiveRecord::Base
|
|
413
|
+
:module
|
|
414
|
+
rescue => e
|
|
415
|
+
:unknown
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|