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,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for model introspection
|
|
6
|
+
class IntrospectCommand < BaseCommand
|
|
7
|
+
def execute(model_class, *options)
|
|
8
|
+
error_message = ModelValidator.validate_for_schema(model_class)
|
|
9
|
+
if error_message
|
|
10
|
+
puts pastel.red("Error: #{error_message}")
|
|
11
|
+
return nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Parse options
|
|
15
|
+
opts = parse_options(options)
|
|
16
|
+
|
|
17
|
+
# Collect introspection data
|
|
18
|
+
collector = Services::IntrospectionCollector.new(model_class)
|
|
19
|
+
data = collector.collect
|
|
20
|
+
|
|
21
|
+
# If specific option requested, handle it
|
|
22
|
+
if opts[:callbacks_only]
|
|
23
|
+
return handle_callbacks_only(model_class, data[:callbacks])
|
|
24
|
+
elsif opts[:enums_only]
|
|
25
|
+
return handle_enums_only(model_class, data[:enums])
|
|
26
|
+
elsif opts[:concerns_only]
|
|
27
|
+
return handle_concerns_only(model_class, data[:concerns])
|
|
28
|
+
elsif opts[:scopes_only]
|
|
29
|
+
return handle_scopes_only(model_class, data[:scopes])
|
|
30
|
+
elsif opts[:validations_only]
|
|
31
|
+
return handle_validations_only(model_class, data[:validations])
|
|
32
|
+
elsif opts[:method_source]
|
|
33
|
+
return handle_method_source(model_class, opts[:method_source])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Return full result
|
|
37
|
+
IntrospectResult.new(
|
|
38
|
+
model: model_class,
|
|
39
|
+
callbacks: data[:callbacks],
|
|
40
|
+
enums: data[:enums],
|
|
41
|
+
concerns: data[:concerns],
|
|
42
|
+
scopes: data[:scopes],
|
|
43
|
+
validations: data[:validations],
|
|
44
|
+
lifecycle_hooks: data[:lifecycle_hooks]
|
|
45
|
+
)
|
|
46
|
+
rescue => e
|
|
47
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :introspect)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_options(options)
|
|
53
|
+
opts = {}
|
|
54
|
+
options.each do |opt|
|
|
55
|
+
case opt
|
|
56
|
+
when :callbacks, 'callbacks'
|
|
57
|
+
opts[:callbacks_only] = true
|
|
58
|
+
when :enums, 'enums'
|
|
59
|
+
opts[:enums_only] = true
|
|
60
|
+
when :concerns, 'concerns'
|
|
61
|
+
opts[:concerns_only] = true
|
|
62
|
+
when :scopes, 'scopes'
|
|
63
|
+
opts[:scopes_only] = true
|
|
64
|
+
when :validations, 'validations'
|
|
65
|
+
opts[:validations_only] = true
|
|
66
|
+
else
|
|
67
|
+
# Check if it's a method name for source lookup
|
|
68
|
+
if opt.is_a?(Symbol) || opt.is_a?(String)
|
|
69
|
+
opts[:method_source] = opt
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
opts
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def handle_callbacks_only(model_class, callbacks)
|
|
77
|
+
if callbacks.empty?
|
|
78
|
+
puts pastel.yellow("No callbacks found for #{model_class.name}")
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
print_callbacks_summary(model_class, callbacks)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_enums_only(model_class, enums)
|
|
87
|
+
if enums.empty?
|
|
88
|
+
puts pastel.yellow("No enums found for #{model_class.name}")
|
|
89
|
+
return nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
print_enums_summary(model_class, enums)
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_concerns_only(model_class, concerns)
|
|
97
|
+
if concerns.empty?
|
|
98
|
+
puts pastel.yellow("No concerns found for #{model_class.name}")
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
print_concerns_summary(model_class, concerns)
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_scopes_only(model_class, scopes)
|
|
107
|
+
if scopes.empty?
|
|
108
|
+
puts pastel.yellow("No scopes found for #{model_class.name}")
|
|
109
|
+
return nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
print_scopes_summary(model_class, scopes)
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handle_validations_only(model_class, validations)
|
|
117
|
+
if validations.empty?
|
|
118
|
+
puts pastel.yellow("No validations found for #{model_class.name}")
|
|
119
|
+
return nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
print_validations_summary(model_class, validations)
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def handle_method_source(model_class, method_name)
|
|
127
|
+
collector = Services::IntrospectionCollector.new(model_class)
|
|
128
|
+
location = collector.method_source_location(method_name)
|
|
129
|
+
|
|
130
|
+
if location.nil?
|
|
131
|
+
puts pastel.yellow("Method '#{method_name}' not found or source location unavailable")
|
|
132
|
+
return nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
print_method_source(method_name, location)
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Quick print methods for specific data
|
|
140
|
+
def print_callbacks_summary(model_class, callbacks)
|
|
141
|
+
puts pastel.bold.bright_blue("Callbacks for #{model_class.name}:")
|
|
142
|
+
puts pastel.dim("─" * 60)
|
|
143
|
+
|
|
144
|
+
callbacks.each do |type, chain|
|
|
145
|
+
puts "\n#{pastel.cyan(type.to_s)}:"
|
|
146
|
+
chain.each_with_index do |callback, index|
|
|
147
|
+
conditions = []
|
|
148
|
+
conditions << "if: #{callback[:if].join(', ')}" if callback[:if]
|
|
149
|
+
conditions << "unless: #{callback[:unless].join(', ')}" if callback[:unless]
|
|
150
|
+
|
|
151
|
+
condition_str = conditions.any? ? " (#{conditions.join(', ')})" : ""
|
|
152
|
+
puts " #{index + 1}. #{pastel.green(callback[:name])}#{condition_str}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def print_enums_summary(model_class, enums)
|
|
158
|
+
puts pastel.bold.bright_blue("Enums for #{model_class.name}:")
|
|
159
|
+
puts pastel.dim("─" * 60)
|
|
160
|
+
|
|
161
|
+
enums.each do |name, data|
|
|
162
|
+
puts "\n#{pastel.cyan(name)}:"
|
|
163
|
+
puts " Type: #{pastel.yellow(data[:type])}"
|
|
164
|
+
puts " Values: #{pastel.green(data[:values].join(', '))}"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def print_concerns_summary(model_class, concerns)
|
|
169
|
+
puts pastel.bold.bright_blue("Concerns for #{model_class.name}:")
|
|
170
|
+
puts pastel.dim("─" * 60)
|
|
171
|
+
|
|
172
|
+
concerns.each do |concern|
|
|
173
|
+
type_badge = case concern[:type]
|
|
174
|
+
when :concern then pastel.green('[Concern]')
|
|
175
|
+
when :class then pastel.blue('[Class]')
|
|
176
|
+
else pastel.yellow('[Module]')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
puts "\n#{type_badge} #{pastel.cyan(concern[:name])}"
|
|
180
|
+
if concern[:location]
|
|
181
|
+
puts " #{pastel.dim(concern[:location][:file])}:#{concern[:location][:line]}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def print_scopes_summary(model_class, scopes)
|
|
187
|
+
puts pastel.bold.bright_blue("Scopes for #{model_class.name}:")
|
|
188
|
+
puts pastel.dim("─" * 60)
|
|
189
|
+
|
|
190
|
+
scopes.each do |name, data|
|
|
191
|
+
puts "\n#{pastel.cyan(name)}:"
|
|
192
|
+
puts " #{pastel.dim(data[:sql])}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def print_validations_summary(model_class, validations)
|
|
197
|
+
puts pastel.bold.bright_blue("Validations for #{model_class.name}:")
|
|
198
|
+
puts pastel.dim("─" * 60)
|
|
199
|
+
|
|
200
|
+
validations.each do |attribute, validators|
|
|
201
|
+
puts "\n#{pastel.cyan(attribute)}:"
|
|
202
|
+
validators.each do |validator|
|
|
203
|
+
opts_str = validator[:options].map { |k, v| "#{k}: #{v}" }.join(', ')
|
|
204
|
+
opts_str = " (#{opts_str})" unless opts_str.empty?
|
|
205
|
+
puts " - #{pastel.green(validator[:type])}#{opts_str}"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def print_method_source(method_name, location)
|
|
211
|
+
puts pastel.bold.bright_blue("Method: #{method_name}")
|
|
212
|
+
puts pastel.dim("─" * 60)
|
|
213
|
+
puts " Owner: #{pastel.cyan(location[:owner])}"
|
|
214
|
+
puts " Type: #{pastel.yellow(location[:type])}"
|
|
215
|
+
puts " Location: #{pastel.green(location[:file])}:#{location[:line]}"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for interactive query building
|
|
6
|
+
class QueryBuilderCommand < BaseCommand
|
|
7
|
+
def execute(model_class, &block)
|
|
8
|
+
return disabled_message unless enabled?
|
|
9
|
+
return pastel.red("#{model_class} is not an ActiveRecord model") unless valid_model?(model_class)
|
|
10
|
+
|
|
11
|
+
builder = QueryBuilder.new(model_class)
|
|
12
|
+
|
|
13
|
+
if block_given?
|
|
14
|
+
builder.instance_eval(&block)
|
|
15
|
+
builder.build
|
|
16
|
+
else
|
|
17
|
+
builder.build
|
|
18
|
+
end
|
|
19
|
+
rescue => e
|
|
20
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :query_builder)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def enabled?
|
|
26
|
+
RailsConsolePro.config.enabled && RailsConsolePro.config.query_builder_command_enabled
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def disabled_message
|
|
30
|
+
pastel.yellow('Query builder command is disabled. Enable it via RailsConsolePro.configure { |c| c.query_builder_command_enabled = true }')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def valid_model?(model_class)
|
|
34
|
+
ModelValidator.valid_model?(model_class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def config
|
|
38
|
+
RailsConsolePro.config
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
@@ -45,5 +45,20 @@ module RailsConsolePro
|
|
|
45
45
|
def snippets(action = :list, *args, **kwargs, &block)
|
|
46
46
|
SnippetsCommand.new.execute(action, *args, **kwargs, &block)
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
# Model introspection command
|
|
50
|
+
def introspect(model_class, *options)
|
|
51
|
+
IntrospectCommand.new.execute(model_class, *options)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Query comparison command
|
|
55
|
+
def compare(&block)
|
|
56
|
+
CompareCommand.new.execute(&block)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Query builder command
|
|
60
|
+
def query(model_class, &block)
|
|
61
|
+
QueryBuilderCommand.new.execute(model_class, &block)
|
|
62
|
+
end
|
|
48
63
|
end
|
|
49
64
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object for query comparison results
|
|
5
|
+
class CompareResult
|
|
6
|
+
Comparison = Struct.new(
|
|
7
|
+
:name,
|
|
8
|
+
:duration_ms,
|
|
9
|
+
:query_count,
|
|
10
|
+
:result,
|
|
11
|
+
:error,
|
|
12
|
+
:sql_queries,
|
|
13
|
+
:memory_usage_kb,
|
|
14
|
+
keyword_init: true
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
attr_reader :comparisons, :winner, :timestamp
|
|
18
|
+
|
|
19
|
+
def initialize(comparisons:, winner: nil, timestamp: Time.current)
|
|
20
|
+
@comparisons = Array(comparisons)
|
|
21
|
+
@winner = winner
|
|
22
|
+
@timestamp = timestamp
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def fastest
|
|
26
|
+
comparisons.min_by { |c| c.duration_ms || Float::INFINITY }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def slowest
|
|
30
|
+
comparisons.max_by { |c| c.duration_ms || 0 }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def has_errors?
|
|
34
|
+
comparisons.any? { |c| c.error }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def error_count
|
|
38
|
+
comparisons.count { |c| c.error }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def total_queries
|
|
42
|
+
comparisons.sum { |c| c.query_count || 0 }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fastest_name
|
|
46
|
+
fastest&.name
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def slowest_name
|
|
50
|
+
slowest&.name
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def performance_ratio
|
|
54
|
+
return nil if comparisons.size < 2 || fastest.nil? || slowest.nil?
|
|
55
|
+
return nil if fastest.duration_ms.nil? || slowest.duration_ms.nil? || fastest.duration_ms.zero?
|
|
56
|
+
|
|
57
|
+
(slowest.duration_ms / fastest.duration_ms).round(2)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Export to JSON
|
|
61
|
+
def to_json(pretty: true)
|
|
62
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Export to YAML
|
|
66
|
+
def to_yaml
|
|
67
|
+
FormatExporter.to_yaml(self)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Export to HTML
|
|
71
|
+
def to_html(style: :default)
|
|
72
|
+
FormatExporter.to_html(self, title: "Query Comparison", style: style)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Export to file
|
|
76
|
+
def export_to_file(file_path, format: nil)
|
|
77
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
@@ -51,6 +51,9 @@ module RailsConsolePro
|
|
|
51
51
|
attr_accessor :snippets_command_enabled
|
|
52
52
|
attr_accessor :profile_command_enabled
|
|
53
53
|
attr_accessor :queue_command_enabled
|
|
54
|
+
attr_accessor :introspect_command_enabled
|
|
55
|
+
attr_accessor :compare_command_enabled
|
|
56
|
+
attr_accessor :query_builder_command_enabled
|
|
54
57
|
attr_accessor :active_record_printer_enabled
|
|
55
58
|
attr_accessor :relation_printer_enabled
|
|
56
59
|
attr_accessor :collection_printer_enabled
|
|
@@ -99,6 +102,9 @@ module RailsConsolePro
|
|
|
99
102
|
@snippets_command_enabled = true
|
|
100
103
|
@profile_command_enabled = true
|
|
101
104
|
@queue_command_enabled = true
|
|
105
|
+
@introspect_command_enabled = true
|
|
106
|
+
@compare_command_enabled = true
|
|
107
|
+
@query_builder_command_enabled = true
|
|
102
108
|
@active_record_printer_enabled = true
|
|
103
109
|
@relation_printer_enabled = true
|
|
104
110
|
@collection_printer_enabled = true
|
|
@@ -212,6 +218,9 @@ module RailsConsolePro
|
|
|
212
218
|
@stats_command_enabled = false
|
|
213
219
|
@diff_command_enabled = false
|
|
214
220
|
@queue_command_enabled = false
|
|
221
|
+
@introspect_command_enabled = false
|
|
222
|
+
@compare_command_enabled = false
|
|
223
|
+
@query_builder_command_enabled = false
|
|
215
224
|
@active_record_printer_enabled = false
|
|
216
225
|
@relation_printer_enabled = false
|
|
217
226
|
@collection_printer_enabled = false
|
|
@@ -227,6 +236,9 @@ module RailsConsolePro
|
|
|
227
236
|
@stats_command_enabled = true
|
|
228
237
|
@diff_command_enabled = true
|
|
229
238
|
@queue_command_enabled = true
|
|
239
|
+
@introspect_command_enabled = true
|
|
240
|
+
@compare_command_enabled = true
|
|
241
|
+
@query_builder_command_enabled = true
|
|
230
242
|
@active_record_printer_enabled = true
|
|
231
243
|
@relation_printer_enabled = true
|
|
232
244
|
@collection_printer_enabled = true
|
|
@@ -83,6 +83,12 @@ module RailsConsolePro
|
|
|
83
83
|
serialize_explain_result(data)
|
|
84
84
|
when ProfileResult
|
|
85
85
|
serialize_profile_result(data)
|
|
86
|
+
when IntrospectResult
|
|
87
|
+
serialize_introspect_result(data)
|
|
88
|
+
when CompareResult
|
|
89
|
+
serialize_compare_result(data)
|
|
90
|
+
when QueryBuilderResult
|
|
91
|
+
serialize_query_builder_result(data)
|
|
86
92
|
when ActiveRecord::Base
|
|
87
93
|
serialize_active_record(data)
|
|
88
94
|
when ActiveRecord::Relation
|
|
@@ -137,6 +143,18 @@ module RailsConsolePro
|
|
|
137
143
|
Serializers::ProfileSerializer.serialize(result, self)
|
|
138
144
|
end
|
|
139
145
|
|
|
146
|
+
def serialize_introspect_result(result)
|
|
147
|
+
Serializers::IntrospectSerializer.serialize(result, self)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def serialize_compare_result(result)
|
|
151
|
+
Serializers::CompareSerializer.serialize(result, self)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def serialize_query_builder_result(result)
|
|
155
|
+
Serializers::QueryBuilderSerializer.serialize(result, self)
|
|
156
|
+
end
|
|
157
|
+
|
|
140
158
|
def serialize_active_record(record)
|
|
141
159
|
Serializers::ActiveRecordSerializer.serialize(record, self)
|
|
142
160
|
end
|
|
@@ -396,6 +414,12 @@ module RailsConsolePro
|
|
|
396
414
|
"SQL Explain Analysis"
|
|
397
415
|
when ProfileResult
|
|
398
416
|
"Profile: #{data.label || 'Session'}"
|
|
417
|
+
when IntrospectResult
|
|
418
|
+
"Introspection: #{data.model.name}"
|
|
419
|
+
when CompareResult
|
|
420
|
+
"Query Comparison (#{data.comparisons.size} strategies)"
|
|
421
|
+
when QueryBuilderResult
|
|
422
|
+
"Query Builder: #{data.model_class.name}"
|
|
399
423
|
when ActiveRecord::Base
|
|
400
424
|
"#{data.class.name} ##{data.id}"
|
|
401
425
|
when ActiveRecord::Relation
|
|
@@ -52,3 +52,15 @@ def snippets(action = :list, *args, **kwargs, &block)
|
|
|
52
52
|
RailsConsolePro::Commands.snippets(action, *args, **kwargs, &block)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def introspect(model_class, *options)
|
|
56
|
+
RailsConsolePro::Commands.introspect(model_class, *options)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def compare(&block)
|
|
60
|
+
RailsConsolePro::Commands.compare(&block)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def query(model_class, &block)
|
|
64
|
+
RailsConsolePro::Commands.query(model_class, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
@@ -33,6 +33,10 @@ module RailsConsolePro
|
|
|
33
33
|
autoload :DiffResult, "rails_console_pro/diff_result"
|
|
34
34
|
autoload :ProfileResult, "rails_console_pro/profile_result"
|
|
35
35
|
autoload :QueueInsightsResult, "rails_console_pro/queue_insights_result"
|
|
36
|
+
autoload :IntrospectResult, "rails_console_pro/introspect_result"
|
|
37
|
+
autoload :CompareResult, "rails_console_pro/compare_result"
|
|
38
|
+
autoload :QueryBuilderResult, "rails_console_pro/query_builder_result"
|
|
39
|
+
autoload :QueryBuilder, "rails_console_pro/query_builder"
|
|
36
40
|
autoload :AssociationNavigator, "rails_console_pro/association_navigator"
|
|
37
41
|
autoload :Commands, "rails_console_pro/commands"
|
|
38
42
|
autoload :FormatExporter, "rails_console_pro/format_exporter"
|
|
@@ -49,9 +53,12 @@ module RailsConsolePro
|
|
|
49
53
|
autoload :StatsPrinter, "rails_console_pro/printers/stats_printer"
|
|
50
54
|
autoload :DiffPrinter, "rails_console_pro/printers/diff_printer"
|
|
51
55
|
autoload :ProfilePrinter, "rails_console_pro/printers/profile_printer"
|
|
56
|
+
autoload :IntrospectPrinter, "rails_console_pro/printers/introspect_printer"
|
|
52
57
|
autoload :SnippetCollectionPrinter, "rails_console_pro/printers/snippet_collection_printer"
|
|
53
58
|
autoload :SnippetPrinter, "rails_console_pro/printers/snippet_printer"
|
|
54
59
|
autoload :QueueInsightsPrinter, "rails_console_pro/printers/queue_insights_printer"
|
|
60
|
+
autoload :ComparePrinter, "rails_console_pro/printers/compare_printer"
|
|
61
|
+
autoload :QueryBuilderPrinter, "rails_console_pro/printers/query_builder_printer"
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
# Main dispatcher - optimized with early returns
|
|
@@ -96,7 +103,10 @@ module RailsConsolePro
|
|
|
96
103
|
return Printers::StatsPrinter if value.is_a?(StatsResult)
|
|
97
104
|
return Printers::DiffPrinter if value.is_a?(DiffResult)
|
|
98
105
|
return Printers::ProfilePrinter if value.is_a?(ProfileResult)
|
|
106
|
+
return Printers::IntrospectPrinter if value.is_a?(IntrospectResult)
|
|
99
107
|
return Printers::QueueInsightsPrinter if value.is_a?(QueueInsightsResult)
|
|
108
|
+
return Printers::ComparePrinter if value.is_a?(CompareResult)
|
|
109
|
+
return Printers::QueryBuilderPrinter if value.is_a?(QueryBuilderResult)
|
|
100
110
|
if defined?(Snippets::CollectionResult) && value.is_a?(Snippets::CollectionResult)
|
|
101
111
|
return Printers::SnippetCollectionPrinter
|
|
102
112
|
end
|
|
@@ -158,6 +168,7 @@ require_relative 'services/profile_collector'
|
|
|
158
168
|
require_relative 'services/snippet_repository'
|
|
159
169
|
require_relative 'services/queue_action_service'
|
|
160
170
|
require_relative 'services/queue_insight_fetcher'
|
|
171
|
+
require_relative 'services/introspection_collector'
|
|
161
172
|
|
|
162
173
|
# Load command classes (needed by Commands module)
|
|
163
174
|
require_relative 'commands/base_command'
|
|
@@ -166,9 +177,12 @@ require_relative 'commands/explain_command'
|
|
|
166
177
|
require_relative 'commands/stats_command'
|
|
167
178
|
require_relative 'commands/diff_command'
|
|
168
179
|
require_relative 'commands/export_command'
|
|
180
|
+
require_relative 'commands/introspect_command'
|
|
169
181
|
require_relative 'commands/snippets_command'
|
|
170
182
|
require_relative 'commands/profile_command'
|
|
171
183
|
require_relative 'commands/jobs_command'
|
|
184
|
+
require_relative 'commands/compare_command'
|
|
185
|
+
require_relative 'commands/query_builder_command'
|
|
172
186
|
|
|
173
187
|
# Load Commands module (uses command classes)
|
|
174
188
|
require_relative 'commands'
|
|
@@ -180,6 +194,9 @@ require_relative 'serializers/stats_serializer'
|
|
|
180
194
|
require_relative 'serializers/explain_serializer'
|
|
181
195
|
require_relative 'serializers/diff_serializer'
|
|
182
196
|
require_relative 'serializers/profile_serializer'
|
|
197
|
+
require_relative 'serializers/introspect_serializer'
|
|
198
|
+
require_relative 'serializers/compare_serializer'
|
|
199
|
+
require_relative 'serializers/query_builder_serializer'
|
|
183
200
|
require_relative 'serializers/active_record_serializer'
|
|
184
201
|
require_relative 'serializers/relation_serializer'
|
|
185
202
|
require_relative 'serializers/array_serializer'
|
|
@@ -196,6 +213,6 @@ end
|
|
|
196
213
|
if RailsConsolePro.config.show_welcome_message && defined?(Pry)
|
|
197
214
|
pastel = RailsConsolePro::PASTEL
|
|
198
215
|
puts pastel.bright_green("🚀 Rails Console Pro Loaded!")
|
|
199
|
-
puts pastel.cyan("📊 Use `schema
|
|
216
|
+
puts pastel.cyan("📊 Use `schema`, `explain`, `stats`, `introspect`, `diff`, `navigate`, `profile`, or `jobs` commands")
|
|
200
217
|
puts pastel.dim("💾 Export support: Use `.to_json`, `.to_yaml`, `.to_html`, or `.export_to_file` on any result")
|
|
201
218
|
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Value object for model introspection results
|
|
5
|
+
class IntrospectResult
|
|
6
|
+
attr_reader :model, :callbacks, :enums, :concerns, :scopes,
|
|
7
|
+
:validations, :lifecycle_hooks, :timestamp
|
|
8
|
+
|
|
9
|
+
def initialize(model:, callbacks: {}, enums: {}, concerns: [],
|
|
10
|
+
scopes: {}, validations: [], lifecycle_hooks: {},
|
|
11
|
+
timestamp: Time.current)
|
|
12
|
+
@model = model
|
|
13
|
+
@callbacks = callbacks
|
|
14
|
+
@enums = enums
|
|
15
|
+
@concerns = concerns
|
|
16
|
+
@scopes = scopes
|
|
17
|
+
@validations = validations
|
|
18
|
+
@lifecycle_hooks = lifecycle_hooks
|
|
19
|
+
@timestamp = timestamp
|
|
20
|
+
validate_model!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(self.class) && other.model == model && other.timestamp == timestamp
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Query methods
|
|
28
|
+
def has_callbacks?
|
|
29
|
+
callbacks.any?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def has_enums?
|
|
33
|
+
enums.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def has_concerns?
|
|
37
|
+
concerns.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def has_scopes?
|
|
41
|
+
scopes.any?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def has_validations?
|
|
45
|
+
validations.any?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Get callbacks by type
|
|
49
|
+
def callbacks_by_type(type)
|
|
50
|
+
callbacks[type] || []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get validations for attribute
|
|
54
|
+
def validations_for(attribute)
|
|
55
|
+
validations[attribute] || []
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get enum values
|
|
59
|
+
def enum_values(enum_name)
|
|
60
|
+
enums.dig(enum_name.to_s, :values) || []
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get scope SQL
|
|
64
|
+
def scope_sql(scope_name)
|
|
65
|
+
scopes.dig(scope_name.to_sym, :sql)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Get method source location
|
|
69
|
+
def method_source(method_name)
|
|
70
|
+
collector = Services::IntrospectionCollector.new(model)
|
|
71
|
+
collector.method_source_location(method_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Export to JSON
|
|
75
|
+
def to_json(pretty: true)
|
|
76
|
+
FormatExporter.to_json(self, pretty: pretty)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Export to YAML
|
|
80
|
+
def to_yaml
|
|
81
|
+
FormatExporter.to_yaml(self)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Export to HTML
|
|
85
|
+
def to_html(style: :default)
|
|
86
|
+
FormatExporter.to_html(self, title: "Introspection: #{model.name}", style: style)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Export to file
|
|
90
|
+
def export_to_file(file_path, format: nil)
|
|
91
|
+
FormatExporter.export_to_file(self, file_path, format: format)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def validate_model!
|
|
97
|
+
ModelValidator.validate_model!(model)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|