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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +12 -0
  3. data/.rspec +4 -0
  4. data/.rspec_status +240 -0
  5. data/.rubocop.yml +26 -0
  6. data/CHANGELOG.md +24 -0
  7. data/CONTRIBUTING.md +76 -0
  8. data/LICENSE.txt +22 -0
  9. data/QUICK_START.md +112 -0
  10. data/README.md +124 -0
  11. data/Rakefile +13 -0
  12. data/config/database.yml +3 -0
  13. data/docs/ASSOCIATION_NAVIGATION.md +85 -0
  14. data/docs/EXPORT.md +95 -0
  15. data/docs/FORMATTING.md +86 -0
  16. data/docs/MODEL_STATISTICS.md +72 -0
  17. data/docs/OBJECT_DIFFING.md +87 -0
  18. data/docs/SCHEMA_INSPECTION.md +60 -0
  19. data/docs/SQL_EXPLAIN.md +70 -0
  20. data/lib/generators/rails_console_pro/install_generator.rb +16 -0
  21. data/lib/generators/rails_console_pro/templates/rails_console_pro.rb +44 -0
  22. data/lib/rails_console_pro/active_record_extensions.rb +113 -0
  23. data/lib/rails_console_pro/association_navigator.rb +273 -0
  24. data/lib/rails_console_pro/base_printer.rb +74 -0
  25. data/lib/rails_console_pro/color_helper.rb +36 -0
  26. data/lib/rails_console_pro/commands/base_command.rb +17 -0
  27. data/lib/rails_console_pro/commands/diff_command.rb +135 -0
  28. data/lib/rails_console_pro/commands/explain_command.rb +118 -0
  29. data/lib/rails_console_pro/commands/export_command.rb +16 -0
  30. data/lib/rails_console_pro/commands/schema_command.rb +20 -0
  31. data/lib/rails_console_pro/commands/stats_command.rb +93 -0
  32. data/lib/rails_console_pro/commands.rb +34 -0
  33. data/lib/rails_console_pro/configuration.rb +219 -0
  34. data/lib/rails_console_pro/diff_result.rb +56 -0
  35. data/lib/rails_console_pro/error_handler.rb +60 -0
  36. data/lib/rails_console_pro/explain_result.rb +47 -0
  37. data/lib/rails_console_pro/format_exporter.rb +403 -0
  38. data/lib/rails_console_pro/global_methods.rb +42 -0
  39. data/lib/rails_console_pro/initializer.rb +176 -0
  40. data/lib/rails_console_pro/model_validator.rb +219 -0
  41. data/lib/rails_console_pro/paginator.rb +204 -0
  42. data/lib/rails_console_pro/printers/active_record_printer.rb +30 -0
  43. data/lib/rails_console_pro/printers/collection_printer.rb +34 -0
  44. data/lib/rails_console_pro/printers/diff_printer.rb +97 -0
  45. data/lib/rails_console_pro/printers/explain_printer.rb +151 -0
  46. data/lib/rails_console_pro/printers/relation_printer.rb +25 -0
  47. data/lib/rails_console_pro/printers/schema_printer.rb +188 -0
  48. data/lib/rails_console_pro/printers/stats_printer.rb +129 -0
  49. data/lib/rails_console_pro/pry_commands.rb +241 -0
  50. data/lib/rails_console_pro/pry_integration.rb +9 -0
  51. data/lib/rails_console_pro/railtie.rb +29 -0
  52. data/lib/rails_console_pro/schema_inspector_result.rb +43 -0
  53. data/lib/rails_console_pro/serializers/active_record_serializer.rb +18 -0
  54. data/lib/rails_console_pro/serializers/array_serializer.rb +31 -0
  55. data/lib/rails_console_pro/serializers/base_serializer.rb +25 -0
  56. data/lib/rails_console_pro/serializers/diff_serializer.rb +24 -0
  57. data/lib/rails_console_pro/serializers/explain_serializer.rb +35 -0
  58. data/lib/rails_console_pro/serializers/relation_serializer.rb +25 -0
  59. data/lib/rails_console_pro/serializers/schema_serializer.rb +121 -0
  60. data/lib/rails_console_pro/serializers/stats_serializer.rb +24 -0
  61. data/lib/rails_console_pro/services/column_stats_calculator.rb +64 -0
  62. data/lib/rails_console_pro/services/index_analyzer.rb +110 -0
  63. data/lib/rails_console_pro/services/stats_calculator.rb +40 -0
  64. data/lib/rails_console_pro/services/table_size_calculator.rb +43 -0
  65. data/lib/rails_console_pro/stats_result.rb +66 -0
  66. data/lib/rails_console_pro/version.rb +6 -0
  67. data/lib/rails_console_pro.rb +14 -0
  68. data/lib/tasks/rails_console_pro.rake +10 -0
  69. data/rails_console_pro.gemspec +60 -0
  70. metadata +240 -0
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # ActiveRecord extensions for export functionality
5
+ module ActiveRecordExtensions
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ # Export schema to file
10
+ def export_schema_to_file(file_path, format: nil)
11
+ result = Commands.schema(self)
12
+ result&.export_to_file(file_path, format: format)
13
+ end
14
+
15
+ # Export schema to JSON
16
+ def schema_to_json(pretty: true)
17
+ result = Commands.schema(self)
18
+ result&.to_json(pretty: pretty)
19
+ end
20
+
21
+ # Export schema to YAML
22
+ def schema_to_yaml
23
+ result = Commands.schema(self)
24
+ result&.to_yaml
25
+ end
26
+
27
+ # Export schema to HTML
28
+ def schema_to_html(style: :default)
29
+ result = Commands.schema(self)
30
+ result&.to_html(style: style)
31
+ end
32
+ end
33
+
34
+ # Export record to JSON
35
+ def to_json_export(pretty: true)
36
+ FormatExporter.to_json(self, pretty: pretty)
37
+ end
38
+
39
+ # Export record to YAML
40
+ def to_yaml_export
41
+ FormatExporter.to_yaml(self)
42
+ end
43
+
44
+ # Export record to HTML
45
+ def to_html_export(style: :default)
46
+ FormatExporter.to_html(self, title: "#{self.class.name} ##{id}", style: style)
47
+ end
48
+
49
+ # Export record to file
50
+ def export_to_file(file_path, format: nil)
51
+ FormatExporter.export_to_file(self, file_path, format: format)
52
+ end
53
+ end
54
+
55
+ # ActiveRecord::Relation extensions
56
+ module RelationExtensions
57
+ # Export relation to JSON
58
+ def to_json_export(pretty: true)
59
+ FormatExporter.to_json(self, pretty: pretty)
60
+ end
61
+
62
+ # Export relation to YAML
63
+ def to_yaml_export
64
+ FormatExporter.to_yaml(self)
65
+ end
66
+
67
+ # Export relation to HTML
68
+ def to_html_export(style: :default)
69
+ FormatExporter.to_html(self, title: "#{klass.name} Collection (#{count} records)", style: style)
70
+ end
71
+
72
+ # Export relation to file
73
+ def export_to_file(file_path, format: nil)
74
+ FormatExporter.export_to_file(self, file_path, format: format)
75
+ end
76
+ end
77
+
78
+ # Array extensions for ActiveRecord collections
79
+ module ArrayExtensions
80
+ # Export array to JSON
81
+ def to_json_export(pretty: true)
82
+ FormatExporter.to_json(self, pretty: pretty)
83
+ end
84
+
85
+ # Export array to YAML
86
+ def to_yaml_export
87
+ FormatExporter.to_yaml(self)
88
+ end
89
+
90
+ # Export array to HTML
91
+ def to_html_export(style: :default)
92
+ title = if !empty? && first.is_a?(ActiveRecord::Base)
93
+ "#{first.class.name} Collection (#{size} records)"
94
+ else
95
+ "Array (#{size} items)"
96
+ end
97
+ FormatExporter.to_html(self, title: title, style: style)
98
+ end
99
+
100
+ # Export array to file
101
+ def export_to_file(file_path, format: nil)
102
+ FormatExporter.export_to_file(self, file_path, format: format)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Include extensions in ActiveRecord (only if ActiveRecord is loaded)
108
+ if defined?(ActiveRecord::Base)
109
+ ActiveRecord::Base.include(RailsConsolePro::ActiveRecordExtensions)
110
+ ActiveRecord::Relation.include(RailsConsolePro::RelationExtensions)
111
+ Array.include(RailsConsolePro::ArrayExtensions)
112
+ end
113
+
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # Interactive association navigator
5
+ class AssociationNavigator
6
+ include ColorHelper
7
+
8
+ ASSOCIATION_ICONS = {
9
+ belongs_to: "↖️",
10
+ has_one: "→",
11
+ has_many: "⇒",
12
+ has_and_belongs_to_many: "⇔"
13
+ }.freeze
14
+
15
+ attr_reader :model, :history
16
+
17
+ def initialize(model)
18
+ @model = resolve_model(model)
19
+ @history = []
20
+ validate_model!
21
+ end
22
+
23
+ def start
24
+ current_model = @model
25
+ loop do
26
+ display_menu(current_model)
27
+ choice = get_user_choice(current_model)
28
+
29
+ break if choice == :exit
30
+ next handle_back(current_model) if choice == :back
31
+ next unless choice
32
+
33
+ @history.push(current_model)
34
+ current_model = navigate_to(current_model, choice)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def resolve_model(model_or_string)
41
+ return model_or_string unless model_or_string.is_a?(String)
42
+
43
+ begin
44
+ model_or_string.constantize
45
+ rescue NameError
46
+ raise ArgumentError, "Could not find model '#{model_or_string}'"
47
+ end
48
+ end
49
+
50
+ def validate_model!
51
+ ModelValidator.validate_model!(@model)
52
+ end
53
+
54
+ def display_menu(model)
55
+ puts pastel.bright_cyan.bold("\n" + "═" * 70)
56
+ puts pastel.bright_cyan.bold("🧭 ASSOCIATION NAVIGATOR")
57
+ puts pastel.bright_cyan.bold("═" * 70)
58
+
59
+ print_breadcrumb(model)
60
+ print_model_info(model)
61
+ print_associations_menu(model)
62
+ print_navigation_options
63
+ puts pastel.cyan("\n" + "─" * 70)
64
+ end
65
+
66
+ def print_breadcrumb(model)
67
+ breadcrumb = @history.map(&:name).join(" → ")
68
+ breadcrumb += " → " unless breadcrumb.empty?
69
+ breadcrumb += pastel.bright_green.bold(model.name)
70
+
71
+ puts pastel.yellow.bold("\n📍 Current Location:")
72
+ puts " #{breadcrumb}"
73
+ end
74
+
75
+ def print_model_info(model)
76
+ return unless model.respond_to?(:count)
77
+
78
+ count = model.count rescue "?"
79
+ puts pastel.dim(" (#{count} records in database)")
80
+ end
81
+
82
+ def print_associations_menu(model)
83
+ associations = get_all_associations(model)
84
+
85
+ if associations.empty?
86
+ puts pastel.red(" No associations found for this model")
87
+ return
88
+ end
89
+
90
+ puts pastel.yellow.bold("\n🔗 Available Associations:")
91
+ @menu_items = []
92
+
93
+ group_associations(associations).each do |macro, group|
94
+ print_association_group(macro, group)
95
+ end
96
+ end
97
+
98
+ def group_associations(associations)
99
+ associations.group_by(&:macro)
100
+ end
101
+
102
+ def print_association_group(macro, associations)
103
+ return if associations.empty?
104
+
105
+ puts pastel.cyan("\n #{macro}:")
106
+ associations.each_with_index do |assoc, index|
107
+ menu_index = @menu_items.length + 1
108
+ @menu_items << assoc
109
+
110
+ icon = ASSOCIATION_ICONS[macro] || "•"
111
+ details = format_association_details(assoc, model)
112
+
113
+ puts " #{pastel.bright_blue.bold(menu_index.to_s.rjust(2))}. #{icon} " \
114
+ "#{pastel.white.bold(assoc.name.to_s)} → #{pastel.green(assoc.class_name)}#{details}"
115
+ end
116
+ end
117
+
118
+ def format_association_details(assoc, model)
119
+ details = []
120
+ details << pastel.dim(" [#{assoc.foreign_key}]") if assoc.respond_to?(:foreign_key)
121
+ details << pastel.dim(" (optional)") if assoc.options[:optional]
122
+ details << pastel.yellow(" (#{assoc.options[:dependent]})") if assoc.options[:dependent]
123
+ details << pastel.magenta(" through :#{assoc.options[:through]}") if assoc.options[:through]
124
+ details << pastel.dim(" [#{assoc.join_table}]") if assoc.respond_to?(:join_table) && assoc.join_table
125
+
126
+ # Try to get count for has_many associations
127
+ if assoc.macro == :has_many && model.respond_to?(:first)
128
+ count_str = get_association_count(model, assoc)
129
+ details << count_str if count_str
130
+ end
131
+
132
+ details.join
133
+ end
134
+
135
+ def get_association_count(model, assoc)
136
+ return nil unless model.respond_to?(:first)
137
+
138
+ sample = model.first
139
+ return nil unless sample
140
+
141
+ count = sample.send(assoc.name).count rescue nil
142
+ return nil unless count
143
+
144
+ pastel.dim(" [~#{count} per record]")
145
+ end
146
+
147
+ def print_navigation_options
148
+ puts pastel.yellow.bold("\n📌 Navigation Options:")
149
+ puts " #{pastel.bright_blue.bold('b')} - Go back to previous model" if @history.any?
150
+ puts " #{pastel.bright_blue.bold('q')} - Exit navigator"
151
+ puts " #{pastel.bright_blue.bold('s')} - Show sample records for current model"
152
+ puts " #{pastel.bright_blue.bold('c')} - Show count for all associations"
153
+ end
154
+
155
+ def get_all_associations(model)
156
+ klass = model.is_a?(Class) ? model : model.klass
157
+
158
+ [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many].flat_map do |macro|
159
+ klass.reflect_on_all_associations(macro)
160
+ end
161
+ end
162
+
163
+ def get_user_choice(model)
164
+ print pastel.bright_green("\n➤ Enter choice: ")
165
+ input = gets.chomp.downcase.strip
166
+
167
+ return :exit if input == 'q'
168
+ return :back if input == 'b'
169
+ return handle_sample_records(model) if input == 's'
170
+ return handle_association_counts(model) if input == 'c'
171
+
172
+ resolve_choice(input)
173
+ end
174
+
175
+ def resolve_choice(input)
176
+ if input.match?(/^\d+$/)
177
+ index = input.to_i - 1
178
+ return @menu_items[index] if index >= 0 && index < @menu_items.length
179
+
180
+ puts pastel.red("Invalid choice. Please select a number from the menu.")
181
+ nil
182
+ else
183
+ assoc = @menu_items.find { |a| a.name.to_s == input }
184
+ assoc || (puts pastel.red("Invalid choice.") && nil)
185
+ end
186
+ end
187
+
188
+ def handle_back(current_model)
189
+ if @history.any?
190
+ @history.pop
191
+ else
192
+ puts pastel.yellow("Already at the starting model")
193
+ end
194
+ end
195
+
196
+ def navigate_to(current_model, association)
197
+ klass = current_model.is_a?(Class) ? current_model : current_model.klass
198
+ target_class = association.class_name.constantize
199
+
200
+ puts pastel.green("\n✅ Navigating to #{target_class.name}...")
201
+ target_class
202
+ rescue NameError => e
203
+ puts pastel.red("❌ Error: Could not find model #{association.class_name}")
204
+ puts pastel.yellow(" Make sure the model is loaded and defined.")
205
+ current_model
206
+ rescue => e
207
+ puts pastel.red("❌ Error navigating: #{e.message}")
208
+ current_model
209
+ end
210
+
211
+ def handle_sample_records(model)
212
+ klass = model.is_a?(Class) ? model : model.klass
213
+
214
+ puts pastel.bright_magenta.bold("\n📋 Sample Records from #{klass.name}:")
215
+ puts pastel.dim("─" * 60)
216
+
217
+ records = klass.limit(3).to_a
218
+
219
+ if records.empty?
220
+ puts pastel.yellow(" No records found in database")
221
+ else
222
+ records.each_with_index do |record, index|
223
+ puts pastel.cyan("\n Record ##{index + 1} (ID: #{record.id}):")
224
+ display_record_attributes(record)
225
+ end
226
+ end
227
+
228
+ puts pastel.dim("─" * 60)
229
+ nil
230
+ end
231
+
232
+ def display_record_attributes(record)
233
+ display_attrs = record.attributes.first(5)
234
+ display_attrs.each do |key, value|
235
+ value_str = value.nil? ? pastel.dim("nil") : value.to_s.truncate(50)
236
+ puts " #{pastel.blue(key)}: #{pastel.white(value_str)}"
237
+ end
238
+
239
+ if record.attributes.size > 5
240
+ puts pastel.dim(" ... and #{record.attributes.size - 5} more attributes")
241
+ end
242
+ end
243
+
244
+ def handle_association_counts(model)
245
+ klass = model.is_a?(Class) ? model : model.klass
246
+
247
+ puts pastel.bright_magenta.bold("\n📊 Association Counts for #{klass.name}:")
248
+ puts pastel.dim("─" * 60)
249
+
250
+ sample = klass.first
251
+ unless sample
252
+ puts pastel.yellow(" No records to analyze")
253
+ return nil
254
+ end
255
+
256
+ associations = get_all_associations(model)
257
+ associations.each do |assoc|
258
+ print_association_count(sample, assoc)
259
+ end
260
+
261
+ puts pastel.dim("─" * 60)
262
+ nil
263
+ end
264
+
265
+ def print_association_count(sample, assoc)
266
+ count = sample.send(assoc.name).count
267
+ count_str = count == 0 ? pastel.red(count.to_s) : pastel.green(count.to_s)
268
+ puts " #{pastel.blue(assoc.name.to_s.ljust(25))} → #{count_str} records"
269
+ rescue => e
270
+ puts " #{pastel.blue(assoc.name.to_s.ljust(25))} → #{pastel.dim('error loading')}"
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # Base printer class with common functionality
5
+ class BasePrinter
6
+ include ColorHelper
7
+
8
+ attr_reader :output, :value, :pry_instance
9
+
10
+ def initialize(output, value, pry_instance)
11
+ @output = output
12
+ @value = value
13
+ @pry_instance = pry_instance
14
+ end
15
+
16
+ def print
17
+ Pry::ColorPrinter.default(output, value, pry_instance)
18
+ end
19
+
20
+ protected
21
+
22
+ # Border helper with caching
23
+ BORDER_CACHE = {}
24
+ private_constant :BORDER_CACHE
25
+
26
+ def border(char = nil, length = nil)
27
+ char ||= config.border_char
28
+ length ||= config.header_width
29
+ cache_key = "#{char}#{length}"
30
+ BORDER_CACHE[cache_key] ||= color(config.get_color(:border), char * length)
31
+ output.puts BORDER_CACHE[cache_key]
32
+ end
33
+
34
+ def header(title, width = nil)
35
+ width ||= config.header_width
36
+ header_color = config.get_color(:header)
37
+ output.puts color(header_color, "┌─ #{title} " + "─" * (width - title.length - 4))
38
+ end
39
+
40
+ def footer(width = nil)
41
+ width ||= config.header_width
42
+ footer_color = config.get_color(:footer)
43
+ output.puts color(footer_color, "└" + "─" * width)
44
+ end
45
+
46
+ # Helper for bold colored text (delegates to ColorHelper)
47
+ def bold_color(method, text)
48
+ RailsConsolePro::ColorHelper.pastel.bold.public_send(method, text)
49
+ end
50
+
51
+ # Format value with type-aware coloring
52
+ def format_value(val)
53
+ case val
54
+ when NilClass
55
+ color(config.get_color(:attribute_value_nil), "nil")
56
+ when Numeric
57
+ color(config.get_color(:attribute_value_numeric), val.inspect)
58
+ when TrueClass, FalseClass
59
+ color(config.get_color(:attribute_value_boolean), val.inspect)
60
+ when Time, ActiveSupport::TimeWithZone
61
+ color(config.get_color(:attribute_value_time), val.to_s)
62
+ when String
63
+ color(config.get_color(:attribute_value_string), val.inspect)
64
+ else
65
+ color(config.get_color(:attribute_value_string), val.inspect)
66
+ end
67
+ end
68
+
69
+ # Get configuration instance
70
+ def config
71
+ RailsConsolePro.config
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ # Color helper module with memoization for performance
5
+ module ColorHelper
6
+ extend self
7
+
8
+ # Memoized pastel instance
9
+ def pastel
10
+ @pastel ||= PASTEL
11
+ end
12
+
13
+ # Delegates color methods to pastel with memoization
14
+ def color(method, text)
15
+ pastel.public_send(method, text)
16
+ end
17
+
18
+ # Helper for bold colored text
19
+ def bold_color(method, text)
20
+ pastel.bold.public_send(method, text)
21
+ end
22
+
23
+ # Chainable color methods
24
+ def method_missing(method_name, *args, &block)
25
+ if pastel.respond_to?(method_name)
26
+ pastel.public_send(method_name, *args, &block)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def respond_to_missing?(method_name, include_private = false)
33
+ pastel.respond_to?(method_name, include_private) || super
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ # Base class for commands with shared functionality
6
+ class BaseCommand
7
+ include ColorHelper
8
+
9
+ protected
10
+
11
+ def pastel
12
+ ColorHelper.pastel
13
+ end
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ class DiffCommand < BaseCommand
6
+ def execute(object1, object2)
7
+ return nil if object1.nil? || object2.nil?
8
+
9
+ execute_diff(object1, object2)
10
+ rescue => e
11
+ RailsConsolePro::ErrorHandler.handle(e, context: :diff)
12
+ end
13
+
14
+ private
15
+
16
+ def execute_diff(object1, object2)
17
+ differences = {}
18
+ identical = true
19
+
20
+ # Handle ActiveRecord objects
21
+ if object1.is_a?(ActiveRecord::Base) && object2.is_a?(ActiveRecord::Base)
22
+ differences, identical = diff_active_record_objects(object1, object2)
23
+ # Handle Hash objects
24
+ elsif object1.is_a?(Hash) && object2.is_a?(Hash)
25
+ differences, identical = diff_hashes(object1, object2)
26
+ # Handle plain objects with attributes
27
+ elsif object1.respond_to?(:attributes) && object2.respond_to?(:attributes)
28
+ differences, identical = diff_by_attributes(object1, object2)
29
+ else
30
+ # Simple comparison
31
+ identical = object1 == object2
32
+ differences = identical ? {} : { value: { old_value: object1, new_value: object2 } }
33
+ end
34
+
35
+ DiffResult.new(
36
+ object1: object1,
37
+ object2: object2,
38
+ differences: differences,
39
+ identical: identical
40
+ )
41
+ end
42
+
43
+ def diff_active_record_objects(object1, object2)
44
+ differences = {}
45
+ identical = true
46
+
47
+ # Get all attributes (including virtual ones)
48
+ all_attrs = (object1.attributes.keys | object2.attributes.keys).uniq
49
+
50
+ all_attrs.each do |attr|
51
+ val1 = object1.read_attribute(attr)
52
+ val2 = object2.read_attribute(attr)
53
+
54
+ if val1 != val2
55
+ identical = false
56
+ differences[attr] = {
57
+ old_value: val1,
58
+ new_value: val2
59
+ }
60
+ end
61
+ end
62
+
63
+ [differences, identical]
64
+ end
65
+
66
+ def diff_hashes(hash1, hash2)
67
+ differences = {}
68
+ identical = true
69
+
70
+ all_keys = (hash1.keys | hash2.keys).uniq
71
+
72
+ all_keys.each do |key|
73
+ val1 = hash1[key]
74
+ val2 = hash2[key]
75
+
76
+ if val1 != val2
77
+ identical = false
78
+ if hash1.key?(key) && hash2.key?(key)
79
+ differences[key] = {
80
+ old_value: val1,
81
+ new_value: val2
82
+ }
83
+ elsif hash1.key?(key)
84
+ differences[key] = {
85
+ only_in_object1: val1
86
+ }
87
+ else
88
+ differences[key] = {
89
+ only_in_object2: val2
90
+ }
91
+ end
92
+ end
93
+ end
94
+
95
+ [differences, identical]
96
+ end
97
+
98
+ def diff_by_attributes(object1, object2)
99
+ differences = {}
100
+ identical = true
101
+
102
+ attrs1 = object1.attributes rescue object1.instance_variables.map { |v| v.to_s.delete('@').to_sym }
103
+ attrs2 = object2.attributes rescue object2.instance_variables.map { |v| v.to_s.delete('@').to_sym }
104
+
105
+ all_attrs = (attrs1 | attrs2).uniq
106
+
107
+ all_attrs.each do |attr|
108
+ val1 = object1.respond_to?(attr) ? object1.public_send(attr) : nil
109
+ val2 = object2.respond_to?(attr) ? object2.public_send(attr) : nil
110
+
111
+ if val1 != val2
112
+ identical = false
113
+ if attrs1.include?(attr) && attrs2.include?(attr)
114
+ differences[attr] = {
115
+ old_value: val1,
116
+ new_value: val2
117
+ }
118
+ elsif attrs1.include?(attr)
119
+ differences[attr] = {
120
+ only_in_object1: val1
121
+ }
122
+ else
123
+ differences[attr] = {
124
+ only_in_object2: val2
125
+ }
126
+ end
127
+ end
128
+ end
129
+
130
+ [differences, identical]
131
+ end
132
+ end
133
+ end
134
+ end
135
+