rails_console_pro 0.1.2 → 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.
- checksums.yaml +4 -4
- data/.rspec_status +288 -240
- data/CHANGELOG.md +7 -0
- data/QUICK_START.md +17 -0
- data/README.md +43 -0
- data/docs/FORMATTING.md +5 -0
- data/docs/MODEL_INTROSPECTION.md +371 -0
- data/docs/MODEL_STATISTICS.md +4 -0
- data/docs/OBJECT_DIFFING.md +6 -0
- data/docs/PROFILING.md +91 -0
- data/docs/QUERY_BUILDER.md +385 -0
- data/docs/QUEUE_INSIGHTS.md +82 -0
- data/docs/SCHEMA_INSPECTION.md +5 -0
- data/docs/SNIPPETS.md +71 -0
- data/lib/rails_console_pro/commands/compare_command.rb +151 -0
- data/lib/rails_console_pro/commands/introspect_command.rb +220 -0
- data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
- data/lib/rails_console_pro/commands/profile_command.rb +84 -0
- data/lib/rails_console_pro/commands/query_builder_command.rb +43 -0
- data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
- data/lib/rails_console_pro/commands.rb +30 -0
- data/lib/rails_console_pro/compare_result.rb +81 -0
- data/lib/rails_console_pro/configuration.rb +51 -0
- data/lib/rails_console_pro/format_exporter.rb +32 -0
- data/lib/rails_console_pro/global_methods.rb +24 -0
- data/lib/rails_console_pro/initializer.rb +41 -1
- data/lib/rails_console_pro/introspect_result.rb +101 -0
- data/lib/rails_console_pro/model_validator.rb +1 -1
- data/lib/rails_console_pro/printers/compare_printer.rb +138 -0
- data/lib/rails_console_pro/printers/introspect_printer.rb +282 -0
- data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
- data/lib/rails_console_pro/printers/query_builder_printer.rb +81 -0
- data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
- data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
- data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
- data/lib/rails_console_pro/profile_result.rb +109 -0
- data/lib/rails_console_pro/pry_commands.rb +106 -0
- data/lib/rails_console_pro/query_builder.rb +197 -0
- data/lib/rails_console_pro/query_builder_result.rb +66 -0
- data/lib/rails_console_pro/queue_insights_result.rb +110 -0
- data/lib/rails_console_pro/serializers/compare_serializer.rb +66 -0
- data/lib/rails_console_pro/serializers/introspect_serializer.rb +99 -0
- data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
- data/lib/rails_console_pro/serializers/query_builder_serializer.rb +35 -0
- data/lib/rails_console_pro/services/introspection_collector.rb +420 -0
- data/lib/rails_console_pro/services/profile_collector.rb +245 -0
- data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
- data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
- data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
- data/lib/rails_console_pro/snippets/collection_result.rb +45 -0
- data/lib/rails_console_pro/snippets/single_result.rb +30 -0
- data/lib/rails_console_pro/snippets/snippet.rb +112 -0
- data/lib/rails_console_pro/snippets.rb +13 -0
- data/lib/rails_console_pro/version.rb +1 -1
- data/rails_console_pro.gemspec +1 -1
- metadata +42 -8
|
@@ -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,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
class JobsCommand < BaseCommand
|
|
6
|
+
DEFAULT_LIMIT = 20
|
|
7
|
+
|
|
8
|
+
def execute(options = nil)
|
|
9
|
+
unless feature_enabled?
|
|
10
|
+
puts pastel.yellow("Jobs command is disabled. Enable it with: RailsConsolePro.configure { |c| c.queue_command_enabled = true }")
|
|
11
|
+
return nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless active_job_available?
|
|
15
|
+
puts pastel.red("ActiveJob is not loaded. Queue insights require ActiveJob to be available.")
|
|
16
|
+
return nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
normalized_options = normalize_options(options)
|
|
20
|
+
|
|
21
|
+
fetch_options, filter_options, action_options = split_options(normalized_options)
|
|
22
|
+
action_results = perform_actions(action_options, fetch_options)
|
|
23
|
+
action_results.each { |result| print_action_result(result) }
|
|
24
|
+
|
|
25
|
+
result = fetcher.fetch(**fetch_options)
|
|
26
|
+
return result unless result
|
|
27
|
+
|
|
28
|
+
apply_filters(result, filter_options)
|
|
29
|
+
rescue => e
|
|
30
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :jobs)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def feature_enabled?
|
|
36
|
+
RailsConsolePro.config.queue_command_enabled
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def active_job_available?
|
|
40
|
+
defined?(ActiveJob::Base)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fetcher
|
|
44
|
+
@fetcher ||= Services::QueueInsightFetcher.new
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def action_service
|
|
48
|
+
@action_service ||= Services::QueueActionService.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def perform_actions(action_options, fetch_options)
|
|
52
|
+
return [] if action_options.empty?
|
|
53
|
+
|
|
54
|
+
action_options.each_with_object([]) do |(action_key, value), results|
|
|
55
|
+
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
56
|
+
|
|
57
|
+
result = action_service.perform(
|
|
58
|
+
action: action_key,
|
|
59
|
+
jid: value,
|
|
60
|
+
queue: fetch_options[:queue]
|
|
61
|
+
)
|
|
62
|
+
results << result if result
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def apply_filters(result, filter_options)
|
|
67
|
+
filters = prepare_filters(filter_options)
|
|
68
|
+
filtered_enqueued = result.enqueued_jobs
|
|
69
|
+
filtered_retry = result.retry_jobs
|
|
70
|
+
filtered_recent = result.recent_executions
|
|
71
|
+
additional_warnings = []
|
|
72
|
+
|
|
73
|
+
statuses = filters[:statuses]
|
|
74
|
+
unless statuses.empty?
|
|
75
|
+
show_enqueued = statuses.include?(:enqueued) || statuses.include?(:scheduled) || statuses.include?(:all)
|
|
76
|
+
show_retry = statuses.include?(:retry) || statuses.include?(:retries) || statuses.include?(:all)
|
|
77
|
+
show_recent = statuses.include?(:recent) || statuses.include?(:executing) || statuses.include?(:running) || statuses.include?(:all)
|
|
78
|
+
|
|
79
|
+
filtered_enqueued = [] unless show_enqueued
|
|
80
|
+
filtered_retry = [] unless show_retry
|
|
81
|
+
filtered_recent = [] unless show_recent
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if filters[:job_class]
|
|
85
|
+
matcher = filters[:job_class]
|
|
86
|
+
filtered_enqueued = filtered_enqueued.select { |job| job_class_matches?(job, matcher) }
|
|
87
|
+
filtered_retry = filtered_retry.select { |job| job_class_matches?(job, matcher) }
|
|
88
|
+
filtered_recent = filtered_recent.select { |job| job_class_matches?(job, matcher) }
|
|
89
|
+
if filtered_enqueued.empty? && filtered_retry.empty? && filtered_recent.empty?
|
|
90
|
+
additional_warnings << "No jobs matching class filter '#{matcher}'."
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
filtered_result = result.with_overrides(
|
|
95
|
+
enqueued_jobs: filtered_enqueued,
|
|
96
|
+
retry_jobs: filtered_retry,
|
|
97
|
+
recent_executions: filtered_recent,
|
|
98
|
+
warnings: result.warnings + additional_warnings
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
filtered_result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_options(options)
|
|
105
|
+
case options
|
|
106
|
+
when Numeric
|
|
107
|
+
{ limit: options.to_i }
|
|
108
|
+
when Hash
|
|
109
|
+
symbolized = options.each_with_object({}) do |(key, value), acc|
|
|
110
|
+
sym_key = key.to_sym
|
|
111
|
+
sym_key = :job_class if sym_key == :class
|
|
112
|
+
acc[sym_key] = value
|
|
113
|
+
end
|
|
114
|
+
symbolized
|
|
115
|
+
when nil
|
|
116
|
+
{}
|
|
117
|
+
else
|
|
118
|
+
{}
|
|
119
|
+
end.then do |opts|
|
|
120
|
+
limit = opts.fetch(:limit, DEFAULT_LIMIT)
|
|
121
|
+
limit = limit.to_i
|
|
122
|
+
limit = DEFAULT_LIMIT if limit <= 0
|
|
123
|
+
opts[:limit] = limit
|
|
124
|
+
opts[:job_class] = opts[:job_class].to_s.strip if opts.key?(:job_class) && opts[:job_class]
|
|
125
|
+
[:retry, :delete, :details].each do |key|
|
|
126
|
+
opts[key] = opts[key].to_s.strip if opts.key?(key) && opts[key]
|
|
127
|
+
end
|
|
128
|
+
opts
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def split_options(options)
|
|
133
|
+
fetch_options = {
|
|
134
|
+
limit: options[:limit],
|
|
135
|
+
queue: options[:queue]
|
|
136
|
+
}.compact
|
|
137
|
+
|
|
138
|
+
filter_options = {
|
|
139
|
+
statuses: options[:statuses] || options[:status],
|
|
140
|
+
job_class: options[:job_class]
|
|
141
|
+
}.compact
|
|
142
|
+
|
|
143
|
+
action_options = {
|
|
144
|
+
retry: options[:retry],
|
|
145
|
+
delete: options[:delete],
|
|
146
|
+
details: options[:details]
|
|
147
|
+
}.compact
|
|
148
|
+
|
|
149
|
+
[fetch_options, filter_options, action_options]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def prepare_filters(filter_options)
|
|
153
|
+
statuses =
|
|
154
|
+
Array(filter_options[:statuses]).flat_map { |s| s.to_s.split(',') }
|
|
155
|
+
.map(&:strip)
|
|
156
|
+
.reject(&:empty?)
|
|
157
|
+
.map { |s| s.downcase.to_sym }
|
|
158
|
+
.uniq
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
statuses: statuses,
|
|
162
|
+
job_class: filter_options[:job_class],
|
|
163
|
+
limit: filter_options[:limit]
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def job_class_matches?(job, matcher)
|
|
168
|
+
job_class = job.job_class.to_s
|
|
169
|
+
return false if job_class.empty?
|
|
170
|
+
|
|
171
|
+
matcher_str = matcher.to_s
|
|
172
|
+
job_class.casecmp?(matcher_str) ||
|
|
173
|
+
job_class.split('::').last.casecmp?(matcher_str) ||
|
|
174
|
+
job_class.include?(matcher_str)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_action_result(result)
|
|
178
|
+
return unless result
|
|
179
|
+
|
|
180
|
+
if result.warning
|
|
181
|
+
puts pastel.yellow("⚠️ #{result.warning}")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if result.message
|
|
185
|
+
color_method = result.success ? :green : :cyan
|
|
186
|
+
puts pastel.public_send(color_method, result.message)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if result.details
|
|
190
|
+
puts pastel.cyan("Details:")
|
|
191
|
+
formatted = format_details(result.details)
|
|
192
|
+
puts formatted
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def format_details(details)
|
|
197
|
+
case details
|
|
198
|
+
when String
|
|
199
|
+
" #{details}"
|
|
200
|
+
when Hash
|
|
201
|
+
details.map { |key, value| " #{key}: #{value.inspect}" }.join("\n")
|
|
202
|
+
when Array
|
|
203
|
+
details.map { |value| " - #{value.inspect}" }.join("\n")
|
|
204
|
+
else
|
|
205
|
+
details.inspect
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for profiling arbitrary blocks, callables, or ActiveRecord relations
|
|
6
|
+
class ProfileCommand < BaseCommand
|
|
7
|
+
def execute(target = nil, *args, label: nil, &block)
|
|
8
|
+
return disabled_message unless enabled?
|
|
9
|
+
|
|
10
|
+
label, target, block = normalize_arguments(label, target, block)
|
|
11
|
+
execution = build_execution(target, args, block)
|
|
12
|
+
return execution if execution.nil? || execution.is_a?(String)
|
|
13
|
+
|
|
14
|
+
collector = Services::ProfileCollector.new(config)
|
|
15
|
+
collector.profile(label: label || execution[:label]) do
|
|
16
|
+
execution[:callable].call
|
|
17
|
+
end
|
|
18
|
+
rescue => e
|
|
19
|
+
RailsConsolePro::ErrorHandler.handle(e, context: :profile)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def enabled?
|
|
25
|
+
RailsConsolePro.config.enabled && RailsConsolePro.config.profile_command_enabled
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disabled_message
|
|
29
|
+
pastel.yellow('Profile command is disabled. Enable it via RailsConsolePro.configure { |c| c.profile_command_enabled = true }')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def config
|
|
33
|
+
RailsConsolePro.config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def normalize_arguments(label, target, block)
|
|
37
|
+
if block_provided?(block) && (target.is_a?(String) || target.is_a?(Symbol))
|
|
38
|
+
label ||= target.to_s
|
|
39
|
+
target = nil
|
|
40
|
+
end
|
|
41
|
+
[label, target, block]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def block_provided?(block)
|
|
45
|
+
!block.nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_execution(target, args, block)
|
|
49
|
+
if block_provided?(block)
|
|
50
|
+
{ callable: block, label: nil }
|
|
51
|
+
elsif relation?(target)
|
|
52
|
+
relation = target
|
|
53
|
+
{ callable: -> { relation.load }, label: "#{relation.klass.name} relation" }
|
|
54
|
+
elsif callable?(target)
|
|
55
|
+
callable = target
|
|
56
|
+
{ callable: -> { callable.call(*args) }, label: callable_label(callable) }
|
|
57
|
+
elsif target.nil?
|
|
58
|
+
pastel.red('Nothing to profile. Provide a block, callable, or ActiveRecord relation.')
|
|
59
|
+
else
|
|
60
|
+
{ callable: -> { target }, label: target.class.name }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def relation?(object)
|
|
65
|
+
defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def callable?(object)
|
|
69
|
+
object.respond_to?(:call)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def callable_label(callable)
|
|
73
|
+
if callable.respond_to?(:name) && callable.name
|
|
74
|
+
callable.name
|
|
75
|
+
elsif callable.respond_to?(:receiver) && callable.respond_to?(:name)
|
|
76
|
+
"#{callable.receiver.class}##{callable.name}"
|
|
77
|
+
else
|
|
78
|
+
callable.class.name
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsConsolePro
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for managing, searching, and listing reusable console snippets
|
|
6
|
+
class SnippetsCommand < BaseCommand
|
|
7
|
+
DEFAULT_LIST_LIMIT = 10
|
|
8
|
+
|
|
9
|
+
def execute(action = :list, *args, **kwargs, &block)
|
|
10
|
+
return disabled_message unless enabled?
|
|
11
|
+
|
|
12
|
+
action = infer_action(action, args)
|
|
13
|
+
case action
|
|
14
|
+
when :list
|
|
15
|
+
list_snippets(**kwargs)
|
|
16
|
+
when :search
|
|
17
|
+
search_snippets(*args, **kwargs)
|
|
18
|
+
when :add, :create
|
|
19
|
+
add_snippet(*args, **kwargs, &block)
|
|
20
|
+
when :show
|
|
21
|
+
show_snippet(*args)
|
|
22
|
+
when :delete, :remove
|
|
23
|
+
delete_snippet(*args)
|
|
24
|
+
when :favorite
|
|
25
|
+
toggle_favorite(*args, value: true)
|
|
26
|
+
when :unfavorite
|
|
27
|
+
toggle_favorite(*args, value: false)
|
|
28
|
+
else
|
|
29
|
+
pastel.red("Unknown snippets action: #{action}")
|
|
30
|
+
end
|
|
31
|
+
rescue ArgumentError => e
|
|
32
|
+
pastel.red("Snippets error: #{e.message}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def enabled?
|
|
38
|
+
RailsConsolePro.config.enabled && RailsConsolePro.config.snippets_command_enabled
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def disabled_message
|
|
42
|
+
pastel.yellow('Snippets command is disabled. Enable via RailsConsolePro.configure { |c| c.snippets_command_enabled = true }')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def infer_action(action, args)
|
|
46
|
+
return action.to_sym if action.respond_to?(:to_sym)
|
|
47
|
+
return :search if args.any?
|
|
48
|
+
|
|
49
|
+
:list
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def list_snippets(limit: DEFAULT_LIST_LIMIT, favorites: false)
|
|
53
|
+
snippets = repository.all(limit: limit)
|
|
54
|
+
snippets = snippets.select(&:favorite?) if favorites
|
|
55
|
+
|
|
56
|
+
Snippets::CollectionResult.new(
|
|
57
|
+
snippets: snippets,
|
|
58
|
+
limit: limit,
|
|
59
|
+
total_count: repository.all.size,
|
|
60
|
+
tags: favorites ? ['favorite'] : []
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def search_snippets(term = nil, tags: nil, limit: DEFAULT_LIST_LIMIT)
|
|
65
|
+
search_result = repository.search(term: term, tags: tags, limit: limit)
|
|
66
|
+
snippets = search_result[:results]
|
|
67
|
+
Snippets::CollectionResult.new(
|
|
68
|
+
snippets: snippets,
|
|
69
|
+
query: term,
|
|
70
|
+
tags: Array(tags),
|
|
71
|
+
limit: limit,
|
|
72
|
+
total_count: search_result[:total_count]
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_snippet(body = nil, id: nil, tags: nil, description: nil, favorite: false, metadata: nil, &block)
|
|
77
|
+
snippet_body = extract_body(body, &block)
|
|
78
|
+
|
|
79
|
+
snippet = repository.add(
|
|
80
|
+
body: snippet_body,
|
|
81
|
+
id: id,
|
|
82
|
+
tags: tags,
|
|
83
|
+
description: description,
|
|
84
|
+
favorite: favorite,
|
|
85
|
+
metadata: metadata
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
Snippets::SingleResult.new(snippet: snippet, action: :add, created: true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def show_snippet(id)
|
|
92
|
+
snippet = repository.find(id)
|
|
93
|
+
return pastel.yellow("No snippet found for '#{id}'") unless snippet
|
|
94
|
+
|
|
95
|
+
Snippets::SingleResult.new(snippet: snippet, action: :show, created: false)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delete_snippet(id)
|
|
99
|
+
if repository.delete(id)
|
|
100
|
+
pastel.green("Removed snippet '#{id}'")
|
|
101
|
+
else
|
|
102
|
+
pastel.yellow("No snippet found for '#{id}'")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def toggle_favorite(id, value:)
|
|
107
|
+
snippet = repository.update(id, favorite: value)
|
|
108
|
+
return pastel.yellow("No snippet found for '#{id}'") unless snippet
|
|
109
|
+
|
|
110
|
+
status = value ? 'marked as favorite' : 'removed from favorites'
|
|
111
|
+
message = pastel.green("Snippet '#{id}' #{status}.")
|
|
112
|
+
Snippets::SingleResult.new(
|
|
113
|
+
snippet: snippet,
|
|
114
|
+
action: value ? :favorite : :unfavorite,
|
|
115
|
+
created: false,
|
|
116
|
+
message: message
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def extract_body(body, &block)
|
|
121
|
+
content = if block
|
|
122
|
+
result = block.call
|
|
123
|
+
result.respond_to?(:join) ? result.join("\n") : result.to_s
|
|
124
|
+
else
|
|
125
|
+
body
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
raise ArgumentError, 'Snippet body is required' if content.nil? || content.to_s.strip.empty?
|
|
129
|
+
|
|
130
|
+
content.to_s
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def repository
|
|
134
|
+
@repository ||= RailsConsolePro::Services::SnippetRepository.new(
|
|
135
|
+
store_path: RailsConsolePro.config.snippet_store_path
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|