require 'dry-pipeline' require 'transflow/errors' module Transflow # 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 # Step wrapper object which adds `>>` operator # # @api private class Step include Dry::Pipeline::Mixin # @api private def self.[](op) if op.respond_to?(:>>) op else Step.new(op) end end end # @attr_reader [Hash Proc,#call>] steps The step map # # @api private attr_reader :steps # @attr_reader [Array] step_names The names of registered steps # # @api private attr_reader :step_names # @api private def initialize(steps) @steps = steps @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 Object>] listeners The step=>listener map # # @return [self] # # @api public def subscribe(listeners) if listeners.is_a?(Hash) listeners.each { |step, listener| steps[step].subscribe(listener) } else steps.each { |(_, step)| step.subscribe(listeners) } end self end # 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(:step)).reduce(:>>) handler.call(input) rescue StepError => err raise TransactionFailedError.new(self, err) end alias_method :[], :call # Coerce a transaction into string representation # # @return [String] # # @api public def to_s "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 step(obj) Step[obj] end end end