Class: Plumb::Encoder
- Inherits:
-
Object
- Object
- Plumb::Encoder
- Defined in:
- lib/plumb/encoder.rb
Overview
A reversible, type-aware transform between two representations of a value — an input type and an output type. Neither side need be a "wire" format: a serialized string and a parsed Ruby object, or one in-memory data structure and another, are equally valid. Declared class-based, with the types given as a one-pair Hash literal:
DateRange = Types::Range[Types::Date]
JSONDateRange = Types::Hash[from: Types::Date, to: Types::Date]
class JSONDateRangeEncoder < Plumb::Encoder[JSONDateRange => DateRange]
def encode(range) = { from: range.begin, to: range.end }
def decode(hash) = hash[:from]..hash[:to]
end
The DEFAULT direction is the declared one (input => output, running
#decode) — an encoder composes like a normal Function:
JSONDateRange >> JSONDateRangeEncoder >> DateRange # decode
Composed next to a type that matches its OUTPUT side instead, it transparently runs the inverse (#encode, input/output swapped):
DateRange >> JSONDateRangeEncoder >> JSONDateRange # encode
When the context gives no signal (schema literals, #/, .parse, an
opaque/Any neighbour) the default direction is used; .decoding /
.encoding are the explicit escape hatches. Each direction is a plain
Function whose proc runs the encoder's method — so subtyping, #>>
composition checks and reductions, visitors and the JSON Schema all work
on it with no encoder-specific machinery.
Class Method Summary collapse
- .&(other) ⇒ Object
- ./(other) ⇒ Object
-
.>>(other) ⇒ Object
Left-position composition (
Enc >> X,Enc | X, ...): a Class has no Composable operators, so these singleton versions orient against the right operand and delegate to the oriented step. -
.[](pair) ⇒ Class
Build the parameterized superclass:
Encoder[Input => Output]. - .call(result) ⇒ Object
- .decode(value) ⇒ Object
-
.decoding ⇒ Function
The default step: the declared
input_type -> output_typedirection, running #decode. -
.encode(value) ⇒ Object
Value-level conveniences for each direction.
-
.encoding ⇒ Function
The inverse step:
output_type -> input_type, running #encode. - .input_type ⇒ Object (also: output_type)
-
.input_type_for(_matched_type) ⇒ Composable
The input type to rewrite for a particular matched output type.
- .parse ⇒ Object
- .resolve ⇒ Object
-
.step(direction, input_side: nil, output_side: nil) ⇒ Function
A direction step with one or both sides substituted — used by Codec::Rewriter to splice in a rewritten input type or a narrowed output type, keeping each rewritten field a single Function node.
-
.to_composable ⇒ Object
Direct use runs the default direction — also the Composable.wrap hook for context-free positions (schema literals,
Array[Enc],#/). -
.to_plumb_type(op:, left:) ⇒ Object
Composition-context direction pick when the encoder is the RIGHT operand (see Composable#to_plumb_type).
- .|(other) ⇒ Object
Instance Method Summary collapse
- #decode(_value) ⇒ Object
-
#encode(_value) ⇒ Object
The value-level contract.
Class Method Details
.&(other) ⇒ Object
135 |
# File 'lib/plumb/encoder.rb', line 135 def &(other) = oriented_against(other, :&) & other |
./(other) ⇒ Object
137 |
# File 'lib/plumb/encoder.rb', line 137 def /(other) = decoding / other |
.>>(other) ⇒ Object
Left-position composition (Enc >> X, Enc | X, ...): a Class has no
Composable operators, so these singleton versions orient against the
right operand and delegate to the oriented step.
133 |
# File 'lib/plumb/encoder.rb', line 133 def >>(other) = oriented_against(other, :>>) >> other |
.[](pair) ⇒ Class
Build the parameterized superclass: Encoder[Input => Output].
The pair is a one-pair Hash literal; both sides are wrapped as Plumb
types (a raw Hash becomes a HashClass, so
Encoder[{from: Date} => DateRange] works).
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/plumb/encoder.rb', line 45 def self.[](pair) unless pair.is_a?(::Hash) && pair.size == 1 raise ArgumentError, "#{self}[Input => Output] expects a one-pair Hash (eg. Encoder[Types::String => Types::Date]), " \ "got #{pair.inspect}" end input, output = pair.first input = Composable.wrap(input) output = Composable.wrap(output) # Singleton methods (unlike class ivars) are inherited by the user's # subclass, so `class Foo < Encoder[A => B]` gets .input_type/.output_type. Class.new(self) do define_singleton_method(:input_type) { input } define_singleton_method(:output_type) { output } end end |
.call(result) ⇒ Object
142 |
# File 'lib/plumb/encoder.rb', line 142 def call(result) = decoding.call(result) |
.decode(value) ⇒ Object
148 |
# File 'lib/plumb/encoder.rb', line 148 def decode(value) = decoding.parse(value) |
.decoding ⇒ Function
The default step: the declared input_type -> output_type direction,
running #decode. A plain Function — #call validates the input type,
runs the encoder's method, and validates the produced value against
the output type (a wrong return value becomes an invalid Result, not
silent corruption); subtyping identity, accepted type and #>> checks
all come with Function. Memoized per subclass (steps are frozen).
90 91 92 |
# File 'lib/plumb/encoder.rb', line 90 def decoding @decoding ||= build_step(:decode, input_type, output_type) end |
.encode(value) ⇒ Object
Value-level conveniences for each direction.
147 |
# File 'lib/plumb/encoder.rb', line 147 def encode(value) = encoding.parse(value) |
.encoding ⇒ Function
The inverse step: output_type -> input_type, running #encode.
96 97 98 |
# File 'lib/plumb/encoder.rb', line 96 def encoding @encoding ||= build_step(:encode, output_type, input_type) end |
.input_type ⇒ Object Also known as: output_type
64 65 66 67 |
# File 'lib/plumb/encoder.rb', line 64 def input_type raise ArgumentError, "#{inspect} declares no types; define it as `class #{inspect} < Plumb::Encoder[Input => Output]`" end |
.input_type_for(_matched_type) ⇒ Composable
The input type to rewrite for a particular matched output type.
Fixed for a normal encoder — the declared input type, regardless of what
matched. A GENERIC encoder (one whose output_type is a container top like
Types::Range, matching any Range) overrides this to BUILD its input
from the matched node's structure, so Range[Date] yields a
from: Date, to: Date input and Range[Integer] a from: Integer, … one.
The codec then rewrites that member type through itself as usual. Mirrors
how the Rewriter reads an Array's element type off the matched node.
81 |
# File 'lib/plumb/encoder.rb', line 81 def input_type_for(_matched_type) = input_type |
.parse ⇒ Object
144 |
# File 'lib/plumb/encoder.rb', line 144 def parse(...) = decoding.parse(...) |
.resolve ⇒ Object
143 |
# File 'lib/plumb/encoder.rb', line 143 def resolve(...) = decoding.resolve(...) |
.step(direction, input_side: nil, output_side: nil) ⇒ Function
A direction step with one or both sides substituted — used by Codec::Rewriter to splice in a rewritten input type or a narrowed output type, keeping each rewritten field a single Function node.
The substitutions name the ENCODER's declared sides, not the step's
positions, so the caller doesn't have to transpose them per direction:
input_side always replaces the declared input, output_side the
declared output, whichever end of the step each lands on.
109 110 111 112 113 114 115 |
# File 'lib/plumb/encoder.rb', line 109 def step(direction, input_side: nil, output_side: nil) base = direction == :decode ? decoding : encoding return base unless input_side || output_side in_t, out_t = direction == :decode ? [input_side, output_side] : [output_side, input_side] Function.new(in_t || base.input_type, out_t || base.output_type, base.fn) end |
.to_composable ⇒ Object
Direct use runs the default direction — also the Composable.wrap hook
for context-free positions (schema literals, Array[Enc], #/).
141 |
# File 'lib/plumb/encoder.rb', line 141 def to_composable = decoding |
.to_plumb_type(op:, left:) ⇒ Object
Composition-context direction pick when the encoder is the RIGHT
operand (see Composable#to_plumb_type). For left >> Enc, orient by
what left produces; for a union/intersection, branches describe the
same produced value, so orient by what the sibling produces relative to
what each direction produces. Falls back to the default direction —
the ordinary composition check raises if it genuinely doesn't fit.
123 124 125 126 127 128 |
# File 'lib/plumb/encoder.rb', line 123 def to_plumb_type(op:, left:) produced = Plumb::Subtyping.resolved_output(Composable.wrap(left)) # `left >> Enc` feeds `left`'s output into the step; in a union/ # intersection the branches describe the same produced value. pick_direction(produced, op == :>> ? :consumes : :produces) end |
.|(other) ⇒ Object
134 |
# File 'lib/plumb/encoder.rb', line 134 def |(other) = oriented_against(other, :|) | other |
Instance Method Details
#decode(_value) ⇒ Object
205 |
# File 'lib/plumb/encoder.rb', line 205 def decode(_value) = raise(NotImplementedError, "#{self.class} must implement #decode(value)") |
#encode(_value) ⇒ Object
The value-level contract. These are also the sentinels build_step checks
(instance_method(direction).owner == Encoder), so using an encoder in a
direction it doesn't implement fails at BUILD time with a better message
than these — which is why they stay deliberately plain.
204 |
# File 'lib/plumb/encoder.rb', line 204 def encode(_value) = raise(NotImplementedError, "#{self.class} must implement #encode(value)") |