Class: Plumb::Encoder

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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).

Parameters:

  • pair (Hash)

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

Returns:

  • (Class)


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)

.decodingFunction

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).

Returns:



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)

.encodingFunction

The inverse step: output_type -> input_type, running #encode.

Returns:



96
97
98
# File 'lib/plumb/encoder.rb', line 96

def encoding
  @encoding ||= build_step(:encode, output_type, input_type)
end

.input_typeObject Also known as: output_type

Raises:

  • (ArgumentError)


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.

Parameters:

  • matched_type (Composable)

    the type this encoder matched

Returns:



81
# File 'lib/plumb/encoder.rb', line 81

def input_type_for(_matched_type) = input_type

.parseObject



144
# File 'lib/plumb/encoder.rb', line 144

def parse(...) = decoding.parse(...)

.resolveObject



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.

Returns:



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_composableObject

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

Raises:

  • (NotImplementedError)


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.

Raises:

  • (NotImplementedError)


204
# File 'lib/plumb/encoder.rb', line 204

def encode(_value) = raise(NotImplementedError, "#{self.class} must implement #encode(value)")