Module: Plumb

Defined in:
lib/plumb.rb,
lib/plumb/or.rb,
lib/plumb/and.rb,
lib/plumb/key.rb,
lib/plumb/not.rb,
lib/plumb/codec.rb,
lib/plumb/types.rb,
lib/plumb/union.rb,
lib/plumb/policy.rb,
lib/plumb/result.rb,
lib/plumb/encoder.rb,
lib/plumb/version.rb,
lib/plumb/deferred.rb,
lib/plumb/function.rb,
lib/plumb/hash_map.rb,
lib/plumb/metadata.rb,
lib/plumb/pipeline.rb,
lib/plumb/policies.rb,
lib/plumb/relation.rb,
lib/plumb/any_class.rb,
lib/plumb/decorator.rb,
lib/plumb/optimizer.rb,
lib/plumb/subtyping.rb,
lib/plumb/attributes.rb,
lib/plumb/composable.rb,
lib/plumb/constraint.rb,
lib/plumb/hash_class.rb,
lib/plumb/type_cache.rb,
lib/plumb/typed_step.rb,
lib/plumb/array_class.rb,
lib/plumb/conjunction.rb,
lib/plumb/disjunction.rb,
lib/plumb/never_class.rb,
lib/plumb/node_mapper.rb,
lib/plumb/range_class.rb,
lib/plumb/tagged_hash.rb,
lib/plumb/tuple_class.rb,
lib/plumb/value_class.rb,
lib/plumb/intersection.rb,
lib/plumb/static_class.rb,
lib/plumb/stream_class.rb,
lib/plumb/type_registry.rb,
lib/plumb/implementation.rb,
lib/plumb/interface_class.rb,
lib/plumb/mermaid_visitor.rb,
lib/plumb/covariant_fusion.rb,
lib/plumb/metadata_visitor.rb,
lib/plumb/semantic_matcher.rb,
lib/plumb/visitor_handlers.rb,
lib/plumb/json_schema_visitor.rb,
lib/plumb/attribute_value_match.rb

Defined Under Namespace

Modules: Attributes, Callable, Composable, Conjunction, CovariantFusion, Disjunction, Equality, Implementation, Naming, NodeMapper, Optimizer, SplitPolicy, Subtyping, TypeCache, TypeRegistry, TypedStep, Types, VisitorHandlers Classes: And, AnyClass, ArrayClass, AttributeValueMatch, Codec, Constraint, Decorator, Deferred, Encoder, FilteredHash, Function, GuaranteedFunction, HashClass, HashMap, InterfaceClass, Intersection, JSONSchemaVisitor, Key, MermaidVisitor, Metadata, MetadataVisitor, NeverClass, Not, Or, Pipeline, Policies, Policy, RangeClass, Result, StaticClass, StreamClass, TaggedHash, TupleClass, UndefinedClass, Union, ValueClass

Constant Summary collapse

VERSION =
'0.2.0.beta.1'
ParseError =
Class.new(::TypeError)
TypeError =

Raised by Composable#>> when chaining two steps whose types are provably incompatible (the left's #output_type is not a subtype of the right's #input_type).

Class.new(::TypeError)
Undefined =
UndefinedClass.new.freeze
BLANK_STRING =
''
BLANK_ARRAY =
[].freeze
BLANK_HASH =
{}.freeze
NOOP =
->(result) { result }
COERCION_METHODS =

Ruby's explicit conversion methods and the type each produces. Lets #transform(:to_i) expand to a typed transform to Integer (using :to_i as the callable), instead of spelling out #transform(Integer, &:to_i).

{
  to_s: ::String,
  to_sym: ::Symbol,
  to_i: ::Integer,
  to_f: ::Float,
  to_r: ::Rational,
  to_c: ::Complex,
  to_a: ::Array,
  to_h: ::Hash,
  to_proc: ::Proc
}.freeze

Class Method Summary collapse

Class Method Details

.decorate(type, &block) ⇒ Object



57
58
59
# File 'lib/plumb.rb', line 57

def self.decorate(type, &block)
  Decorator.call(type, &block)
end

.policiesObject



8
9
10
# File 'lib/plumb.rb', line 8

def self.policies
  @policies
end

.policy(name, opts = {}) {|Step, Object, &block| ... } ⇒ Object

Register a policy with the given name and block. Optionally define a method on the Composable method to call the policy. Example:

Plumb.policy(:multiply_by, for_type: Integer, helper: true) do |step, factor, &block|
step.transform(Integer) { |number| number * factor }
end

type = Types::Integer.multiply_by(2) type.parse(10) # => 20

Parameters:

  • name (Symbol)

    the name of the policy

  • opts (Hash) (defaults to: {})

    options for the policy

Yields:

  • (Step, Object, &block)

    the step (type), policy argument, and policy block, if any.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/plumb.rb', line 25

def self.policy(name, opts = {}, &block)
  name = name.to_sym
  if opts.is_a?(Hash) && block_given?
    for_type = opts[:for_type] || Object
    helper = opts[:helper] || false
  elsif opts.respond_to?(:call) && opts.respond_to?(:for_type) && opts.respond_to?(:helper)
    for_type = opts.for_type
    helper = opts.helper
    block = opts.method(:call)
  else
    raise ArgumentError, 'Expected a block or a hash with :for_type and :helper keys'
  end

  policies.register(for_type, name, block)

  return self unless helper

  if Composable.instance_methods.include?(name)
    raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Composable"
  end

  Composable.define_method(name) do |arg = Undefined, &bl|
    if arg == Undefined
      policy(name, &bl)
    else
      policy(name, arg, &bl)
    end
  end

  self
end

.resolve_base_types(node) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/plumb.rb', line 75

def self.resolve_base_types(node)
  return [node] if node.is_a?(::Class)
  return [] unless node.respond_to?(:node_name)

  # A transparent wrapper (Policy / Metadata / #as_node Node) only RE-LABELS the type
  # it wraps, so it has the same base types. Peeled here rather than per branch: a
  # Node's node_name is whatever #as_node was given, so it cannot be a `when` at all,
  # and every #as_node type would report NO base types — which callers read as
  # "unknown base, allow", silently skipping build-time checks like
  # `Types::Email[1..20]`.
  unwrapped = Plumb::Subtyping.unwrap_transparent(node)
  return resolve_base_types(unwrapped) unless unwrapped.equal?(node)

  case node.node_name
  when :or, :union
    node.children.flat_map { |child| resolve_base_types(child) }
  when :function
    resolve_base_types(node.output_type)
  when :intersection
    # A meet narrows a single value, so its base types are its LEFT's —
    # `String.where(size: 1..3)` is still a String.
    resolve_base_types(node.children[0])
  when :and
    # Descend into whichever side carries the resulting type: a value-preserving right
    # NARROWS what the left produces, a converting right REPLACES it.
    #
    # Deliberately NOT `node.output_type`, which encodes the same rule but is not
    # guaranteed to be a smaller node — for `Array[<record with a coercing field>]
    # .where(size: 1..)` it is a DIFFERENT And whose own output type is itself, so
    # following it ping-pongs until the stack blows. A child always terminates.
    left, right = node.children
    resolve_base_types(Plumb::Subtyping.value_preserving?(right) ? left : right)
  when :constraint
    # A refinement matcher carries its base type — resolve that (eg.
    # `Integer[1..10]` => [Integer], `User.check {}` => the User's base types).
    return resolve_base_types(node.base) if node.base

    SemanticMatcher.matcher_domain(node.children.first)
  when :array, :tuple then [::Array]
  when :hash, :hash_map, :tagged_hash, :filtered_hash, :filtered_hash_map then [::Hash]
  when :stream then [::Enumerator]
  when :range then [::Range]
  when :not
    # A complement has no finite base-class representation.
    []
  when :value
    # Literals match by ==, including across numeric classes.
    value = node.children.first
    value.equal?(Undefined) ? [] : [value_base_class(value)]
  when :static
    value = node.children.first
    [value.is_a?(::Class) ? value : value_base_class(value)]
  else
    node.respond_to?(:children) ? node.children.flat_map { |child| resolve_base_types(child) } : []
  end
end

.value_base_class(value) ⇒ Class

Recursively resolve the underlying Ruby classes ("base types") of a node. Types::String => [String] Types::String | Integer => [String, Integer]

This is a temporary helper to preserve type-specific policy resolution (see Composable#policy) until proper subtyping checks are implemented. Returns the value's matching domain, widened to Numeric because numeric equality crosses concrete classes (5 == 5.0). Keeping a concrete numeric class would falsely make compatible literal and numeric types appear disjoint and reduce their intersection to Never.

Parameters:

  • value (Object)

Returns:

  • (Class)


73
# File 'lib/plumb.rb', line 73

def self.value_base_class(value) = SemanticMatcher.value_domain(value)