plumb 0.2.0.beta.1 → 0.2.0.beta.3
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 +160 -93
- data/lib/plumb/array_class.rb +72 -9
- data/lib/plumb/codec.rb +275 -140
- data/lib/plumb/composable.rb +2 -2
- data/lib/plumb/hash_class.rb +5 -4
- data/lib/plumb/hash_map.rb +1 -5
- data/lib/plumb/stream_class.rb +1 -5
- data/lib/plumb/types.rb +21 -4
- data/lib/plumb/version.rb +1 -1
- metadata +1 -1
data/README.md
CHANGED
|
@@ -303,7 +303,7 @@ Types::Never.to_json_schema # => { "not" => {} }
|
|
|
303
303
|
|
|
304
304
|
You rarely write `Types::Never` by hand — it's what an impossible intersection reduces to, which lets the composition algebra prove and discard dead branches (as in `Integer | (String & Integer)` above). It's also useful as a Hash catch-all to forbid undeclared keys — see [`_: Types::Never`](#undeclared-keys-and-the-_-catch-all).
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
## Built-in types
|
|
307
307
|
|
|
308
308
|
* `Types::Value`
|
|
309
309
|
* `Types::Array`
|
|
@@ -338,11 +338,11 @@ For parsing stringy formats (HTML forms, query strings) into these types — wha
|
|
|
338
338
|
|
|
339
339
|
TODO: datetime, others.
|
|
340
340
|
|
|
341
|
-
|
|
341
|
+
## Policies
|
|
342
342
|
|
|
343
343
|
Policies are helpers that encapsulate common compositions. Plumb ships with some handy ones, listed below, and you can also define your own.
|
|
344
344
|
|
|
345
|
-
|
|
345
|
+
### `#present`
|
|
346
346
|
|
|
347
347
|
Checks that the value is not blank (`""` if string, `[]` if array, `{}` if Hash, or `nil`)
|
|
348
348
|
|
|
@@ -351,7 +351,7 @@ Types::String.present.resolve('') # Failure with errors
|
|
|
351
351
|
Types::Array[Types::String].present.resolve([]) # Failure with errors
|
|
352
352
|
```
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
### `#nullable`
|
|
355
355
|
|
|
356
356
|
Allow `nil` values.
|
|
357
357
|
|
|
@@ -368,7 +368,7 @@ Note that this just encapsulates the following composition:
|
|
|
368
368
|
nullable_str = Types::String | Types::Nil
|
|
369
369
|
```
|
|
370
370
|
|
|
371
|
-
|
|
371
|
+
### `#not`
|
|
372
372
|
|
|
373
373
|
Negates a type.
|
|
374
374
|
```ruby
|
|
@@ -394,7 +394,7 @@ NotNil.parse('hello') # 'hello'
|
|
|
394
394
|
NotNil.parse(nil) # error
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
|
|
397
|
+
### `#options`
|
|
398
398
|
|
|
399
399
|
Sets allowed options for value.
|
|
400
400
|
|
|
@@ -442,7 +442,7 @@ Types::Array.where(size: 10) >> Types::Array.where(size: 8..100) # ok: 10 is wit
|
|
|
442
442
|
Types::Array.where(size: 10..15) >> Types::Array.where(size: 11..14) # raises: 10..15 isn't within 11..14
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
### `#transform`
|
|
446
446
|
|
|
447
447
|
Transform value. Requires specifying the resulting type of the value after transformation.
|
|
448
448
|
|
|
@@ -472,7 +472,7 @@ Types::Any.transform(:to_i) # ok — unknown input type, no check
|
|
|
472
472
|
|
|
473
473
|
`#transform` builds a [`Plumb::Function`](#plumbfunctioninput--output) — the underlying typed-conversion node — with a block that takes and returns a plain value. To build one standalone from a callable, or to work at the `Result` level, use `Plumb::Function[]` directly.
|
|
474
474
|
|
|
475
|
-
|
|
475
|
+
### `#invoke`
|
|
476
476
|
|
|
477
477
|
`#invoke` builds a step that will invoke one or more methods on the value.
|
|
478
478
|
|
|
@@ -509,7 +509,7 @@ type.parse([1, 2]) # raises NoMethodError because Array doesn't respond to #stri
|
|
|
509
509
|
|
|
510
510
|
Use with caution.
|
|
511
511
|
|
|
512
|
-
|
|
512
|
+
### `#default`
|
|
513
513
|
|
|
514
514
|
Default value when no value given (ie. when key is missing in Hash payloads. See `Types::Hash` below).
|
|
515
515
|
|
|
@@ -519,6 +519,27 @@ str.parse() # 'nope'
|
|
|
519
519
|
str.parse('yup') # 'yup'
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
+
A block generates the value on every invocation, instead of returning a fixed one:
|
|
523
|
+
|
|
524
|
+
```ruby
|
|
525
|
+
id = Types::UUID::V4.default { SecureRandom.uuid }
|
|
526
|
+
id.parse() # a fresh UUID each time
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
The step _declares_ the type it defaults — so a `Types::Date.default { Date.today }`
|
|
530
|
+
is still a `Date` for subtyping, JSON Schema and [Codecs](#encoders-and-codecs) — and
|
|
531
|
+
what the block returns is checked against it, failing where it is defaulted rather
|
|
532
|
+
than somewhere downstream.
|
|
533
|
+
|
|
534
|
+
What is checked is the type's **output**, and the type itself is not re-run on the
|
|
535
|
+
generated value: a converting type expects the block to produce the converted value.
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
int = Types::String.transform(::Integer, &:to_i)
|
|
539
|
+
int.default { 10 }.parse() # 10
|
|
540
|
+
int.default { '10' }.parse() # raises — a String is not what this type produces
|
|
541
|
+
```
|
|
542
|
+
|
|
522
543
|
Note that this is syntax sugar for:
|
|
523
544
|
|
|
524
545
|
```ruby
|
|
@@ -543,7 +564,7 @@ Same if you want to apply a default to several cases.
|
|
|
543
564
|
str = Types::String | ((Types::Nil | Types::Undefined) >> Types::Static['nope'.freeze])
|
|
544
565
|
```
|
|
545
566
|
|
|
546
|
-
|
|
567
|
+
### `#build`
|
|
547
568
|
|
|
548
569
|
Build a custom object or class.
|
|
549
570
|
|
|
@@ -576,7 +597,7 @@ Note that this case is identical to `#transform` with a block.
|
|
|
576
597
|
StringToMoney = Types::String.transform(Money) { |value| Monetize.parse(value) }
|
|
577
598
|
```
|
|
578
599
|
|
|
579
|
-
|
|
600
|
+
### `#check`
|
|
580
601
|
|
|
581
602
|
Pass the value through an arbitrary validation
|
|
582
603
|
|
|
@@ -586,7 +607,7 @@ type.parse('Role: Manager') # 'Role: Manager'
|
|
|
586
607
|
type.parse('Manager') # fails
|
|
587
608
|
```
|
|
588
609
|
|
|
589
|
-
|
|
610
|
+
### `#value`
|
|
590
611
|
|
|
591
612
|
Constrain a type to a specific value. Compares with `#==`
|
|
592
613
|
|
|
@@ -603,7 +624,7 @@ All scalar types support this:
|
|
|
603
624
|
ten = Types::Integer.value(10)
|
|
604
625
|
```
|
|
605
626
|
|
|
606
|
-
|
|
627
|
+
### `#static`
|
|
607
628
|
|
|
608
629
|
A type that always returns a valid, static value, regardless of input.
|
|
609
630
|
|
|
@@ -642,7 +663,7 @@ type = Types::Integer[100..].static(150) # ok
|
|
|
642
663
|
|
|
643
664
|
So, normally you'd only use this attached to primitive types without further processing (but your use case may vary).
|
|
644
665
|
|
|
645
|
-
|
|
666
|
+
### `#generate`
|
|
646
667
|
|
|
647
668
|
Passing a proc will evaluate the proc on every invocation. Use this for generated values.
|
|
648
669
|
|
|
@@ -661,7 +682,7 @@ random_number.parse # raises Plumb::ParseError because `rand` is not a String
|
|
|
661
682
|
|
|
662
683
|
You can also pass any `#call() => Object` interface as a generator, instead of a proc.
|
|
663
684
|
|
|
664
|
-
|
|
685
|
+
### `#metadata`
|
|
665
686
|
|
|
666
687
|
Add metadata to a type
|
|
667
688
|
|
|
@@ -684,7 +705,7 @@ type.metadata[:note] # 'An email address'
|
|
|
684
705
|
|
|
685
706
|
TODO: document custom visitors.
|
|
686
707
|
|
|
687
|
-
|
|
708
|
+
### `#input_type` and `#output_type`
|
|
688
709
|
|
|
689
710
|
Every type exposes the type it expects as input and the type it produces as output.
|
|
690
711
|
|
|
@@ -712,7 +733,7 @@ For a plain type, both are the type itself. Unions distribute over both sides:
|
|
|
712
733
|
|
|
713
734
|
These power type introspection — for example, the JSON Schema visitor builds its schema from `#input_type`, since a schema describes the values a caller must send.
|
|
714
735
|
|
|
715
|
-
|
|
736
|
+
### Composition type-checks
|
|
716
737
|
|
|
717
738
|
`#>>` is typed by **subsumption**, like function composition in a statically-typed language: everything the left step *produces* must be acceptable to the right step — i.e. the left's output type must be a **subtype** of the right's input type. If not, `#>>` raises `Plumb::TypeError` at build time, so broken data pipelines fail loudly when you define them, not silently at runtime.
|
|
718
739
|
|
|
@@ -763,7 +784,7 @@ Types::Array.where(size: 10) >> Types::Array.where(size: 8..100) # ok: 10 is
|
|
|
763
784
|
Types::Array.where(size: 10..15) >> Types::Array.where(size: 11..14) # raises: 10..15 isn't within 11..14
|
|
764
785
|
```
|
|
765
786
|
|
|
766
|
-
|
|
787
|
+
### Subtype checks: `#<=` and `Plumb::Subtyping`
|
|
767
788
|
|
|
768
789
|
The relation behind the composition check is also available directly. `a <= b` asks "is every value described by `a` also described by `b`?" — i.e. is `a` a subtype/subset of `b`? — with `>=`, `<` and `>` derived from it. `Plumb::Subtyping.subtype?(a, b)` is the same check as a method. Built-in and custom types both participate, and raw Ruby classes/values are accepted on either side (they're normalized):
|
|
769
790
|
|
|
@@ -779,11 +800,11 @@ small = Types::Hash[name: Types::String]
|
|
|
779
800
|
big <= small # true (width + depth subtyping)
|
|
780
801
|
```
|
|
781
802
|
|
|
782
|
-
|
|
803
|
+
## Other policies
|
|
783
804
|
|
|
784
805
|
There's some other built-in "policies" that can be used via the `#policy` method. Helpers such as `#default` and `#present` are shortcuts for this and can also be used via `#policy(default: 'Hello')` or `#policy(:present)` See [custom policies](#custom-policies) for how to define your own policies.
|
|
785
806
|
|
|
786
|
-
|
|
807
|
+
### `:respond_to`
|
|
787
808
|
|
|
788
809
|
Similar to `Types::Interface`, this is a quick way to assert that a value supports one or more methods.
|
|
789
810
|
|
|
@@ -793,7 +814,7 @@ List = Types::Any.policy(respond_to: :each)
|
|
|
793
814
|
List = Types::Any.policy(respond_to: [:each, :[], :size)
|
|
794
815
|
```
|
|
795
816
|
|
|
796
|
-
|
|
817
|
+
### `:excluded_from`
|
|
797
818
|
|
|
798
819
|
The opposite of `#options`, this policy validates that the value _is not_ included in a list.
|
|
799
820
|
|
|
@@ -801,7 +822,7 @@ The opposite of `#options`, this policy validates that the value _is not_ includ
|
|
|
801
822
|
Name = Types::String.policy(excluded_from: ['Joe', 'Joan'])
|
|
802
823
|
```
|
|
803
824
|
|
|
804
|
-
|
|
825
|
+
### :split` (strings only)
|
|
805
826
|
|
|
806
827
|
Splits string values by a separator (default: `,`).
|
|
807
828
|
|
|
@@ -814,7 +835,7 @@ CSVLine = Types::String.split(/\s*;\s*/)
|
|
|
814
835
|
CSVLine.parse('a;b;c') # => ['a', 'b', 'c']
|
|
815
836
|
```
|
|
816
837
|
|
|
817
|
-
|
|
838
|
+
### `:rescue`
|
|
818
839
|
|
|
819
840
|
Wraps a step's execution, rescues a specific exception and returns an invalid result.
|
|
820
841
|
|
|
@@ -835,7 +856,10 @@ type.resolve('2024-02-02') # => Result::Valid with Date object
|
|
|
835
856
|
type.resolve('2024-') # => Result::Invalid with error message
|
|
836
857
|
```
|
|
837
858
|
|
|
838
|
-
|
|
859
|
+
The guard keeps the type it wraps: the example above is still a `Date` for subtyping,
|
|
860
|
+
JSON Schema and [Codecs](#encoders-and-codecs).
|
|
861
|
+
|
|
862
|
+
## `Types::Interface`
|
|
839
863
|
|
|
840
864
|
Use this for objects that must respond to one or more methods.
|
|
841
865
|
|
|
@@ -870,7 +894,7 @@ case args
|
|
|
870
894
|
end
|
|
871
895
|
```
|
|
872
896
|
|
|
873
|
-
|
|
897
|
+
### Merging interfaces
|
|
874
898
|
|
|
875
899
|
Use the `+` operator to merge two interfaces into a new one that must support both sets of method names.
|
|
876
900
|
|
|
@@ -881,7 +905,7 @@ Countable = Types::Interface[:size]
|
|
|
881
905
|
CountableIterable = Iterable + Countable
|
|
882
906
|
```
|
|
883
907
|
|
|
884
|
-
|
|
908
|
+
### Intersecting interfaces
|
|
885
909
|
|
|
886
910
|
Use the `&` operator to produce a new interface with the intersection of method names
|
|
887
911
|
|
|
@@ -895,7 +919,7 @@ I3 = Types::Interface[:b, :c]
|
|
|
895
919
|
|
|
896
920
|
TODO: make this a bit more advanced. Check for method arity.
|
|
897
921
|
|
|
898
|
-
|
|
922
|
+
## `Types::Hash`
|
|
899
923
|
|
|
900
924
|
```ruby
|
|
901
925
|
Employee = Types::Hash[
|
|
@@ -964,7 +988,7 @@ User = Types::Hash[name: Types::Static['Joe'], age: Integer]
|
|
|
964
988
|
User.parse(name: 'Rufus', age: 34) # Valid {name: 'Joe', age: 34}
|
|
965
989
|
```
|
|
966
990
|
|
|
967
|
-
|
|
991
|
+
### Optional keys
|
|
968
992
|
|
|
969
993
|
Keys suffixed with `?` are marked as optional and its values will only be validated and coerced if the key is present in the input hash.
|
|
970
994
|
|
|
@@ -988,7 +1012,7 @@ Types::Hash[
|
|
|
988
1012
|
]
|
|
989
1013
|
```
|
|
990
1014
|
|
|
991
|
-
|
|
1015
|
+
### Merging hash definitions
|
|
992
1016
|
|
|
993
1017
|
Use `Types::Hash#+` to merge two definitions. Keys in the second hash override the first one's.
|
|
994
1018
|
|
|
@@ -998,7 +1022,7 @@ Employee = Types::Hash[name: Types::String, company: Types::String]
|
|
|
998
1022
|
StaffMember = User + Employee # Hash[:name, :age, :company]
|
|
999
1023
|
```
|
|
1000
1024
|
|
|
1001
|
-
|
|
1025
|
+
### Hash intersections
|
|
1002
1026
|
|
|
1003
1027
|
Use `Types::Hash#&` to intersect two hash definitions as maps. It keeps the keys present in **both**, and intersects each shared key's value type:
|
|
1004
1028
|
|
|
@@ -1023,7 +1047,7 @@ Types::Hash[a: Types::String, _: Types::Any] & Types::Hash[a: Types::String, b:
|
|
|
1023
1047
|
# => Hash[a: String, b: Integer] (:b admitted via the left's catch-all)
|
|
1024
1048
|
```
|
|
1025
1049
|
|
|
1026
|
-
|
|
1050
|
+
### `Types::Hash#tagged_by`
|
|
1027
1051
|
|
|
1028
1052
|
Use `#tagged_by` to resolve what definition to use based on the value of a common key.
|
|
1029
1053
|
|
|
@@ -1040,7 +1064,7 @@ Events = Types::Hash.tagged_by(
|
|
|
1040
1064
|
Events.parse(type: 'name_updated', name: 'Joe') # Uses NameUpdatedEvent definition
|
|
1041
1065
|
```
|
|
1042
1066
|
|
|
1043
|
-
|
|
1067
|
+
### Undeclared keys and the `_` catch-all
|
|
1044
1068
|
|
|
1045
1069
|
By default, keys present in the input but **not** declared in the schema are dropped:
|
|
1046
1070
|
|
|
@@ -1095,7 +1119,7 @@ InputHandler.parse(price: 100_000, name: 'iPhone 15', category: 'smartphones')
|
|
|
1095
1119
|
|
|
1096
1120
|
The catch-all also shows up in generated JSON Schema as `additionalProperties`: `_: Any` → `{}` (anything), `_: Integer` → `{ "type": "integer" }`, and `_: Never` → `{ "not": {} }` (nothing allowed).
|
|
1097
1121
|
|
|
1098
|
-
|
|
1122
|
+
### Typed keys
|
|
1099
1123
|
|
|
1100
1124
|
Keys are not limited to symbols. A key can be any type or matcher, and it matches an input key via `key === other`. So you can key by String, or by a pattern, and mix them with a catch-all:
|
|
1101
1125
|
|
|
@@ -1107,7 +1131,7 @@ Types::Hash[Types::String[/^id_/] => Types::Integer, # keys matching /^id_/ hold
|
|
|
1107
1131
|
|
|
1108
1132
|
A typed key is **lenient**: input keys that don't match any declared or typed key follow the catch-all rule above (dropped by default). This is different from a homogeneous map (`Types::Hash[Types::Symbol, Types::Integer]`, a `HashMap` — note the comma, not `=>`), which is **strict** (a non-conforming key is an error) and coerces keys through the key type. Use a `HashMap` for "every key/value has this type"; use typed keys for "keys shaped like this map to that".
|
|
1109
1133
|
|
|
1110
|
-
|
|
1134
|
+
### `Types::Hash#filtered`
|
|
1111
1135
|
|
|
1112
1136
|
The `#filtered` modifier returns a valid Hash with the subset of values that were valid, instead of failing the entire result if one or more values are invalid.
|
|
1113
1137
|
|
|
@@ -1117,7 +1141,7 @@ User.parse(name: 'Joe', age: 40) # => { name: 'Joe', age: 40 }
|
|
|
1117
1141
|
User.parse(name: 'Joe', age: 'nope') # => { name: 'Joe' }
|
|
1118
1142
|
```
|
|
1119
1143
|
|
|
1120
|
-
|
|
1144
|
+
## `Types::Range`
|
|
1121
1145
|
|
|
1122
1146
|
`Types::Range` validates that a value is a Ruby `Range`. On its own it accepts any range:
|
|
1123
1147
|
|
|
@@ -1145,7 +1169,7 @@ Percent.resolve(10..20) # valid
|
|
|
1145
1169
|
Percent.resolve(10..200) # invalid (200 is outside 1..100)
|
|
1146
1170
|
```
|
|
1147
1171
|
|
|
1148
|
-
|
|
1172
|
+
### Open-ended ranges with `#where`
|
|
1149
1173
|
|
|
1150
1174
|
Use `#where` with the `begin`/`end` attributes to constrain the range's own endpoints. Passing `end: nil` matches only endless ranges, and `begin: nil` only beginless ranges:
|
|
1151
1175
|
|
|
@@ -1170,7 +1194,7 @@ NonNegativeStart.resolve(5..10) # valid
|
|
|
1170
1194
|
NonNegativeStart.resolve(-5..10) # invalid
|
|
1171
1195
|
```
|
|
1172
1196
|
|
|
1173
|
-
|
|
1197
|
+
### Composition
|
|
1174
1198
|
|
|
1175
1199
|
`Types::Range` is covariant in its member type and preserves its input value (it validates endpoints without coercing them), so it composes like the other containers. A union absorbs a narrower member into a wider one:
|
|
1176
1200
|
|
|
@@ -1181,7 +1205,7 @@ Types::Range[1..10] <= Types::Range[Integer] # true (covariant)
|
|
|
1181
1205
|
Types::Range[Integer] | Types::Range[1..10] # => Range[Integer]
|
|
1182
1206
|
```
|
|
1183
1207
|
|
|
1184
|
-
|
|
1208
|
+
### JSON Schema
|
|
1185
1209
|
|
|
1186
1210
|
A `Types::Range` whose member pins numeric bounds maps to JSON Schema's native keywords, preserving an exclusive end as `exclusiveMaximum`:
|
|
1187
1211
|
|
|
@@ -1190,7 +1214,7 @@ Plumb::JSONSchemaVisitor.call(Types::Range[0...100], root: false)
|
|
|
1190
1214
|
# => { "type" => "integer", "minimum" => 0, "exclusiveMaximum" => 100 }
|
|
1191
1215
|
```
|
|
1192
1216
|
|
|
1193
|
-
|
|
1217
|
+
## `Types::SymbolizedHash`
|
|
1194
1218
|
|
|
1195
1219
|
This type turns a hash's keys into symbols by calling `#to_sym` on them, and returning a new Hash.
|
|
1196
1220
|
|
|
@@ -1209,7 +1233,7 @@ type = Types::Hash[name: String, age: Integer].symbolized
|
|
|
1209
1233
|
type.parse('name' => 'Joe', 'age' => 20) # {name: 'Joe', age: 20}
|
|
1210
1234
|
```
|
|
1211
1235
|
|
|
1212
|
-
|
|
1236
|
+
## maps
|
|
1213
1237
|
|
|
1214
1238
|
You can also use Hash syntax to define a hash map with specific types for all keys and values:
|
|
1215
1239
|
|
|
@@ -1240,7 +1264,7 @@ Use `Types::Value` to validate specific values (using `#==`)
|
|
|
1240
1264
|
names_and_ones = Types::Hash[String, Types::Integer.value(1)]
|
|
1241
1265
|
```
|
|
1242
1266
|
|
|
1243
|
-
|
|
1267
|
+
### `#filtered`
|
|
1244
1268
|
|
|
1245
1269
|
Calling the `#filtered` modifier on a Hash Map makes it return a sub set of the keys and values that are valid as per the key and value type definitions.
|
|
1246
1270
|
|
|
@@ -1253,7 +1277,7 @@ S3Config.parse(ENV.to_h) # { 'S3_BUCKET' => 'foo', 'S3_REGION' => 'us-east-1' }
|
|
|
1253
1277
|
|
|
1254
1278
|
|
|
1255
1279
|
|
|
1256
|
-
|
|
1280
|
+
## `Types::Array`
|
|
1257
1281
|
|
|
1258
1282
|
```ruby
|
|
1259
1283
|
names = Types::Array[Types::String.present]
|
|
@@ -1271,7 +1295,7 @@ emails = Types::Array[Types::String[/@/]]
|
|
|
1271
1295
|
|
|
1272
1296
|
Prefer the latter (`Types::Array[Types::String[/@/]]`), as that first validates that each element is a `String` before matching against the regular expression.
|
|
1273
1297
|
|
|
1274
|
-
|
|
1298
|
+
### Chained array maps fuse into a single pass
|
|
1275
1299
|
|
|
1276
1300
|
`Types::Array` is covariant in its element type, so mapping `f` over an array and then mapping `g` is the same as mapping `f >> g` once. Composing two arrays applies that, and the collection is traversed once instead of twice:
|
|
1277
1301
|
|
|
@@ -1312,7 +1336,7 @@ Types::Array[Trim] / Types::Stream[Symbolize]
|
|
|
1312
1336
|
|
|
1313
1337
|
That guard is what keeps errors identical: two passes report stage by stage, so if the right map could reject what the left produced, one pass could surface errors two passes never reach. Records (`Types::Hash[name: ...]`) don't fuse either, since a record can drop, add and make keys optional.
|
|
1314
1338
|
|
|
1315
|
-
|
|
1339
|
+
### Concurrent arrays
|
|
1316
1340
|
|
|
1317
1341
|
Use `Types::Array#concurrent` to process array elements concurrently (using Concurrent Ruby for now).
|
|
1318
1342
|
|
|
@@ -1335,13 +1359,13 @@ See the [concurrent downloads example](https://github.com/ismasan/plumb/blob/mai
|
|
|
1335
1359
|
|
|
1336
1360
|
TODO: pluggable concurrency engines (Async?)
|
|
1337
1361
|
|
|
1338
|
-
|
|
1362
|
+
### `#stream`
|
|
1339
1363
|
|
|
1340
1364
|
Turn an Array definition into an enumerator that yields each element wrapped in `Result::Valid` or `Result::Invalid`.
|
|
1341
1365
|
|
|
1342
1366
|
See [`Types::Stream`](#typesstream) below for more.
|
|
1343
1367
|
|
|
1344
|
-
|
|
1368
|
+
### `#filtered`
|
|
1345
1369
|
|
|
1346
1370
|
The `#filtered` modifier makes an array definition return a subset of the input array where the values are valid, as per the array's element type.
|
|
1347
1371
|
|
|
@@ -1352,7 +1376,7 @@ j_names.parse(%w[james ismael joe toby joan isabel]) # ["james", "joe", "joan"]
|
|
|
1352
1376
|
|
|
1353
1377
|
|
|
1354
1378
|
|
|
1355
|
-
|
|
1379
|
+
## `Types::Tuple`
|
|
1356
1380
|
|
|
1357
1381
|
```ruby
|
|
1358
1382
|
Status = Types::Symbol.options(%i[ok error])
|
|
@@ -1384,7 +1408,7 @@ NameAndRegex = Types::Tuple[String, Types::Value[/@/]]
|
|
|
1384
1408
|
|
|
1385
1409
|
|
|
1386
1410
|
|
|
1387
|
-
|
|
1411
|
+
## `Types::Stream`
|
|
1388
1412
|
|
|
1389
1413
|
`Types::Stream` defines an enumerator that validates/coerces each element as it iterates.
|
|
1390
1414
|
|
|
@@ -1410,7 +1434,7 @@ end
|
|
|
1410
1434
|
|
|
1411
1435
|
See a more complete the [CSV Stream example](https://github.com/ismasan/plumb/blob/main/examples/csv_stream.rb)
|
|
1412
1436
|
|
|
1413
|
-
|
|
1437
|
+
### `Types::Stream#filtered`
|
|
1414
1438
|
|
|
1415
1439
|
Use `#filtered` to turn a `Types::Stream` into a stream that only yields valid elements.
|
|
1416
1440
|
|
|
@@ -1421,7 +1445,7 @@ ValidElements.parse(data).each do |valid_row|
|
|
|
1421
1445
|
end
|
|
1422
1446
|
```
|
|
1423
1447
|
|
|
1424
|
-
|
|
1448
|
+
### `Types::Array#stream`
|
|
1425
1449
|
|
|
1426
1450
|
A `Types::Array` definition can be turned into a stream.
|
|
1427
1451
|
|
|
@@ -1436,11 +1460,11 @@ Str.parse(data).each do |row|
|
|
|
1436
1460
|
end
|
|
1437
1461
|
```
|
|
1438
1462
|
|
|
1439
|
-
|
|
1463
|
+
## Types::Data
|
|
1440
1464
|
|
|
1441
1465
|
`Types::Data` provides a superclass to define **immutable** structs or value objects with typed / coercible attributes.
|
|
1442
1466
|
|
|
1443
|
-
|
|
1467
|
+
### `[]` Syntax
|
|
1444
1468
|
|
|
1445
1469
|
The `[]` syntax is a short-hand for struct definition.
|
|
1446
1470
|
Like `Plumb::Types::Hash`, suffixing a key with `?` makes it optional.
|
|
@@ -1473,7 +1497,7 @@ PersonHash = Types::Hash[name: String, age?: Integer]
|
|
|
1473
1497
|
PersonStruct = Types::Data[PersonHash]
|
|
1474
1498
|
```
|
|
1475
1499
|
|
|
1476
|
-
|
|
1500
|
+
### `#with`
|
|
1477
1501
|
|
|
1478
1502
|
Note that these instances cannot be mutated (there's no attribute setters), but they can be copied with partial attributes with `#with`
|
|
1479
1503
|
|
|
@@ -1481,7 +1505,7 @@ Note that these instances cannot be mutated (there's no attribute setters), but
|
|
|
1481
1505
|
another_person = person.with(age: 20)
|
|
1482
1506
|
```
|
|
1483
1507
|
|
|
1484
|
-
|
|
1508
|
+
### `.attribute` syntax
|
|
1485
1509
|
|
|
1486
1510
|
This syntax allows defining struct classes with typed attributes, including nested structs.
|
|
1487
1511
|
|
|
@@ -1564,7 +1588,7 @@ Note that this does NOT work with union'd or piped structs.
|
|
|
1564
1588
|
attribute :company, Company | Person do
|
|
1565
1589
|
```
|
|
1566
1590
|
|
|
1567
|
-
|
|
1591
|
+
### Shorthand array syntax
|
|
1568
1592
|
|
|
1569
1593
|
```ruby
|
|
1570
1594
|
attribute :things, [] # Same as attribute :things, Types::Array
|
|
@@ -1581,7 +1605,7 @@ Note that, if you want to match an attribute value against a literal array, you
|
|
|
1581
1605
|
attribute :one_two_three, Types::Array.value[[1, 2, 3]])
|
|
1582
1606
|
```
|
|
1583
1607
|
|
|
1584
|
-
|
|
1608
|
+
### Optional Attributes
|
|
1585
1609
|
|
|
1586
1610
|
Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
|
|
1587
1611
|
|
|
@@ -1589,7 +1613,7 @@ Using `attribute?` allows for optional attributes. If the attribute is not prese
|
|
|
1589
1613
|
attribute? :company, Company
|
|
1590
1614
|
```
|
|
1591
1615
|
|
|
1592
|
-
|
|
1616
|
+
### Before steps, symbolizing keys
|
|
1593
1617
|
|
|
1594
1618
|
The optional `.step` helper adds arbitrary Plumb steps to a Data constructor's internal pipeline.
|
|
1595
1619
|
|
|
@@ -1632,7 +1656,7 @@ person.last_name # => 'BLOGGS'
|
|
|
1632
1656
|
|
|
1633
1657
|
A Data class steps are inherited to its child classes.
|
|
1634
1658
|
|
|
1635
|
-
|
|
1659
|
+
### Inheritance
|
|
1636
1660
|
|
|
1637
1661
|
Data structs can inherit from other structs. This is useful for defining a base struct with common attributes.
|
|
1638
1662
|
|
|
@@ -1646,7 +1670,7 @@ class Person < BasePerson
|
|
|
1646
1670
|
end
|
|
1647
1671
|
```
|
|
1648
1672
|
|
|
1649
|
-
|
|
1673
|
+
### Equality with `#==`
|
|
1650
1674
|
|
|
1651
1675
|
`#==` is implemented to compare attributes, recursively.
|
|
1652
1676
|
|
|
@@ -1656,7 +1680,7 @@ person2 = Person.new(name: 'Joe', age: 20)
|
|
|
1656
1680
|
person1 == person2 # true
|
|
1657
1681
|
```
|
|
1658
1682
|
|
|
1659
|
-
|
|
1683
|
+
### Struct composition
|
|
1660
1684
|
|
|
1661
1685
|
`Types::Data` supports all the composition operators and helpers.
|
|
1662
1686
|
|
|
@@ -1678,7 +1702,7 @@ Payload = Types::Hash[
|
|
|
1678
1702
|
]
|
|
1679
1703
|
```
|
|
1680
1704
|
|
|
1681
|
-
|
|
1705
|
+
### Attribute writers
|
|
1682
1706
|
|
|
1683
1707
|
By default `Types::Data` classes are inmutable, but you can define attribute writers to allow for mutation using the `writer: true` option.
|
|
1684
1708
|
|
|
@@ -1702,7 +1726,7 @@ config.valid? # true
|
|
|
1702
1726
|
config.errors # {}
|
|
1703
1727
|
```
|
|
1704
1728
|
|
|
1705
|
-
|
|
1729
|
+
### Recursive struct definitions
|
|
1706
1730
|
|
|
1707
1731
|
You can use `#defer`. See [recursive types](#recursive-types).
|
|
1708
1732
|
|
|
@@ -1717,11 +1741,11 @@ person.friend.name # 'joan'
|
|
|
1717
1741
|
person.friend.friend # nil
|
|
1718
1742
|
```
|
|
1719
1743
|
|
|
1720
|
-
|
|
1744
|
+
## Plumb::Pipeline
|
|
1721
1745
|
|
|
1722
1746
|
`Plumb::Pipeline` offers a sequential, step-by-step syntax for composing processing steps, as well as a simple middleware API to wrap steps for metrics, logging, debugging, caching and more. See the [command objects](https://github.com/ismasan/plumb/blob/main/examples/command_objects.rb) example for a worked use case.
|
|
1723
1747
|
|
|
1724
|
-
|
|
1748
|
+
### `#pipeline` helper
|
|
1725
1749
|
|
|
1726
1750
|
All plumb steps have a `#pipeline` helper.
|
|
1727
1751
|
|
|
@@ -1753,7 +1777,7 @@ result = CreateUser.resolve(name: 'Joe', age: 40)
|
|
|
1753
1777
|
# result.value => User
|
|
1754
1778
|
```
|
|
1755
1779
|
|
|
1756
|
-
|
|
1780
|
+
#### `#step` (non-strict) and `#step!` (strict)
|
|
1757
1781
|
|
|
1758
1782
|
A pipeline is a sequence of validators/coercions that progressively narrows its data, so **`#step` is non-strict**: it chains with [`#/`](#composition-type-checks), skipping the composition check (a later step may legitimately narrow what an earlier one produced). Use **`#step!`** for the strict [`#>>` check](#composition-type-checks) — a build-time `Plumb::TypeError` if a step could never accept the previous step's output.
|
|
1759
1783
|
|
|
@@ -1787,7 +1811,7 @@ IsJoe = User.check('must be named joe') { |user|
|
|
|
1787
1811
|
CreateIfJoe = IsJoe >> CreateUser
|
|
1788
1812
|
```
|
|
1789
1813
|
|
|
1790
|
-
|
|
1814
|
+
#### `#around`
|
|
1791
1815
|
|
|
1792
1816
|
Use `#around` in a pipeline definition to add a middleware step that wraps all other steps registered.
|
|
1793
1817
|
|
|
@@ -1841,7 +1865,7 @@ pl.around do |step, result|
|
|
|
1841
1865
|
end
|
|
1842
1866
|
```
|
|
1843
1867
|
|
|
1844
|
-
|
|
1868
|
+
### As stand-alone `Plumb::Pipeline` class
|
|
1845
1869
|
|
|
1846
1870
|
`Plumb::Pipeline` can also be used on its own, sub-classed, and it can take class-level `around` middleware.
|
|
1847
1871
|
|
|
@@ -1886,7 +1910,7 @@ pipe = DebuggablePipeline.new do |pl|
|
|
|
1886
1910
|
end
|
|
1887
1911
|
```
|
|
1888
1912
|
|
|
1889
|
-
|
|
1913
|
+
### Pipelines all the way down :turtle:
|
|
1890
1914
|
|
|
1891
1915
|
Pipelines are full Plumb steps, so they can themselves be used as steps.
|
|
1892
1916
|
|
|
@@ -1902,7 +1926,7 @@ Pipe2 = DebuggablePipeline.new do |pl|
|
|
|
1902
1926
|
end
|
|
1903
1927
|
```
|
|
1904
1928
|
|
|
1905
|
-
|
|
1929
|
+
## Recursive types
|
|
1906
1930
|
|
|
1907
1931
|
You can use a proc to defer evaluation of recursive definitions.
|
|
1908
1932
|
|
|
@@ -1935,11 +1959,11 @@ LinkedList = Types::Hash[
|
|
|
1935
1959
|
|
|
1936
1960
|
|
|
1937
1961
|
|
|
1938
|
-
|
|
1962
|
+
## Encoders and Codecs
|
|
1939
1963
|
|
|
1940
1964
|
A one-way coercion can parse an external representation (a date string) into a parsed value (a `Date`), but not back. **Encoders** generalize that into pluggable, two-way serialization, and **Codecs** group encoders and apply them to whole schemas — Ruby data structures to JSON-ready structures and back, for example.
|
|
1941
1965
|
|
|
1942
|
-
|
|
1966
|
+
### Defining encoders
|
|
1943
1967
|
|
|
1944
1968
|
An encoder is a class declaring an input and an output type, with `#decode` (input ⇒ output) and `#encode` (output ⇒ input) methods:
|
|
1945
1969
|
|
|
@@ -1979,7 +2003,7 @@ JSONDateRangeEncoder.encode(Date.new(2024, 1, 1)..Date.new(2024, 2, 1))
|
|
|
1979
2003
|
|
|
1980
2004
|
Encoders also express lenient unions — `Types::Date | SomeDateEncoder` accepts a `Date` or decodes a string into one.
|
|
1981
2005
|
|
|
1982
|
-
|
|
2006
|
+
### Codecs
|
|
1983
2007
|
|
|
1984
2008
|
A codec groups encoders and applies them to whole types at composition time. Codecs know nothing about any particular format — only their encoders. Types that are already valid in the target format are declared with `.noop`:
|
|
1985
2009
|
|
|
@@ -2067,7 +2091,50 @@ JSONPerson.to_json_schema
|
|
|
2067
2091
|
# "dates" is described as { "type" => "object", "properties" => { "from" => { "type" => "string" }, ... } }
|
|
2068
2092
|
```
|
|
2069
2093
|
|
|
2070
|
-
|
|
2094
|
+
### Codec instances: a registry of pre-built pairs
|
|
2095
|
+
|
|
2096
|
+
Composing a codec rewrites the whole type tree, so it belongs at boot — not on the path of every message. A codec _instance_ is a registry of `[decoder, encoder]` pairs, each built once by `register` and then looked up by key:
|
|
2097
|
+
|
|
2098
|
+
```ruby
|
|
2099
|
+
CODECS = JSONCodec.new do |c|
|
|
2100
|
+
c.register('person.created', Person)
|
|
2101
|
+
c.register('company.created', Company)
|
|
2102
|
+
c.register(Types::Date) # the key defaults to the type itself
|
|
2103
|
+
end
|
|
2104
|
+
|
|
2105
|
+
CODECS.decode('person.created', payload) # => a Person hash
|
|
2106
|
+
CODECS.encode('person.created', person) # => JSON structures
|
|
2107
|
+
CODECS.decode(Types::Date, '2024-01-01') # => Date
|
|
2108
|
+
```
|
|
2109
|
+
|
|
2110
|
+
Keys are yours to choose — a message name, a content type, the type itself. `decode` and `encode` only `#parse`, so the rewrite is paid for once.
|
|
2111
|
+
|
|
2112
|
+
An instance built with a block is frozen when the block returns. Without one it stays open, and `register` chains:
|
|
2113
|
+
|
|
2114
|
+
```ruby
|
|
2115
|
+
registry = JSONCodec.new
|
|
2116
|
+
registry.register('day', Types::Date).register('person', Person)
|
|
2117
|
+
registry.freeze
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
`key?` asks what is registered; an unknown key raises `Plumb::Codec::NoEntryError` (a `KeyError`). Payloads are still validated by their type — a bad one raises `Plumb::ParseError` as usual.
|
|
2121
|
+
|
|
2122
|
+
An open registry also composes _type_ keys on first use, so you need not enumerate every type you exchange:
|
|
2123
|
+
|
|
2124
|
+
```ruby
|
|
2125
|
+
CODECS = JSONCodec.new # no block, so it stays open
|
|
2126
|
+
|
|
2127
|
+
CODECS.encode(Person, person) # composes Person now, reuses it after
|
|
2128
|
+
CODECS.decode(Person, payload)
|
|
2129
|
+
CODECS.decode(Types::Date, '2024-01-01')
|
|
2130
|
+
CODECS.decode(::Date, '2024-01-01') # raw Ruby classes work too
|
|
2131
|
+
```
|
|
2132
|
+
|
|
2133
|
+
Only keys that _are_ types compose themselves — an app-owned tag like `'person.created'` names nothing the codec could build, so it still raises `NoEntryError` until you register it. Freezing is how you declare the set closed: a sealed registry never composes anything new, which is what you want at boot when every type is known.
|
|
2134
|
+
|
|
2135
|
+
Open registries are safe to share between threads — composition happens outside a lock, and racing threads simply compose the same (pure) rewrite twice. Note that type keys match by value, so a type literal built fresh on each call (`CODECS.decode(Types::Hash[on: Types::Date], payload)`) adds an entry per call: pass constants, or seal the registry.
|
|
2136
|
+
|
|
2137
|
+
### `Codec::Forms`: string-based formats
|
|
2071
2138
|
|
|
2072
2139
|
The second built-in codec targets HTML forms, query strings and other formats where **every value arrives as a string**. Unlike `Codec::JSON` there are almost no native scalars: strings pass through, untyped containers recurse (Rack-style nested params), and everything else maps through an encoder with a strictly-patterned string input type — integers (`/\A-?\d+\z/`), floats, decimals, booleans (`"true"/"1"`, `"false"/"0"`, case-insensitive), ISO 8601 dates and times, scheme-prefixed URIs, and the empty string for `nil` (so `Types::Date | Types::Nil` decodes `''` to `nil`).
|
|
2073
2140
|
|
|
@@ -2098,7 +2165,7 @@ Things to know:
|
|
|
2098
2165
|
* Registering `noop Types::Hash` / `Types::Array` only covers *untyped* containers — structured schemas (and struct classes) are always recursed into, so a generic noop can't accidentally skip encoding of nested fields.
|
|
2099
2166
|
* Decoding a struct runs the rewritten schema and then the struct's own validation — correct, but a struct attribute with a non-idempotent transform would apply it twice. Struct attributes should be validators/coercions, as they already must be for `#with`.
|
|
2100
2167
|
|
|
2101
|
-
|
|
2168
|
+
## Custom types
|
|
2102
2169
|
|
|
2103
2170
|
Every Plumb type exposes the following one-method interface:
|
|
2104
2171
|
|
|
@@ -2110,7 +2177,7 @@ As long as an object implements this interface, it can be composed into Plumb wo
|
|
|
2110
2177
|
|
|
2111
2178
|
The `Result::Valid` class has helper methods `#valid(value) => Result::Valid` and `#invalid(errors:) => Result::Invalid` to facilitate returning valid or invalid values from your own steps.
|
|
2112
2179
|
|
|
2113
|
-
|
|
2180
|
+
### Compose procs or lambdas directly
|
|
2114
2181
|
|
|
2115
2182
|
Piping any `#call` object onto Plumb types wraps your object in a composable step, with all methods necessary for further composition.
|
|
2116
2183
|
|
|
@@ -2118,7 +2185,7 @@ Piping any `#call` object onto Plumb types wraps your object in a composable ste
|
|
|
2118
2185
|
Greeting = Types::String >> ->(result) { result.valid("Hello #{result.value}") }
|
|
2119
2186
|
```
|
|
2120
2187
|
|
|
2121
|
-
|
|
2188
|
+
### `Plumb::Function[input => output]`
|
|
2122
2189
|
|
|
2123
2190
|
To build a standalone, typed function from a callable — one not already piped onto a type — use `Plumb::Function[]`. Declaring both ends gives you a typed function: the input is validated before your callable runs, and the value it produces is validated against the output type.
|
|
2124
2191
|
|
|
@@ -2174,7 +2241,7 @@ Either way, `Greeting` is a full Plumb step, which comes with all the Plumb meth
|
|
|
2174
2241
|
LoudGreeting = Greeting.default('no greeting').invoke(:upcase)
|
|
2175
2242
|
```
|
|
2176
2243
|
|
|
2177
|
-
|
|
2244
|
+
### A custom `#call` class
|
|
2178
2245
|
|
|
2179
2246
|
Or write a custom class that responds to `#call(Result::Valid) => Result::Valid | Result::Invalid`
|
|
2180
2247
|
|
|
@@ -2197,7 +2264,7 @@ MyType = Types::String >> Greeting.new('Hola')
|
|
|
2197
2264
|
|
|
2198
2265
|
This is useful when you want to parameterize your custom steps, for example by initialising them with arguments like the example above.
|
|
2199
2266
|
|
|
2200
|
-
|
|
2267
|
+
### Include `Plumb::Composable` to make instance of a class full "steps"
|
|
2201
2268
|
|
|
2202
2269
|
The class above will be wrapped in a composable step when piped into other steps, but it doesn't support Plumb methods on its own.
|
|
2203
2270
|
|
|
@@ -2229,7 +2296,7 @@ Now you can use your class as a composition starting point directly.
|
|
|
2229
2296
|
LoudGreeting = Greeting.new('Hola').default('no greeting').invoke(:upcase)
|
|
2230
2297
|
```
|
|
2231
2298
|
|
|
2232
|
-
|
|
2299
|
+
### Extend a class with `Plumb::Composable` to make the class itself a composable step.
|
|
2233
2300
|
|
|
2234
2301
|
```ruby
|
|
2235
2302
|
class User
|
|
@@ -2244,7 +2311,7 @@ end
|
|
|
2244
2311
|
|
|
2245
2312
|
This is how [Plumb::Types::Data](#typesdata) is implemented.
|
|
2246
2313
|
|
|
2247
|
-
|
|
2314
|
+
### Include `Plumb::Implementation[input => output]` to declare a class' types
|
|
2248
2315
|
|
|
2249
2316
|
`Plumb::Composable` makes your instances composable, but Plumb knows nothing about what they accept or produce — they're opaque, so they opt out of [composition type-checks](#composition-type-checks) and subtype checks.
|
|
2250
2317
|
|
|
@@ -2306,7 +2373,7 @@ class AdminFinder < UserFinder
|
|
|
2306
2373
|
end
|
|
2307
2374
|
```
|
|
2308
2375
|
|
|
2309
|
-
|
|
2376
|
+
### Extend `Plumb::Implementation[input => output]` to make the class itself a typed step
|
|
2310
2377
|
|
|
2311
2378
|
Just as with [`Plumb::Composable`](#extend-a-class-with-plumbcomposable-to-make-the-class-itself-a-composable-step), `extend` instead of `include` puts the whole interface on the class: no instantiation, the class implements `self._call(result)` and answers `.input_type` / `.output_type`.
|
|
2312
2379
|
|
|
@@ -2329,7 +2396,7 @@ The two forms are alternatives — pick one per class. The extended form deliber
|
|
|
2329
2396
|
Plumb::Subtyping.subtype?(ParseUUID, Types::String) # => true
|
|
2330
2397
|
```
|
|
2331
2398
|
|
|
2332
|
-
|
|
2399
|
+
### Participating in subtype & composition checks
|
|
2333
2400
|
|
|
2334
2401
|
The subtype (`#<=`) and [`#>>` composition](#composition-type-checks) checks are built on a single hook that every `Plumb::Composable` already implements with a sensible default — `#>>` is just `subtype?(produced, accepted)`, so there's nothing extra to implement for composition. A custom type participates **without changing any core library code**: it either relies on the default or overrides the hook. `Plumb::Subtyping` itself only knows the composition algebra (the top type `Types::Any`, the bottom type `Types::Never`, union `#|`, intersection `#&`, refinement/sequencing `#>>`, and conversion `#transform`); everything else is delegated to the type.
|
|
2335
2402
|
|
|
@@ -2338,7 +2405,7 @@ The default leans on two methods your type already has:
|
|
|
2338
2405
|
- `#children` — the sub-types this type is built from, as an array. A type whose single child is a **raw Ruby matcher** (a Class, Range, Regexp or literal — as `Plumb::Constraint` wraps) is treated as *atomic* and compared with Ruby semantics. A type whose children are themselves Plumb types (like `Array`, `Tuple`, `HashMap`) is treated as a **covariant container** — so exposing `#children` is all a custom container needs to compare covariantly.
|
|
2339
2406
|
- `#==` — structural equality (provided by `Plumb::Composable`).
|
|
2340
2407
|
|
|
2341
|
-
|
|
2408
|
+
#### The hook
|
|
2342
2409
|
|
|
2343
2410
|
| Hook | Returns | Used by | Default |
|
|
2344
2411
|
| --- | --- | --- | --- |
|
|
@@ -2369,7 +2436,7 @@ even <= Types::Numeric # => true
|
|
|
2369
2436
|
even <= Types::String # => false
|
|
2370
2437
|
```
|
|
2371
2438
|
|
|
2372
|
-
|
|
2439
|
+
#### Type flow: `#input_type` / `#output_type`
|
|
2373
2440
|
|
|
2374
2441
|
The `#>>` check (and the [JSON Schema visitor](#json-schema)) ask what a type accepts and produces; both [default to `self`](#input_type-and-output_type). Override them when your type changes the value or is opaque about it:
|
|
2375
2442
|
|
|
@@ -2378,7 +2445,7 @@ The `#>>` check (and the [JSON Schema visitor](#json-schema)) ask what a type ac
|
|
|
2378
2445
|
|
|
2379
2446
|
Custom types are **values/leaves** in the algebra — you compose them with the built-in combinators (`#>>`, `#|`, `#transform`, `Types::Any`) rather than re-implementing those.
|
|
2380
2447
|
|
|
2381
|
-
|
|
2448
|
+
## Custom policies
|
|
2382
2449
|
|
|
2383
2450
|
`Plumb.policy` can be used to encapsulate common type compositions, or compositions that can be configurable by parameters.
|
|
2384
2451
|
|
|
@@ -2404,7 +2471,7 @@ The `#policy` helper supports applying multiply policies.
|
|
|
2404
2471
|
Types::String.policy(default_if_nil: 'nothing here', size: (10..20))
|
|
2405
2472
|
```
|
|
2406
2473
|
|
|
2407
|
-
|
|
2474
|
+
### Policies as helper methods
|
|
2408
2475
|
|
|
2409
2476
|
Use the `helper: true` option to register the policy as a method you can call on types directly.
|
|
2410
2477
|
|
|
@@ -2431,7 +2498,7 @@ AccountName = Types::String.admin
|
|
|
2431
2498
|
AccountName.metadata # => { admin: true }
|
|
2432
2499
|
```
|
|
2433
2500
|
|
|
2434
|
-
|
|
2501
|
+
### Type-specific policies
|
|
2435
2502
|
|
|
2436
2503
|
You can use the `for_type:` option to define policies that only apply to steps that output certain types. This example is only applicable for types that return `Integer` values.
|
|
2437
2504
|
|
|
@@ -2447,7 +2514,7 @@ Doubled.parse(2) # 4
|
|
|
2447
2514
|
DoubledString = Types::String.multiply_by(2) # raises error
|
|
2448
2515
|
```
|
|
2449
2516
|
|
|
2450
|
-
|
|
2517
|
+
### Interface-specific policies
|
|
2451
2518
|
|
|
2452
2519
|
`for_type`also supports a Symbol for a method name, so that the policy can be applied to any types that support that method.
|
|
2453
2520
|
|
|
@@ -2463,7 +2530,7 @@ DoubledNumeric = Types::Numeric.multiply_by(2)
|
|
|
2463
2530
|
DoubledMoney = Types::Any[Money].multiply_by(2)
|
|
2464
2531
|
```
|
|
2465
2532
|
|
|
2466
|
-
|
|
2533
|
+
### Self-contained policy modules
|
|
2467
2534
|
|
|
2468
2535
|
You can register a module, class or object with a three-method interface as a policy. This is so that policies can have their own namespace if they need local constants or private methods. For example, this is how the `:split` policy for strings is defined.
|
|
2469
2536
|
|
|
@@ -2482,7 +2549,7 @@ end
|
|
|
2482
2549
|
Plumb.policy :split, SplitPolicy
|
|
2483
2550
|
```
|
|
2484
2551
|
|
|
2485
|
-
|
|
2552
|
+
## JSON Schema
|
|
2486
2553
|
|
|
2487
2554
|
Plumb ships with a JSON schema visitor that compiles a type composition into a JSON Schema Hash. All Plumb types support a `#to_json_schema` method.
|
|
2488
2555
|
|
|
@@ -2552,7 +2619,7 @@ Types::DateTime.to_json_schema
|
|
|
2552
2619
|
# {"type"=>"string", "format"=>"date-time"}
|
|
2553
2620
|
```
|
|
2554
2621
|
|
|
2555
|
-
|
|
2622
|
+
#### Node names for compositions
|
|
2556
2623
|
|
|
2557
2624
|
Two-sided compositions report one of four `#node_name`s, depending on whether the node is a *computation* (some side changes the value) or a *type* (no side does):
|
|
2558
2625
|
|
|
@@ -2565,7 +2632,7 @@ Two-sided compositions report one of four `#node_name`s, depending on whether th
|
|
|
2565
2632
|
|
|
2566
2633
|
For a visitor this matters because an `:intersection` describes one value (merge both sides' specs) while an `:and` may describe a conversion (build from the input side). Visitors that don't need the distinction can register just `on(:and)` / `on(:or)`: `:intersection` and `:union` fall back to those when no specific handler is defined.
|
|
2567
2634
|
|
|
2568
|
-
|
|
2635
|
+
## Mermaid diagrams
|
|
2569
2636
|
|
|
2570
2637
|
Because a composition is just a tree of `>>` (sequence) and `|` (choice) nodes, it can also be rendered as a [Mermaid](https://mermaid.js.org) `flowchart`. Every Plumb type supports `#to_mermaid`. `>>` becomes sequential arrows; `|` becomes a fork, where the preceding step fans out to each alternative (and a following step joins them back).
|
|
2571
2638
|
|
|
@@ -2616,7 +2683,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
2616
2683
|
|
|
2617
2684
|
## Contributing
|
|
2618
2685
|
|
|
2619
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/ismasan/plumb.
|
|
2686
|
+
Bug reports and pull requests are welcome on GitHub at [github.com/ismasan/plumb](https://github.com/ismasan/plumb).
|
|
2620
2687
|
|
|
2621
2688
|
## License
|
|
2622
2689
|
|