Module: Plumb::Implementation

Defined in:
lib/plumb/implementation.rb

Overview

Turn a custom Ruby class into a first-class Plumb type.

Function wraps a callable you hand it; an Implementation goes the other way round — YOUR class owns its constructor and state, and the mixin gives it the typed-node interface (#input_type, #output_type, composition, subtyping, visitors). The declared pair is given as a one-pair Hash literal, exactly like Encoder.

The mixin owns the public #call(Result) => Result; you implement a private #_call(Result) => Result. #call runs the declared checks around it:

result.map(input_type).map(_call).map(output_type)

ie. the input is validated (and coerced, if the input type converts) before #_call sees it, and what it returns is validated against the output type.

INCLUDE it to make INSTANCES typed steps — the case for a parameterized step, where the constructor arguments are part of what the step does:

class UserFinder
include Plumb::Implementation[Types::UUID::V4 => User]

def initialize(scope) = @scope = scope

private def _call(result)
  user = User.where(level: @scope).find_by(id: result.value)
  return result.invalid(errors: 'no user!') unless user

  result.valid(user)
end
end

finder = UserFinder.new('admin')
finder.parse(some_uuid)      # => a User (raises unless the input is a UUID)
finder >> some_other_step    # composition, type-checked at build time
Types::UUID::V4 >> finder    # ...on both sides
finder <= User               # true: identified by what it produces
finder.to_json_schema        # describes the INPUT side, like any function

EXTEND it to make THE CLASS ITSELF the step — no instantiation, the class implements self._call(result) and answers .input_type / .output_type:

class ParseUUID
extend Plumb::Implementation[Types::String => Types::UUID::V4]

def self._call(result) = result.valid(result.value.downcase)
end

ParseUUID.parse(str)         # the class is the step
ParseUUID >> UserFinder.new('admin')
Types::Hash[id: ParseUUID]

This mirrors include Plumb::Composable / extend Plumb::Composable, and like those the two forms are alternatives — pick one per class. The extended form deliberately does NOT get Naming/Equality (see Composable.included): a class must keep Ruby's own #name, #inspect, #== and #<= (module ancestry). Ask for the subtype relation explicitly instead: Plumb::Subtyping.subtype?(ParseUUID, Types::String).

Either way the step reports node_name :function, so every visitor, JSON Schema handler and base-type resolution that already understands a conversion node understands it too. Define your own #node_name (or self.node_name) after the mixin if you have visitors of your own.

Declaring no pair (include/extend Plumb::Implementation) is the OPAQUE case: both ends are Types::Any, like Function.opaque — the checks can't fail and #>> opts out of type-checking.

Defined Under Namespace

Modules: Inspect, TypeInterface

Class Method Summary collapse

Class Method Details

.[](pair) ⇒ Module

Build the mixin: Implementation[Input => Output]. Both sides are wrapped as Plumb types, so raw Ruby classes/Hashes work (Implementation[{id: Types::String} => User]).

Parameters:

  • pair (Hash)

    a one-pair Hash: input type => output type

Returns:

  • (Module)

    a mixin to include (typed instances) or extend (typed class)



81
82
83
84
85
86
87
88
89
90
# File 'lib/plumb/implementation.rb', line 81

def self.[](pair)
  unless pair.is_a?(::Hash) && pair.size == 1
    raise ArgumentError,
          'Plumb::Implementation[Input => Output] expects a one-pair Hash ' \
          "(eg. Plumb::Implementation[Types::String => User]), got #{pair.inspect}"
  end

  input, output = pair.first
  build(Composable.wrap(input), Composable.wrap(output))
end

.extended(base) ⇒ Object



95
# File 'lib/plumb/implementation.rb', line 95

def self.extended(base) = setup_class(base, Types::Any, Types::Any)

.included(base) ⇒ Object

include/extend Plumb::Implementation with no declared types — the opaque case.



94
# File 'lib/plumb/implementation.rb', line 94

def self.included(base) = setup_instances(base, Types::Any, Types::Any)

.setup_class(base, input, output) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The EXTEND path: base itself becomes a typed step. Same interface, all of it on the singleton — and WITHOUT Naming/Equality, which would otherwise take over the class' own #name/#inspect/#==/#<= (this is why it goes through #extend rather than singleton_class.include, mirroring extend Plumb::Composable).



136
137
138
139
140
141
# File 'lib/plumb/implementation.rb', line 136

def self.setup_class(base, input, output)
  base.extend(Composable)
  base.extend(TypeInterface)
  base.extend(types_module(input, output))
  base.define_singleton_method(:node_name) { :function }
end

.setup_instances(base, input, output) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

The INCLUDE path: instances of base become typed steps.

Order matters. Composable goes in FIRST so that TypeInterface — included after it, therefore ahead of it in the ancestor chain — wins over the #input_type = self / #output_type = self defaults every plain type carries, and over Callable#call. #node_name is defined on the class itself because Naming defines it there too (a module could not override it); a def node_name in the class body after the include still wins over both.



121
122
123
124
125
126
127
# File 'lib/plumb/implementation.rb', line 121

def self.setup_instances(base, input, output)
  base.include(Composable)
  base.include(TypeInterface)
  base.include(Inspect)
  base.include(types_module(input, output))
  base.define_method(:node_name) { :function }
end