All Files
(
100.0%
covered at
3.84
hits/line
)
20 files in total.
203 relevant lines,
203 lines covered and
0 lines missed.
(
100.0%
)
18 total branches,
18 branches covered and
0 branches missed.
(
100.0%
)
-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.tag = File.basename __FILE__, ".rb"
-
1
loader.push_dir __dir__
-
1
loader.setup
-
end
-
-
# Main namespace.
-
1
module Pipeable
-
1
def self.[](container) = Builder.new(container)
-
-
1
def self.included(descendant) = descendant.include Builder.new
-
-
1
def self.loader registry = Zeitwerk::Registry
-
10
@loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/array"
-
-
1
module Pipeable
-
# Defines the pipe and and associated step methods for an object.
-
1
class Builder < Module
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Array
-
-
1
def initialize container = Steps::Container, pipe: Pipe
-
7
super()
-
-
7
@container = container
-
7
@pipe = pipe
-
-
7
define_pipe
-
7
define_steps
-
-
7
freeze
-
end
-
-
1
private
-
-
1
attr_reader :container, :pipe
-
-
1
def define_pipe pipeline = pipe
-
7
define_method :pipe do |input, *steps|
-
11
then: 2
else: 5
steps.each { |step| steps.supplant step, method(step) if step.is_a? Symbol }
-
4
pipeline.call(input, *steps)
-
end
-
end
-
-
1
def define_steps vessel = container
-
7
vessel.each_key do |key|
-
72
define_method key do |*positionals, **keywords, &block|
-
3
step = vessel[key]
-
3
then: 1
else: 2
step.is_a?(Proc) ? step : step.new(*positionals, **keywords, &block)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
# Allows objects to be functionally composable.
-
1
module Composable
-
1
def >>(other) = method(:call) >> other
-
-
1
def <<(other) = method(:call) << other
-
-
1
def call = fail NoMethodError, "`#{self.class.name}##{__method__}` must be implemented."
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Pipeable
-
# Provids low-level functionality processing a sequence of steps.
-
1
Pipe = lambda do |input, *steps|
-
12
then: 1
else: 11
fail ArgumentError, "Pipe must have at least one step." if steps.empty?
-
-
11
then: 2
else: 9
result = input.is_a?(Dry::Monads::Result) ? input : Dry::Monads::Success(input)
-
-
11
steps.reduce(&:>>).call result
-
rescue NoMethodError
-
1
raise TypeError, "Step must be functionally composable and answer a monad."
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Pipeable
-
1
module Steps
-
# Provides a custom step blueprint.
-
1
class Abstract
-
1
include Dry::Monads[:result]
-
1
include Composable
-
-
1
def initialize *positionals, **keywords, &block
-
67
@base_positionals = positionals
-
67
@base_keywords = keywords
-
67
@base_block = block
-
end
-
-
1
protected
-
-
1
attr_reader :base_positionals, :base_keywords, :base_block
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Wraps Dry Monads `#alt_map` method as a step.
-
1
class Amap < Abstract
-
2
def call(result) = result.alt_map { |object| base_block.call object }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Messages object, with optional arguments, as different result.
-
1
class As < Abstract
-
1
def call result
-
7
result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Wraps Dry Monads `#bind` method as a step.
-
1
class Bind < Abstract
-
3
def call(result) = result.bind { |object| base_block.call object }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "marameters"
-
-
1
module Pipeable
-
1
module Steps
-
# Checks if proof is true and answers success (passthrough) or failure (with optional argument).
-
1
class Check < Abstract
-
1
def initialize proof, message
-
3
super()
-
3
@proof = proof
-
3
@message = message
-
end
-
-
1
def call result
-
3
result.bind do |object|
-
3
answer = question object
-
3
then: 2
else: 1
answer == true || answer.is_a?(Success) ? result : Failure(object)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :proof, :message
-
-
1
def question object
-
3
splat = Marameters.categorize proof.method(message).parameters, object
-
3
proof.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "containable"
-
-
1
module Pipeable
-
1
module Steps
-
# Registers default steps.
-
1
module Container
-
1
extend Containable
-
-
1
register :alt, Or
-
1
register :amap, Amap
-
1
register :as, As
-
1
register :bind, Bind
-
1
register :check, Check
-
1
register :fmap, Fmap
-
1
register :insert, Insert
-
1
register :map, Map
-
1
register :merge, Merge
-
1
register :tee, Tee
-
1
register :to, To
-
1
register :try, Try
-
1
register :use, Use
-
1
register :validate, Validate
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Wraps Dry Monads `#fmap` method as a step.
-
1
class Fmap < Abstract
-
2
def call(result) = result.fmap { |object| base_block.call object }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Inserts elements before or after an object.
-
1
class Insert < Abstract
-
1
LAST = -1
-
-
1
def initialize(*, at: LAST)
-
15
super(*)
-
15
@at = at
-
end
-
-
1
def call result
-
15
result.fmap do |object|
-
15
then: 7
else: 8
cast = object.is_a?(Array) ? object : [object]
-
15
cast.insert(at, *base_positionals)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :at
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Maps over an enumerable, processes each element, and answers a new enumerable.
-
1
class Map < Abstract
-
2
def call(result) = result.fmap { |collection| collection.map(&base_block) }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Merges initialized attributes with step object for use by subsequent step.
-
1
class Merge < Abstract
-
1
def initialize(as: :step, **)
-
4
super(**)
-
4
@as = as
-
end
-
-
1
def call result
-
4
result.fmap do |object|
-
3
then: 1
if object.is_a? Hash
-
1
object.merge! base_keywords
-
else: 2
else
-
2
{as => object}.merge!(base_keywords)
-
end
-
end
-
end
-
-
1
private
-
-
1
attr_reader :as
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Wraps Dry Monads `#or` method as a step.
-
1
class Or < Abstract
-
2
def call(result) = result.or { |object| base_block.call object }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Messages operation, without any checks, while passing input through as output.
-
1
class Tee < Abstract
-
1
def initialize(operation, *, **)
-
5
super(*, **)
-
5
@operation = operation
-
end
-
-
1
def call result
-
5
operation.public_send(*base_positionals, **base_keywords)
-
5
result
-
end
-
-
1
private
-
-
1
attr_reader :operation
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "marameters"
-
-
1
module Pipeable
-
1
module Steps
-
# Delegates to a non-callable object which automatically wraps the result if necessary.
-
1
class To < Abstract
-
1
def initialize(object, message, **)
-
5
super(**)
-
5
@object = object
-
5
@message = message
-
end
-
-
1
def call result
-
5
result.bind do |arguments|
-
4
splat = Marameters.categorize object.method(message).parameters, arguments
-
4
wrap object.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :object, :message
-
-
5
then: 3
else: 1
def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Sends a risky message to an object which may pass or fail.
-
1
class Try < Abstract
-
1
def initialize(*, catch:, **)
-
7
super(*, **)
-
7
@catch = catch
-
end
-
-
1
def call result
-
13
result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
-
rescue *Array(catch) => error
-
2
Failure error
-
end
-
-
1
private
-
-
1
attr_reader :catch
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Messages a command (or pipe) which answers a result.
-
1
class Use < Abstract
-
1
def initialize(command, **)
-
2
super(**)
-
2
@command = command
-
end
-
-
2
def call(result) = result.bind { |input| command.call input }
-
-
1
private
-
-
1
attr_reader :command
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Pipeable
-
1
module Steps
-
# Validates result via a callable contract.
-
1
class Validate < Abstract
-
1
def initialize contract, as: nil
-
5
super()
-
5
@contract = contract
-
5
@as = as
-
end
-
-
5
def call(result) = result.bind { |payload| cast payload }
-
-
1
private
-
-
1
attr_reader :contract, :as
-
-
1
def cast payload
-
7
then: 2
else: 1
contract.call(payload).to_monad.fmap { |data| as ? data.public_send(as) : data }
-
end
-
end
-
end
-
end