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,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