rails_console_pro 0.1.3 → 0.1.4

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