loading
Generated 2025-10-09T00:00:05+00:00

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% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/pipeable.rb 100.00 % 20 10 10 0 1.90 100.00 % 0 0 0
lib/pipeable/builder.rb 100.00 % 45 24 24 0 6.67 100.00 % 4 4 0
lib/pipeable/composable.rb 100.00 % 12 5 5 0 1.00 100.00 % 0 0 0
lib/pipeable/pipe.rb 100.00 % 16 7 7 0 5.43 100.00 % 4 4 0
lib/pipeable/steps/abstract.rb 100.00 % 23 12 12 0 17.50 100.00 % 0 0 0
lib/pipeable/steps/amap.rb 100.00 % 10 4 4 0 1.25 100.00 % 0 0 0
lib/pipeable/steps/as.rb 100.00 % 12 5 5 0 2.20 100.00 % 0 0 0
lib/pipeable/steps/bind.rb 100.00 % 10 4 4 0 1.50 100.00 % 0 0 0
lib/pipeable/steps/check.rb 100.00 % 32 17 17 0 1.94 100.00 % 2 2 0
lib/pipeable/steps/container.rb 100.00 % 27 19 19 0 1.00 100.00 % 0 0 0
lib/pipeable/steps/fmap.rb 100.00 % 10 4 4 0 1.25 100.00 % 0 0 0
lib/pipeable/steps/insert.rb 100.00 % 26 13 13 0 6.38 100.00 % 2 2 0
lib/pipeable/steps/map.rb 100.00 % 10 4 4 0 1.25 100.00 % 0 0 0
lib/pipeable/steps/merge.rb 100.00 % 27 13 13 0 1.92 100.00 % 2 2 0
lib/pipeable/steps/or.rb 100.00 % 10 4 4 0 1.25 100.00 % 0 0 0
lib/pipeable/steps/tee.rb 100.00 % 22 11 11 0 2.45 100.00 % 0 0 0
lib/pipeable/steps/to.rb 100.00 % 29 15 15 0 2.73 100.00 % 2 2 0
lib/pipeable/steps/try.rb 100.00 % 23 11 11 0 3.27 100.00 % 0 0 0
lib/pipeable/steps/use.rb 100.00 % 19 9 9 0 1.33 100.00 % 0 0 0
lib/pipeable/steps/validate.rb 100.00 % 24 12 12 0 2.83 100.00 % 2 2 0

lib/pipeable.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.tag = File.basename __FILE__, ".rb"
  5. 1 loader.push_dir __dir__
  6. 1 loader.setup
  7. end
  8. # Main namespace.
  9. 1 module Pipeable
  10. 1 def self.[](container) = Builder.new(container)
  11. 1 def self.included(descendant) = descendant.include Builder.new
  12. 1 def self.loader registry = Zeitwerk::Registry
  13. 10 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  14. end
  15. end

lib/pipeable/builder.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/array"
  4. 1 module Pipeable
  5. # Defines the pipe and and associated step methods for an object.
  6. 1 class Builder < Module
  7. 1 include Dry::Monads[:result]
  8. 1 using Refinements::Array
  9. 1 def initialize container = Steps::Container, pipe: Pipe
  10. 7 super()
  11. 7 @container = container
  12. 7 @pipe = pipe
  13. 7 define_pipe
  14. 7 define_steps
  15. 7 freeze
  16. end
  17. 1 private
  18. 1 attr_reader :container, :pipe
  19. 1 def define_pipe pipeline = pipe
  20. 7 define_method :pipe do |input, *steps|
  21. 11 then: 2 else: 5 steps.each { |step| steps.supplant step, method(step) if step.is_a? Symbol }
  22. 4 pipeline.call(input, *steps)
  23. end
  24. end
  25. 1 def define_steps vessel = container
  26. 7 vessel.each_key do |key|
  27. 72 define_method key do |*positionals, **keywords, &block|
  28. 3 step = vessel[key]
  29. 3 then: 1 else: 2 step.is_a?(Proc) ? step : step.new(*positionals, **keywords, &block)
  30. end
  31. end
  32. end
  33. end
  34. end

lib/pipeable/composable.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. # Allows objects to be functionally composable.
  4. 1 module Composable
  5. 1 def >>(other) = method(:call) >> other
  6. 1 def <<(other) = method(:call) << other
  7. 1 def call = fail NoMethodError, "`#{self.class.name}##{__method__}` must be implemented."
  8. end
  9. end

lib/pipeable/pipe.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Pipeable
  4. # Provids low-level functionality processing a sequence of steps.
  5. 1 Pipe = lambda do |input, *steps|
  6. 12 then: 1 else: 11 fail ArgumentError, "Pipe must have at least one step." if steps.empty?
  7. 11 then: 2 else: 9 result = input.is_a?(Dry::Monads::Result) ? input : Dry::Monads::Success(input)
  8. 11 steps.reduce(&:>>).call result
  9. rescue NoMethodError
  10. 1 raise TypeError, "Step must be functionally composable and answer a monad."
  11. end
  12. end

lib/pipeable/steps/abstract.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Pipeable
  4. 1 module Steps
  5. # Provides a custom step blueprint.
  6. 1 class Abstract
  7. 1 include Dry::Monads[:result]
  8. 1 include Composable
  9. 1 def initialize *positionals, **keywords, &block
  10. 67 @base_positionals = positionals
  11. 67 @base_keywords = keywords
  12. 67 @base_block = block
  13. end
  14. 1 protected
  15. 1 attr_reader :base_positionals, :base_keywords, :base_block
  16. end
  17. end
  18. end

lib/pipeable/steps/amap.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Wraps Dry Monads `#alt_map` method as a step.
  5. 1 class Amap < Abstract
  6. 2 def call(result) = result.alt_map { |object| base_block.call object }
  7. end
  8. end
  9. end

lib/pipeable/steps/as.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Messages object, with optional arguments, as different result.
  5. 1 class As < Abstract
  6. 1 def call result
  7. 7 result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
  8. end
  9. end
  10. end
  11. end

lib/pipeable/steps/bind.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Wraps Dry Monads `#bind` method as a step.
  5. 1 class Bind < Abstract
  6. 3 def call(result) = result.bind { |object| base_block.call object }
  7. end
  8. end
  9. end

lib/pipeable/steps/check.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "marameters"
  3. 1 module Pipeable
  4. 1 module Steps
  5. # Checks if proof is true and answers success (passthrough) or failure (with optional argument).
  6. 1 class Check < Abstract
  7. 1 def initialize proof, message
  8. 3 super()
  9. 3 @proof = proof
  10. 3 @message = message
  11. end
  12. 1 def call result
  13. 3 result.bind do |object|
  14. 3 answer = question object
  15. 3 then: 2 else: 1 answer == true || answer.is_a?(Success) ? result : Failure(object)
  16. end
  17. end
  18. 1 private
  19. 1 attr_reader :proof, :message
  20. 1 def question object
  21. 3 splat = Marameters.categorize proof.method(message).parameters, object
  22. 3 proof.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
  23. end
  24. end
  25. end
  26. end

lib/pipeable/steps/container.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "containable"
  3. 1 module Pipeable
  4. 1 module Steps
  5. # Registers default steps.
  6. 1 module Container
  7. 1 extend Containable
  8. 1 register :alt, Or
  9. 1 register :amap, Amap
  10. 1 register :as, As
  11. 1 register :bind, Bind
  12. 1 register :check, Check
  13. 1 register :fmap, Fmap
  14. 1 register :insert, Insert
  15. 1 register :map, Map
  16. 1 register :merge, Merge
  17. 1 register :tee, Tee
  18. 1 register :to, To
  19. 1 register :try, Try
  20. 1 register :use, Use
  21. 1 register :validate, Validate
  22. end
  23. end
  24. end

lib/pipeable/steps/fmap.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Wraps Dry Monads `#fmap` method as a step.
  5. 1 class Fmap < Abstract
  6. 2 def call(result) = result.fmap { |object| base_block.call object }
  7. end
  8. end
  9. end

lib/pipeable/steps/insert.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Inserts elements before or after an object.
  5. 1 class Insert < Abstract
  6. 1 LAST = -1
  7. 1 def initialize(*, at: LAST)
  8. 15 super(*)
  9. 15 @at = at
  10. end
  11. 1 def call result
  12. 15 result.fmap do |object|
  13. 15 then: 7 else: 8 cast = object.is_a?(Array) ? object : [object]
  14. 15 cast.insert(at, *base_positionals)
  15. end
  16. end
  17. 1 private
  18. 1 attr_reader :at
  19. end
  20. end
  21. end

lib/pipeable/steps/map.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Maps over an enumerable, processes each element, and answers a new enumerable.
  5. 1 class Map < Abstract
  6. 2 def call(result) = result.fmap { |collection| collection.map(&base_block) }
  7. end
  8. end
  9. end

lib/pipeable/steps/merge.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Merges initialized attributes with step object for use by subsequent step.
  5. 1 class Merge < Abstract
  6. 1 def initialize(as: :step, **)
  7. 4 super(**)
  8. 4 @as = as
  9. end
  10. 1 def call result
  11. 4 result.fmap do |object|
  12. 3 then: 1 if object.is_a? Hash
  13. 1 object.merge! base_keywords
  14. else: 2 else
  15. 2 {as => object}.merge!(base_keywords)
  16. end
  17. end
  18. end
  19. 1 private
  20. 1 attr_reader :as
  21. end
  22. end
  23. end

lib/pipeable/steps/or.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Wraps Dry Monads `#or` method as a step.
  5. 1 class Or < Abstract
  6. 2 def call(result) = result.or { |object| base_block.call object }
  7. end
  8. end
  9. end

lib/pipeable/steps/tee.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Messages operation, without any checks, while passing input through as output.
  5. 1 class Tee < Abstract
  6. 1 def initialize(operation, *, **)
  7. 5 super(*, **)
  8. 5 @operation = operation
  9. end
  10. 1 def call result
  11. 5 operation.public_send(*base_positionals, **base_keywords)
  12. 5 result
  13. end
  14. 1 private
  15. 1 attr_reader :operation
  16. end
  17. end
  18. end

lib/pipeable/steps/to.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "marameters"
  3. 1 module Pipeable
  4. 1 module Steps
  5. # Delegates to a non-callable object which automatically wraps the result if necessary.
  6. 1 class To < Abstract
  7. 1 def initialize(object, message, **)
  8. 5 super(**)
  9. 5 @object = object
  10. 5 @message = message
  11. end
  12. 1 def call result
  13. 5 result.bind do |arguments|
  14. 4 splat = Marameters.categorize object.method(message).parameters, arguments
  15. 4 wrap object.public_send(message, *splat.positionals, **splat.keywords, &splat.block)
  16. end
  17. end
  18. 1 private
  19. 1 attr_reader :object, :message
  20. 5 then: 3 else: 1 def wrap(result) = result.is_a?(Dry::Monads::Result) ? result : Success(result)
  21. end
  22. end
  23. end

lib/pipeable/steps/try.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Sends a risky message to an object which may pass or fail.
  5. 1 class Try < Abstract
  6. 1 def initialize(*, catch:, **)
  7. 7 super(*, **)
  8. 7 @catch = catch
  9. end
  10. 1 def call result
  11. 13 result.fmap { |object| object.public_send(*base_positionals, **base_keywords) }
  12. rescue *Array(catch) => error
  13. 2 Failure error
  14. end
  15. 1 private
  16. 1 attr_reader :catch
  17. end
  18. end
  19. end

lib/pipeable/steps/use.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Messages a command (or pipe) which answers a result.
  5. 1 class Use < Abstract
  6. 1 def initialize(command, **)
  7. 2 super(**)
  8. 2 @command = command
  9. end
  10. 2 def call(result) = result.bind { |input| command.call input }
  11. 1 private
  12. 1 attr_reader :command
  13. end
  14. end
  15. end

lib/pipeable/steps/validate.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Pipeable
  3. 1 module Steps
  4. # Validates result via a callable contract.
  5. 1 class Validate < Abstract
  6. 1 def initialize contract, as: nil
  7. 5 super()
  8. 5 @contract = contract
  9. 5 @as = as
  10. end
  11. 5 def call(result) = result.bind { |payload| cast payload }
  12. 1 private
  13. 1 attr_reader :contract, :as
  14. 1 def cast payload
  15. 7 then: 2 else: 1 contract.call(payload).to_monad.fmap { |data| as ? data.public_send(as) : data }
  16. end
  17. end
  18. end
  19. end