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