Module: DataShifter::Internal::Output
- Defined in:
- lib/data_shifter/internal/output.rb
Overview
Output formatting utilities for data shift runs. All methods are stateless module functions that accept IO and context parameters.
Constant Summary collapse
- TRANSACTION_MODE_LABELS =
{ single: "single (all-or-nothing)", per_record: "per-record", none: "none", }.freeze
- SKIP_REASONS_DISPLAY_LIMIT =
10- DIVIDER =
"=" * 60
- SEPARATOR =
"-" * 60
Class Method Summary collapse
- .build_status_line(status_interval) ⇒ Object
- .mode_label(dry_run:, io:) ⇒ Object
- .print_continue_from_hint(io:, task_name:, last_successful_id:, dry_run:, transaction_mode:, errors:) ⇒ Object
- .print_dry_run_instructions(io:, task_name:) ⇒ Object
- .print_errors(io:, errors:) ⇒ Object
-
.print_header(io:, shift_class:, total:, label:, dry_run:, transaction_mode:, status_interval:) ⇒ Object
— Public header methods —.
- .print_header_bottom(io:, status_interval:) ⇒ Object
-
.print_header_top(io:, shift_class:, dry_run:) ⇒ Object
— Private helpers —.
- .print_interrupt_warning(io:, transaction_mode:, dry_run:) ⇒ Object
- .print_progress(io:, stats:, errors:, start_time:, status_interval:, skip_reasons: {}) ⇒ Object
- .print_single_error(io:, err:) ⇒ Object
- .print_skip_reasons(io:, skip_reasons:) ⇒ Object
- .print_stats(io:, stats:, start_time:, skip_reasons:) ⇒ Object
-
.print_summary(io:, stats:, errors:, start_time:, dry_run:, transaction_mode:, interrupted:, task_name:, last_successful_id:, skip_reasons: {}) ⇒ Object
— Public summary/progress methods —.
- .print_task_header(io:, shift_class:, block_count:, dry_run:, transaction_mode:, status_interval:) ⇒ Object
- .status_trigger(status_interval) ⇒ Object
- .summary_divider(has_failures:, io:) ⇒ Object
- .summary_title(dry_run:, interrupted:, has_failures: false, io: $stdout) ⇒ Object
- .task_transaction_label(mode) ⇒ Object
Class Method Details
.build_status_line(status_interval) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/data_shifter/internal/output.rb', line 177 def build_status_line(status_interval) status_tips = [] status_tips << "Ctrl+T" if Signal.list.key?("INFO") status_tips << "kill -USR1 #{Process.pid}" if Signal.list.key?("USR1") if status_interval interval_msg = "STATUS_INTERVAL is set to #{status_interval}s." status_tips.any? ? "#{interval_msg} Or: #{status_tips.join(", ")}" : interval_msg elsif status_tips.any? status_tips.join(" or ") end end |
.mode_label(dry_run:, io:) ⇒ Object
115 116 117 118 119 120 121 |
# File 'lib/data_shifter/internal/output.rb', line 115 def mode_label(dry_run:, io:) if dry_run "#{Colors.cyan("DRY RUN", io:)} (no changes will be persisted)" else Colors.warning("LIVE", io:) end end |
.print_continue_from_hint(io:, task_name:, last_successful_id:, dry_run:, transaction_mode:, errors:) ⇒ Object
169 170 171 172 173 174 175 |
# File 'lib/data_shifter/internal/output.rb', line 169 def print_continue_from_hint(io:, task_name:, last_successful_id:, dry_run:, transaction_mode:, errors:) return if dry_run || transaction_mode != :none || errors.empty? || !last_successful_id || !task_name.present? io.puts "" io.puts "To resume from the last successful record:" io.puts " #{Colors.bold("CONTINUE_FROM=#{last_successful_id} COMMIT=1 rake data:shift:#{task_name}", io:)}" end |
.print_dry_run_instructions(io:, task_name:) ⇒ Object
160 161 162 163 164 165 166 167 |
# File 'lib/data_shifter/internal/output.rb', line 160 def print_dry_run_instructions(io:, task_name:) io.puts "" io.puts Colors.cyan("[!] No changes were saved.", io:) return unless task_name.present? io.puts "To apply these changes, run:" io.puts " #{Colors.bold("COMMIT=1 rake data:shift:#{task_name}", io:)}" end |
.print_errors(io:, errors:) ⇒ Object
74 75 76 77 78 |
# File 'lib/data_shifter/internal/output.rb', line 74 def print_errors(io:, errors:) io.puts "" io.puts Colors.error("ERRORS:", io:) errors.each { |err| print_single_error(io:, err:) } end |
.print_header(io:, shift_class:, total:, label:, dry_run:, transaction_mode:, status_interval:) ⇒ Object
— Public header methods —
24 25 26 27 28 29 |
# File 'lib/data_shifter/internal/output.rb', line 24 def print_header(io:, shift_class:, total:, label:, dry_run:, transaction_mode:, status_interval:) print_header_top(io:, shift_class:, dry_run:) io.puts "Records: #{total} #{label}" io.puts "Transaction: #{TRANSACTION_MODE_LABELS[transaction_mode]}" print_header_bottom(io:, status_interval:) end |
.print_header_bottom(io:, status_interval:) ⇒ Object
91 92 93 94 95 96 |
# File 'lib/data_shifter/internal/output.rb', line 91 def print_header_bottom(io:, status_interval:) status_line = build_status_line(status_interval) io.puts Colors.dim("Status: #{status_line} for live progress (no abort)", io:) if status_line io.puts Colors.dim(DIVIDER, io:) io.puts "" end |
.print_header_top(io:, shift_class:, dry_run:) ⇒ Object
— Private helpers —
82 83 84 85 86 87 88 89 |
# File 'lib/data_shifter/internal/output.rb', line 82 def print_header_top(io:, shift_class:, dry_run:) io.puts "" io.puts Colors.dim(DIVIDER, io:) io.puts Colors.bold(shift_class.name || "DataShifter::Shift (anonymous)", io:) io.puts Colors.dim("\"#{shift_class.description}\"", io:) if shift_class.description.present? io.puts Colors.dim(SEPARATOR, io:) io.puts "Mode: #{mode_label(dry_run:, io:)}" end |
.print_interrupt_warning(io:, transaction_mode:, dry_run:) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/data_shifter/internal/output.rb', line 147 def print_interrupt_warning(io:, transaction_mode:, dry_run:) msg = if transaction_mode == :none "`transaction false` mode was active. Some DB changes may have been applied." elsif dry_run "All DB changes have been rolled back (dry run)." else "DB transaction has been rolled back. No DB changes were persisted." end io.puts "" io.puts "#{Colors.warning("[!] INTERRUPTED:", io:)} #{msg}" io.puts " Non-DB side effects (API calls, emails, etc.) are not rolled back." end |
.print_progress(io:, stats:, errors:, start_time:, status_interval:, skip_reasons: {}) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/data_shifter/internal/output.rb', line 59 def print_progress(io:, stats:, errors:, start_time:, status_interval:, skip_reasons: {}) return unless start_time io.puts "" io.puts Colors.cyan(DIVIDER, io:) io.puts "#{Colors.cyan("STATUS (still running)", io:)} — triggered by #{status_trigger(status_interval)}" io.puts Colors.dim(SEPARATOR, io:) print_stats(io:, stats:, start_time:, skip_reasons:) print_errors(io:, errors:) if errors.any? io.puts Colors.cyan(DIVIDER, io:) io.puts "" end |
.print_single_error(io:, err:) ⇒ Object
108 109 110 111 112 113 |
# File 'lib/data_shifter/internal/output.rb', line 108 def print_single_error(io:, err:) lines = err[:error].to_s.split("\n") io.puts " #{Colors.red(err[:record].to_s, io:)}: #{lines.first}" lines.drop(1).each { |line| io.puts " #{line}" } err[:backtrace]&.each { |line| io.puts Colors.dim(" #{line}", io:) } end |
.print_skip_reasons(io:, skip_reasons:) ⇒ Object
190 191 192 193 194 195 196 |
# File 'lib/data_shifter/internal/output.rb', line 190 def print_skip_reasons(io:, skip_reasons:) return if skip_reasons.empty? top = skip_reasons.sort_by { |_reason, count| -count }.first(SKIP_REASONS_DISPLAY_LIMIT) formatted = top.map { |reason, count| "\"#{reason}\" (#{count})" }.join(", ") io.puts " #{formatted}" end |
.print_stats(io:, stats:, start_time:, skip_reasons:) ⇒ Object
98 99 100 101 102 103 104 105 106 |
# File 'lib/data_shifter/internal/output.rb', line 98 def print_stats(io:, stats:, start_time:, skip_reasons:) elapsed = (Time.current - start_time).round(1) io.puts "Duration: #{elapsed}s" io.puts "Processed: #{stats[:processed]}" io.puts "Succeeded: #{Colors.green(stats[:succeeded].to_s, io:)}" io.puts "Failed: #{Colors.red(stats[:failed].to_s, io:)}" if stats[:failed].positive? io.puts "Skipped: #{Colors.yellow(stats[:skipped].to_s, io:)}" if stats[:skipped].positive? print_skip_reasons(io:, skip_reasons:) if skip_reasons.any? end |
.print_summary(io:, stats:, errors:, start_time:, dry_run:, transaction_mode:, interrupted:, task_name:, last_successful_id:, skip_reasons: {}) ⇒ Object
— Public summary/progress methods —
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/data_shifter/internal/output.rb', line 40 def print_summary(io:, stats:, errors:, start_time:, dry_run:, transaction_mode:, interrupted:, task_name:, last_successful_id:, skip_reasons: {}) return unless start_time has_failures = stats[:failed].positive? || interrupted io.puts "" io.puts summary_divider(has_failures:, io:) io.puts summary_title(dry_run:, interrupted:, has_failures:, io:) io.puts Colors.dim(SEPARATOR, io:) print_stats(io:, stats:, start_time:, skip_reasons:) print_errors(io:, errors:) if errors.any? print_interrupt_warning(io:, transaction_mode:, dry_run:) if interrupted print_dry_run_instructions(io:, task_name:) if dry_run && !interrupted print_continue_from_hint(io:, task_name:, last_successful_id:, dry_run:, transaction_mode:, errors:) io.puts summary_divider(has_failures:, io:) end |
.print_task_header(io:, shift_class:, block_count:, dry_run:, transaction_mode:, status_interval:) ⇒ Object
31 32 33 34 35 36 |
# File 'lib/data_shifter/internal/output.rb', line 31 def print_task_header(io:, shift_class:, block_count:, dry_run:, transaction_mode:, status_interval:) print_header_top(io:, shift_class:, dry_run:) io.puts "Tasks: #{block_count}" if block_count >= 2 io.puts "Transaction: #{task_transaction_label(transaction_mode)}" print_header_bottom(io:, status_interval:) end |
.status_trigger(status_interval) ⇒ Object
137 138 139 140 141 142 143 144 145 |
# File 'lib/data_shifter/internal/output.rb', line 137 def status_trigger(status_interval) if status_interval "every #{status_interval}s (STATUS_INTERVAL)" elsif Signal.list.key?("INFO") "Ctrl+T" else "SIGUSR1" end end |
.summary_divider(has_failures:, io:) ⇒ Object
127 128 129 |
# File 'lib/data_shifter/internal/output.rb', line 127 def summary_divider(has_failures:, io:) has_failures ? Colors.red(DIVIDER, io:) : Colors.green(DIVIDER, io:) end |
.summary_title(dry_run:, interrupted:, has_failures: false, io: $stdout) ⇒ Object
131 132 133 134 135 |
# File 'lib/data_shifter/internal/output.rb', line 131 def summary_title(dry_run:, interrupted:, has_failures: false, io: $stdout) base = dry_run ? "SUMMARY (DRY RUN)" : "SUMMARY" title = interrupted ? "#{base} - INTERRUPTED" : base has_failures ? Colors.error(title, io:) : Colors.success(title, io:) end |
.task_transaction_label(mode) ⇒ Object
123 124 125 |
# File 'lib/data_shifter/internal/output.rb', line 123 def task_transaction_label(mode) mode == :per_record ? "per-task" : TRANSACTION_MODE_LABELS[mode] end |