Class: Plumb::Codec
Overview
A group of Encoders that applies itself to whole types at composition
time. A Codec knows nothing about any particular format — only its
encoders, plus a list of pass-through types (.noop) already valid in
the target format.
class JSONCodec < Plumb::Codec
noop Types::String, Types::Integer, Types::Float, Types::Numeric,
Types::True, Types::False, Types::Nil, Types::Hash, Types::Array
encoder JSONDateRangeEncoder # Hash[from: Date, to: Date] <=> DateRange
encoder ISODateEncoder # String <=> Date
end
JSONPerson = JSONCodec >> Person # decode: input structures -> Person
EncodedPerson = Person >> JSONCodec # encode: Person -> output structures
Composing rewrites the type deeply: every field (at any depth) whose type matches an encoder's output type is replaced with the oriented encoder step (a plain Function, see Encoder.step); noop-matched types pass through unchanged; anything else raises Plumb::TypeError at composition time, naming the field path. An encoder's input type is itself rewritten through the same codec, so nested non-native values resolve via other encoders in the group.
The Codec leaves no runtime node behind — the result is ordinary Plumb
algebra, so parsing, subtyping and visitors work on it unchanged. It works
on any type, not just Hash schemas: JSONCodec >> Types::Date returns the
matched encoder's decode step.
Defined Under Namespace
Classes: DateEncoder, DecimalEncoder, Forms, JSON, RangeEncoder, Rewriter, SymbolEncoder, TimeEncoder, URIEncoder
Constant Summary collapse
- TIME_EXPR =
Shared, format-neutral encoders — string representations standardized by ISO 8601 (dates, times) and RFC 3986 (URIs) — registered by both built-in codecs, and usable per-field in any schema. Their input types carry
format:metadata, so generated JSON Schemas describe the fields as eg.{type: "string", format: "date"}. /\A\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?)?\z/- URI_EXPR =
Scheme-prefixed URI strings, per RFC 3986 — URI.parse alone is too permissive (a blank string is a valid URI).
/\A[a-z][a-z0-9+\-.]*:/i- FLOAT_EXPR =
Decimal or scientific notation — Float#to_s emits scientific for very small/large magnitudes ("1.0e-05"), so the input type must accept what encode produces or a valid Float fails its own round-trip.
/\A-?\d+(\.\d+)?([eE][+-]?\d+)?\z/- HTTPURIEncoder =
Re-parameterized subclasses: same input type and methods, narrower output — the produced URI is validated against the declared class.
- FileURIEncoder =
Instance Attribute Summary collapse
-
#encoders ⇒ Object
readonly
Returns the value of attribute encoders.
-
#noop_types ⇒ Object
readonly
Returns the value of attribute noop_types.
Class Method Summary collapse
- .&(other) ⇒ Object
- .>>(other) ⇒ Object
- .call(result) ⇒ Object
-
.encoder(*encoder_classes) ⇒ Object
Register one or more Encoder subclasses, in matching-priority order.
-
.encoders ⇒ Object
All registered encoders, inherited first, own last (later registrations win ties in matching).
- .for(type) ⇒ Object
-
.instance ⇒ Object
Class-level composition delegates to a memoized instance, so a Codec subclass composes directly:
JSONCodec >> Person. -
.noop(*types) ⇒ Object
Register pass-through types: values of these types are already valid in the codec's target format and are left untouched — they rewrite to nothing.
-
.noop_types ⇒ Object
All registered pass-through types, inherited first.
- .to_composable ⇒ Object
- .to_plumb_type(op:, left:) ⇒ Object
- .|(other) ⇒ Object
Instance Method Summary collapse
-
#>>(other) ⇒ Object
Decode direction: rewrite
otherso it accepts the encoders' input form and produces the output valuesotherdescribes. - #at_path(path) ⇒ Object
- #call(_result) ⇒ Object
-
#encoder_for(type, path = BLANK_ARRAY) ⇒ Object
The best matching encoder for
type, or nil. -
#for(type) ⇒ Array(Composable, Composable)
Both directions for a type, as a [decoding, encoding] pair:.
-
#initialize(*extra_encoders) ⇒ Codec
constructor
A new instance of Codec.
-
#noop?(type, direction) ⇒ Boolean
Is
type(one side of it, per direction) covered by the registered noop types? Decode checks what the type ACCEPTS (it will be fed raw input data); encode checks what it PRODUCES (its output lands in the encoded document). -
#noop_value?(value) ⇒ Boolean
Is this concrete VALUE covered by the noop types? Used for Static nodes, whose fixed value can be validated directly — subtyping over the node can't relate an atomic Static to a container noop (
Static[[]]vsTypes::Array), but the value itself can just be checked. -
#to_plumb_type(op:, left:) ⇒ Object
Encode direction, reached when the codec is the RIGHT operand (
Person >> JSONCodec— see Composable#to_plumb_type): build an encode rewrite of whatleftproduces. - #|(_other) ⇒ Object (also: #&)
Methods included from Composable
#/, #[], #absorb_input, #absorb_output, #accepted_type, #as_node, #build, #check, #children, #defer, #fusable_step?, #fuse_with, #generate, #idempotent?, included, #input_type, #invalid, #invoke, #match, #metadata, #not, #output_type, #pipeline, #policy, resolve_operand, #static, #subtype_identity, #to_json_schema, #to_mermaid, #to_s, #transform, #value, #value_preserving?, #where, #with, wrap
Methods included from Callable
Constructor Details
#initialize(*extra_encoders) ⇒ Codec
Returns a new instance of Codec.
98 99 100 101 102 103 |
# File 'lib/plumb/codec.rb', line 98 def initialize(*extra_encoders) @encoders = self.class.encoders + extra_encoders @noop_types = self.class.noop_types @noop_union = @noop_types.reduce(:|) freeze end |
Instance Attribute Details
#encoders ⇒ Object (readonly)
Returns the value of attribute encoders.
94 95 96 |
# File 'lib/plumb/codec.rb', line 94 def encoders @encoders end |
#noop_types ⇒ Object (readonly)
Returns the value of attribute noop_types.
94 95 96 |
# File 'lib/plumb/codec.rb', line 94 def noop_types @noop_types end |
Class Method Details
.&(other) ⇒ Object
75 |
# File 'lib/plumb/codec.rb', line 75 def &(other) = instance & other |
.>>(other) ⇒ Object
73 |
# File 'lib/plumb/codec.rb', line 73 def >>(other) = instance >> other |
.call(result) ⇒ Object
79 |
# File 'lib/plumb/codec.rb', line 79 def call(result) = instance.call(result) |
.encoder(*encoder_classes) ⇒ Object
Register one or more Encoder subclasses, in matching-priority order. The registry is inheritable: subclassing a codec extends it, and a subclass's encoders take precedence over inherited ones when their types are equivalent.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/plumb/codec.rb', line 43 def encoder(*encoder_classes) encoder_classes.each do |enc| unless enc.is_a?(::Class) && enc < Encoder raise ArgumentError, "expected an Encoder subclass, got #{enc.inspect}" end own_encoders << enc end self end |
.encoders ⇒ Object
All registered encoders, inherited first, own last (later registrations win ties in matching).
65 |
# File 'lib/plumb/codec.rb', line 65 def encoders = inherited_registry(:encoders, own_encoders) |
.for(type) ⇒ Object
76 |
# File 'lib/plumb/codec.rb', line 76 def for(type) = instance.for(type) |
.instance ⇒ Object
Class-level composition delegates to a memoized instance, so a Codec
subclass composes directly: JSONCodec >> Person.
72 |
# File 'lib/plumb/codec.rb', line 72 def instance = @instance ||= new |
.noop(*types) ⇒ Object
Register pass-through types: values of these types are already valid in the codec's target format and are left untouched — they rewrite to nothing. Real encoders always match first, so a noop never shadows a registered encoder for the same type.
58 59 60 61 |
# File 'lib/plumb/codec.rb', line 58 def noop(*types) types.each { |t| own_noop_types << Composable.wrap(t) } self end |
.noop_types ⇒ Object
All registered pass-through types, inherited first.
68 |
# File 'lib/plumb/codec.rb', line 68 def noop_types = inherited_registry(:noop_types, own_noop_types) |
.to_composable ⇒ Object
78 |
# File 'lib/plumb/codec.rb', line 78 def to_composable = instance |
.to_plumb_type(op:, left:) ⇒ Object
77 |
# File 'lib/plumb/codec.rb', line 77 def to_plumb_type(op:, left:) = instance.to_plumb_type(op:, left:) |
.|(other) ⇒ Object
74 |
# File 'lib/plumb/codec.rb', line 74 def |(other) = instance | other |
Instance Method Details
#>>(other) ⇒ Object
Decode direction: rewrite other so it accepts the encoders' input form
and produces the output values other describes. The rewritten type
REPLACES the composition — no codec node remains.
108 109 110 |
# File 'lib/plumb/codec.rb', line 108 def >>(other) Rewriter.new(self, :decode).call(Composable.wrap(other)) end |
#at_path(path) ⇒ Object
203 |
# File 'lib/plumb/codec.rb', line 203 def at_path(path) = path.empty? ? 'the root type' : "field `#{path.join('.')}`" |
#call(_result) ⇒ Object
146 147 148 |
# File 'lib/plumb/codec.rb', line 146 def call(_result) raise Plumb::TypeError, "#{inspect} is not a runtime type; compose it with a type via #>>" end |
#encoder_for(type, path = BLANK_ARRAY) ⇒ Object
The best matching encoder for type, or nil. Matching is against each
encoder's output type — schemas are written in output
terms in both directions. Most-specific wins; equivalent types
tie-break to the last registered; incomparable multi-matches raise.
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/plumb/codec.rb', line 154 def encoder_for(type, path = BLANK_ARRAY) matches = encoders.select { |e| Plumb::Subtyping.subtype?(type, e.output_type) } return nil if matches.empty? return matches.first if matches.size == 1 minimal = matches.reject do |e| # e is dominated when another match's output type is strictly narrower. matches.any? do |o| !o.equal?(e) && Plumb::Subtyping.strict_subtype?(o.output_type, e.output_type) end end first = minimal.first unless minimal.all? { |e| Plumb::Subtyping.equivalent?(e.output_type, first.output_type) } raise Plumb::TypeError, "#{inspect}: #{at_path(path)} (#{type.inspect}) matches multiple incomparable encoders: " \ "#{minimal.map(&:inspect).join(', ')}. Register a more specific encoder or restructure." end minimal.last end |
#for(type) ⇒ Array(Composable, Composable)
120 121 122 123 |
# File 'lib/plumb/codec.rb', line 120 def for(type) type = Composable.wrap(type) [self >> type, type >> self] end |
#noop?(type, direction) ⇒ Boolean
Is type (one side of it, per direction) covered by the registered noop
types? Decode checks what the type ACCEPTS (it will be fed raw input
data); encode checks what it PRODUCES (its output lands in the encoded
document).
180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/plumb/codec.rb', line 180 def noop?(type, direction) return false unless @noop_union side = direction == :decode ? Plumb::Subtyping.accepted_type(type) : Plumb::Subtyping.resolved_output(type) # A pure filter with an opaque side is judged by the type itself: a # bare-matcher Constraint (a branch of a factored refinement union, eg. # the `String[/\Atrue\z/i] | String['1']` boolean input type) reports Any # as its accepted type, but as a value-preserving refinement it IS its # own honest description. side = type if side.is_a?(AnyClass) && Plumb::Subtyping.value_preserving?(type) Plumb::Subtyping.subtype?(side, @noop_union) end |
#noop_value?(value) ⇒ Boolean
Is this concrete VALUE covered by the noop types? Used for Static nodes,
whose fixed value can be validated directly — subtyping over the node
can't relate an atomic Static to a container noop (Static[[]] vs
Types::Array), but the value itself can just be checked.
197 198 199 200 201 |
# File 'lib/plumb/codec.rb', line 197 def noop_value?(value) return false unless @noop_union @noop_union === value end |
#to_plumb_type(op:, left:) ⇒ Object
Encode direction, reached when the codec is the RIGHT operand
(Person >> JSONCodec — see Composable#to_plumb_type): build an encode
rewrite of what left produces. Composable#>> then composes
And(left, rewrite): the left validates its input once, the
rewrite encodes. Building from left's OUTPUT (not left itself)
avoids re-running its coercions on already-parsed values.
131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/plumb/codec.rb', line 131 def to_plumb_type(op:, left:) unless op == :>> raise Plumb::TypeError, "#{inspect} only composes with #>> (got #{op}); a Codec is not a value type" end left = Composable.wrap(left) # A plain-include struct wraps as an opaque Step (output Any), so its # rewrite target is the struct node itself, not its resolved output. target = Plumb::Attributes.struct_class(left) ? left : Plumb::Subtyping.resolved_output(left) Rewriter.new(self, :encode).call(target) end |