plumb 0.0.18 → 0.2.0.beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +887 -64
- data/bench/compare_dry_schema.rb +79 -0
- data/bench/compare_dry_types.rb +37 -0
- data/bench/compare_parametric_schema.rb +2 -80
- data/bench/dry_schema_hash.rb +103 -0
- data/bench/dry_types_hash.rb +125 -0
- data/bench/json_schema_profile.rb +107 -0
- data/bench/plumb_hash.rb +17 -11
- data/bench/results_allocations.rb +137 -0
- data/bench/sample_data.rb +78 -0
- data/examples/command_objects.rb +1 -1
- data/examples/concurrent_downloads.rb +16 -9
- data/examples/event_registry.rb +6 -1
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/and.rb +63 -6
- data/lib/plumb/any_class.rb +12 -2
- data/lib/plumb/array_class.rb +133 -25
- data/lib/plumb/attribute_value_match.rb +41 -1
- data/lib/plumb/attributes.rb +59 -19
- data/lib/plumb/codec.rb +886 -0
- data/lib/plumb/composable.rb +451 -39
- data/lib/plumb/conjunction.rb +50 -0
- data/lib/plumb/constraint.rb +234 -0
- data/lib/plumb/covariant_fusion.rb +46 -0
- data/lib/plumb/decorator.rb +12 -22
- data/lib/plumb/deferred.rb +13 -5
- data/lib/plumb/disjunction.rb +112 -0
- data/lib/plumb/encoder.rb +207 -0
- data/lib/plumb/function.rb +347 -0
- data/lib/plumb/hash_class.rb +339 -32
- data/lib/plumb/hash_map.rb +58 -14
- data/lib/plumb/implementation.rb +247 -0
- data/lib/plumb/interface_class.rb +21 -2
- data/lib/plumb/intersection.rb +47 -0
- data/lib/plumb/json_schema_visitor.rb +255 -36
- data/lib/plumb/key.rb +63 -13
- data/lib/plumb/mermaid_visitor.rb +129 -0
- data/lib/plumb/metadata.rb +10 -1
- data/lib/plumb/metadata_visitor.rb +36 -34
- data/lib/plumb/never_class.rb +38 -0
- data/lib/plumb/node_mapper.rb +97 -0
- data/lib/plumb/not.rb +34 -2
- data/lib/plumb/optimizer.rb +444 -0
- data/lib/plumb/or.rb +26 -29
- data/lib/plumb/pipeline.rb +99 -11
- data/lib/plumb/policy.rb +17 -4
- data/lib/plumb/range_class.rb +46 -0
- data/lib/plumb/relation.rb +57 -0
- data/lib/plumb/result.rb +55 -23
- data/lib/plumb/semantic_matcher.rb +393 -0
- data/lib/plumb/static_class.rb +20 -1
- data/lib/plumb/stream_class.rb +28 -6
- data/lib/plumb/subtyping.rb +461 -0
- data/lib/plumb/tagged_hash.rb +45 -4
- data/lib/plumb/tuple_class.rb +21 -4
- data/lib/plumb/type_cache.rb +41 -0
- data/lib/plumb/type_registry.rb +71 -0
- data/lib/plumb/typed_step.rb +67 -0
- data/lib/plumb/types.rb +44 -43
- data/lib/plumb/union.rb +30 -0
- data/lib/plumb/value_class.rb +20 -1
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +20 -4
- data/lib/plumb.rb +90 -3
- metadata +30 -8
- data/lib/plumb/build.rb +0 -22
- data/lib/plumb/match_class.rb +0 -42
- data/lib/plumb/schema.rb +0 -195
- data/lib/plumb/step.rb +0 -27
- data/lib/plumb/transform.rb +0 -26
|
@@ -7,6 +7,10 @@ module Plumb
|
|
|
7
7
|
attr_reader :type, :attr_name, :value
|
|
8
8
|
|
|
9
9
|
def initialize(type, attr_name, value)
|
|
10
|
+
unless attr_name.is_a?(::Symbol) || attr_name.is_a?(::String)
|
|
11
|
+
raise ArgumentError, "attribute name must be a Symbol or String, got #{attr_name.inspect}"
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
@type = type
|
|
11
15
|
@attr_name = attr_name
|
|
12
16
|
@value = value
|
|
@@ -15,12 +19,48 @@ module Plumb
|
|
|
15
19
|
freeze
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# Two attribute constraints are equal when they constrain the same attribute
|
|
23
|
+
# of the same base type to the same value. (The default Composable#== only
|
|
24
|
+
# compares #children, which an AttributeValueMatch doesn't expose, so it
|
|
25
|
+
# would treat every constraint as equal.)
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.is_a?(self.class) &&
|
|
28
|
+
attr_name == other.attr_name &&
|
|
29
|
+
value == other.value &&
|
|
30
|
+
type == other.type
|
|
31
|
+
end
|
|
32
|
+
|
|
18
33
|
def metadata = type.metadata
|
|
19
34
|
|
|
35
|
+
# An attribute-value constraint narrows by value, so it accepts any input
|
|
36
|
+
# (opts out of #>> composition type-checks).
|
|
37
|
+
def input_type = Types::Any
|
|
38
|
+
|
|
39
|
+
# It checks an attribute and returns the value unchanged — a pure refinement.
|
|
40
|
+
def value_preserving? = true
|
|
41
|
+
|
|
42
|
+
# Subtyping for an attribute constraint. Against another constraint on the
|
|
43
|
+
# SAME attribute, the base types must be compatible and this constraint's
|
|
44
|
+
# value must be a subtype of the other's — eg. `size: 10` <= `size: 8..100`,
|
|
45
|
+
# but `size: 10..15` </= `size: 11..14`. Against any other type, an
|
|
46
|
+
# attribute-constrained type is simply a subtype of its base type (eg.
|
|
47
|
+
# `Array.where(size: 10) <= Array`).
|
|
48
|
+
def subtype_of?(other)
|
|
49
|
+
if other.is_a?(AttributeValueMatch)
|
|
50
|
+
attr_name == other.attr_name &&
|
|
51
|
+
Plumb::Subtyping.subtype?(type, other.type) &&
|
|
52
|
+
Plumb::Subtyping.value_subtype?(value, other.value)
|
|
53
|
+
else
|
|
54
|
+
Plumb::Subtyping.subtype?(type, other)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
20
58
|
def call(result)
|
|
59
|
+
# Missing attributes fail the constraint instead of raising.
|
|
60
|
+
return result.invalid!(errors: @error) unless result.value.respond_to?(attr_name)
|
|
21
61
|
return result if value === result.value.public_send(attr_name)
|
|
22
62
|
|
|
23
|
-
result.invalid(errors: @error)
|
|
63
|
+
result.invalid!(errors: @error)
|
|
24
64
|
end
|
|
25
65
|
|
|
26
66
|
private def _inspect = @inspect_line
|
data/lib/plumb/attributes.rb
CHANGED
|
@@ -113,6 +113,25 @@ module Plumb
|
|
|
113
113
|
base.define_singleton_method(:__plumb_struct_class__) { base }
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# The struct class behind `node`, or nil. Structs appear in two shapes: a
|
|
117
|
+
# Plumb::Attributes class itself (Types::Data subclasses are Composable
|
|
118
|
+
# classes), or the Function that Composable.wrap builds around a plain
|
|
119
|
+
# `include Plumb::Attributes` class — the class is the wrapped callable, so
|
|
120
|
+
# it is reached via #fn (a Function's #children are its types). This is the
|
|
121
|
+
# one owner of that representation fact — used by #build_nested and Codec's
|
|
122
|
+
# rewriter.
|
|
123
|
+
#
|
|
124
|
+
# Keyed on Function#wraps_callable? rather than on the types the node declares:
|
|
125
|
+
# boundary absorption can move a neighbouring type into a wrapper's slot (see
|
|
126
|
+
# Function#absorb_input), and it wraps the same class either way.
|
|
127
|
+
def self.struct_class(node)
|
|
128
|
+
return node if node.is_a?(::Class) && node <= Attributes
|
|
129
|
+
return nil unless node.is_a?(Plumb::Function) && node.wraps_callable?
|
|
130
|
+
|
|
131
|
+
callable = node.fn
|
|
132
|
+
callable.is_a?(::Class) && callable <= Attributes ? callable : nil
|
|
133
|
+
end
|
|
134
|
+
|
|
116
135
|
attr_reader :errors, :attributes
|
|
117
136
|
|
|
118
137
|
def initialize(attrs = {})
|
|
@@ -186,7 +205,7 @@ module Plumb
|
|
|
186
205
|
# Add a step to the processing pipeline that runs before attribute validation.
|
|
187
206
|
# This allows you to transform or validate the input data before it's assigned to attributes.
|
|
188
207
|
#
|
|
189
|
-
# @param st [Plumb::
|
|
208
|
+
# @param st [Plumb::Composable, #call, nil] A step object to add to the pipeline
|
|
190
209
|
# @param block [Proc, nil] A block to use as a step (if st is nil)
|
|
191
210
|
# @return [Class] Returns self for method chaining
|
|
192
211
|
#
|
|
@@ -230,14 +249,21 @@ module Plumb
|
|
|
230
249
|
|
|
231
250
|
MUST_BE_HASH = ['Must be a Hash of attributes'].freeze
|
|
232
251
|
|
|
233
|
-
# The Plumb::
|
|
234
|
-
# @param result [Plumb::Result
|
|
235
|
-
# @return [Plumb::Result
|
|
252
|
+
# The Plumb::Callable interface
|
|
253
|
+
# @param result [Plumb::Result]
|
|
254
|
+
# @return [Plumb::Result]
|
|
236
255
|
def call(result)
|
|
237
256
|
return result if result.value.is_a?(self)
|
|
238
257
|
return result.invalid(errors: MUST_BE_HASH) unless result.value.respond_to?(:to_h)
|
|
239
258
|
|
|
240
|
-
|
|
259
|
+
# Some #to_h implementations reject malformed contents; treat that as invalid input.
|
|
260
|
+
begin
|
|
261
|
+
attributes = result.value.to_h
|
|
262
|
+
rescue ::TypeError, ::ArgumentError
|
|
263
|
+
return result.invalid(errors: MUST_BE_HASH)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
instance = new(attributes)
|
|
241
267
|
instance.valid? ? result.valid(instance) : result.invalid(instance, errors: instance.errors.to_h)
|
|
242
268
|
end
|
|
243
269
|
|
|
@@ -254,6 +280,22 @@ module Plumb
|
|
|
254
280
|
# node name for visitors
|
|
255
281
|
def node_name = :data
|
|
256
282
|
|
|
283
|
+
# A Data type joins Composable via `extend`, so it doesn't pick up the
|
|
284
|
+
# Equality hooks the subtype engine calls. Structural subtyping delegates
|
|
285
|
+
# to the underlying schema (a HashClass): a Data type is a subtype of
|
|
286
|
+
# `other` exactly when its schema is. `other` may be another Data type
|
|
287
|
+
# (compared schema-to-schema), a HashClass, or any Plumb type. Recurse via
|
|
288
|
+
# Plumb::Subtyping.subtype?, never #<= (which on a Class is Ruby's own
|
|
289
|
+
# class-hierarchy operator).
|
|
290
|
+
def subtype_of?(other)
|
|
291
|
+
other = other._schema if other.respond_to?(:node_name) && other.node_name == :data
|
|
292
|
+
Plumb::Subtyping.subtype?(_schema, other)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Mirror hook (see Composable#supertype_of?). A Data type claims a subtype
|
|
296
|
+
# only where its schema does — which by default is nothing.
|
|
297
|
+
def supertype_of?(other) = _schema.supertype_of?(other)
|
|
298
|
+
|
|
257
299
|
# attribute(:friend) { attribute(:name, String) }
|
|
258
300
|
# attribute(:friend, MyStruct) { attribute(:name, String) }
|
|
259
301
|
# attribute(:name, String)
|
|
@@ -277,9 +319,12 @@ module Plumb
|
|
|
277
319
|
child = node.children.first
|
|
278
320
|
child = __plumb_struct_class__ if child == Types::Any
|
|
279
321
|
Types::Array[build_nested(name, child, &block)]
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
322
|
+
# A wrapper function holds a caller-supplied callable — possibly a struct
|
|
323
|
+
# class, possibly not; #build_nested raises if it isn't. Deliberately NOT
|
|
324
|
+
# every Function: a #transform / #build / coercion wraps a lambda Plumb
|
|
325
|
+
# built, which a nested-attributes block has no business rewriting.
|
|
326
|
+
elsif (node.is_a?(Plumb::Function) && node.wraps_callable?) ||
|
|
327
|
+
(node.is_a?(Class) && node <= Plumb::Attributes)
|
|
283
328
|
build_nested(name, node, &block)
|
|
284
329
|
else
|
|
285
330
|
node
|
|
@@ -317,19 +362,14 @@ module Plumb
|
|
|
317
362
|
end
|
|
318
363
|
|
|
319
364
|
def build_nested(name, node, &block)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
365
|
+
klass = Plumb::Attributes.struct_class(node)
|
|
366
|
+
unless klass
|
|
367
|
+
raise ArgumentError,
|
|
368
|
+
"attribute #{name.inspect} was given a nested-attributes block, " \
|
|
369
|
+
"but its type #{node.inspect} is not a struct class"
|
|
325
370
|
end
|
|
326
371
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
child = node.children.first
|
|
330
|
-
return node unless child <= Plumb::Attributes
|
|
331
|
-
|
|
332
|
-
sub = Class.new(child)
|
|
372
|
+
sub = Class.new(klass)
|
|
333
373
|
sub.instance_exec(&block)
|
|
334
374
|
__set_nested_class__(name, sub)
|
|
335
375
|
Composable.wrap(sub)
|