lib/trailblazer/operation/pipetree.rb in trailblazer-operation-0.0.9 vs lib/trailblazer/operation/pipetree.rb in trailblazer-operation-0.0.10

- old
+ new

@@ -1,7 +1,7 @@ require "pipetree" -require "pipetree/flow" +require "pipetree/railway" require "trailblazer/operation/result" require "uber/option" if RUBY_VERSION == "1.9.3" require "trailblazer/operation/1.9.3/option" # TODO: rename to something better. @@ -12,124 +12,131 @@ class Trailblazer::Operation New = ->(klass, options) { klass.new(options) } # returns operation instance. # Implements the API to populate the operation's pipetree and # `Operation::call` to invoke the latter. - # http://trailblazer.to/gems/operation/2.0/pipetree.html + # Learn more about the Pipetree gem here: https://github.com/apotonick/pipetree module Pipetree def self.included(includer) includer.extend ClassMethods # ::call, ::inititalize_pipetree! includer.extend DSL # ::|, ::> and friends. includer.initialize_pipetree! - includer._insert(:>>, New, name: "operation.new", wrap: false) end module ClassMethods # Top-level, this method is called when you do Create.() and where # all the fun starts, ends, and hopefully starts again. def call(options) pipe = self["pipetree"] # TODO: injectable? WTF? how cool is that? last, operation = pipe.(self, options) - # The reason the Result wraps the Skill object (`options`), not the operation - # itself is because the op should be irrelevant, plus when stopping the pipe - # before op instantiation, this would be confusing (and wrong!). - Result.new(last == ::Pipetree::Flow::Right, options) + # Any subclass of Right will be interpreted as successful. + Result.new(!!(last <= Railway::Right), options) end # This method would be redundant if Ruby had a Class::finalize! method the way # Dry.RB provides it. It has to be executed with every subclassing. def initialize_pipetree! heritage.record :initialize_pipetree! - self["pipetree"] = ::Pipetree::Flow.new - end - end - module DSL - # They all inherit. - def >(*args); _insert(:>, *args) end - def &(*args); _insert(:&, *args) end - def <(*args); _insert(:<, *args) end + self["pipetree"] = Railway.new - def |(cfg, user_options={}) - DSL.import(self, self["pipetree"], cfg, user_options) && - heritage.record(:|, cfg, user_options) + strut = ->(last, input, options) { [last, New.(input, options)] } # first step in pipe. + self["pipetree"].add(Railway::Right, strut, name: "operation.new") # DISCUSS: using pipe API directly here. clever? end + end - alias_method :step, :| - alias_method :failure, :< - alias_method :success, :> - alias_method :override, :| - alias_method :~, :override + class Railway < ::Pipetree::Railway + FailFast = Class.new(Left) + PassFast = Class.new(Right) - # :private: - # High-level user step API that allows ->(options) procs. - def _insert(operator, proc, options={}) - heritage.record(:_insert, operator, proc, options) + def self.fail! ; Left end + def self.fail_fast!; FailFast end + def self.pass! ; Right end + def self.pass_fast!; PassFast end + end - DSL.insert(self["pipetree"], operator, proc, options, definer_name: self.name) + # The Strut wrapping each step. Makes sure that Track signals are returned immediately. + class Switch < ::Pipetree::Railway::Strut + Decider = ->(result, config, *args) do + return result if result.is_a?(Class) && result <= Railway::Track # this might be pretty slow? + + config[:decider_class].(result, config, *args) # e.g. And::Decider.(result, ..) end + end - # :public: - # Wrap the step into a proc that only passes `options` to the step. - # This is pure convenience for the developer and will be the default - # API for steps. ATM, we also automatically generate a step `:name`. - def self.insert(pipe, operator, proc, options={}, kws={}) # TODO: definer_name is a hack for debugging, only. - _proc = - if options[:wrap] == false - proc - else - Option::KW.(proc) do |type| - options[:name] ||= proc if type == :symbol - options[:name] ||= "#{kws[:definer_name]}:#{proc.source_location.last}" if proc.is_a? Proc if type == :proc - options[:name] ||= proc.class if type == :callable - end - end + # Strut that doesn't evaluate the step's result but stays on `last` or configured :signal. + class Stay < ::Pipetree::Railway::Strut + Decider = ->(result, config, last, *) { config[:signal] || last } + end - pipe.send(operator, _proc, options) # ex: pipetree.> Validate, after: Model::Build - end + module DSL + def success(*args); add(Railway::Right, Stay::Decider, *args) end + def failure(*args); add(Railway::Left, Stay::Decider, *args) end + def step(*args) ; add(Railway::Right, Railway::And::Decider, *args) end - # note: does not calls heritage.record - def self.import(operation, pipe, cfg, user_options={}) - # a normal step is added as "consider"/"may deviate", so its result matters. - return insert(pipe, :&, cfg, user_options, {}) unless cfg.is_a?(Array) + private + # Operation-level entry point. + def add(track, decider_class, proc, options={}) + heritage.record(:add, track, decider_class, proc, options) - # e.g. from Contract::Validate - mod, args, block = cfg + DSL.insert(self["pipetree"], track, decider_class, proc, options) + end - import = Import.new(pipe, user_options) # API object. + def self.insert(pipe, track, decider_class, proc, options={}) # TODO: make :name required arg. + _proc, options = proc.is_a?(Array) ? macro!(proc, options) : step!(proc, options) - mod.import!(operation, import, *args, &block) + options = options.merge(replace: options[:name]) if options[:override] # :override + strut_class, strut_options = AddOptions.(decider_class, options) # :fail_fast and friends. + + pipe.add(track, strut_class.new(_proc, strut_options), options) end - # Try to abstract as much as possible from the imported module. This is for - # forward-compatibility. - # Note that Import#call will push the step directly on the pipetree which gives it the - # low-level (input, options) interface. - Import = Struct.new(:pipetree, :user_options) do - def call(operator, step, options) - insert_options = options.merge(user_options) + def self.macro!(proc, options) + _proc, macro_options = proc - # Inheritance: when the step is already defined in the pipe, - # simply replace it with the new. - if name = insert_options[:name] - insert_options[:replace] = name if pipetree.index(name) - end + [ _proc, macro_options.merge(options) ] + end - pipetree.send operator, step, insert_options + def self.step!(proc, options) + name = "" + _proc = Option::KW.(proc) do |type| + name = proc if type == :symbol + name = "#{proc.source_location[0].split("/").last}:#{proc.source_location.last}" if proc.is_a? Proc if type == :proc + name = proc.class if type == :callable end + + [ _proc, { name: name }.merge(options) ] end - Macros = Module.new - # create a class method on `target`, e.g. Contract::Validate() for step macros. - def self.macro!(name, constant, target=Macros) - target.send :define_method, name do |*args, &block| - [constant, args, block] + AddOptions = ->(decider_class, options) do + # for #failure and #success: + if decider_class == Stay::Decider + return [Stay, signal: Railway::FailFast] if options[:fail_fast] + return [Stay, signal: Railway::PassFast] if options[:pass_fast] + return [Stay, {}] + else # for #step: + return [Switch, decider_class: decider_class, on_false: Railway::FailFast] if options[:fail_fast] + return [Switch, decider_class: decider_class, on_true: Railway::PassFast] if options[:pass_fast] + return [Switch, decider_class: decider_class] end end end # DSL end - extend Pipetree::DSL::Macros + # Allows defining dependencies and inject/override them via runtime options, if desired. + class Pipetree::Step + include Uber::Callable + + def initialize(step, dependencies={}) + @step, @dependencies = step, dependencies + end + + def call(input, options) + @dependencies.each { |k, v| options[k] ||= v } # not sure i like this, but the step's API is cool. + + @step.(input, options) + end + end end