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