# encoding: utf-8
module ActiveMerge
# Service Object responds for merging given ActiveRecord instances into the
# first one
#
# Object initializes either with ActiveRecord::Association or array of objects.
# If the argument is an array, then only those items taken into account, that:
#
# * inherited from the ActiveRecord::Base
# * persisted
# * are objects of one class
#
# == Merging algorithm
#
# * The item with minimal id selected from the service argument. It will remaine intact.
# * All objects, that the other items links to via "has_many" assotiation, are selected...
# * ... and are rebound to the first item
# * After rebinding any item from the service argument, except the first one, to be deleted.
#
# As a result it is only the first object from the argument remains intact,
# and all the instances that belonged to the other ones are now belongs to it.
#
# == Example:
#
# # Let any kingdom has many shires and men
# class Kingdom < ActiveRecord::Base
# has_many :shires
# has_many :men
# end
#
# class Shire < ActiveRecord::Base
# belongs_to :kingdom
# end
#
# class Man < ActiveRecord::Base
# belongs_to :kingdom
# end
#
# # Britain has 10 shires and 100 thousands men lives there
# britain = Kingdom.create!
# 100000.times.each{ britain.men.create! }
# 10.times.each{ britain.shires.create! }
#
# # Scotland has 5 shires and 30 thousands living men
# scotland = Kingdom.create!
# 30000.times.each{ scotland.men.create! }
# 5.times.each{ scotland.shires.create! }
#
# # Lets merge all the kingdoms:
# Service.new(Kingdom.all).provide
#
# # Now union Britain (because it is Britain that was created first)
# # has all those 15 shires and 130 thousand men
# # from both the old good Britain and the old good Scotland.
# britain.reload.men.count # => 130000
# britain.reload.shires.count # => 15
#
# # And, alas, the Scotland Kingdom doesn't exists any more
# Kingdom.find_by(scotland.id) # => nil
#
# == Warning!
#
# The merge provided as a whole transaction. In case of any error, all
# changes roll back.
#
# Let (see the example above) the shire cannot be rebount to another kingdom:
#
# class Shire
# attr_readonly :kingdom_id
# end
#
# In this case the merge won't be finished. Not only shires, but also
# scots remain living in their old good Scotland!
#
# == Skipping activerecord validations
#
# You can use the validate: false option.
#
# With this option set any activerecord validation and callback are skipped.
#
# Service.new(Kingdom.all).provide validate: false
#
class Service < ActivePatterns::BaseService
include ActiveModel::Validations
def initialize(list = nil)
list = Items.new list
@item, @items = list.first, Array(list[1..-1])
end
attr_reader :item, :items, :klass, :klasses
validates :items, presence: true
# Merges all the #items to the #item as a whole transaction
def provide(options = {})
transaction do
items.each do |item|
service = SimpleService.new(self.item, item)
change(service) { service.provide(options) }
end
end
end
private
# Returns only persisted items of the same ActiveRecord model
class Items < Array
def initialize(items)
items = Array(items).map{ |item| Item.new item }.compact
items = [] if items.map{ |item| item.class }.uniq.count > 1
super items.sort_by{ |item| item.id }
end
end
# Initializes a persisted item of an ActiveRecord model
class Item
def self.new(item)
return unless item.class.ancestors.include? ActiveRecord::Base
return unless item.persisted?
item
end
end
end
end