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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +887 -64
  3. data/bench/compare_dry_schema.rb +79 -0
  4. data/bench/compare_dry_types.rb +37 -0
  5. data/bench/compare_parametric_schema.rb +2 -80
  6. data/bench/dry_schema_hash.rb +103 -0
  7. data/bench/dry_types_hash.rb +125 -0
  8. data/bench/json_schema_profile.rb +107 -0
  9. data/bench/plumb_hash.rb +17 -11
  10. data/bench/results_allocations.rb +137 -0
  11. data/bench/sample_data.rb +78 -0
  12. data/examples/command_objects.rb +1 -1
  13. data/examples/concurrent_downloads.rb +16 -9
  14. data/examples/event_registry.rb +6 -1
  15. data/examples/weekdays.rb +1 -1
  16. data/lib/plumb/and.rb +63 -6
  17. data/lib/plumb/any_class.rb +12 -2
  18. data/lib/plumb/array_class.rb +133 -25
  19. data/lib/plumb/attribute_value_match.rb +41 -1
  20. data/lib/plumb/attributes.rb +59 -19
  21. data/lib/plumb/codec.rb +886 -0
  22. data/lib/plumb/composable.rb +451 -39
  23. data/lib/plumb/conjunction.rb +50 -0
  24. data/lib/plumb/constraint.rb +234 -0
  25. data/lib/plumb/covariant_fusion.rb +46 -0
  26. data/lib/plumb/decorator.rb +12 -22
  27. data/lib/plumb/deferred.rb +13 -5
  28. data/lib/plumb/disjunction.rb +112 -0
  29. data/lib/plumb/encoder.rb +207 -0
  30. data/lib/plumb/function.rb +347 -0
  31. data/lib/plumb/hash_class.rb +339 -32
  32. data/lib/plumb/hash_map.rb +58 -14
  33. data/lib/plumb/implementation.rb +247 -0
  34. data/lib/plumb/interface_class.rb +21 -2
  35. data/lib/plumb/intersection.rb +47 -0
  36. data/lib/plumb/json_schema_visitor.rb +255 -36
  37. data/lib/plumb/key.rb +63 -13
  38. data/lib/plumb/mermaid_visitor.rb +129 -0
  39. data/lib/plumb/metadata.rb +10 -1
  40. data/lib/plumb/metadata_visitor.rb +36 -34
  41. data/lib/plumb/never_class.rb +38 -0
  42. data/lib/plumb/node_mapper.rb +97 -0
  43. data/lib/plumb/not.rb +34 -2
  44. data/lib/plumb/optimizer.rb +444 -0
  45. data/lib/plumb/or.rb +26 -29
  46. data/lib/plumb/pipeline.rb +99 -11
  47. data/lib/plumb/policy.rb +17 -4
  48. data/lib/plumb/range_class.rb +46 -0
  49. data/lib/plumb/relation.rb +57 -0
  50. data/lib/plumb/result.rb +55 -23
  51. data/lib/plumb/semantic_matcher.rb +393 -0
  52. data/lib/plumb/static_class.rb +20 -1
  53. data/lib/plumb/stream_class.rb +28 -6
  54. data/lib/plumb/subtyping.rb +461 -0
  55. data/lib/plumb/tagged_hash.rb +45 -4
  56. data/lib/plumb/tuple_class.rb +21 -4
  57. data/lib/plumb/type_cache.rb +41 -0
  58. data/lib/plumb/type_registry.rb +71 -0
  59. data/lib/plumb/typed_step.rb +67 -0
  60. data/lib/plumb/types.rb +44 -43
  61. data/lib/plumb/union.rb +30 -0
  62. data/lib/plumb/value_class.rb +20 -1
  63. data/lib/plumb/version.rb +1 -1
  64. data/lib/plumb/visitor_handlers.rb +20 -4
  65. data/lib/plumb.rb +90 -3
  66. metadata +30 -8
  67. data/lib/plumb/build.rb +0 -22
  68. data/lib/plumb/match_class.rb +0 -42
  69. data/lib/plumb/schema.rb +0 -195
  70. data/lib/plumb/step.rb +0 -27
  71. 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
@@ -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::Step, nil] A step object to add to the pipeline
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::Step interface
234
- # @param result [Plumb::Result::Valid]
235
- # @return [Plumb::Result::Valid, Plumb::Result::Invalid]
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
- instance = new(result.value.to_h)
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
- elsif node.is_a?(Plumb::Step)
281
- build_nested(name, node, &block)
282
- elsif node.is_a?(Class) && node <= Plumb::Attributes
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
- if node.is_a?(Class) && node <= Plumb::Attributes
321
- sub = Class.new(node)
322
- sub.instance_exec(&block)
323
- __set_nested_class__(name, sub)
324
- return Composable.wrap(sub)
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
- return node unless node.is_a?(Plumb::Step)
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)