Module: ConcernsOnRails::Models::Duplicable

Extended by:
ActiveSupport::Concern
Defined in:
lib/concerns_on_rails/models/duplicable.rb

Overview

Concern-aware deep copy ("clone this invoice/template"). A bare ActiveRecord dup copies identity-bearing columns — the slug, the API token, the invoice number, the audit trail, even created_at (which AR preserves on save when present) — so naive copies collide with unique indexes or lie about their history. Duplicable knows its sibling concerns and blanks exactly those columns, letting each concern regenerate fresh values on save.

class Invoice < ApplicationRecord
include ConcernsOnRails::Models::Duplicable
include ConcernsOnRails::Models::Sequenceable

has_many :line_items
sequenceable_by :sequence, into: :number, prefix: "INV-"
duplicable_by associations: %i[line_items],
              reset: %i[issued_at],
              suffix: { title: " (copy)" }
end

copy = invoice.duplicate                  # unsaved deep copy
copy = invoice.duplicate!(title: "Q3")    # saved, with overrides

What gets blanked automatically (identity, not business state):

* created_at / updated_at (AR keeps a present created_at on save)
* Sluggable slug — regenerated from the source field on save
* Tokenizable / Hashable columns — regenerated on create
* Sequenceable sequence + into: columns — next number on create
* Auditable trail column — a copy inherits no history (its own
creation is then audited normally, like any create)
* SoftDeletable timestamp — a copy of trash is a live record
* Lockable attempts (0) / locked_at (nil) — a copy starts unlocked

Business state (Publishable/Stateable/Activatable/...) is a judgment call, so it is NOT auto-reset — list those columns in reset:.

Associations (associations: allow-list, declared before the macro):

* has_many / has_one — children are deep-copied. A child whose class
also includes Duplicable is copied via ITS OWN `duplicate` (own
resets, own nested associations) — recursive graphs stay declarative.
* has_and_belongs_to_many — the copy links to the SAME records (join
rows are duplicated, the associated records are not).
* has_many :through — rejected; duplicate the direct association.
* belongs_to — rejected; a copy shares its parent by keeping the FK.

duplicate returns an UNSAVED record with unsaved children (persisted together by duplicate! / save! via autosave). Override on_duplicate(copy) for custom tweaks — it runs last, before return.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

LABEL =
"ConcernsOnRails::Models::Duplicable".freeze
SUPPORTED_MACROS =
%i[has_many has_one has_and_belongs_to_many].freeze
TIMESTAMP_COLUMNS =
%w[created_at updated_at].freeze

Instance Method Summary collapse

Instance Method Details

#duplicate(overrides = {}) ⇒ Object

Unsaved deep copy: attributes via dup, identity columns blanked, reset: columns blanked, suffix: strings appended, overrides assigned, allow-listed associations copied, then on_duplicate.



116
117
118
119
120
121
122
123
124
# File 'lib/concerns_on_rails/models/duplicable.rb', line 116

def duplicate(overrides = {})
  copy = dup
  duplicable_reset_attributes(copy)
  duplicable_apply_suffixes(copy)
  overrides.each { |attribute, value| copy.public_send("#{attribute}=", value) }
  duplicable_copy_associations(copy)
  on_duplicate(copy)
  copy
end

#duplicate!(overrides = {}) ⇒ Object

Persisted deep copy — the copy and its copied children save together (autosave) inside one transaction. Returns the saved copy.



128
129
130
131
132
# File 'lib/concerns_on_rails/models/duplicable.rb', line 128

def duplicate!(overrides = {})
  copy = duplicate(overrides)
  transaction { copy.save! }
  copy
end

#on_duplicate(_copy) ⇒ Object

Override point — receives the UNSAVED copy as the last step of duplicate, so tweaks apply before any save.



111
# File 'lib/concerns_on_rails/models/duplicable.rb', line 111

def on_duplicate(_copy); end