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::Valid) => Result::Valid | Result::Invalid`

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.



116
117
118
119
# File 'lib/plumb/composable.rb', line 116

def self.included(base)
  base.send(:include, Naming)
  base.send(:include, Equality)
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) # => Result::Valid
ten.resolve(11) # => Result::Invalid

Parameters:

  • callable (Object)

Returns:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/plumb/composable.rb', line 135

def self.wrap(callable)
  if callable.is_a?(Composable)
    callable
  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)
    Step.new(callable)
  else
    MatchClass.new(callable)
  end
end

Instance Method Details

#>>(other) ⇒ And

Chain two composable objects together. A.K.A “and” or “sequence”

Examples:

Step1 >> Step2 >> Step3

Parameters:

Returns:



175
176
177
# File 'lib/plumb/composable.rb', line 175

def >>(other)
  And.new(self, Composable.wrap(other))
end

#[](val) ⇒ Object



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

def [](val) = match(val)

#as_node(node_name, metadata = 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)
  • metadata (Hash) (defaults to: BLANK_HASH)

Returns:



296
297
298
# File 'lib/plumb/composable.rb', line 296

def as_node(node_name,  = BLANK_HASH)
  Node.new(node_name, self, )
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:



345
346
347
# File 'lib/plumb/composable.rb', line 345

def build(cns, factory_method = :new, &block)
  self >> Build.new(cns, factory_method:, &block)
end

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

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:



206
207
208
# File 'lib/plumb/composable.rb', line 206

def check(errors = 'did not pass the check', &block)
  self >> MatchClass.new(block, error: errors, label: errors)
end

#childrenArray<Composable>

Visitors expect a #node_name and #children interface.

Returns:



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

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 }
]


164
165
166
# File 'lib/plumb/composable.rb', line 164

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

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


376
377
378
379
380
381
# File 'lib/plumb/composable.rb', line 376

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

  Step.new(->(r) { r.valid(generator.call) }, 'generator') >> self
end

#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:



241
242
243
# File 'lib/plumb/composable.rb', line 241

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

#invoke(*args, &block) ⇒ Step

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:



411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/plumb/composable.rb', line 411

def invoke(*args, &block)
  case args
  in [::Symbol => method_name, *rest]
    self >> Step.new(
      ->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
      [method_name.inspect, rest.inspect].join(' ')
    )
  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) ⇒ And

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

Examples:

email = Types::String['@']

Parameters:

  • args (Array<Object>)

Returns:



266
267
268
# File 'lib/plumb/composable.rb', line 266

def match(*args)
  self >> MatchClass.new(*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:



217
218
219
220
221
222
223
# File 'lib/plumb/composable.rb', line 217

def (data = Undefined)
  if data == Undefined
    MetadataVisitor.call(self)
  else
    self >> Metadata.new(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:



233
234
235
# File 'lib/plumb/composable.rb', line 233

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

#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:



392
393
394
# File 'lib/plumb/composable.rb', line 392

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

#policy(*args, &blk) ⇒ Step

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:



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/plumb/composable.rb', line 306

def policy(*args, &blk)
  case args
  in [::Symbol => name, *rest] # #policy(:name, arg)
    types = Array([: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:



358
359
360
361
362
363
364
365
366
# File 'lib/plumb/composable.rb', line 358

def static(value)
  my_type = Array([:type]).first
  unless my_type.nil? || value.instance_of?(my_type)
    raise ArgumentError,
          "can't set a static #{value.class} value for a #{my_type} step"
  end

  StaticClass.new(value) >> self
end

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


402
403
404
# File 'lib/plumb/composable.rb', line 402

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

#to_sObject



396
397
398
# File 'lib/plumb/composable.rb', line 396

def to_s
  inspect
end

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

Transform value. Requires specifying the resulting type of the value after transformation.

Examples:

Types::String.transform(Types::Symbol, &:to_sym)

Parameters:

  • target_type (Class)

    what type this step will transform the value to

  • 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:



195
196
197
# File 'lib/plumb/composable.rb', line 195

def transform(target_type, callable = nil, &block)
  self >> Transform.new(target_type, callable || block)
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)


255
256
257
# File 'lib/plumb/composable.rb', line 255

def value(val)
  self >> ValueClass.new(val)
end

#|(other) ⇒ Or

Chain two composable objects together as a disjunction (“or”).

Parameters:

Returns:



183
184
185
# File 'lib/plumb/composable.rb', line 183

def |(other)
  Or.new(self, Composable.wrap(other))
end