Module: Plumb::Composable

Overview

 Composable mixes in composition methods to classes. such as #>>, #|, #not, and others. Any Composable class can participate in Plumb compositions. A host object only needs to implement the Step interface call(Result) => Result

Defined Under Namespace

Classes: Node

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Callable

#call, #parse, #resolve

Class Method Details

.included(base) ⇒ Object

This only runs when including Composable, not extending classes with it.



211
212
213
214
# File 'lib/plumb/composable.rb', line 211

def self.included(base)
  base.send(:include, Naming)
  base.send(:include, Equality)
end

.resolve_operand(other, op:, left:) ⇒ Composable

Resolve other against a composition context and wrap it as a Composable — the full normalization for the RIGHT operand of a composition operator. Consults the operand's #to_plumb_type hook when it has one (raw values — procs, hash literals — don't, and skip straight to wrapping), so callers never need a #respond_to? check. EVERY custom operator implementation should route its operand through here.

Parameters:

  • other (Object)

    the right operand

  • op (Symbol)

    the composition operator being resolved (:>>, :| or :&)

  • left (Composable)

    the left operand

Returns:



274
275
276
277
# File 'lib/plumb/composable.rb', line 274

def self.resolve_operand(other, op:, left:)
  other = other.to_plumb_type(op:, left:) if other.respond_to?(:to_plumb_type)
  wrap(other)
end

.wrap(callable) ⇒ Composable

Wrap an object in a Composable instance. Anything that includes Composable is a noop. A Hash is assumed to be a HashClass schema. An Array with zero or 1 element is assumed to be an ArrayClass. Any #call(Result) => Result interface is wrapped in a Step. Anything else is assumed to be something you want to match against via #===.

Examples:

ten = Composable.wrap(10)
ten.resolve(10) # => a valid Result
ten.resolve(11) # => an invalid Result

Parameters:

  • callable (Object)

Returns:



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/plumb/composable.rb', line 230

def self.wrap(callable)
  if callable.is_a?(Composable)
    callable
  elsif callable.respond_to?(:to_composable)
    # The context-free resolution hook for objects that become a type on
    # demand: an Encoder class resolves to its default (declared)
    # direction, a Codec class to its instance. Used in schema literals,
    # `Array[Enc]`, `#/`, etc. The composition operators (#>>, #|, #&)
    # consult #to_plumb_type BEFORE wrapping, so a composed operand can
    # resolve against context and never reaches this branch.
    wrap(callable.to_composable)
  elsif callable.is_a?(::Hash)
    HashClass.new(schema: callable)
  elsif callable.is_a?(::Array)
    element_type = case callable.size
                   when 0
                     Types::Any
                   when 1
                     callable.first
                   else
                     raise ArgumentError, '[element_type] syntax allows a single element type'
                   end
    Types::Array[element_type]
  elsif callable.respond_to?(:call)
    Function.opaque(callable)
  elsif Constraint.literal_matcher?(callable)
    # Equality literals use ValueClass; broader matchers use Constraint.
    ValueClass.new(callable)
  else
    Constraint.new(callable)
  end
end

Instance Method Details

#&(other) ⇒ Composable

Intersection ("and"/meet) — the symmetric dual of #|. Builds the greatest lower bound of self and other: the type describing values that satisfy BOTH. Unlike #>>, it is order-independent and not subtype-checked. The reducer (Subtyping.intersect) narrows where it can — intersecting Ranges/ Sets (Integer[2..] & Integer[0..100] == Integer[2..100]), covariant containers, and distributing over unions — and collapses a PROVABLY-empty intersection to Types::Never (Integer[2..10] & Integer[11..100], String & Integer). When it can prove neither a narrowing nor emptiness it falls back to Conjunction.build — a runtime intersection where both sides must pass.

Parameters:

Returns:



462
463
464
465
# File 'lib/plumb/composable.rb', line 462

def &(other)
  other = Composable.resolve_operand(other, op: :&, left: self)
  Plumb::Subtyping.intersect(self, other) || Conjunction.build(self, other)
end

#/(other) ⇒ Composable

Compose like #>> but WITHOUT the strict subtype check — the escape hatch for chains the checker can't prove safe but you know are (eg. a narrowing like Types::Integer / Types::Integer[1..10], or feeding a producer whose output you know the right side accepts). You assert the composition is valid; it is still runtime-checked when data flows through. Reduces and builds the same refinement as #>> (just skipping the build-time check), so the result participates in subtyping like any other refinement. The / reads as Pathname#/ does — "join the next segment". When self is the Any top the right side stands alone, consistent with #[].

Parameters:

Returns:



430
431
432
# File 'lib/plumb/composable.rb', line 430

def /(other)
  constrain(Composable.wrap(other))
end

#>>(other) ⇒ And

Chain two composable objects together. A.K.A "and" or "sequence" Type-checks the composition by subsumption: everything self produces must be acceptable to other (self's output a subtype of other's input), else it raises Plumb::TypeError — eg. String >> Integer, or Integer[0..40] >> Integer[2..10] (the left can emit values the right rejects). To narrow a value, use #[] / #transform(...)[...] (a refinement is a runtime-checked cast, built directly and not subtype- checked). The check is permissive only where types are unknown: opaque steps (plain procs, transforms, narrowing matchers) report Any and opt out.

Examples:

Step1 >> Step2 >> Step3

Parameters:

Returns:

Raises:



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'lib/plumb/composable.rb', line 400

def >>(other)
  other = Composable.resolve_operand(other, op: :>>, left: self)
  # `X >> X` is redundant for a value-preserving validator (eg. Types::String):
  # validating the same value twice is the same as once. Gated on #idempotent?
  # so transforms — where `X >> X` would apply the change twice — never collapse.
  return self if idempotent? && self == other

  Plumb::Subtyping.check_composable!(self, other)
  # Drop what `other` re-asserts that `self` already guarantees. reduce_step
  # folds a base-type gate into a Constraint chain (`Integer[0..100] >>
  # Integer[-10..110]` -> `Integer[0..100]`, `::Integer` validated once);
  # redundant_refinement? does the same for any value-preserving `other` that
  # `self` already subsumes (`String.where(size: 3..10) >> .where(size: 0..)`
  # -> the former). A non-redundant `other` (a transform, or a narrowing
  # refinement) stays an And.
  Plumb::Optimizer.rewrite_step(self, other)
end

#[](*args) ⇒ Object

Sugar over #match: a splatted list of values becomes a Set membership matcher, so Integer[1, 2, 3] == Integer[Set[1, 2, 3]] (and composes / reduces like any Set constraint). A single argument is used as-is — a Range, Regexp, Set, class or literal value.

Examples:

Types::String['a', 'b', 'c'] # one of these three strings


605
# File 'lib/plumb/composable.rb', line 605

def [](*args) = match(args.size > 1 ? ::Set.new(args) : args.first)

#absorb_input(_type) ⇒ Object



381
# File 'lib/plumb/composable.rb', line 381

def absorb_input(_type) = nil

#absorb_output(_type) ⇒ Object

BOUNDARY ABSORPTION — the one-sided companions to #fuse_with, for a seam where only one side is a typed step and the other is a plain type. A typed step already RUNS its boundary types as steps (result.map(input).map(fn) .map(output)), so a neighbouring type can move into the matching slot and the node then does the same work in one hop instead of two.

`self >> type` -> #absorb_output(type), asked of the LEFT (the step)
`type >> self` -> #absorb_input(type),  asked of the RIGHT (the step)

Return the rebuilt node, or nil to decline. Implemented by Function (which owns the soundness conditions) and re-associated by And, so a step in the middle of a chain is reachable. Reached from Optimizer.reduce_step as its LAST rung: absorption only fires where no other reduction applies.



380
# File 'lib/plumb/composable.rb', line 380

def absorb_output(_type) = nil

#accepted_typeObject

The type this step accepts as the consumer of a left >> self chain — the values its #call processes without rejecting outright. Defaults to what it takes as input (its resolved #input_type): right for plain matchers and for conversion/consumer types (Function, Stream, Pipeline — they accept their declared input, so they need no override). Two kinds of type override it:

- a refinement (And): its #input_type is only the type the chain opens
with and would drop the constraint it adds, so it accepts its *output*;
- a Hash: it relaxes each field to what that field accepts.

Consulted by Plumb::Subtyping when checking #>>.



331
# File 'lib/plumb/composable.rb', line 331

def accepted_type = Plumb::Subtyping.resolved_input(self)

#as_node(node_name, args = BLANK_HASH) ⇒ Node

Wrap a Step in a node with a custom #node_name which is expected by visitors. So that we can define special visitors for certain compositions. Ex. Types::Boolean is a compoition of Types::True | Types::False, but we want to treat it as a single node.

Parameters:

  • node_name (Symbol)
  • args (Hash) (defaults to: BLANK_HASH)

Returns:



683
684
685
# File 'lib/plumb/composable.rb', line 683

def as_node(node_name, args = BLANK_HASH)
  Node.new(node_name, self, args)
end

#build(cns, factory_method = :new, &block) ⇒ And

Compose a step that instantiates a class. It sets the class as the output type of the step. Optionally takes a block.

type = Types::String.build(Money) { |value| Monetize.parse(value) }

Examples:

type = Types::String.build(MyClass, :new)
thing = type.parse('foo') # same as MyClass.new('foo')

Parameters:

  • cns (Class)

    constructor class or object.

  • factory_method (Symbol) (defaults to: :new)

    method to call on the class to instantiate it.

Returns:



756
757
758
759
760
761
# File 'lib/plumb/composable.rb', line 756

def build(cns, factory_method = :new, &block)
  # Without a block the callable is a fresh lambda per call, so name what the
  # step actually IS — the (class, factory method) pair — as its identity.
  transform_step(cns, block || ->(value) { cns.send(factory_method, value) },
                 identity: block || [cns, factory_method])
end

#check(errors = 'did not pass the check', &block) ⇒ Constraint

Pass the value through an arbitrary validation

Examples:

type = Types::String.check('must start with "Role:"') { |value| value.start_with?('Role:') }

Parameters:

  • errors (String) (defaults to: 'did not pass the check')

    error message to use when validation fails

  • block (Proc)

    a block that will be applied to the value

Returns:



521
522
523
524
525
# File 'lib/plumb/composable.rb', line 521

def check(errors = 'did not pass the check', &block)
  # A refinement, not a sequence: build the matcher over `self` as its base so
  # the checked value keeps `self`'s type (`User.check { … }` is still a User).
  Constraint.new(block, base: self, error: errors, label: errors)
end

#childrenArray<Composable>

Visitors expect a #node_name and #children interface.

Returns:



741
# File 'lib/plumb/composable.rb', line 741

def children = BLANK_ARRAY

#defer(definition = nil, &block) ⇒ Object

A helper to wrap a block in a Step that will defer execution. This so that types can be used recursively in compositions.

Examples:

LinkedList = Types::Hash[
  value: Types::Any,
  next: Types::Any.defer { LinkedList }
]


286
287
288
# File 'lib/plumb/composable.rb', line 286

def defer(definition = nil, &block)
  Deferred.new(definition || block)
end

#fusable_step?Boolean

Can Function#fuse_with drop this node's boundary checks? Only the node knows: the answer is yes exactly when its #call runs the standard input -> fn -> output mapping, so the checks removed at a seam are checks it would really have run. Default false — a node opts in by saying so.

Returns:

  • (Boolean)

See Also:



365
# File 'lib/plumb/composable.rb', line 365

def fusable_step? = false

#fuse_with(_other) ⇒ Object

Fuse self >> other into a single node when the runtime checks at the boundary between them are provably redundant, or nil when fusion doesn't apply. A reduction rung in Optimizer.reduce_step, so both #>> and #/ reach it. Implemented by Function (transform fusion) and CovariantFusion (the functor law for containers).



358
# File 'lib/plumb/composable.rb', line 358

def fuse_with(_other) = nil

#generate(generator = nil, &block) ⇒ And

Return the output of a block or #call interface, regardless of input. The block will be called to get the value, on every invocation.

Examples:

now = Types::Integer.generate { Time.now.to_i }

Parameters:

  • generator (#call, nil) (defaults to: nil)

    a callable that will be applied to the value, or nil if block

  • block (Proc)

    a block that will be applied to the value, or nil if callable

Returns:

Raises:

  • (ArgumentError)


819
820
821
822
823
824
825
826
# File 'lib/plumb/composable.rb', line 819

def generate(generator = nil, &block)
  generator ||= block
  raise ArgumentError, 'expected a generator' unless generator.respond_to?(:call)

  Function.opaque(inspect: 'generator', identity: [:generate, generator]) do |r|
    r.valid(generator.call)
  end >> self
end

#idempotent?Boolean

Whether running this step twice in a row is the same as running it once, i.e. it validates without changing the value. Lets #>> drop a redundant X >> X. Default false; only types that never transform the value opt in (see Constraint). A transform must NOT be idempotent — X >> X would apply it twice.

Returns:

  • (Boolean)


338
# File 'lib/plumb/composable.rb', line 338

def idempotent? = false

#input_typeObject



290
# File 'lib/plumb/composable.rb', line 290

def input_type = self

#invalid(errors: nil) ⇒ Not

Like #not, but with a custom error message.

Parameters:

  • errors (Hash) (defaults to: nil)

    a customizable set of options

Options Hash (errors:):

  • error (String)

    message to use when validation fails

Returns:



558
559
560
# File 'lib/plumb/composable.rb', line 558

def invalid(errors: nil)
  Not.new(self, errors:)
end

#invoke(*args, &block) ⇒ Composable

Build a step that will invoke one or more methods on the value. Ex 1: Types::String.invoke(:downcase) Ex 2: Types::Array.invoke(:[], 1) Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])

Returns:



864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
# File 'lib/plumb/composable.rb', line 864

def invoke(*args, &block)
  case args
  in [::Symbol => method_name, *rest]
    label = [method_name.inspect, rest.inspect].join(' ')
    # The block is new on each call, but the step it implements is determined
    # by the method and its arguments — so name that as the identity, and two
    # `invoke(:downcase)` steps stay #== (see Function#==).
    self >> Function.opaque(inspect: label, identity: [:invoke, method_name, rest, block]) do |result|
      result.valid(result.value.public_send(method_name, *rest, &block))
    end
  in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
    methods.reduce(self) { |step, method| step.invoke(method) }
  else
    raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
  end
end

#match(*args) ⇒ Constraint

Alias of #[] Match a value using #===

Examples:

email = Types::String['@']

Parameters:

  • args (Array<Object>)

Returns:



583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
# File 'lib/plumb/composable.rb', line 583

def match(*args)
  # A refinement returns a base-carrying Constraint directly (not an
  # `And(self, matcher)`): the matcher records `self` as its base, so it
  # subtypes and composes as "a `self` narrowed by the matcher". When `self`
  # is the Any top the matcher stands alone (`Any[String]` == the String
  # matcher), matching the collapsing `AnyClass#>>` provides. Routed through
  # Constraint.narrow so stacked Range refinements intersect
  # (`Integer[0..100][10..]` == `Integer[10..100]`).
  base = is_a?(AnyClass) ? nil : self
  # A bare equality literal uses ValueClass so `Any[5]` and `Value[5]` share
  # one subtype identity. A based literal remains a Constraint.
  return ValueClass.new(args.first) if base.nil? && args.size == 1 && Constraint.literal_matcher?(args.first)

  Constraint.narrow(base, *args)
end

#metadata(data = Undefined) ⇒ Hash, And

Return a new Step with added metadata, or build step metadata if no argument is provided.

Examples:

type = Types::String.(label: 'Name')
type. # => { type: String, label: 'Name' }

Parameters:

  • data (Hash) (defaults to: Undefined)

    metadata to add to the step

Returns:



534
535
536
537
538
539
540
# File 'lib/plumb/composable.rb', line 534

def (data = Undefined)
  if data == Undefined
    MetadataVisitor.call(self)
  else
    Metadata.new(self, data)
  end
end

#not(other = self) ⇒ Not

Negate the result of a step. Ie. if the step is valid, it will be invalid, and vice versa.

Examples:

type = Types::String.not
type.resolve('foo') # invalid
type.resolve(10) # valid

Returns:



550
551
552
# File 'lib/plumb/composable.rb', line 550

def not(other = self)
  Not.new(other)
end

#output_typeObject



291
# File 'lib/plumb/composable.rb', line 291

def output_type = self

#pipeline(&block) ⇒ Pipeline

Build a Plumb::Pipeline with this object as the starting step. end

Examples:

pipe = Types::Data[name: String].pipeline do |pl|
  pl.step Validate
  pl.step Debug
  pl.step Log

Returns:



837
838
839
# File 'lib/plumb/composable.rb', line 837

def pipeline(&block)
  Pipeline.new(type: self, &block)
end

#policy(*args, &blk) ⇒ Composable

Register a policy for this step. Mode 1.a: #policy(:name, arg) a single policy with an argument Mode 1.b: #policy(:name) a single policy without an argument Mode 2: #policy(p1: value, p2: value) multiple policies with arguments The latter mode will be expanded to multiple #policy calls.

Returns:



717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# File 'lib/plumb/composable.rb', line 717

def policy(*args, &blk)
  case args
  in [::Symbol => name, *rest] # #policy(:name, arg)
    types = Plumb.resolve_base_types(output_type).uniq

    bargs = [self]
    arg = Undefined
    if rest.size.positive?
      bargs << rest.first
      arg = rest.first
    end
    block = Plumb.policies.get(types, name)
    pol = block.call(*bargs, &blk)

    Policy.new(name, arg, pol)
  in [::Hash => opts] # #policy(p1: value, p2: value)
    opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
  else
    raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
  end
end

#static(value) ⇒ And

Always return a static value, regardless of the input.

Examples:

type = Types::Integer.static(10)
type.parse(10) # => 10
type.parse(100) # => 10
type.parse # => 10

Parameters:

  • value (Object)

Returns:



807
808
809
# File 'lib/plumb/composable.rb', line 807

def static(value)
  StaticClass.new(value) >> self
end

#subtype_identityObject

The type that carries this node's identity for the subtype relation. A value-preserving type IS its own identity (the default). A value-converting type is identified by what it produces, so it projects onto a DISTINCT type (see Function#subtype_identity => output_type): Plumb::Subtyping.subtype? reduces a <= b to produced(a) <= b before consulting the leaf hooks.

CONTRACT: only return a value other than self when that value is a genuinely different node. Returning self here is a no-op (subtype? guards with !equal?(self), so it simply won't reduce); returning a node whose own #subtype_identity loops back would recurse forever. This is the extension point for building custom transforming types that play well with subtyping WITHOUT subclassing Function.



320
# File 'lib/plumb/composable.rb', line 320

def subtype_identity = self

#to_json_schema(root: false) ⇒ Hash

Parameters:

  • root (Hash) (defaults to: false)

    a customizable set of options

Options Hash (root:):

  • whether (Boolean)

    to include JSON Schema $schema property

Returns:

  • (Hash)


847
848
849
# File 'lib/plumb/composable.rb', line 847

def to_json_schema(root: false)
  JSONSchemaVisitor.call(self, root:)
end

#to_mermaid(direction: 'LR') ⇒ String

Render this composition as a Mermaid flowchart. >> becomes sequential arrows and | becomes a fork. See Plumb::MermaidVisitor.

Parameters:

  • direction (Hash) (defaults to: 'LR')

    a customizable set of options

Options Hash (direction:):

  • flowchart (String)

    direction ('LR', 'TB', …)

Returns:

  • (String)


855
856
857
# File 'lib/plumb/composable.rb', line 855

def to_mermaid(direction: 'LR')
  MermaidVisitor.call(self, direction:)
end

#to_plumb_type(op:, left:) ⇒ Composable

Composition-context resolution hook, consulted on the RIGHT operand of #>>, #| and #& before wrapping/type-checking — via Composable.resolve_operand, which any custom operator implementation should route its operand through. It lets an operand resolve itself against the composition context: an Encoder picks the direction to run in from what left produces; a Codec builds an encode rewrite of left's output. The returned value replaces the operand in the composition. Default: identity — every ordinary type composes as itself.

Parameters:

  • op (Symbol)

    the composition operator (:>>, :| or :&)

  • left (Composable)

    the left operand

Returns:



306
# File 'lib/plumb/composable.rb', line 306

def to_plumb_type(op:, left:) = self

#to_sObject



841
842
843
# File 'lib/plumb/composable.rb', line 841

def to_s
  inspect
end

#transform(target_type, callable = nil, &block) ⇒ Function

Transform value. Requires specifying the resulting type of the value after transformation. Shorthand: a single conversion symbol (see COERCION_METHODS) expands to a typed transform to the method's result type, using the symbol as the callable. When the input's base Ruby type is known, it also validates that the type actually responds to the method.

Examples:

Types::String.transform(Types::Symbol, &:to_sym)
Types::String.transform(:to_i)   # => Transform to Integer, via :to_i
Types::Integer.transform(:to_sym) # raises: Integer has no #to_sym

Parameters:

  • target_type (Class, Symbol)

    the output type, or a conversion symbol

  • callable (#call, nil) (defaults to: nil)

    a callable that will be applied to the value, or nil if block provided

  • block (Proc)

    a block that will be applied to the value, or nil if callable provided

Returns:



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/plumb/composable.rb', line 483

def transform(target_type, callable = nil, &block)
  if target_type.is_a?(::Symbol) && callable.nil? && block.nil? && (out = COERCION_METHODS[target_type])
    return coercion_transform(target_type, out)
  end

  # Explicit output type + a coercion symbol as the callable
  # (eg. `#transform(::Numeric, :to_f)`). Since the symbol's result type is
  # known, we can compare it against the declared output at composition time:
  #  - `ret <= target_type`  => the produced value is always within the
  #    declared type, so the runtime output check is redundant (guaranteed).
  #  - `ret` and `target_type` disjoint (neither is a subtype of the other)
  #    => no value can be an instance of both, so the transform would reject
  #    EVERY input. That's a composition error, not a runtime one — raise now.
  #  - otherwise (`target_type < ret`, a genuine narrowing) => may or may not
  #    hold at runtime; leave the output check to do its job.
  if callable.is_a?(::Symbol) && block.nil? && (ret = COERCION_METHODS[callable])
    guaranteed = false
    if target_type.is_a?(::Module)
      if ret <= target_type
        guaranteed = true
      elsif !(target_type <= ret)
        raise ArgumentError,
              ":#{callable} produces a #{ret}, which is never a #{target_type}"
      end
    end
    return transform_step(target_type, callable.to_proc, guaranteed:)
  end

  transform_step(target_type, callable || block || Plumb::NOOP)
end

#value(val) ⇒ Object

 Match a value using #== Normally you'll build matchers via ``#[], which uses #===. Use this if you want to match against concrete instances of things that respond to #===`

Examples:

regex = Types::Any.value(/foo/)
regex.resolve('foo') # invalid. We're matching against the regex itself.
regex.resolve(/foo/) # valid

Parameters:

  • value (Object)


572
573
574
# File 'lib/plumb/composable.rb', line 572

def value(val)
  constrain(ValueClass.new(val))
end

#value_preserving?Boolean

Whether this type returns its input value UNCHANGED on success — a coreflexive refinement (a pure filter). Lets #| absorb a redundant branch (Integer | Numeric == Numeric) without dropping a coercion. See Optimizer.reduce_union, which memoizes this per frozen node in TypeCache. Default false; refinements opt in, transforms stay false. A covariant container (Array/Tuple/HashMap) preserves the value exactly when all its element/child types do — so Array[Integer] >> Array[Numeric] collapses like the scalar Integer >> Numeric — while a container that reshapes the value (a filtered map dropping entries, a record dropping undeclared keys) stays false. A stronger property than #idempotent? (value-preserving ⟹ idempotent).

Returns:

  • (Boolean)


351
# File 'lib/plumb/composable.rb', line 351

def value_preserving? = false

#where(attrs) ⇒ Object

Check attributes of an object against values, using #===

Examples:

type = Types::Array.where(size: 1..10)
type = Types::String.where(bytesize: 1..10)

Parameters:

  • attrs (Hash{Symbol, String => Object})

    attribute name => matcher



693
694
695
696
697
698
699
700
701
702
703
# File 'lib/plumb/composable.rb', line 693

def where(attrs)
  unless attrs.is_a?(::Hash) && !attrs.empty?
    raise ArgumentError,
          '#where expects a non-empty Hash of attribute => matcher ' \
          "(eg. `where(size: 1..10)`), got #{attrs.inspect}"
  end

  attrs.reduce(self) do |t, (name, value)|
    t >> AttributeValueMatch.new(t, name, value)
  end
end

#withObject

Deprecated.

User #where instead



706
707
708
709
# File 'lib/plumb/composable.rb', line 706

def with(...)
  warn 'Composable#with() is deprecated. Use #where() instead. #with is reserved to make copies of Data structs'
  where(...)
end

#|(other) ⇒ Composable

Chain two composable objects together as a disjunction ("or"). When one value-preserving branch subsumes the other (Integer | Numeric, or X | X), the union absorbs to the wider branch — see Optimizer.reduce_union. Functions/containers never reduce (they may accept inputs the survivor rejects), so coercion unions are preserved.

Parameters:

Returns:



442
443
444
445
446
447
# File 'lib/plumb/composable.rb', line 442

def |(other)
  other = Composable.resolve_operand(other, op: :|, left: self)
  return self if other.is_a?(NeverClass) # X | Never == X

  Plumb::Optimizer.rewrite_union(self, other)
end