# -*- ruby encoding: utf-8 -*-

require 'transaction/simple'

# A transaction group is an object wrapper that manages a group of objects
# as if they were a single object for the purpose of transaction management.
# All transactions for this group of objects should be performed against the
# transaction group object, not against individual objects in the group.
#
# == Transaction Group Usage
#   require 'transaction/simple/group'
#
#   x = "Hello, you."
#   y = "And you, too."
#
#   g = Transaction::Simple::Group.new(x, y)
#   g.start_transaction(:first)     # -> [ x, y ]
#   g.transaction_open?(:first)     # -> true
#   x.transaction_open?(:first)     # -> true
#   y.transaction_open?(:first)     # -> true
#
#   x.gsub!(/you/, "world")         # -> "Hello, world."
#   y.gsub!(/you/, "me")            # -> "And me, too."
#
#   g.start_transaction(:second)    # -> [ x, y ]
#   x.gsub!(/world/, "HAL")         # -> "Hello, HAL."
#   y.gsub!(/me/, "Dave")           # -> "And Dave, too."
#   g.rewind_transaction(:second)   # -> [ x, y ]
#   x                               # -> "Hello, world."
#   y                               # -> "And me, too."
#
#   x.gsub!(/world/, "HAL")         # -> "Hello, HAL."
#   y.gsub!(/me/, "Dave")           # -> "And Dave, too."
#
#   g.commit_transaction(:second)   # -> [ x, y ]
#   x                               # -> "Hello, HAL."
#   y                               # -> "And Dave, too."
#
#   g.abort_transaction(:first)     # -> [ x, y ]
#   x                               = -> "Hello, you."
#   y                               = -> "And you, too."
class Transaction::Simple::Group
  # Creates a transaction group for the provided objects. If a block is
  # provided, the transaction group object is yielded to the block; when the
  # block is finished, the transaction group object will be cleared with
  # #clear.
  def initialize(*objects)
    @objects = objects || []
    @objects.freeze
    @objects.each { |obj| obj.extend(Transaction::Simple) }

    if block_given?
      begin
        yield self
      ensure
        self.clear
      end
    end
  end

  # Returns the objects that are covered by this transaction group.
  attr_reader :objects

  # Clears the object group. Removes references to the objects so that they
  # can be garbage collected.
  def clear
    @objects = @objects.dup.clear
  end

  # Tests to see if all of the objects in the group have an open
  # transaction. See Transaction::Simple#transaction_open? for more
  # information.
  def transaction_open?(name = nil)
    @objects.inject(true) do |val, obj|
      val = val and obj.transaction_open?(name)
    end
  end

  # Returns the current name of the transaction for the group. Transactions
  # not explicitly named are named +nil+.
  def transaction_name
    @objects[0].transaction_name
  end

  # Starts a transaction for the group. Stores the current object state. If
  # a transaction name is specified, the transaction will be named.
  # Transaction names must be unique. Transaction names of +nil+ will be
  # treated as unnamed transactions.
  def start_transaction(name = nil)
    @objects.each { |obj| obj.start_transaction(name) }
  end

  # Rewinds the transaction. If +name+ is specified, then the intervening
  # transactions will be aborted and the named transaction will be rewound.
  # Otherwise, only the current transaction is rewound.
  def rewind_transaction(name = nil)
    @objects.each { |obj| obj.rewind_transaction(name) }
  end

  # Aborts the transaction. Resets the object state to what it was before
  # the transaction was started and closes the transaction. If +name+ is
  # specified, then the intervening transactions and the named transaction
  # will be aborted. Otherwise, only the current transaction is aborted.
  #
  # If the current or named transaction has been started by a block
  # (Transaction::Simple.start), then the execution of the block will be
  # halted with +break+ +self+.
  def abort_transaction(name = nil)
    @objects.each { |obj| obj.abort_transaction(name) }
  end

  # If +name+ is +nil+ (default), the current transaction level is closed
  # out and the changes are committed.
  #
  # If +name+ is specified and +name+ is in the list of named transactions,
  # then all transactions are closed and committed until the named
  # transaction is reached.
  def commit_transaction(name = nil)
    @objects.each { |obj| obj.commit_transaction(name) }
  end

  # Alternative method for calling the transaction methods. An optional name
  # can be specified for named transaction support.
  #
  # #transaction(:start)::  #start_transaction
  # #transaction(:rewind):: #rewind_transaction
  # #transaction(:abort)::  #abort_transaction
  # #transaction(:commit):: #commit_transaction
  # #transaction(:name)::   #transaction_name
  # #transaction::          #transaction_open?
  def transaction(action = nil, name = nil)
    @objects.each { |obj| obj.transaction(action, name) }
  end
end

# vim: syntax=ruby
