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,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Utility module for validating ActiveRecord models and handling edge cases
|
|
5
|
+
module ModelValidator
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
# Check if model is a valid ActiveRecord model
|
|
9
|
+
def valid_model?(model_class)
|
|
10
|
+
return false unless model_class.is_a?(Class)
|
|
11
|
+
return false unless model_class < ActiveRecord::Base
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Check if model has a database table
|
|
16
|
+
def has_table?(model_class)
|
|
17
|
+
return false unless valid_model?(model_class)
|
|
18
|
+
return false if abstract_class?(model_class)
|
|
19
|
+
|
|
20
|
+
# Use ActiveRecord's table_exists? method
|
|
21
|
+
model_class.table_exists?
|
|
22
|
+
rescue => e
|
|
23
|
+
# If table_exists? fails, assume no table
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check if model is abstract
|
|
28
|
+
def abstract_class?(model_class)
|
|
29
|
+
return false unless valid_model?(model_class)
|
|
30
|
+
model_class.abstract_class?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if model uses Single Table Inheritance (STI)
|
|
34
|
+
def sti_model?(model_class)
|
|
35
|
+
return false unless valid_model?(model_class)
|
|
36
|
+
return false unless has_table?(model_class)
|
|
37
|
+
|
|
38
|
+
# Check if model has a type column (STI indicator)
|
|
39
|
+
model_class.column_names.include?(model_class.inheritance_column)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if model has created_at column (for growth rate calculations)
|
|
43
|
+
def has_timestamp_column?(model_class, column_name = 'created_at')
|
|
44
|
+
return false unless valid_model?(model_class)
|
|
45
|
+
return false unless has_table?(model_class)
|
|
46
|
+
|
|
47
|
+
model_class.column_names.include?(column_name.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if table is very large (for performance considerations)
|
|
51
|
+
def large_table?(model_class, threshold: nil)
|
|
52
|
+
return false unless valid_model?(model_class)
|
|
53
|
+
return false unless has_table?(model_class)
|
|
54
|
+
|
|
55
|
+
threshold ||= RailsConsolePro.config.stats_large_table_threshold
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
count = model_class.count
|
|
59
|
+
count > threshold
|
|
60
|
+
rescue => e
|
|
61
|
+
# If count fails, assume not large (safer)
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get model type information
|
|
67
|
+
def model_info(model_class)
|
|
68
|
+
{
|
|
69
|
+
valid: valid_model?(model_class),
|
|
70
|
+
has_table: has_table?(model_class),
|
|
71
|
+
abstract: abstract_class?(model_class),
|
|
72
|
+
sti: sti_model?(model_class),
|
|
73
|
+
has_created_at: has_timestamp_column?(model_class),
|
|
74
|
+
large: large_table?(model_class)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Validate model and return error message if invalid
|
|
79
|
+
def validate_for_schema(model_class)
|
|
80
|
+
return "Not an ActiveRecord model" unless valid_model?(model_class)
|
|
81
|
+
return "Abstract class - no database table" if abstract_class?(model_class)
|
|
82
|
+
return "Model has no database table" unless has_table?(model_class)
|
|
83
|
+
|
|
84
|
+
nil # Valid
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validate model and return error message if invalid for stats
|
|
88
|
+
def validate_for_stats(model_class)
|
|
89
|
+
schema_error = validate_for_schema(model_class)
|
|
90
|
+
return schema_error if schema_error
|
|
91
|
+
|
|
92
|
+
nil # Valid
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check if associations are valid (not empty or broken)
|
|
96
|
+
def valid_associations?(model_class, association_name)
|
|
97
|
+
return false unless valid_model?(model_class)
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
association = model_class.reflect_on_association(association_name)
|
|
101
|
+
return false unless association
|
|
102
|
+
|
|
103
|
+
# Check if associated class exists
|
|
104
|
+
associated_class = association.klass
|
|
105
|
+
return false unless associated_class < ActiveRecord::Base
|
|
106
|
+
|
|
107
|
+
true
|
|
108
|
+
rescue => e
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Safely get table name
|
|
114
|
+
def safe_table_name(model_class)
|
|
115
|
+
return nil unless valid_model?(model_class)
|
|
116
|
+
|
|
117
|
+
begin
|
|
118
|
+
model_class.table_name
|
|
119
|
+
rescue => e
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Safely get column names
|
|
125
|
+
def safe_column_names(model_class)
|
|
126
|
+
return [] unless valid_model?(model_class)
|
|
127
|
+
return [] unless has_table?(model_class)
|
|
128
|
+
|
|
129
|
+
begin
|
|
130
|
+
model_class.column_names
|
|
131
|
+
rescue => e
|
|
132
|
+
[]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Safely get columns
|
|
137
|
+
def safe_columns(model_class)
|
|
138
|
+
return [] unless valid_model?(model_class)
|
|
139
|
+
return [] unless has_table?(model_class)
|
|
140
|
+
|
|
141
|
+
begin
|
|
142
|
+
model_class.columns
|
|
143
|
+
rescue => e
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Safely get indexes
|
|
149
|
+
def safe_indexes(model_class)
|
|
150
|
+
return [] unless valid_model?(model_class)
|
|
151
|
+
return [] unless has_table?(model_class)
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
table_name = safe_table_name(model_class)
|
|
155
|
+
return [] unless table_name
|
|
156
|
+
|
|
157
|
+
model_class.connection.indexes(table_name)
|
|
158
|
+
rescue => e
|
|
159
|
+
[]
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Safely get associations
|
|
164
|
+
def safe_associations(model_class, macro = nil)
|
|
165
|
+
return [] unless valid_model?(model_class)
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
if macro
|
|
169
|
+
model_class.reflect_on_all_associations(macro)
|
|
170
|
+
else
|
|
171
|
+
model_class.reflect_on_all_associations
|
|
172
|
+
end
|
|
173
|
+
rescue => e
|
|
174
|
+
[]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Check if model has unusual inheritance patterns
|
|
179
|
+
def unusual_inheritance?(model_class)
|
|
180
|
+
return false unless valid_model?(model_class)
|
|
181
|
+
|
|
182
|
+
# Check for non-standard inheritance patterns
|
|
183
|
+
# This is a heuristic - models that inherit from non-standard base classes
|
|
184
|
+
base_class = model_class.superclass
|
|
185
|
+
|
|
186
|
+
# If superclass is not ActiveRecord::Base and not abstract, it's unusual
|
|
187
|
+
if base_class != ActiveRecord::Base && !base_class.abstract_class?
|
|
188
|
+
# Check if it's a legitimate ActiveRecord model
|
|
189
|
+
return true unless base_class < ActiveRecord::Base
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
false
|
|
193
|
+
rescue => e
|
|
194
|
+
# If we can't determine, assume it's fine
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Validate model and raise error if invalid (for use in initializers)
|
|
199
|
+
def validate_model!(model_class)
|
|
200
|
+
unless valid_model?(model_class)
|
|
201
|
+
raise ArgumentError, "#{model_class} is not an ActiveRecord model"
|
|
202
|
+
end
|
|
203
|
+
model_class
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Validate model for schema inspection and raise error if invalid
|
|
207
|
+
def validate_model_for_schema!(model_class)
|
|
208
|
+
validate_model!(model_class)
|
|
209
|
+
if abstract_class?(model_class)
|
|
210
|
+
raise ArgumentError, "#{model_class} is an abstract class and has no database table"
|
|
211
|
+
end
|
|
212
|
+
unless has_table?(model_class)
|
|
213
|
+
raise ArgumentError, "#{model_class} has no database table"
|
|
214
|
+
end
|
|
215
|
+
model_class
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
# Smart paginator for large collections with lazy enumeration
|
|
5
|
+
# Uses advanced Ruby concepts: Enumerator, lazy evaluation, and interactive I/O
|
|
6
|
+
class Paginator
|
|
7
|
+
include ColorHelper
|
|
8
|
+
|
|
9
|
+
# Navigation commands
|
|
10
|
+
COMMANDS = {
|
|
11
|
+
next: ['n', 'next', ''],
|
|
12
|
+
previous: ['p', 'prev', 'previous'],
|
|
13
|
+
first: ['f', 'first'],
|
|
14
|
+
last: ['l', 'last'],
|
|
15
|
+
jump: ['j', 'jump', 'goto', 'g'],
|
|
16
|
+
quit: ['q', 'quit', 'exit']
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(output, collection, total_count, config, record_printer_proc)
|
|
20
|
+
@output = output
|
|
21
|
+
@collection = collection
|
|
22
|
+
@total_count = total_count
|
|
23
|
+
@config = config
|
|
24
|
+
@record_printer_proc = record_printer_proc
|
|
25
|
+
@page_size = config.pagination_page_size
|
|
26
|
+
@current_page = 1
|
|
27
|
+
@total_pages = calculate_total_pages
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Start paginated display
|
|
31
|
+
def paginate
|
|
32
|
+
return print_all if @total_count <= @config.pagination_threshold || !@config.pagination_enabled
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
print_page
|
|
36
|
+
command = get_user_command
|
|
37
|
+
break if command == :quit
|
|
38
|
+
|
|
39
|
+
handle_command(command)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def calculate_total_pages
|
|
46
|
+
(@total_count.to_f / @page_size).ceil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def print_all
|
|
50
|
+
# For small collections, print everything without pagination
|
|
51
|
+
@collection.each_with_index do |record, index|
|
|
52
|
+
print_record(record, index)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def print_page
|
|
57
|
+
clear_screen_info
|
|
58
|
+
print_header
|
|
59
|
+
|
|
60
|
+
records = page_records
|
|
61
|
+
records.each_with_index do |record, index|
|
|
62
|
+
global_index = page_start_index + index
|
|
63
|
+
print_record(record, global_index)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
print_footer
|
|
67
|
+
print_pagination_controls
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def page_records
|
|
71
|
+
# Use lazy enumeration to avoid loading unnecessary records
|
|
72
|
+
start_index = page_start_index
|
|
73
|
+
|
|
74
|
+
if @collection.is_a?(ActiveRecord::Relation)
|
|
75
|
+
# For Relations, use offset/limit for efficient database queries
|
|
76
|
+
@collection.offset(start_index).limit(@page_size).to_a
|
|
77
|
+
elsif @collection.respond_to?(:to_ary)
|
|
78
|
+
# For arrays, use slice for better performance (O(1) with range)
|
|
79
|
+
@collection[start_index, @page_size] || []
|
|
80
|
+
elsif @collection.respond_to?(:each)
|
|
81
|
+
# For other enumerables, use lazy enumeration
|
|
82
|
+
@collection.lazy.drop(start_index).take(@page_size).to_a
|
|
83
|
+
else
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def page_start_index
|
|
89
|
+
(@current_page - 1) * @page_size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def page_end_index
|
|
93
|
+
[page_start_index + @page_size - 1, @total_count - 1].min
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_record(record, index)
|
|
97
|
+
@output.puts color(@config.get_color(:info), "[#{index}]")
|
|
98
|
+
@record_printer_proc.call(record)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def print_header
|
|
102
|
+
model_name = extract_model_name
|
|
103
|
+
showing = "#{page_start_index + 1}-#{page_end_index + 1}"
|
|
104
|
+
header = "#{model_name} Collection (Showing #{showing} of #{@total_count} records)"
|
|
105
|
+
@output.puts color(@config.get_color(:success), header)
|
|
106
|
+
@output.puts
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def print_footer
|
|
110
|
+
@output.puts
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def print_pagination_controls
|
|
114
|
+
page_info = color(@config.get_color(:info), "Page #{@current_page}/#{@total_pages}")
|
|
115
|
+
controls = "Commands: " +
|
|
116
|
+
color(:dim, "[n]ext") + " " +
|
|
117
|
+
color(:dim, "[p]rev") + " " +
|
|
118
|
+
color(:dim, "[f]irst") + " " +
|
|
119
|
+
color(:dim, "[l]ast") + " " +
|
|
120
|
+
color(:dim, "[j]ump") + " " +
|
|
121
|
+
color(:dim, "[#]page") + " " +
|
|
122
|
+
color(:dim, "[q]uit")
|
|
123
|
+
|
|
124
|
+
@output.puts page_info
|
|
125
|
+
@output.puts controls
|
|
126
|
+
@output.puts
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def extract_model_name
|
|
130
|
+
if @collection.is_a?(ActiveRecord::Relation)
|
|
131
|
+
@collection.klass.name
|
|
132
|
+
elsif @collection.respond_to?(:to_ary) && !@collection.empty?
|
|
133
|
+
# For arrays, safely check first element
|
|
134
|
+
first_item = @collection[0]
|
|
135
|
+
first_item.is_a?(ActiveRecord::Base) ? first_item.class.name : "Collection"
|
|
136
|
+
else
|
|
137
|
+
"Collection"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def clear_screen_info
|
|
142
|
+
# Clear previous pagination info (simple approach - just add spacing)
|
|
143
|
+
@output.puts
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def get_user_command
|
|
147
|
+
@output.print color(@config.get_color(:success), "⤠Enter command: ")
|
|
148
|
+
input = $stdin.gets&.chomp&.downcase&.strip || 'q'
|
|
149
|
+
|
|
150
|
+
normalize_command(input)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def normalize_command(input)
|
|
154
|
+
COMMANDS.each do |command, aliases|
|
|
155
|
+
return command if aliases.include?(input)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check if it's a page number (direct jump)
|
|
159
|
+
if input.match?(/^\d+$/)
|
|
160
|
+
page_num = input.to_i
|
|
161
|
+
if page_num.between?(1, @total_pages)
|
|
162
|
+
@current_page = page_num
|
|
163
|
+
return :page_changed
|
|
164
|
+
else
|
|
165
|
+
@output.puts color(@config.get_color(:error), "Invalid page number (1-#{@total_pages}). Staying on page #{@current_page}.")
|
|
166
|
+
return :noop
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Default to next if unrecognized
|
|
171
|
+
:next
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def handle_command(command)
|
|
175
|
+
case command
|
|
176
|
+
when :next
|
|
177
|
+
@current_page = [@current_page + 1, @total_pages].min
|
|
178
|
+
when :previous
|
|
179
|
+
@current_page = [@current_page - 1, 1].max
|
|
180
|
+
when :first
|
|
181
|
+
@current_page = 1
|
|
182
|
+
when :last
|
|
183
|
+
@current_page = @total_pages
|
|
184
|
+
when :jump
|
|
185
|
+
jump_to_page
|
|
186
|
+
when :page_changed, :noop
|
|
187
|
+
# Already handled in normalize_command
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def jump_to_page
|
|
192
|
+
@output.print color(@config.get_color(:success), "⤠Enter page number (1-#{@total_pages}): ")
|
|
193
|
+
input = $stdin.gets&.chomp&.strip
|
|
194
|
+
page_num = input.to_i
|
|
195
|
+
|
|
196
|
+
if page_num.between?(1, @total_pages)
|
|
197
|
+
@current_page = page_num
|
|
198
|
+
else
|
|
199
|
+
@output.puts color(@config.get_color(:error), "Invalid page number. Staying on page #{@current_page}.")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for ActiveRecord::Base instances
|
|
6
|
+
class ActiveRecordPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
class_name = value.class.name
|
|
9
|
+
id = value.id || "new"
|
|
10
|
+
header("#{class_name} ##{id}", 50)
|
|
11
|
+
|
|
12
|
+
# Use each_pair for better performance than each
|
|
13
|
+
value.attributes.each_pair do |key, val|
|
|
14
|
+
print_attribute(key, val)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
footer(50)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def print_attribute(key, val)
|
|
23
|
+
key_color = config.get_color(:attribute_key)
|
|
24
|
+
key_str = bold_color(key_color, key.to_s)
|
|
25
|
+
val_str = format_value(val)
|
|
26
|
+
output.puts "ā #{key_str}: #{val_str}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for Array of ActiveRecord objects
|
|
6
|
+
class CollectionPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
return print_empty_collection if value.empty?
|
|
9
|
+
return print_non_active_record_array unless active_record_array?
|
|
10
|
+
|
|
11
|
+
# Use pagination for large collections
|
|
12
|
+
total_count = value.size
|
|
13
|
+
record_printer = proc { |record| ActiveRecordPrinter.new(output, record, pry_instance).print }
|
|
14
|
+
|
|
15
|
+
Paginator.new(output, value, total_count, config, record_printer).paginate
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def active_record_array?
|
|
21
|
+
!value.empty? && value.first.is_a?(ActiveRecord::Base)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def print_empty_collection
|
|
25
|
+
output.puts color(config.get_color(:warning), "Empty collection")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def print_non_active_record_array
|
|
29
|
+
# Fall back to default printer for non-AR arrays
|
|
30
|
+
BasePrinter.new(output, value, pry_instance).print
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Printers
|
|
5
|
+
# Printer for object comparison results
|
|
6
|
+
class DiffPrinter < BasePrinter
|
|
7
|
+
def print
|
|
8
|
+
print_header
|
|
9
|
+
print_type_comparison
|
|
10
|
+
print_identical_status
|
|
11
|
+
print_differences
|
|
12
|
+
print_footer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def print_header
|
|
18
|
+
header_color = config.get_color(:header)
|
|
19
|
+
output.puts bold_color(header_color, "ā" * config.header_width)
|
|
20
|
+
output.puts bold_color(header_color, "š OBJECT COMPARISON")
|
|
21
|
+
output.puts bold_color(header_color, "ā" * config.header_width)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def print_type_comparison
|
|
25
|
+
return unless value.different_types?
|
|
26
|
+
|
|
27
|
+
output.puts bold_color(config.get_color(:warning), "\nā ļø Type Mismatch:")
|
|
28
|
+
output.puts " Object 1: #{color(config.get_color(:attribute_value_string), value.object1_type)}"
|
|
29
|
+
output.puts " Object 2: #{color(config.get_color(:attribute_value_string), value.object2_type)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def print_identical_status
|
|
33
|
+
if value.identical
|
|
34
|
+
output.puts bold_color(config.get_color(:success), "\nā
Objects are identical")
|
|
35
|
+
else
|
|
36
|
+
output.puts bold_color(config.get_color(:warning), "\nā ļø Objects differ (#{value.diff_count} #{pluralize(value.diff_count, 'difference')})")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_differences
|
|
41
|
+
return if value.identical || !value.has_differences?
|
|
42
|
+
|
|
43
|
+
output.puts bold_color(config.get_color(:warning), "\nš Differences:")
|
|
44
|
+
|
|
45
|
+
value.differences.each do |attribute, diff_data|
|
|
46
|
+
print_attribute_diff(attribute, diff_data)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def print_attribute_diff(attribute, diff_data)
|
|
51
|
+
key_color = config.get_color(:attribute_key)
|
|
52
|
+
output.puts "\n #{bold_color(key_color, attribute.to_s)}:"
|
|
53
|
+
|
|
54
|
+
case diff_data
|
|
55
|
+
when Hash
|
|
56
|
+
if diff_data[:old_value] && diff_data[:new_value]
|
|
57
|
+
# Show old ā new
|
|
58
|
+
output.puts " #{color(:dim, 'Old:')} #{format_value(diff_data[:old_value])}"
|
|
59
|
+
output.puts " #{color(:dim, 'New:')} #{format_value(diff_data[:new_value])}"
|
|
60
|
+
|
|
61
|
+
# Show change indicator
|
|
62
|
+
change_type = determine_change_type(diff_data[:old_value], diff_data[:new_value])
|
|
63
|
+
change_icon = change_type == :added ? "ā" : change_type == :removed ? "ā" : "š"
|
|
64
|
+
change_color = change_type == :added ? config.get_color(:success) :
|
|
65
|
+
change_type == :removed ? config.get_color(:error) : config.get_color(:warning)
|
|
66
|
+
output.puts " #{color(change_color, "#{change_icon} Changed")}"
|
|
67
|
+
elsif diff_data[:only_in_object1]
|
|
68
|
+
output.puts " #{color(config.get_color(:error), "Only in Object 1:")} #{format_value(diff_data[:only_in_object1])}"
|
|
69
|
+
elsif diff_data[:only_in_object2]
|
|
70
|
+
output.puts " #{color(config.get_color(:success), "Only in Object 2:")} #{format_value(diff_data[:only_in_object2])}"
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
# Simple value comparison
|
|
74
|
+
output.puts " #{color(config.get_color(:error), 'Object 1:')} #{format_value(value.object1)}"
|
|
75
|
+
output.puts " #{color(config.get_color(:success), 'Object 2:')} #{format_value(value.object2)}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def determine_change_type(old_value, new_value)
|
|
80
|
+
return :modified unless old_value.nil? || new_value.nil?
|
|
81
|
+
return :added if old_value.nil?
|
|
82
|
+
return :removed if new_value.nil?
|
|
83
|
+
:modified
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def print_footer
|
|
87
|
+
footer_color = config.get_color(:footer)
|
|
88
|
+
output.puts bold_color(footer_color, "\n" + "ā" * config.header_width)
|
|
89
|
+
output.puts color(:dim, "Generated: #{value.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def pluralize(count, word)
|
|
93
|
+
count == 1 ? word : "#{word}s"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|