plumb 0.2.0.beta.2 → 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 +108 -93
- data/lib/plumb/codec.rb +48 -4
- data/lib/plumb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c347e5312c27a5db7f2c1f668676028fb2613a7b5f459ac5f2d558577599cbd0
|
|
4
|
+
data.tar.gz: 360a9c7eb0ece5c922eb0447c952a5b449e8164892386d49c781eaf66448e9be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a55f939ef7a815cc1a49be1f7c9dcb15e3ebefcb5c2ee4e2256cb157f85c90f4dda64a0316d5e9afb22566ccac236960fcab2103b25e92ba17aff41971f3c884
|
|
7
|
+
data.tar.gz: e4b5480376bbcc9904404eea5215b7edabfdb3eb8c439eeae96108a597061f6de338bcdeab5edf667a4f2cdf149761fbe64d7f4e2cd7af3ed1bdab779a01259a
|
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
|
|
|
@@ -564,7 +564,7 @@ Same if you want to apply a default to several cases.
|
|
|
564
564
|
str = Types::String | ((Types::Nil | Types::Undefined) >> Types::Static['nope'.freeze])
|
|
565
565
|
```
|
|
566
566
|
|
|
567
|
-
|
|
567
|
+
### `#build`
|
|
568
568
|
|
|
569
569
|
Build a custom object or class.
|
|
570
570
|
|
|
@@ -597,7 +597,7 @@ Note that this case is identical to `#transform` with a block.
|
|
|
597
597
|
StringToMoney = Types::String.transform(Money) { |value| Monetize.parse(value) }
|
|
598
598
|
```
|
|
599
599
|
|
|
600
|
-
|
|
600
|
+
### `#check`
|
|
601
601
|
|
|
602
602
|
Pass the value through an arbitrary validation
|
|
603
603
|
|
|
@@ -607,7 +607,7 @@ type.parse('Role: Manager') # 'Role: Manager'
|
|
|
607
607
|
type.parse('Manager') # fails
|
|
608
608
|
```
|
|
609
609
|
|
|
610
|
-
|
|
610
|
+
### `#value`
|
|
611
611
|
|
|
612
612
|
Constrain a type to a specific value. Compares with `#==`
|
|
613
613
|
|
|
@@ -624,7 +624,7 @@ All scalar types support this:
|
|
|
624
624
|
ten = Types::Integer.value(10)
|
|
625
625
|
```
|
|
626
626
|
|
|
627
|
-
|
|
627
|
+
### `#static`
|
|
628
628
|
|
|
629
629
|
A type that always returns a valid, static value, regardless of input.
|
|
630
630
|
|
|
@@ -663,7 +663,7 @@ type = Types::Integer[100..].static(150) # ok
|
|
|
663
663
|
|
|
664
664
|
So, normally you'd only use this attached to primitive types without further processing (but your use case may vary).
|
|
665
665
|
|
|
666
|
-
|
|
666
|
+
### `#generate`
|
|
667
667
|
|
|
668
668
|
Passing a proc will evaluate the proc on every invocation. Use this for generated values.
|
|
669
669
|
|
|
@@ -682,7 +682,7 @@ random_number.parse # raises Plumb::ParseError because `rand` is not a String
|
|
|
682
682
|
|
|
683
683
|
You can also pass any `#call() => Object` interface as a generator, instead of a proc.
|
|
684
684
|
|
|
685
|
-
|
|
685
|
+
### `#metadata`
|
|
686
686
|
|
|
687
687
|
Add metadata to a type
|
|
688
688
|
|
|
@@ -705,7 +705,7 @@ type.metadata[:note] # 'An email address'
|
|
|
705
705
|
|
|
706
706
|
TODO: document custom visitors.
|
|
707
707
|
|
|
708
|
-
|
|
708
|
+
### `#input_type` and `#output_type`
|
|
709
709
|
|
|
710
710
|
Every type exposes the type it expects as input and the type it produces as output.
|
|
711
711
|
|
|
@@ -733,7 +733,7 @@ For a plain type, both are the type itself. Unions distribute over both sides:
|
|
|
733
733
|
|
|
734
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.
|
|
735
735
|
|
|
736
|
-
|
|
736
|
+
### Composition type-checks
|
|
737
737
|
|
|
738
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.
|
|
739
739
|
|
|
@@ -784,7 +784,7 @@ Types::Array.where(size: 10) >> Types::Array.where(size: 8..100) # ok: 10 is
|
|
|
784
784
|
Types::Array.where(size: 10..15) >> Types::Array.where(size: 11..14) # raises: 10..15 isn't within 11..14
|
|
785
785
|
```
|
|
786
786
|
|
|
787
|
-
|
|
787
|
+
### Subtype checks: `#<=` and `Plumb::Subtyping`
|
|
788
788
|
|
|
789
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):
|
|
790
790
|
|
|
@@ -800,11 +800,11 @@ small = Types::Hash[name: Types::String]
|
|
|
800
800
|
big <= small # true (width + depth subtyping)
|
|
801
801
|
```
|
|
802
802
|
|
|
803
|
-
|
|
803
|
+
## Other policies
|
|
804
804
|
|
|
805
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.
|
|
806
806
|
|
|
807
|
-
|
|
807
|
+
### `:respond_to`
|
|
808
808
|
|
|
809
809
|
Similar to `Types::Interface`, this is a quick way to assert that a value supports one or more methods.
|
|
810
810
|
|
|
@@ -814,7 +814,7 @@ List = Types::Any.policy(respond_to: :each)
|
|
|
814
814
|
List = Types::Any.policy(respond_to: [:each, :[], :size)
|
|
815
815
|
```
|
|
816
816
|
|
|
817
|
-
|
|
817
|
+
### `:excluded_from`
|
|
818
818
|
|
|
819
819
|
The opposite of `#options`, this policy validates that the value _is not_ included in a list.
|
|
820
820
|
|
|
@@ -822,7 +822,7 @@ The opposite of `#options`, this policy validates that the value _is not_ includ
|
|
|
822
822
|
Name = Types::String.policy(excluded_from: ['Joe', 'Joan'])
|
|
823
823
|
```
|
|
824
824
|
|
|
825
|
-
|
|
825
|
+
### :split` (strings only)
|
|
826
826
|
|
|
827
827
|
Splits string values by a separator (default: `,`).
|
|
828
828
|
|
|
@@ -835,7 +835,7 @@ CSVLine = Types::String.split(/\s*;\s*/)
|
|
|
835
835
|
CSVLine.parse('a;b;c') # => ['a', 'b', 'c']
|
|
836
836
|
```
|
|
837
837
|
|
|
838
|
-
|
|
838
|
+
### `:rescue`
|
|
839
839
|
|
|
840
840
|
Wraps a step's execution, rescues a specific exception and returns an invalid result.
|
|
841
841
|
|
|
@@ -859,7 +859,7 @@ type.resolve('2024-') # => Result::Invalid with error message
|
|
|
859
859
|
The guard keeps the type it wraps: the example above is still a `Date` for subtyping,
|
|
860
860
|
JSON Schema and [Codecs](#encoders-and-codecs).
|
|
861
861
|
|
|
862
|
-
|
|
862
|
+
## `Types::Interface`
|
|
863
863
|
|
|
864
864
|
Use this for objects that must respond to one or more methods.
|
|
865
865
|
|
|
@@ -894,7 +894,7 @@ case args
|
|
|
894
894
|
end
|
|
895
895
|
```
|
|
896
896
|
|
|
897
|
-
|
|
897
|
+
### Merging interfaces
|
|
898
898
|
|
|
899
899
|
Use the `+` operator to merge two interfaces into a new one that must support both sets of method names.
|
|
900
900
|
|
|
@@ -905,7 +905,7 @@ Countable = Types::Interface[:size]
|
|
|
905
905
|
CountableIterable = Iterable + Countable
|
|
906
906
|
```
|
|
907
907
|
|
|
908
|
-
|
|
908
|
+
### Intersecting interfaces
|
|
909
909
|
|
|
910
910
|
Use the `&` operator to produce a new interface with the intersection of method names
|
|
911
911
|
|
|
@@ -919,7 +919,7 @@ I3 = Types::Interface[:b, :c]
|
|
|
919
919
|
|
|
920
920
|
TODO: make this a bit more advanced. Check for method arity.
|
|
921
921
|
|
|
922
|
-
|
|
922
|
+
## `Types::Hash`
|
|
923
923
|
|
|
924
924
|
```ruby
|
|
925
925
|
Employee = Types::Hash[
|
|
@@ -988,7 +988,7 @@ User = Types::Hash[name: Types::Static['Joe'], age: Integer]
|
|
|
988
988
|
User.parse(name: 'Rufus', age: 34) # Valid {name: 'Joe', age: 34}
|
|
989
989
|
```
|
|
990
990
|
|
|
991
|
-
|
|
991
|
+
### Optional keys
|
|
992
992
|
|
|
993
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.
|
|
994
994
|
|
|
@@ -1012,7 +1012,7 @@ Types::Hash[
|
|
|
1012
1012
|
]
|
|
1013
1013
|
```
|
|
1014
1014
|
|
|
1015
|
-
|
|
1015
|
+
### Merging hash definitions
|
|
1016
1016
|
|
|
1017
1017
|
Use `Types::Hash#+` to merge two definitions. Keys in the second hash override the first one's.
|
|
1018
1018
|
|
|
@@ -1022,7 +1022,7 @@ Employee = Types::Hash[name: Types::String, company: Types::String]
|
|
|
1022
1022
|
StaffMember = User + Employee # Hash[:name, :age, :company]
|
|
1023
1023
|
```
|
|
1024
1024
|
|
|
1025
|
-
|
|
1025
|
+
### Hash intersections
|
|
1026
1026
|
|
|
1027
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:
|
|
1028
1028
|
|
|
@@ -1047,7 +1047,7 @@ Types::Hash[a: Types::String, _: Types::Any] & Types::Hash[a: Types::String, b:
|
|
|
1047
1047
|
# => Hash[a: String, b: Integer] (:b admitted via the left's catch-all)
|
|
1048
1048
|
```
|
|
1049
1049
|
|
|
1050
|
-
|
|
1050
|
+
### `Types::Hash#tagged_by`
|
|
1051
1051
|
|
|
1052
1052
|
Use `#tagged_by` to resolve what definition to use based on the value of a common key.
|
|
1053
1053
|
|
|
@@ -1064,7 +1064,7 @@ Events = Types::Hash.tagged_by(
|
|
|
1064
1064
|
Events.parse(type: 'name_updated', name: 'Joe') # Uses NameUpdatedEvent definition
|
|
1065
1065
|
```
|
|
1066
1066
|
|
|
1067
|
-
|
|
1067
|
+
### Undeclared keys and the `_` catch-all
|
|
1068
1068
|
|
|
1069
1069
|
By default, keys present in the input but **not** declared in the schema are dropped:
|
|
1070
1070
|
|
|
@@ -1119,7 +1119,7 @@ InputHandler.parse(price: 100_000, name: 'iPhone 15', category: 'smartphones')
|
|
|
1119
1119
|
|
|
1120
1120
|
The catch-all also shows up in generated JSON Schema as `additionalProperties`: `_: Any` → `{}` (anything), `_: Integer` → `{ "type": "integer" }`, and `_: Never` → `{ "not": {} }` (nothing allowed).
|
|
1121
1121
|
|
|
1122
|
-
|
|
1122
|
+
### Typed keys
|
|
1123
1123
|
|
|
1124
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:
|
|
1125
1125
|
|
|
@@ -1131,7 +1131,7 @@ Types::Hash[Types::String[/^id_/] => Types::Integer, # keys matching /^id_/ hold
|
|
|
1131
1131
|
|
|
1132
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".
|
|
1133
1133
|
|
|
1134
|
-
|
|
1134
|
+
### `Types::Hash#filtered`
|
|
1135
1135
|
|
|
1136
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.
|
|
1137
1137
|
|
|
@@ -1141,7 +1141,7 @@ User.parse(name: 'Joe', age: 40) # => { name: 'Joe', age: 40 }
|
|
|
1141
1141
|
User.parse(name: 'Joe', age: 'nope') # => { name: 'Joe' }
|
|
1142
1142
|
```
|
|
1143
1143
|
|
|
1144
|
-
|
|
1144
|
+
## `Types::Range`
|
|
1145
1145
|
|
|
1146
1146
|
`Types::Range` validates that a value is a Ruby `Range`. On its own it accepts any range:
|
|
1147
1147
|
|
|
@@ -1169,7 +1169,7 @@ Percent.resolve(10..20) # valid
|
|
|
1169
1169
|
Percent.resolve(10..200) # invalid (200 is outside 1..100)
|
|
1170
1170
|
```
|
|
1171
1171
|
|
|
1172
|
-
|
|
1172
|
+
### Open-ended ranges with `#where`
|
|
1173
1173
|
|
|
1174
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:
|
|
1175
1175
|
|
|
@@ -1194,7 +1194,7 @@ NonNegativeStart.resolve(5..10) # valid
|
|
|
1194
1194
|
NonNegativeStart.resolve(-5..10) # invalid
|
|
1195
1195
|
```
|
|
1196
1196
|
|
|
1197
|
-
|
|
1197
|
+
### Composition
|
|
1198
1198
|
|
|
1199
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:
|
|
1200
1200
|
|
|
@@ -1205,7 +1205,7 @@ Types::Range[1..10] <= Types::Range[Integer] # true (covariant)
|
|
|
1205
1205
|
Types::Range[Integer] | Types::Range[1..10] # => Range[Integer]
|
|
1206
1206
|
```
|
|
1207
1207
|
|
|
1208
|
-
|
|
1208
|
+
### JSON Schema
|
|
1209
1209
|
|
|
1210
1210
|
A `Types::Range` whose member pins numeric bounds maps to JSON Schema's native keywords, preserving an exclusive end as `exclusiveMaximum`:
|
|
1211
1211
|
|
|
@@ -1214,7 +1214,7 @@ Plumb::JSONSchemaVisitor.call(Types::Range[0...100], root: false)
|
|
|
1214
1214
|
# => { "type" => "integer", "minimum" => 0, "exclusiveMaximum" => 100 }
|
|
1215
1215
|
```
|
|
1216
1216
|
|
|
1217
|
-
|
|
1217
|
+
## `Types::SymbolizedHash`
|
|
1218
1218
|
|
|
1219
1219
|
This type turns a hash's keys into symbols by calling `#to_sym` on them, and returning a new Hash.
|
|
1220
1220
|
|
|
@@ -1233,7 +1233,7 @@ type = Types::Hash[name: String, age: Integer].symbolized
|
|
|
1233
1233
|
type.parse('name' => 'Joe', 'age' => 20) # {name: 'Joe', age: 20}
|
|
1234
1234
|
```
|
|
1235
1235
|
|
|
1236
|
-
|
|
1236
|
+
## maps
|
|
1237
1237
|
|
|
1238
1238
|
You can also use Hash syntax to define a hash map with specific types for all keys and values:
|
|
1239
1239
|
|
|
@@ -1264,7 +1264,7 @@ Use `Types::Value` to validate specific values (using `#==`)
|
|
|
1264
1264
|
names_and_ones = Types::Hash[String, Types::Integer.value(1)]
|
|
1265
1265
|
```
|
|
1266
1266
|
|
|
1267
|
-
|
|
1267
|
+
### `#filtered`
|
|
1268
1268
|
|
|
1269
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.
|
|
1270
1270
|
|
|
@@ -1277,7 +1277,7 @@ S3Config.parse(ENV.to_h) # { 'S3_BUCKET' => 'foo', 'S3_REGION' => 'us-east-1' }
|
|
|
1277
1277
|
|
|
1278
1278
|
|
|
1279
1279
|
|
|
1280
|
-
|
|
1280
|
+
## `Types::Array`
|
|
1281
1281
|
|
|
1282
1282
|
```ruby
|
|
1283
1283
|
names = Types::Array[Types::String.present]
|
|
@@ -1295,7 +1295,7 @@ emails = Types::Array[Types::String[/@/]]
|
|
|
1295
1295
|
|
|
1296
1296
|
Prefer the latter (`Types::Array[Types::String[/@/]]`), as that first validates that each element is a `String` before matching against the regular expression.
|
|
1297
1297
|
|
|
1298
|
-
|
|
1298
|
+
### Chained array maps fuse into a single pass
|
|
1299
1299
|
|
|
1300
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:
|
|
1301
1301
|
|
|
@@ -1336,7 +1336,7 @@ Types::Array[Trim] / Types::Stream[Symbolize]
|
|
|
1336
1336
|
|
|
1337
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.
|
|
1338
1338
|
|
|
1339
|
-
|
|
1339
|
+
### Concurrent arrays
|
|
1340
1340
|
|
|
1341
1341
|
Use `Types::Array#concurrent` to process array elements concurrently (using Concurrent Ruby for now).
|
|
1342
1342
|
|
|
@@ -1359,13 +1359,13 @@ See the [concurrent downloads example](https://github.com/ismasan/plumb/blob/mai
|
|
|
1359
1359
|
|
|
1360
1360
|
TODO: pluggable concurrency engines (Async?)
|
|
1361
1361
|
|
|
1362
|
-
|
|
1362
|
+
### `#stream`
|
|
1363
1363
|
|
|
1364
1364
|
Turn an Array definition into an enumerator that yields each element wrapped in `Result::Valid` or `Result::Invalid`.
|
|
1365
1365
|
|
|
1366
1366
|
See [`Types::Stream`](#typesstream) below for more.
|
|
1367
1367
|
|
|
1368
|
-
|
|
1368
|
+
### `#filtered`
|
|
1369
1369
|
|
|
1370
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.
|
|
1371
1371
|
|
|
@@ -1376,7 +1376,7 @@ j_names.parse(%w[james ismael joe toby joan isabel]) # ["james", "joe", "joan"]
|
|
|
1376
1376
|
|
|
1377
1377
|
|
|
1378
1378
|
|
|
1379
|
-
|
|
1379
|
+
## `Types::Tuple`
|
|
1380
1380
|
|
|
1381
1381
|
```ruby
|
|
1382
1382
|
Status = Types::Symbol.options(%i[ok error])
|
|
@@ -1408,7 +1408,7 @@ NameAndRegex = Types::Tuple[String, Types::Value[/@/]]
|
|
|
1408
1408
|
|
|
1409
1409
|
|
|
1410
1410
|
|
|
1411
|
-
|
|
1411
|
+
## `Types::Stream`
|
|
1412
1412
|
|
|
1413
1413
|
`Types::Stream` defines an enumerator that validates/coerces each element as it iterates.
|
|
1414
1414
|
|
|
@@ -1434,7 +1434,7 @@ end
|
|
|
1434
1434
|
|
|
1435
1435
|
See a more complete the [CSV Stream example](https://github.com/ismasan/plumb/blob/main/examples/csv_stream.rb)
|
|
1436
1436
|
|
|
1437
|
-
|
|
1437
|
+
### `Types::Stream#filtered`
|
|
1438
1438
|
|
|
1439
1439
|
Use `#filtered` to turn a `Types::Stream` into a stream that only yields valid elements.
|
|
1440
1440
|
|
|
@@ -1445,7 +1445,7 @@ ValidElements.parse(data).each do |valid_row|
|
|
|
1445
1445
|
end
|
|
1446
1446
|
```
|
|
1447
1447
|
|
|
1448
|
-
|
|
1448
|
+
### `Types::Array#stream`
|
|
1449
1449
|
|
|
1450
1450
|
A `Types::Array` definition can be turned into a stream.
|
|
1451
1451
|
|
|
@@ -1460,11 +1460,11 @@ Str.parse(data).each do |row|
|
|
|
1460
1460
|
end
|
|
1461
1461
|
```
|
|
1462
1462
|
|
|
1463
|
-
|
|
1463
|
+
## Types::Data
|
|
1464
1464
|
|
|
1465
1465
|
`Types::Data` provides a superclass to define **immutable** structs or value objects with typed / coercible attributes.
|
|
1466
1466
|
|
|
1467
|
-
|
|
1467
|
+
### `[]` Syntax
|
|
1468
1468
|
|
|
1469
1469
|
The `[]` syntax is a short-hand for struct definition.
|
|
1470
1470
|
Like `Plumb::Types::Hash`, suffixing a key with `?` makes it optional.
|
|
@@ -1497,7 +1497,7 @@ PersonHash = Types::Hash[name: String, age?: Integer]
|
|
|
1497
1497
|
PersonStruct = Types::Data[PersonHash]
|
|
1498
1498
|
```
|
|
1499
1499
|
|
|
1500
|
-
|
|
1500
|
+
### `#with`
|
|
1501
1501
|
|
|
1502
1502
|
Note that these instances cannot be mutated (there's no attribute setters), but they can be copied with partial attributes with `#with`
|
|
1503
1503
|
|
|
@@ -1505,7 +1505,7 @@ Note that these instances cannot be mutated (there's no attribute setters), but
|
|
|
1505
1505
|
another_person = person.with(age: 20)
|
|
1506
1506
|
```
|
|
1507
1507
|
|
|
1508
|
-
|
|
1508
|
+
### `.attribute` syntax
|
|
1509
1509
|
|
|
1510
1510
|
This syntax allows defining struct classes with typed attributes, including nested structs.
|
|
1511
1511
|
|
|
@@ -1588,7 +1588,7 @@ Note that this does NOT work with union'd or piped structs.
|
|
|
1588
1588
|
attribute :company, Company | Person do
|
|
1589
1589
|
```
|
|
1590
1590
|
|
|
1591
|
-
|
|
1591
|
+
### Shorthand array syntax
|
|
1592
1592
|
|
|
1593
1593
|
```ruby
|
|
1594
1594
|
attribute :things, [] # Same as attribute :things, Types::Array
|
|
@@ -1605,7 +1605,7 @@ Note that, if you want to match an attribute value against a literal array, you
|
|
|
1605
1605
|
attribute :one_two_three, Types::Array.value[[1, 2, 3]])
|
|
1606
1606
|
```
|
|
1607
1607
|
|
|
1608
|
-
|
|
1608
|
+
### Optional Attributes
|
|
1609
1609
|
|
|
1610
1610
|
Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
|
|
1611
1611
|
|
|
@@ -1613,7 +1613,7 @@ Using `attribute?` allows for optional attributes. If the attribute is not prese
|
|
|
1613
1613
|
attribute? :company, Company
|
|
1614
1614
|
```
|
|
1615
1615
|
|
|
1616
|
-
|
|
1616
|
+
### Before steps, symbolizing keys
|
|
1617
1617
|
|
|
1618
1618
|
The optional `.step` helper adds arbitrary Plumb steps to a Data constructor's internal pipeline.
|
|
1619
1619
|
|
|
@@ -1656,7 +1656,7 @@ person.last_name # => 'BLOGGS'
|
|
|
1656
1656
|
|
|
1657
1657
|
A Data class steps are inherited to its child classes.
|
|
1658
1658
|
|
|
1659
|
-
|
|
1659
|
+
### Inheritance
|
|
1660
1660
|
|
|
1661
1661
|
Data structs can inherit from other structs. This is useful for defining a base struct with common attributes.
|
|
1662
1662
|
|
|
@@ -1670,7 +1670,7 @@ class Person < BasePerson
|
|
|
1670
1670
|
end
|
|
1671
1671
|
```
|
|
1672
1672
|
|
|
1673
|
-
|
|
1673
|
+
### Equality with `#==`
|
|
1674
1674
|
|
|
1675
1675
|
`#==` is implemented to compare attributes, recursively.
|
|
1676
1676
|
|
|
@@ -1680,7 +1680,7 @@ person2 = Person.new(name: 'Joe', age: 20)
|
|
|
1680
1680
|
person1 == person2 # true
|
|
1681
1681
|
```
|
|
1682
1682
|
|
|
1683
|
-
|
|
1683
|
+
### Struct composition
|
|
1684
1684
|
|
|
1685
1685
|
`Types::Data` supports all the composition operators and helpers.
|
|
1686
1686
|
|
|
@@ -1702,7 +1702,7 @@ Payload = Types::Hash[
|
|
|
1702
1702
|
]
|
|
1703
1703
|
```
|
|
1704
1704
|
|
|
1705
|
-
|
|
1705
|
+
### Attribute writers
|
|
1706
1706
|
|
|
1707
1707
|
By default `Types::Data` classes are inmutable, but you can define attribute writers to allow for mutation using the `writer: true` option.
|
|
1708
1708
|
|
|
@@ -1726,7 +1726,7 @@ config.valid? # true
|
|
|
1726
1726
|
config.errors # {}
|
|
1727
1727
|
```
|
|
1728
1728
|
|
|
1729
|
-
|
|
1729
|
+
### Recursive struct definitions
|
|
1730
1730
|
|
|
1731
1731
|
You can use `#defer`. See [recursive types](#recursive-types).
|
|
1732
1732
|
|
|
@@ -1741,11 +1741,11 @@ person.friend.name # 'joan'
|
|
|
1741
1741
|
person.friend.friend # nil
|
|
1742
1742
|
```
|
|
1743
1743
|
|
|
1744
|
-
|
|
1744
|
+
## Plumb::Pipeline
|
|
1745
1745
|
|
|
1746
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.
|
|
1747
1747
|
|
|
1748
|
-
|
|
1748
|
+
### `#pipeline` helper
|
|
1749
1749
|
|
|
1750
1750
|
All plumb steps have a `#pipeline` helper.
|
|
1751
1751
|
|
|
@@ -1777,7 +1777,7 @@ result = CreateUser.resolve(name: 'Joe', age: 40)
|
|
|
1777
1777
|
# result.value => User
|
|
1778
1778
|
```
|
|
1779
1779
|
|
|
1780
|
-
|
|
1780
|
+
#### `#step` (non-strict) and `#step!` (strict)
|
|
1781
1781
|
|
|
1782
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.
|
|
1783
1783
|
|
|
@@ -1811,7 +1811,7 @@ IsJoe = User.check('must be named joe') { |user|
|
|
|
1811
1811
|
CreateIfJoe = IsJoe >> CreateUser
|
|
1812
1812
|
```
|
|
1813
1813
|
|
|
1814
|
-
|
|
1814
|
+
#### `#around`
|
|
1815
1815
|
|
|
1816
1816
|
Use `#around` in a pipeline definition to add a middleware step that wraps all other steps registered.
|
|
1817
1817
|
|
|
@@ -1865,7 +1865,7 @@ pl.around do |step, result|
|
|
|
1865
1865
|
end
|
|
1866
1866
|
```
|
|
1867
1867
|
|
|
1868
|
-
|
|
1868
|
+
### As stand-alone `Plumb::Pipeline` class
|
|
1869
1869
|
|
|
1870
1870
|
`Plumb::Pipeline` can also be used on its own, sub-classed, and it can take class-level `around` middleware.
|
|
1871
1871
|
|
|
@@ -1910,7 +1910,7 @@ pipe = DebuggablePipeline.new do |pl|
|
|
|
1910
1910
|
end
|
|
1911
1911
|
```
|
|
1912
1912
|
|
|
1913
|
-
|
|
1913
|
+
### Pipelines all the way down :turtle:
|
|
1914
1914
|
|
|
1915
1915
|
Pipelines are full Plumb steps, so they can themselves be used as steps.
|
|
1916
1916
|
|
|
@@ -1926,7 +1926,7 @@ Pipe2 = DebuggablePipeline.new do |pl|
|
|
|
1926
1926
|
end
|
|
1927
1927
|
```
|
|
1928
1928
|
|
|
1929
|
-
|
|
1929
|
+
## Recursive types
|
|
1930
1930
|
|
|
1931
1931
|
You can use a proc to defer evaluation of recursive definitions.
|
|
1932
1932
|
|
|
@@ -1959,11 +1959,11 @@ LinkedList = Types::Hash[
|
|
|
1959
1959
|
|
|
1960
1960
|
|
|
1961
1961
|
|
|
1962
|
-
|
|
1962
|
+
## Encoders and Codecs
|
|
1963
1963
|
|
|
1964
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.
|
|
1965
1965
|
|
|
1966
|
-
|
|
1966
|
+
### Defining encoders
|
|
1967
1967
|
|
|
1968
1968
|
An encoder is a class declaring an input and an output type, with `#decode` (input ⇒ output) and `#encode` (output ⇒ input) methods:
|
|
1969
1969
|
|
|
@@ -2003,7 +2003,7 @@ JSONDateRangeEncoder.encode(Date.new(2024, 1, 1)..Date.new(2024, 2, 1))
|
|
|
2003
2003
|
|
|
2004
2004
|
Encoders also express lenient unions — `Types::Date | SomeDateEncoder` accepts a `Date` or decodes a string into one.
|
|
2005
2005
|
|
|
2006
|
-
|
|
2006
|
+
### Codecs
|
|
2007
2007
|
|
|
2008
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`:
|
|
2009
2009
|
|
|
@@ -2091,7 +2091,7 @@ JSONPerson.to_json_schema
|
|
|
2091
2091
|
# "dates" is described as { "type" => "object", "properties" => { "from" => { "type" => "string" }, ... } }
|
|
2092
2092
|
```
|
|
2093
2093
|
|
|
2094
|
-
|
|
2094
|
+
### Codec instances: a registry of pre-built pairs
|
|
2095
2095
|
|
|
2096
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
2097
|
|
|
@@ -2119,7 +2119,22 @@ registry.freeze
|
|
|
2119
2119
|
|
|
2120
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
2121
|
|
|
2122
|
-
|
|
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
|
|
2123
2138
|
|
|
2124
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`).
|
|
2125
2140
|
|
|
@@ -2150,7 +2165,7 @@ Things to know:
|
|
|
2150
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.
|
|
2151
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`.
|
|
2152
2167
|
|
|
2153
|
-
|
|
2168
|
+
## Custom types
|
|
2154
2169
|
|
|
2155
2170
|
Every Plumb type exposes the following one-method interface:
|
|
2156
2171
|
|
|
@@ -2162,7 +2177,7 @@ As long as an object implements this interface, it can be composed into Plumb wo
|
|
|
2162
2177
|
|
|
2163
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.
|
|
2164
2179
|
|
|
2165
|
-
|
|
2180
|
+
### Compose procs or lambdas directly
|
|
2166
2181
|
|
|
2167
2182
|
Piping any `#call` object onto Plumb types wraps your object in a composable step, with all methods necessary for further composition.
|
|
2168
2183
|
|
|
@@ -2170,7 +2185,7 @@ Piping any `#call` object onto Plumb types wraps your object in a composable ste
|
|
|
2170
2185
|
Greeting = Types::String >> ->(result) { result.valid("Hello #{result.value}") }
|
|
2171
2186
|
```
|
|
2172
2187
|
|
|
2173
|
-
|
|
2188
|
+
### `Plumb::Function[input => output]`
|
|
2174
2189
|
|
|
2175
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.
|
|
2176
2191
|
|
|
@@ -2226,7 +2241,7 @@ Either way, `Greeting` is a full Plumb step, which comes with all the Plumb meth
|
|
|
2226
2241
|
LoudGreeting = Greeting.default('no greeting').invoke(:upcase)
|
|
2227
2242
|
```
|
|
2228
2243
|
|
|
2229
|
-
|
|
2244
|
+
### A custom `#call` class
|
|
2230
2245
|
|
|
2231
2246
|
Or write a custom class that responds to `#call(Result::Valid) => Result::Valid | Result::Invalid`
|
|
2232
2247
|
|
|
@@ -2249,7 +2264,7 @@ MyType = Types::String >> Greeting.new('Hola')
|
|
|
2249
2264
|
|
|
2250
2265
|
This is useful when you want to parameterize your custom steps, for example by initialising them with arguments like the example above.
|
|
2251
2266
|
|
|
2252
|
-
|
|
2267
|
+
### Include `Plumb::Composable` to make instance of a class full "steps"
|
|
2253
2268
|
|
|
2254
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.
|
|
2255
2270
|
|
|
@@ -2281,7 +2296,7 @@ Now you can use your class as a composition starting point directly.
|
|
|
2281
2296
|
LoudGreeting = Greeting.new('Hola').default('no greeting').invoke(:upcase)
|
|
2282
2297
|
```
|
|
2283
2298
|
|
|
2284
|
-
|
|
2299
|
+
### Extend a class with `Plumb::Composable` to make the class itself a composable step.
|
|
2285
2300
|
|
|
2286
2301
|
```ruby
|
|
2287
2302
|
class User
|
|
@@ -2296,7 +2311,7 @@ end
|
|
|
2296
2311
|
|
|
2297
2312
|
This is how [Plumb::Types::Data](#typesdata) is implemented.
|
|
2298
2313
|
|
|
2299
|
-
|
|
2314
|
+
### Include `Plumb::Implementation[input => output]` to declare a class' types
|
|
2300
2315
|
|
|
2301
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.
|
|
2302
2317
|
|
|
@@ -2358,7 +2373,7 @@ class AdminFinder < UserFinder
|
|
|
2358
2373
|
end
|
|
2359
2374
|
```
|
|
2360
2375
|
|
|
2361
|
-
|
|
2376
|
+
### Extend `Plumb::Implementation[input => output]` to make the class itself a typed step
|
|
2362
2377
|
|
|
2363
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`.
|
|
2364
2379
|
|
|
@@ -2381,7 +2396,7 @@ The two forms are alternatives — pick one per class. The extended form deliber
|
|
|
2381
2396
|
Plumb::Subtyping.subtype?(ParseUUID, Types::String) # => true
|
|
2382
2397
|
```
|
|
2383
2398
|
|
|
2384
|
-
|
|
2399
|
+
### Participating in subtype & composition checks
|
|
2385
2400
|
|
|
2386
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.
|
|
2387
2402
|
|
|
@@ -2390,7 +2405,7 @@ The default leans on two methods your type already has:
|
|
|
2390
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.
|
|
2391
2406
|
- `#==` — structural equality (provided by `Plumb::Composable`).
|
|
2392
2407
|
|
|
2393
|
-
|
|
2408
|
+
#### The hook
|
|
2394
2409
|
|
|
2395
2410
|
| Hook | Returns | Used by | Default |
|
|
2396
2411
|
| --- | --- | --- | --- |
|
|
@@ -2421,7 +2436,7 @@ even <= Types::Numeric # => true
|
|
|
2421
2436
|
even <= Types::String # => false
|
|
2422
2437
|
```
|
|
2423
2438
|
|
|
2424
|
-
|
|
2439
|
+
#### Type flow: `#input_type` / `#output_type`
|
|
2425
2440
|
|
|
2426
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:
|
|
2427
2442
|
|
|
@@ -2430,7 +2445,7 @@ The `#>>` check (and the [JSON Schema visitor](#json-schema)) ask what a type ac
|
|
|
2430
2445
|
|
|
2431
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.
|
|
2432
2447
|
|
|
2433
|
-
|
|
2448
|
+
## Custom policies
|
|
2434
2449
|
|
|
2435
2450
|
`Plumb.policy` can be used to encapsulate common type compositions, or compositions that can be configurable by parameters.
|
|
2436
2451
|
|
|
@@ -2456,7 +2471,7 @@ The `#policy` helper supports applying multiply policies.
|
|
|
2456
2471
|
Types::String.policy(default_if_nil: 'nothing here', size: (10..20))
|
|
2457
2472
|
```
|
|
2458
2473
|
|
|
2459
|
-
|
|
2474
|
+
### Policies as helper methods
|
|
2460
2475
|
|
|
2461
2476
|
Use the `helper: true` option to register the policy as a method you can call on types directly.
|
|
2462
2477
|
|
|
@@ -2483,7 +2498,7 @@ AccountName = Types::String.admin
|
|
|
2483
2498
|
AccountName.metadata # => { admin: true }
|
|
2484
2499
|
```
|
|
2485
2500
|
|
|
2486
|
-
|
|
2501
|
+
### Type-specific policies
|
|
2487
2502
|
|
|
2488
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.
|
|
2489
2504
|
|
|
@@ -2499,7 +2514,7 @@ Doubled.parse(2) # 4
|
|
|
2499
2514
|
DoubledString = Types::String.multiply_by(2) # raises error
|
|
2500
2515
|
```
|
|
2501
2516
|
|
|
2502
|
-
|
|
2517
|
+
### Interface-specific policies
|
|
2503
2518
|
|
|
2504
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.
|
|
2505
2520
|
|
|
@@ -2515,7 +2530,7 @@ DoubledNumeric = Types::Numeric.multiply_by(2)
|
|
|
2515
2530
|
DoubledMoney = Types::Any[Money].multiply_by(2)
|
|
2516
2531
|
```
|
|
2517
2532
|
|
|
2518
|
-
|
|
2533
|
+
### Self-contained policy modules
|
|
2519
2534
|
|
|
2520
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.
|
|
2521
2536
|
|
|
@@ -2534,7 +2549,7 @@ end
|
|
|
2534
2549
|
Plumb.policy :split, SplitPolicy
|
|
2535
2550
|
```
|
|
2536
2551
|
|
|
2537
|
-
|
|
2552
|
+
## JSON Schema
|
|
2538
2553
|
|
|
2539
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.
|
|
2540
2555
|
|
|
@@ -2604,7 +2619,7 @@ Types::DateTime.to_json_schema
|
|
|
2604
2619
|
# {"type"=>"string", "format"=>"date-time"}
|
|
2605
2620
|
```
|
|
2606
2621
|
|
|
2607
|
-
|
|
2622
|
+
#### Node names for compositions
|
|
2608
2623
|
|
|
2609
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):
|
|
2610
2625
|
|
|
@@ -2617,7 +2632,7 @@ Two-sided compositions report one of four `#node_name`s, depending on whether th
|
|
|
2617
2632
|
|
|
2618
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.
|
|
2619
2634
|
|
|
2620
|
-
|
|
2635
|
+
## Mermaid diagrams
|
|
2621
2636
|
|
|
2622
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).
|
|
2623
2638
|
|
data/lib/plumb/codec.rb
CHANGED
|
@@ -200,8 +200,23 @@ module Plumb
|
|
|
200
200
|
Entry = Data.define(:decoder, :encoder)
|
|
201
201
|
NoEntryError = Class.new(KeyError)
|
|
202
202
|
|
|
203
|
+
# An INSTANCE is a registry of composed [decoder, encoder] pairs, so the
|
|
204
|
+
# per-message path is just #parse. Keys are the app's own — a tag for
|
|
205
|
+
# self-describing payloads, or the type itself:
|
|
206
|
+
#
|
|
207
|
+
# registry = Plumb::Codec::JSON.new do |c|
|
|
208
|
+
# c.register('person.created', Person) # tag -> type
|
|
209
|
+
# c.register(Types::Date) # key defaults to the type
|
|
210
|
+
# end
|
|
211
|
+
# registry.decode('person.created', payload)
|
|
212
|
+
# registry.encode(Types::Date, Date.today)
|
|
213
|
+
#
|
|
214
|
+
# Built with a block it is FROZEN — a closed set, declared at boot. Built
|
|
215
|
+
# without one it stays open and composes type keys on first use, so an app
|
|
216
|
+
# need not enumerate them; #freeze seals it later.
|
|
203
217
|
def initialize(&)
|
|
204
218
|
@entries = {}
|
|
219
|
+
@lock = Mutex.new
|
|
205
220
|
return unless block_given?
|
|
206
221
|
|
|
207
222
|
yield self
|
|
@@ -216,12 +231,14 @@ module Plumb
|
|
|
216
231
|
# Named, not splatted: both sides are types that #parse, so a swapped pair would
|
|
217
232
|
# decode where it should encode without anything raising.
|
|
218
233
|
def register(key, type = key)
|
|
219
|
-
|
|
220
|
-
|
|
234
|
+
raise FrozenError, "#{inspect} is sealed; register before freezing it" if frozen?
|
|
235
|
+
|
|
236
|
+
entry = build_entry(type)
|
|
237
|
+
@lock.synchronize { @entries[key] = entry }
|
|
221
238
|
self
|
|
222
239
|
end
|
|
223
240
|
|
|
224
|
-
def key?(key) =
|
|
241
|
+
def key?(key) = !read(key).nil?
|
|
225
242
|
|
|
226
243
|
def decode(key, payload) = entry(key).decoder.parse(payload)
|
|
227
244
|
def encode(key, payload) = entry(key).encoder.parse(payload)
|
|
@@ -880,7 +897,34 @@ module Plumb
|
|
|
880
897
|
private
|
|
881
898
|
|
|
882
899
|
def entry(key)
|
|
883
|
-
|
|
900
|
+
read(key) || lazy_entry(key) || raise(NoEntryError, "no encoder/decoder registered for #{key}")
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
# Sealed, @entries can never change again, so reads need no synchronization —
|
|
904
|
+
# the shared-global case is also the cheapest one.
|
|
905
|
+
def read(key) = frozen? ? @entries[key] : @lock.synchronize { @entries[key] }
|
|
906
|
+
|
|
907
|
+
# Fill in on demand, so an app need not enumerate every type it exchanges.
|
|
908
|
+
# Only for keys that ARE types: an app-owned tag ('person.created') names
|
|
909
|
+
# nothing the codec could compile, and still raises. Freezing is how an app
|
|
910
|
+
# declares its set closed — a sealed registry never fills in.
|
|
911
|
+
#
|
|
912
|
+
# Keys match by value, so a type literal built fresh per call adds an entry
|
|
913
|
+
# per call. Constants and classes are stable; sealing rules it out entirely.
|
|
914
|
+
def lazy_entry(key)
|
|
915
|
+
return nil if frozen? || !(key.is_a?(Composable) || key.is_a?(::Module))
|
|
916
|
+
|
|
917
|
+
# Composed OUTSIDE the lock, as TypeCache does: holding it across a rewrite
|
|
918
|
+
# would convoy every other thread's lookups behind one cold type. Racing
|
|
919
|
+
# threads both compose; the rewrite is a pure function of the type, so the
|
|
920
|
+
# loser's copy is equivalent and simply dropped.
|
|
921
|
+
entry = build_entry(key)
|
|
922
|
+
@lock.synchronize { @entries[key] ||= entry }
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def build_entry(type)
|
|
926
|
+
decoder, encoder = self.class.for(type)
|
|
927
|
+
Entry.new(decoder:, encoder:)
|
|
884
928
|
end
|
|
885
929
|
end
|
|
886
930
|
end
|
data/lib/plumb/version.rb
CHANGED