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
-
.[](pair) ⇒ Module
Build the mixin:
Implementation[Input => Output]. - .extended(base) ⇒ Object
-
.included(base) ⇒ Object
include/extend Plumb::Implementationwith no declared types — the opaque case. -
.setup_class(base, input, output) ⇒ Object
private
The EXTEND path:
baseitself becomes a typed step. -
.setup_instances(base, input, output) ⇒ Object
private
The INCLUDE path: instances of
basebecome typed steps.
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]).
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 |