Class: DataShifter::Shift
- Inherits:
-
Object
- Object
- DataShifter::Shift
- Includes:
- Axn
- Defined in:
- lib/data_shifter/shift.rb
Class Method Summary collapse
-
.allow_external_requests(hosts) ⇒ Object
Allow these hosts (or regexes) for HTTP during dry run only.
- .description(text = nil) ⇒ Object
-
.progress(enabled = nil) ⇒ Object
Per-shift override for progress bar visibility.
- .run! ⇒ Object
-
.suppress_repeated_logs(enabled) ⇒ Object
Enable/disable log deduplication for this shift.
-
.task(label = nil, axn = nil, **axn_kwargs, &block) ⇒ Object
Define a task to run instead of collection/process_record.
- .task_name(value = nil) ⇒ Object
- .throttle(interval, per: 1) ⇒ Object
- .transaction(mode) ⇒ Object
Instance Method Summary collapse
-
#call ⇒ Object
--- Public API (intentionally exposed to subclasses) ---.
- #dry_run? ⇒ Boolean
- #find_exactly!(model, ids) ⇒ Object
-
#inline_csv(**csv_opts) ⇒ Object
Parse CSV colocated with the shift, after a
__END__marker, so small data sets can live alongside the code. - #log(message) ⇒ Object
- #skip!(reason = nil) ⇒ Object
Class Method Details
.allow_external_requests(hosts) ⇒ Object
Allow these hosts (or regexes) for HTTP during dry run only. Combines with DataShifter.config.allow_external_requests. Has no effect in commit mode — HTTP is unrestricted when dry_run is false. Example: allow_external_requests ["api.readonly.example.com", %r\\.internal\\.internal\.company\z]
136 137 138 |
# File 'lib/data_shifter/shift.rb', line 136 def allow_external_requests(hosts) self._allow_external_requests = Array(hosts) end |
.description(text = nil) ⇒ Object
86 87 88 89 90 91 92 |
# File 'lib/data_shifter/shift.rb', line 86 def description(text = nil) if text.nil? _description else self._description = text.to_s.presence end end |
.progress(enabled = nil) ⇒ Object
Per-shift override for progress bar visibility. Overrides DataShifter.config.progress_enabled.
Thin alias over the progress_enabled override accessor generated by DataShifter.overrides.
progress false # set override (coerced to a boolean)
progress # read the raw override (nil when unset; does NOT fall back to config)
119 120 121 122 123 124 125 126 |
# File 'lib/data_shifter/shift.rb', line 119 def progress(enabled = nil) if enabled.nil? raw = progress_enabled_override Axn::Configurable::UNSET.equal?(raw) ? nil : raw else progress_enabled(!!enabled) end end |
.run! ⇒ Object
175 176 177 178 179 180 |
# File 'lib/data_shifter/shift.rb', line 175 def run! dry_run = Internal::Env.dry_run? result = call(dry_run:) raise result.exception if result.exception raise StandardError, result.error unless result.ok? end |
.suppress_repeated_logs(enabled) ⇒ Object
Enable/disable log deduplication for this shift. Overrides DataShifter.config.suppress_repeated_logs.
Thin alias over the suppress_repeated_logs override accessor generated by DataShifter.overrides
(which would otherwise read the resolved value when called with no args); we always treat a call
with an argument as a set, coercing to a boolean.
Example: suppress_repeated_logs false
145 146 147 |
# File 'lib/data_shifter/shift.rb', line 145 def suppress_repeated_logs(enabled) super(!!enabled) end |
.task(label = nil, axn = nil, **axn_kwargs, &block) ⇒ Object
Define a task to run instead of collection/process_record. Multiple tasks run in sequence; labels appear in errors and summary.
Block form (arbitrary code, runtime/instance state available):
task "Fix user A" do
User.find(123).update!(...)
end
Axn class form (sugar for one helper axn; kwargs are static, evaluated at class-load time, and forwarded to .call! so a failure is never silently swallowed). Use the block form when you need runtime values:
task "Recalculate totals", RecalculateTotals, company_id: 123
# => RecalculateTotals.call!(company_id: 123)
162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/data_shifter/shift.rb', line 162 def task(label = nil, axn = nil, **axn_kwargs, &block) if axn raise ArgumentError, "task accepts either an Axn class or a block, not both" if block raise ArgumentError, "task expected an Axn class but got #{axn.inspect}" unless axn.is_a?(Class) && axn.include?(Axn) block = -> { axn.call!(**axn_kwargs) } elsif block.nil? raise ArgumentError, "task requires a block or an Axn class" end self._task_blocks = (_task_blocks || []).dup + [{ label: label.presence, block: }] end |
.task_name(value = nil) ⇒ Object
94 95 96 97 98 99 100 |
# File 'lib/data_shifter/shift.rb', line 94 def task_name(value = nil) if value.nil? _task_name else self._task_name = value.to_s.presence end end |
.throttle(interval, per: 1) ⇒ Object
128 129 130 131 |
# File 'lib/data_shifter/shift.rb', line 128 def throttle(interval, per: 1) self._throttle_interval = interval self._throttle_per = per end |
.transaction(mode) ⇒ Object
102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/data_shifter/shift.rb', line 102 def transaction(mode) case mode when :per_record self._transaction_mode = :per_record when :none, false self._transaction_mode = :none when :single, true self._transaction_mode = :single else raise ArgumentError, "Invalid transaction mode: #{mode.inspect}. Expected :single, :per_record, :none, true, or false." end end |
Instance Method Details
#call ⇒ Object
--- Public API (intentionally exposed to subclasses) ---
185 186 187 188 189 190 191 |
# File 'lib/data_shifter/shift.rb', line 185 def call if self.class._task_blocks.any? _run_task_blocks else _for_each_record_in(collection) { |record| process_record(record) } end end |
#dry_run? ⇒ Boolean
222 |
# File 'lib/data_shifter/shift.rb', line 222 def dry_run? = dry_run |
#find_exactly!(model, ids) ⇒ Object
193 194 195 196 197 198 199 200 201 202 |
# File 'lib/data_shifter/shift.rb', line 193 def find_exactly!(model, ids) ids = Array(ids).compact.uniq return model.none if ids.empty? records_by_id = model.where(id: ids).index_by(&:id) missing = ids.reject { |id| records_by_id.key?(id) } raise "Expected #{model.name} with ids #{ids.inspect}, but missing: #{missing.inspect}" if missing.any? ids.map { |id| records_by_id[id] } end |
#inline_csv(**csv_opts) ⇒ Object
Parse CSV colocated with the shift, after a __END__ marker, so small
data sets can live alongside the code. Returns the data rows as an array
(CSV::Row objects when headers: true, the default — so row["id"] works);
extra options forward straight to CSV.parse. Typically used as the
collection:
def collection = inline_csv
def process_record(row) = User.find(row["id"]).update!(...)
__END__
id,...
Lazily requires csv (a bundled gem on Ruby 3.4+); raises with a hint to
add it to the Gemfile if unavailable.
216 217 218 219 220 |
# File 'lib/data_shifter/shift.rb', line 216 def inline_csv(**csv_opts) _require_csv! parsed = CSV.parse(_inline_data_body, headers: true, **csv_opts) parsed.is_a?(CSV::Table) ? parsed.each.to_a : parsed end |
#log(message) ⇒ Object
231 232 233 |
# File 'lib/data_shifter/shift.rb', line 231 def log() puts Internal::Colors.dim() end |
#skip!(reason = nil) ⇒ Object
224 225 226 227 228 229 |
# File 'lib/data_shifter/shift.rb', line 224 def skip!(reason = nil) @stats[:skipped] += 1 key = reason.to_s.presence || "(no reason given)" @skip_reasons[key] += 1 raise SkipRecord end |