module Hyperstack class Operation class Exit < StandardError attr_reader :state attr_reader :result def initialize(state, result = nil) @state = state @result = result end def to_s @state end end class Railway def tracks self.class.tracks end class << self def tracks @tracks ||= [] end def build_tie(tie, args, block) if args.count.zero? { run: block } elsif args[0].is_a?(Hash) { scope: args[0][:class] ? :class : args[0][:scope], run: args[0][:class] || args[0][:run] || block } elsif args[0] == :class && block { run: block, scope: :class } elsif args[0].is_a?(Class) && args[0] < Operation { run: proc { args[0].run(params) } } else scope = args[1][:scope] if args[1].is_a? Hash { run: args[0], scope: scope } end.merge(tie: instance_method(tie)) end [:step, :failed, :async].each do |tie| define_method :"add_#{tie}" do |*args, &block| tracks << build_tie(tie, args, block) end end def abort!(arg) raise Exit.new(:failed, arg) end def succeed!(arg) raise Exit.new(:success, arg) end end def step(opts) @promise_chain = @promise_chain .then { |result| apply(result, :success, opts) } end def failed(opts) @promise_chain = @promise_chain .always do |result| @state = :failed if @promise_chain.rejected? && @state != :abort apply(result, :failed, opts) end end def async(opts) @promise_chain = @promise_chain_start = Promise.new @promise_chain.resolve(@last_async_result) step(opts) end def apply(result, state, opts) return result unless @state == state if opts[:scope] == :class args = [@operation, *result] instance = @operation.class else args = result instance = @operation end block = opts[:run] block = instance.method(block) if block.is_a? Symbol last_result = if block.arity.zero? instance.instance_exec(&block) elsif args.is_a?(Array) && block.arity == args.count instance.instance_exec(*args, &block) else instance.instance_exec(args, &block) end @last_async_result = last_result unless last_result.is_a? Promise last_result rescue Exit => e # the promise chain ends with an always block which will process # any immediate exits by checking the value of @state. All other # step/failed/async blocks will be skipped because state will not equal # :succeed or :failed if e.state == :failed @state = :abort # exit via the final always block with the exception raise e.result.is_a?(Exception) ? e.result : e else @state = :succeed # exit via the final then block with the success value e.result end rescue Exception => e @state = :failed raise e end def run # if @operation.has_errors? || @state # @last_result ||= ValidationException.new(@operation.instance_variable_get('@errors')) # # following handles abort out of validation. if state is already set then we are aborting # # otherwise if state is not set but we have errors then we are failed # @state ||= :failed # else # @state = :success # end @state ||= :success @promise_chain_start = @promise_chain = Promise.new @promise_chain_start.resolve(@last_result) tracks.each { |opts| opts[:tie].bind(self).call(opts) } unless @state == :abort end def result @result ||= @promise_chain.always do |e| if %i[abort failed].include? @state if e.is_a? Exception raise e else raise Promise::Fail.new(e) end else e end end end end end end