lib/transflow/transaction.rb in transflow-0.0.2 vs lib/transflow/transaction.rb in transflow-0.1.0
- old
+ new
@@ -1,42 +1,170 @@
+require 'transproc'
+
module Transflow
class TransactionFailedError < StandardError
attr_reader :transaction
attr_reader :original_error
def initialize(transaction, original_error)
@transaction = transaction
@original_error = original_error
- super("#{transaction} failed")
+ super("#{transaction} failed [#{original_error.class}: #{original_error.message}]")
set_backtrace(original_error.backtrace)
end
end
+ # Transaction encapsulates calling individual steps registered within a transflow
+ # constructor.
+ #
+ # It's responsible for calling steps in the right order and optionally currying
+ # arguments for specific steps.
+ #
+ # Furthermore you can subscribe event listeners to individual steps within a
+ # transaction.
+ #
+ # @api public
class Transaction
- attr_reader :handler
+ # Internal function factory using Transproc extension
+ #
+ # @api private
+ module Registry
+ extend Transproc::Registry
+ end
+ # @attr_reader [Hash<Symbol => Proc,#call>] steps The step map
+ #
+ # @api private
attr_reader :steps
- def initialize(steps, handler)
+ # @attr_reader [Array<Symbol>] step_names The names of registered steps
+ #
+ # @api private
+ attr_reader :step_names
+
+ # @api private
+ def initialize(steps)
@steps = steps
- @handler = handler
+ @step_names = steps.keys.reverse
end
+ # Subscribe event listeners to specific steps
+ #
+ # @example
+ # transaction = Transflow(container: my_container) {
+ # step(:one) { step(:two, publish: true }
+ # }
+ #
+ # class MyListener
+ # def self.two_success(*args)
+ # puts 'yes!'
+ # end
+ #
+ # def self.two_failure(*args)
+ # puts 'oh noez!'
+ # end
+ # end
+ #
+ # transaction.subscribe(two: my_listener)
+ #
+ # transaction.call(some_input)
+ #
+ # @param [Hash<Symbol => Object>] listeners The step=>listener map
+ #
+ # @return [self]
+ #
+ # @api public
def subscribe(listeners)
listeners.each { |step, listener| steps[step].subscribe(listener) }
+ self
end
- def call(*args)
- handler.call(*args)
+ # Call the transaction
+ #
+ # Once transaction is called it will call the first step and its result
+ # will be passed to the second step and so on.
+ #
+ # @example
+ # my_container = {
+ # add_one: -> i { i + 1 },
+ # add_two: -> j { j + 2 }
+ # }
+ #
+ # transaction = Transflow(container: my_container) {
+ # step(:one, with: :add_one) { step(:two, with: :add_two) }
+ # }
+ #
+ # transaction.call(1) # 4
+ #
+ # @param [Object] input The input for the first step
+ #
+ # @param [Hash] options The curry-args map, optional
+ #
+ # @return [Object]
+ #
+ # @raises TransactionFailedError
+ #
+ # @api public
+ def call(input, options = {})
+ handler = handler_steps(options).map(&method(:fn)).reduce(:>>)
+ handler.call(input)
rescue Transproc::MalformedInputError => err
- raise TransactionFailedError.new(self, err)
+ raise TransactionFailedError.new(self, err.original_error)
end
alias_method :[], :call
+ # Coerce a transaction into string representation
+ #
+ # @return [String]
+ #
+ # @api public
def to_s
- "Transaction(#{steps.keys.join(' => ')})"
+ "Transaction(#{step_names.join(' => ')})"
+ end
+
+ private
+
+ # @api private
+ def handler_steps(options)
+ if options.any?
+ assert_valid_options(options)
+
+ steps.map { |(name, op)|
+ args = options[name]
+
+ if args
+ op.curry.call(args)
+ else
+ op
+ end
+ }
+ else
+ steps.values
+ end.reverse
+ end
+
+ # @api private
+ def assert_valid_options(options)
+ options.each_key do |name|
+ unless step_names.include?(name)
+ raise ArgumentError, "+#{name}+ is not a valid step name"
+ end
+ end
+ end
+
+ # Wrap a proc into composable transproc function
+ #
+ # @param [#call]
+ #
+ # @api private
+ def fn(obj)
+ if obj.respond_to?(:>>)
+ obj
+ else
+ Registry[obj]
+ end
end
end
end