rails_console_pro 0.1.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 +7 -0
- data/.editorconfig +12 -0
- data/.rspec +4 -0
- data/.rspec_status +240 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +24 -0
- data/CONTRIBUTING.md +76 -0
- data/LICENSE.txt +22 -0
- data/QUICK_START.md +112 -0
- data/README.md +124 -0
- data/Rakefile +13 -0
- data/config/database.yml +3 -0
- data/docs/ASSOCIATION_NAVIGATION.md +85 -0
- data/docs/EXPORT.md +95 -0
- data/docs/FORMATTING.md +86 -0
- data/docs/MODEL_STATISTICS.md +72 -0
- data/docs/OBJECT_DIFFING.md +87 -0
- data/docs/SCHEMA_INSPECTION.md +60 -0
- data/docs/SQL_EXPLAIN.md +70 -0
- data/lib/generators/rails_console_pro/install_generator.rb +16 -0
- data/lib/generators/rails_console_pro/templates/rails_console_pro.rb +44 -0
- data/lib/rails_console_pro/active_record_extensions.rb +113 -0
- data/lib/rails_console_pro/association_navigator.rb +273 -0
- data/lib/rails_console_pro/base_printer.rb +74 -0
- data/lib/rails_console_pro/color_helper.rb +36 -0
- data/lib/rails_console_pro/commands/base_command.rb +17 -0
- data/lib/rails_console_pro/commands/diff_command.rb +135 -0
- data/lib/rails_console_pro/commands/explain_command.rb +118 -0
- data/lib/rails_console_pro/commands/export_command.rb +16 -0
- data/lib/rails_console_pro/commands/schema_command.rb +20 -0
- data/lib/rails_console_pro/commands/stats_command.rb +93 -0
- data/lib/rails_console_pro/commands.rb +34 -0
- data/lib/rails_console_pro/configuration.rb +219 -0
- data/lib/rails_console_pro/diff_result.rb +56 -0
- data/lib/rails_console_pro/error_handler.rb +60 -0
- data/lib/rails_console_pro/explain_result.rb +47 -0
- data/lib/rails_console_pro/format_exporter.rb +403 -0
- data/lib/rails_console_pro/global_methods.rb +42 -0
- data/lib/rails_console_pro/initializer.rb +176 -0
- data/lib/rails_console_pro/model_validator.rb +219 -0
- data/lib/rails_console_pro/paginator.rb +204 -0
- data/lib/rails_console_pro/printers/active_record_printer.rb +30 -0
- data/lib/rails_console_pro/printers/collection_printer.rb +34 -0
- data/lib/rails_console_pro/printers/diff_printer.rb +97 -0
- data/lib/rails_console_pro/printers/explain_printer.rb +151 -0
- data/lib/rails_console_pro/printers/relation_printer.rb +25 -0
- data/lib/rails_console_pro/printers/schema_printer.rb +188 -0
- data/lib/rails_console_pro/printers/stats_printer.rb +129 -0
- data/lib/rails_console_pro/pry_commands.rb +241 -0
- data/lib/rails_console_pro/pry_integration.rb +9 -0
- data/lib/rails_console_pro/railtie.rb +29 -0
- data/lib/rails_console_pro/schema_inspector_result.rb +43 -0
- data/lib/rails_console_pro/serializers/active_record_serializer.rb +18 -0
- data/lib/rails_console_pro/serializers/array_serializer.rb +31 -0
- data/lib/rails_console_pro/serializers/base_serializer.rb +25 -0
- data/lib/rails_console_pro/serializers/diff_serializer.rb +24 -0
- data/lib/rails_console_pro/serializers/explain_serializer.rb +35 -0
- data/lib/rails_console_pro/serializers/relation_serializer.rb +25 -0
- data/lib/rails_console_pro/serializers/schema_serializer.rb +121 -0
- data/lib/rails_console_pro/serializers/stats_serializer.rb +24 -0
- data/lib/rails_console_pro/services/column_stats_calculator.rb +64 -0
- data/lib/rails_console_pro/services/index_analyzer.rb +110 -0
- data/lib/rails_console_pro/services/stats_calculator.rb +40 -0
- data/lib/rails_console_pro/services/table_size_calculator.rb +43 -0
- data/lib/rails_console_pro/stats_result.rb +66 -0
- data/lib/rails_console_pro/version.rb +6 -0
- data/lib/rails_console_pro.rb +14 -0
- data/lib/tasks/rails_console_pro.rake +10 -0
- data/rails_console_pro.gemspec +60 -0
- metadata +240 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module RailsConsolePro
|
|
7
|
+
# Format exporter for converting data to JSON, YAML, and HTML
|
|
8
|
+
module FormatExporter
|
|
9
|
+
extend self
|
|
10
|
+
|
|
11
|
+
# Export to JSON format
|
|
12
|
+
# Similar to awesome_print philosophy: convert objects to JSON-serializable structures
|
|
13
|
+
# Supports both pretty-printed and compact formats
|
|
14
|
+
def to_json(data, pretty: true, options: {})
|
|
15
|
+
json_data = serialize_data(data)
|
|
16
|
+
|
|
17
|
+
# Apply awesome_print-style options if provided
|
|
18
|
+
if pretty
|
|
19
|
+
# Ruby's JSON.pretty_generate uses 2-space indentation by default
|
|
20
|
+
JSON.pretty_generate(json_data)
|
|
21
|
+
else
|
|
22
|
+
JSON.generate(json_data)
|
|
23
|
+
end
|
|
24
|
+
rescue JSON::GeneratorError => e
|
|
25
|
+
# Fallback for complex objects that can't be serialized
|
|
26
|
+
{ error: "Could not serialize to JSON", message: e.message, type: data.class.name }.to_json
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Export to YAML format
|
|
30
|
+
def to_yaml(data)
|
|
31
|
+
yaml_data = serialize_data(data)
|
|
32
|
+
# Convert symbols to strings for YAML.safe_load compatibility
|
|
33
|
+
yaml_data = convert_symbols_to_strings(yaml_data)
|
|
34
|
+
yaml_data.to_yaml
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Export to HTML format
|
|
38
|
+
def to_html(data, title: nil, style: :default)
|
|
39
|
+
html_data = serialize_data(data)
|
|
40
|
+
generate_html(html_data, title: title || infer_title(data), style: style)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Export to file
|
|
44
|
+
def export_to_file(data, file_path, format: nil)
|
|
45
|
+
return nil if data.nil?
|
|
46
|
+
|
|
47
|
+
format ||= infer_format_from_path(file_path)
|
|
48
|
+
content = case format.to_s.downcase
|
|
49
|
+
when 'json'
|
|
50
|
+
to_json(data)
|
|
51
|
+
when 'yaml', 'yml'
|
|
52
|
+
to_yaml(data)
|
|
53
|
+
when 'html', 'htm'
|
|
54
|
+
to_html(data, title: infer_title(data))
|
|
55
|
+
else
|
|
56
|
+
raise ArgumentError, "Unsupported format: #{format}. Supported: json, yaml, html"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
File.write(file_path, content)
|
|
60
|
+
file_path
|
|
61
|
+
rescue ArgumentError, Errno::ENOENT, Errno::EACCES, Errno::ENOSPC => e
|
|
62
|
+
# Handle file system errors gracefully
|
|
63
|
+
nil
|
|
64
|
+
rescue => e
|
|
65
|
+
# Handle any other errors
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Serialize data to a hash structure
|
|
72
|
+
# Similar to awesome_print's approach: handle native types directly,
|
|
73
|
+
# convert complex objects to structured data
|
|
74
|
+
def serialize_data(data)
|
|
75
|
+
case data
|
|
76
|
+
when SchemaInspectorResult
|
|
77
|
+
serialize_schema_result(data)
|
|
78
|
+
when StatsResult
|
|
79
|
+
serialize_stats_result(data)
|
|
80
|
+
when DiffResult
|
|
81
|
+
serialize_diff_result(data)
|
|
82
|
+
when ExplainResult
|
|
83
|
+
serialize_explain_result(data)
|
|
84
|
+
when ActiveRecord::Base
|
|
85
|
+
serialize_active_record(data)
|
|
86
|
+
when ActiveRecord::Relation
|
|
87
|
+
serialize_relation(data)
|
|
88
|
+
when Array
|
|
89
|
+
serialize_array(data)
|
|
90
|
+
when Hash
|
|
91
|
+
# Recursively serialize hash values (awesome_print-style)
|
|
92
|
+
serialize_hash(data)
|
|
93
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
94
|
+
# Native JSON types - return as-is
|
|
95
|
+
data
|
|
96
|
+
when Symbol
|
|
97
|
+
# Convert symbols to strings for JSON/YAML compatibility
|
|
98
|
+
data.to_s
|
|
99
|
+
when Regexp
|
|
100
|
+
# Convert regexp to string for JSON/YAML compatibility
|
|
101
|
+
data.to_s
|
|
102
|
+
when Time, Date, DateTime, ActiveSupport::TimeWithZone
|
|
103
|
+
# Convert time objects to ISO8601 strings (JSON-friendly)
|
|
104
|
+
data.iso8601
|
|
105
|
+
else
|
|
106
|
+
# Fallback: try to_json if available, otherwise use inspect
|
|
107
|
+
if data.respond_to?(:to_json)
|
|
108
|
+
JSON.parse(data.to_json)
|
|
109
|
+
elsif data.respond_to?(:attributes)
|
|
110
|
+
# Object with attributes (like ActiveRecord but not ActiveRecord::Base)
|
|
111
|
+
serialize_object_with_attributes(data)
|
|
112
|
+
else
|
|
113
|
+
{ type: data.class.name, value: data.inspect }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def serialize_schema_result(result)
|
|
119
|
+
Serializers::SchemaSerializer.serialize(result, self)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def serialize_explain_result(result)
|
|
123
|
+
Serializers::ExplainSerializer.serialize(result, self)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def serialize_stats_result(result)
|
|
127
|
+
Serializers::StatsSerializer.serialize(result, self)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def serialize_diff_result(result)
|
|
131
|
+
Serializers::DiffSerializer.serialize(result, self)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def serialize_active_record(record)
|
|
135
|
+
Serializers::ActiveRecordSerializer.serialize(record, self)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def serialize_relation(relation)
|
|
139
|
+
Serializers::RelationSerializer.serialize(relation, self)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def serialize_array(array)
|
|
143
|
+
Serializers::ArraySerializer.serialize(array, self)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def serialize_hash(hash)
|
|
147
|
+
# Recursively serialize hash values (awesome_print-style deep conversion)
|
|
148
|
+
# Convert symbol keys to strings for YAML compatibility
|
|
149
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
150
|
+
string_key = key.is_a?(Symbol) ? key.to_s : key
|
|
151
|
+
result[string_key] = serialize_data(value)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def serialize_object_with_attributes(obj)
|
|
156
|
+
# Handle objects with attributes method (similar to awesome_print's approach)
|
|
157
|
+
attrs = obj.attributes rescue {}
|
|
158
|
+
{
|
|
159
|
+
_type: obj.class.name,
|
|
160
|
+
**attrs.transform_values { |v| serialize_data(v) }
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def generate_html(data, title:, style:)
|
|
166
|
+
html_title = escape_html(title)
|
|
167
|
+
html_content = generate_html_content(data)
|
|
168
|
+
css = generate_css(style)
|
|
169
|
+
|
|
170
|
+
<<~HTML
|
|
171
|
+
<!DOCTYPE html>
|
|
172
|
+
<html lang="en">
|
|
173
|
+
<head>
|
|
174
|
+
<meta charset="UTF-8">
|
|
175
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
176
|
+
<title>#{html_title}</title>
|
|
177
|
+
<style>#{css}</style>
|
|
178
|
+
</head>
|
|
179
|
+
<body>
|
|
180
|
+
<div class="container">
|
|
181
|
+
<h1 class="title">#{html_title}</h1>
|
|
182
|
+
<div class="content">
|
|
183
|
+
#{html_content}
|
|
184
|
+
</div>
|
|
185
|
+
<div class="footer">
|
|
186
|
+
<p>Generated by Enhanced Console Printer at #{Time.current.strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</body>
|
|
190
|
+
</html>
|
|
191
|
+
HTML
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def generate_html_content(data, depth: 0)
|
|
195
|
+
case data
|
|
196
|
+
when Hash
|
|
197
|
+
generate_hash_html(data, depth)
|
|
198
|
+
when Array
|
|
199
|
+
generate_array_html(data, depth)
|
|
200
|
+
when String, Numeric, TrueClass, FalseClass, NilClass
|
|
201
|
+
generate_value_html(data)
|
|
202
|
+
else
|
|
203
|
+
generate_value_html(data.inspect)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def generate_hash_html(hash, depth)
|
|
208
|
+
return '<span class="empty">Empty hash</span>' if hash.empty?
|
|
209
|
+
|
|
210
|
+
html = '<dl class="hash">'
|
|
211
|
+
hash.each do |key, value|
|
|
212
|
+
key_class = value.is_a?(Hash) || value.is_a?(Array) ? 'key-expandable' : 'key'
|
|
213
|
+
html += "<dt class=\"#{key_class}\">#{escape_html(key.to_s)}</dt>"
|
|
214
|
+
html += "<dd class=\"value\">#{generate_html_content(value, depth: depth + 1)}</dd>"
|
|
215
|
+
end
|
|
216
|
+
html + '</dl>'
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def generate_array_html(array, depth)
|
|
220
|
+
return '<span class="empty">Empty array</span>' if array.empty?
|
|
221
|
+
|
|
222
|
+
html = '<ul class="array">'
|
|
223
|
+
array.each do |item|
|
|
224
|
+
html += "<li>#{generate_html_content(item, depth: depth + 1)}</li>"
|
|
225
|
+
end
|
|
226
|
+
html + '</ul>'
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def generate_value_html(value)
|
|
230
|
+
case value
|
|
231
|
+
when NilClass
|
|
232
|
+
'<span class="nil">nil</span>'
|
|
233
|
+
when TrueClass
|
|
234
|
+
'<span class="boolean true">true</span>'
|
|
235
|
+
when FalseClass
|
|
236
|
+
'<span class="boolean false">false</span>'
|
|
237
|
+
when Numeric
|
|
238
|
+
"<span class=\"number\">#{escape_html(value.to_s)}</span>"
|
|
239
|
+
when String
|
|
240
|
+
"<span class=\"string\">#{escape_html(value)}</span>"
|
|
241
|
+
else
|
|
242
|
+
"<span class=\"other\">#{escape_html(value.inspect)}</span>"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def generate_css(style)
|
|
247
|
+
case style
|
|
248
|
+
when :default
|
|
249
|
+
default_css
|
|
250
|
+
when :minimal
|
|
251
|
+
minimal_css
|
|
252
|
+
else
|
|
253
|
+
default_css
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def default_css
|
|
258
|
+
<<~CSS
|
|
259
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
260
|
+
body {
|
|
261
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
262
|
+
line-height: 1.6;
|
|
263
|
+
color: #333;
|
|
264
|
+
background: #f5f5f5;
|
|
265
|
+
padding: 20px;
|
|
266
|
+
}
|
|
267
|
+
.container {
|
|
268
|
+
max-width: 1200px;
|
|
269
|
+
margin: 0 auto;
|
|
270
|
+
background: white;
|
|
271
|
+
border-radius: 8px;
|
|
272
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
273
|
+
padding: 30px;
|
|
274
|
+
}
|
|
275
|
+
.title {
|
|
276
|
+
color: #2c3e50;
|
|
277
|
+
border-bottom: 3px solid #3498db;
|
|
278
|
+
padding-bottom: 10px;
|
|
279
|
+
margin-bottom: 20px;
|
|
280
|
+
}
|
|
281
|
+
.content {
|
|
282
|
+
margin-bottom: 30px;
|
|
283
|
+
}
|
|
284
|
+
dl.hash {
|
|
285
|
+
margin: 10px 0;
|
|
286
|
+
}
|
|
287
|
+
dt.key, dt.key-expandable {
|
|
288
|
+
font-weight: bold;
|
|
289
|
+
color: #2980b9;
|
|
290
|
+
margin-top: 10px;
|
|
291
|
+
margin-bottom: 5px;
|
|
292
|
+
}
|
|
293
|
+
dt.key-expandable {
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
user-select: none;
|
|
296
|
+
}
|
|
297
|
+
dt.key-expandable:hover {
|
|
298
|
+
color: #1f5f8b;
|
|
299
|
+
}
|
|
300
|
+
dd.value {
|
|
301
|
+
margin-left: 20px;
|
|
302
|
+
margin-bottom: 10px;
|
|
303
|
+
}
|
|
304
|
+
ul.array {
|
|
305
|
+
margin-left: 20px;
|
|
306
|
+
list-style-type: disc;
|
|
307
|
+
}
|
|
308
|
+
ul.array li {
|
|
309
|
+
margin: 5px 0;
|
|
310
|
+
}
|
|
311
|
+
.nil { color: #95a5a6; font-style: italic; }
|
|
312
|
+
.boolean.true { color: #27ae60; font-weight: bold; }
|
|
313
|
+
.boolean.false { color: #e74c3c; font-weight: bold; }
|
|
314
|
+
.number { color: #3498db; font-weight: bold; }
|
|
315
|
+
.string { color: #2ecc71; }
|
|
316
|
+
.other { color: #7f8c8d; }
|
|
317
|
+
.empty { color: #95a5a6; font-style: italic; }
|
|
318
|
+
.footer {
|
|
319
|
+
margin-top: 30px;
|
|
320
|
+
padding-top: 20px;
|
|
321
|
+
border-top: 1px solid #ecf0f1;
|
|
322
|
+
text-align: center;
|
|
323
|
+
color: #95a5a6;
|
|
324
|
+
font-size: 0.9em;
|
|
325
|
+
}
|
|
326
|
+
@media print {
|
|
327
|
+
body { background: white; padding: 0; }
|
|
328
|
+
.container { box-shadow: none; }
|
|
329
|
+
}
|
|
330
|
+
CSS
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def minimal_css
|
|
334
|
+
<<~CSS
|
|
335
|
+
body { font-family: monospace; padding: 20px; }
|
|
336
|
+
.container { max-width: 800px; margin: 0 auto; }
|
|
337
|
+
.title { border-bottom: 1px solid #ccc; padding-bottom: 10px; }
|
|
338
|
+
dt { font-weight: bold; margin-top: 10px; }
|
|
339
|
+
dd { margin-left: 20px; }
|
|
340
|
+
ul { margin-left: 20px; }
|
|
341
|
+
.nil { color: #999; }
|
|
342
|
+
.boolean.true { color: green; }
|
|
343
|
+
.boolean.false { color: red; }
|
|
344
|
+
.number { color: blue; }
|
|
345
|
+
.string { color: #333; }
|
|
346
|
+
CSS
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def escape_html(text)
|
|
350
|
+
text.to_s
|
|
351
|
+
.gsub('&', '&')
|
|
352
|
+
.gsub('<', '<')
|
|
353
|
+
.gsub('>', '>')
|
|
354
|
+
.gsub('"', '"')
|
|
355
|
+
.gsub("'", ''')
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Convert all symbols in data structure to strings for YAML.safe_load compatibility
|
|
359
|
+
def convert_symbols_to_strings(data)
|
|
360
|
+
case data
|
|
361
|
+
when Hash
|
|
362
|
+
data.each_with_object({}) do |(key, value), result|
|
|
363
|
+
string_key = key.is_a?(Symbol) ? key.to_s : key
|
|
364
|
+
result[string_key] = convert_symbols_to_strings(value)
|
|
365
|
+
end
|
|
366
|
+
when Array
|
|
367
|
+
data.map { |item| convert_symbols_to_strings(item) }
|
|
368
|
+
when Symbol
|
|
369
|
+
data.to_s
|
|
370
|
+
else
|
|
371
|
+
data
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def infer_format_from_path(path)
|
|
376
|
+
ext = File.extname(path).downcase.delete('.')
|
|
377
|
+
return 'json' if ext.empty? # Default to JSON if no extension
|
|
378
|
+
ext
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def infer_title(data)
|
|
382
|
+
case data
|
|
383
|
+
when SchemaInspectorResult
|
|
384
|
+
"Schema: #{data.model.name}"
|
|
385
|
+
when StatsResult
|
|
386
|
+
"Statistics: #{data.model.name}"
|
|
387
|
+
when DiffResult
|
|
388
|
+
"Diff Comparison: #{data.object1_type} vs #{data.object2_type}"
|
|
389
|
+
when ExplainResult
|
|
390
|
+
"SQL Explain Analysis"
|
|
391
|
+
when ActiveRecord::Base
|
|
392
|
+
"#{data.class.name} ##{data.id}"
|
|
393
|
+
when ActiveRecord::Relation
|
|
394
|
+
"#{data.klass.name} Collection (#{data.count} records)"
|
|
395
|
+
when Array
|
|
396
|
+
data.first.is_a?(ActiveRecord::Base) ? "#{data.first.class.name} Collection (#{data.size} records)" : "Array (#{data.size} items)"
|
|
397
|
+
else
|
|
398
|
+
"Export"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Global helper methods available in console
|
|
4
|
+
def schema(model_class)
|
|
5
|
+
RailsConsolePro::Commands.schema(model_class)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def explain(relation_or_model, *args)
|
|
9
|
+
RailsConsolePro::Commands.explain(relation_or_model, *args)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def navigate(model_or_string)
|
|
13
|
+
pastel = RailsConsolePro::ColorHelper.pastel
|
|
14
|
+
if model_or_string.is_a?(String)
|
|
15
|
+
begin
|
|
16
|
+
model = model_or_string.constantize
|
|
17
|
+
rescue NameError
|
|
18
|
+
puts pastel.red("Error: Could not find model '#{model_or_string}'")
|
|
19
|
+
puts pastel.yellow("Make sure the model name is correct and loaded.")
|
|
20
|
+
return nil
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
model = model_or_string
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless RailsConsolePro::ModelValidator.valid_model?(model)
|
|
27
|
+
puts pastel.red("Error: #{model} is not an ActiveRecord model")
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
navigator = RailsConsolePro::AssociationNavigator.new(model)
|
|
32
|
+
navigator.start
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stats(model_class)
|
|
36
|
+
RailsConsolePro::Commands.stats(model_class)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def diff(object1, object2)
|
|
40
|
+
RailsConsolePro::Commands.diff(object1, object2)
|
|
41
|
+
end
|
|
42
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pastel'
|
|
4
|
+
require 'tty-color'
|
|
5
|
+
|
|
6
|
+
module RailsConsolePro
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
# Singleton Pastel instance
|
|
10
|
+
PASTEL = Pastel.new(enabled: TTY::Color.color?)
|
|
11
|
+
|
|
12
|
+
# Configuration instance
|
|
13
|
+
def config
|
|
14
|
+
@config ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Configuration DSL
|
|
18
|
+
def configure
|
|
19
|
+
yield config if block_given?
|
|
20
|
+
config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Autoload all components
|
|
24
|
+
autoload :Configuration, "rails_console_pro/configuration"
|
|
25
|
+
autoload :ColorHelper, "rails_console_pro/color_helper"
|
|
26
|
+
autoload :BasePrinter, "rails_console_pro/base_printer"
|
|
27
|
+
autoload :ModelValidator, "rails_console_pro/model_validator"
|
|
28
|
+
autoload :SchemaInspectorResult, "rails_console_pro/schema_inspector_result"
|
|
29
|
+
autoload :ExplainResult, "rails_console_pro/explain_result"
|
|
30
|
+
autoload :StatsResult, "rails_console_pro/stats_result"
|
|
31
|
+
autoload :DiffResult, "rails_console_pro/diff_result"
|
|
32
|
+
autoload :AssociationNavigator, "rails_console_pro/association_navigator"
|
|
33
|
+
autoload :Commands, "rails_console_pro/commands"
|
|
34
|
+
autoload :FormatExporter, "rails_console_pro/format_exporter"
|
|
35
|
+
autoload :ErrorHandler, "rails_console_pro/error_handler"
|
|
36
|
+
autoload :Paginator, "rails_console_pro/paginator"
|
|
37
|
+
|
|
38
|
+
module Printers
|
|
39
|
+
autoload :ActiveRecordPrinter, "rails_console_pro/printers/active_record_printer"
|
|
40
|
+
autoload :RelationPrinter, "rails_console_pro/printers/relation_printer"
|
|
41
|
+
autoload :CollectionPrinter, "rails_console_pro/printers/collection_printer"
|
|
42
|
+
autoload :SchemaPrinter, "rails_console_pro/printers/schema_printer"
|
|
43
|
+
autoload :ExplainPrinter, "rails_console_pro/printers/explain_printer"
|
|
44
|
+
autoload :StatsPrinter, "rails_console_pro/printers/stats_printer"
|
|
45
|
+
autoload :DiffPrinter, "rails_console_pro/printers/diff_printer"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Main dispatcher - optimized with early returns
|
|
49
|
+
# Supports both Pry (with pry_instance) and IRB (without pry_instance)
|
|
50
|
+
def call(output, value, pry_instance = nil)
|
|
51
|
+
unless config.enabled
|
|
52
|
+
return default_print(output, value, pry_instance)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
printer_class = printer_for(value)
|
|
56
|
+
printer_class.new(output, value, pry_instance).print
|
|
57
|
+
rescue => e
|
|
58
|
+
# Show error in development to help debug
|
|
59
|
+
if Rails.env.development? || ENV['RAILS_CONSOLE_PRO_DEBUG']
|
|
60
|
+
pastel = ColorHelper.pastel
|
|
61
|
+
output.puts pastel.red.bold("💥 RailsConsolePro Error: #{e.class}: #{e.message}")
|
|
62
|
+
output.puts pastel.dim(e.backtrace.first(5).join("\n"))
|
|
63
|
+
end
|
|
64
|
+
handle_error(output, e, value, pry_instance)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Optimized printer selection with class hierarchy checks
|
|
70
|
+
PRINTER_MAP = {
|
|
71
|
+
ActiveRecord::Base => Printers::ActiveRecordPrinter,
|
|
72
|
+
ActiveRecord::Relation => Printers::RelationPrinter,
|
|
73
|
+
Array => Printers::CollectionPrinter
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
def printer_for(value)
|
|
77
|
+
# Check inheritance hierarchy (covers exact matches too)
|
|
78
|
+
PRINTER_MAP.each do |klass, printer|
|
|
79
|
+
if value.is_a?(klass) && printer_enabled?(printer)
|
|
80
|
+
return printer
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check for result objects
|
|
85
|
+
return Printers::SchemaPrinter if value.is_a?(SchemaInspectorResult)
|
|
86
|
+
return Printers::ExplainPrinter if value.is_a?(ExplainResult)
|
|
87
|
+
return Printers::StatsPrinter if value.is_a?(StatsResult)
|
|
88
|
+
return Printers::DiffPrinter if value.is_a?(DiffResult)
|
|
89
|
+
|
|
90
|
+
# Fallback to base printer
|
|
91
|
+
BasePrinter
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def printer_enabled?(printer_class)
|
|
95
|
+
case printer_class
|
|
96
|
+
when Printers::ActiveRecordPrinter
|
|
97
|
+
config.active_record_printer_enabled
|
|
98
|
+
when Printers::RelationPrinter
|
|
99
|
+
config.relation_printer_enabled
|
|
100
|
+
when Printers::CollectionPrinter
|
|
101
|
+
config.collection_printer_enabled
|
|
102
|
+
else
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_error(output, error, value, pry_instance)
|
|
108
|
+
pastel = ColorHelper.pastel
|
|
109
|
+
output.puts pastel.red.bold("💥 #{error.class}: #{error.message}")
|
|
110
|
+
output.puts pastel.dim(error.backtrace.first(3).join("\n"))
|
|
111
|
+
default_print(output, value, pry_instance)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Default print method that works for both Pry and IRB
|
|
115
|
+
def default_print(output, value, pry_instance)
|
|
116
|
+
if defined?(Pry) && pry_instance
|
|
117
|
+
Pry::ColorPrinter.default(output, value, pry_instance)
|
|
118
|
+
else
|
|
119
|
+
# IRB fallback - use standard inspect
|
|
120
|
+
output.puts value.inspect
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Load Pry integration and commands
|
|
126
|
+
require_relative 'pry_integration'
|
|
127
|
+
require_relative 'pry_commands'
|
|
128
|
+
|
|
129
|
+
# Load global helper methods
|
|
130
|
+
require_relative 'global_methods'
|
|
131
|
+
|
|
132
|
+
# Load ErrorHandler (needed by Commands)
|
|
133
|
+
require_relative 'error_handler'
|
|
134
|
+
|
|
135
|
+
# Load service objects (needed by StatsCommand)
|
|
136
|
+
require_relative 'services/stats_calculator'
|
|
137
|
+
require_relative 'services/table_size_calculator'
|
|
138
|
+
require_relative 'services/index_analyzer'
|
|
139
|
+
require_relative 'services/column_stats_calculator'
|
|
140
|
+
|
|
141
|
+
# Load command classes (needed by Commands module)
|
|
142
|
+
require_relative 'commands/base_command'
|
|
143
|
+
require_relative 'commands/schema_command'
|
|
144
|
+
require_relative 'commands/explain_command'
|
|
145
|
+
require_relative 'commands/stats_command'
|
|
146
|
+
require_relative 'commands/diff_command'
|
|
147
|
+
require_relative 'commands/export_command'
|
|
148
|
+
|
|
149
|
+
# Load Commands module (uses command classes)
|
|
150
|
+
require_relative 'commands'
|
|
151
|
+
|
|
152
|
+
# Load serializers (needed by FormatExporter)
|
|
153
|
+
require_relative 'serializers/base_serializer'
|
|
154
|
+
require_relative 'serializers/schema_serializer'
|
|
155
|
+
require_relative 'serializers/stats_serializer'
|
|
156
|
+
require_relative 'serializers/explain_serializer'
|
|
157
|
+
require_relative 'serializers/diff_serializer'
|
|
158
|
+
require_relative 'serializers/active_record_serializer'
|
|
159
|
+
require_relative 'serializers/relation_serializer'
|
|
160
|
+
require_relative 'serializers/array_serializer'
|
|
161
|
+
|
|
162
|
+
# Load FormatExporter (uses serializers)
|
|
163
|
+
require_relative 'format_exporter'
|
|
164
|
+
|
|
165
|
+
# Load ActiveRecord extensions (only if ActiveRecord is available)
|
|
166
|
+
if defined?(ActiveRecord::Base)
|
|
167
|
+
require_relative 'active_record_extensions'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Print welcome message if enabled (only for Pry)
|
|
171
|
+
if RailsConsolePro.config.show_welcome_message && defined?(Pry)
|
|
172
|
+
pastel = ColorHelper.pastel
|
|
173
|
+
puts pastel.bright_green("🚀 Rails Console Pro Loaded!")
|
|
174
|
+
puts pastel.cyan("📊 Use `schema ModelName`, `explain Query`, `stats ModelName`, `diff obj1, obj2`, or `navigate ModelName`")
|
|
175
|
+
puts pastel.dim("💾 Export support: Use `.to_json`, `.to_yaml`, `.to_html`, or `.export_to_file` on any result")
|
|
176
|
+
end
|