plumb 0.2.0.beta.3 → 0.3.1
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 +23 -14
- data/lib/plumb/and.rb +12 -3
- data/lib/plumb/hash_map.rb +22 -0
- data/lib/plumb/subtyping.rb +5 -2
- data/lib/plumb/types.rb +2 -2
- 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: 3c32b91fda7581018e0febe58953ab0256b9850d9848cfe9ba468ef81d6003e3
|
|
4
|
+
data.tar.gz: 340f1681daffc709e724254a9ee031f1fd0bc12c2cdd80e53f47ce1384e5827c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6e3c5d2bbaeeae28946cbaef50bf65edc9d98815d0546e8d62a69a0097dc889448e8edc68863f44ff922f5a808fbdb1266cc8fde6569bcde52c713c80856df7
|
|
7
|
+
data.tar.gz: ea269dc275559ccb3fb61c94311707881cc11151cbbb433daafb4040218748d33e5461b84be951d73bdc4b5c02ec5ef5622498ea0ac23efa77efcc110411670f
|
data/README.md
CHANGED
|
@@ -96,7 +96,7 @@ StringToInt.parse('100lol') # fails
|
|
|
96
96
|
|
|
97
97
|
### `#resolve(value) => Result`
|
|
98
98
|
|
|
99
|
-
`#resolve` takes an input value and returns a `Result
|
|
99
|
+
`#resolve` takes an input value and returns a `Plumb::Result`, which is either valid or invalid (check with `#valid?` / `#invalid?`).
|
|
100
100
|
|
|
101
101
|
```ruby
|
|
102
102
|
result = Types::Integer.resolve(10)
|
|
@@ -846,14 +846,14 @@ Example: parsing strings with `Date.parse` and turning `Date::Error` exceptions
|
|
|
846
846
|
```ruby
|
|
847
847
|
# Accept a string that can be parsed into a Date
|
|
848
848
|
# via Date.parse
|
|
849
|
-
# If Date.parse raises a Date::Error, return
|
|
849
|
+
# If Date.parse raises a Date::Error, return an invalid Result with
|
|
850
850
|
# the exception's message as error message.
|
|
851
851
|
type = Types::String
|
|
852
852
|
.build(::Date, :parse)
|
|
853
853
|
.policy(:rescue, ::Date::Error)
|
|
854
854
|
|
|
855
|
-
type.resolve('2024-02-02') # => Result
|
|
856
|
-
type.resolve('2024-') # => Result
|
|
855
|
+
type.resolve('2024-02-02') # => valid Result with Date object
|
|
856
|
+
type.resolve('2024-') # => invalid Result with error message
|
|
857
857
|
```
|
|
858
858
|
|
|
859
859
|
The guard keeps the type it wraps: the example above is still a `Date` for subtyping,
|
|
@@ -1216,9 +1216,9 @@ Plumb::JSONSchemaVisitor.call(Types::Range[0...100], root: false)
|
|
|
1216
1216
|
|
|
1217
1217
|
## `Types::SymbolizedHash`
|
|
1218
1218
|
|
|
1219
|
-
This type turns a hash's keys into symbols
|
|
1219
|
+
This type recursively turns a hash's String keys into symbols, returning a new Hash. Symbol keys and any other keys pass through unchanged, so it accepts any Hash.
|
|
1220
1220
|
|
|
1221
|
-
|
|
1221
|
+
You can use it as a _transform_.
|
|
1222
1222
|
|
|
1223
1223
|
```ruby
|
|
1224
1224
|
UserHash = Types::Hash[name: String]
|
|
@@ -1233,6 +1233,13 @@ 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
|
+
Hash maps support `#symbolized` too. It raises `Plumb::TypeError` if the map's key type can't accept Symbols, since every symbolized key would be rejected.
|
|
1237
|
+
|
|
1238
|
+
```ruby
|
|
1239
|
+
Types::Hash[Symbol, Integer].symbolized.parse('a' => 1) # {a: 1}
|
|
1240
|
+
Types::Hash[String, Integer].symbolized # raises Plumb::TypeError
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1236
1243
|
## maps
|
|
1237
1244
|
|
|
1238
1245
|
You can also use Hash syntax to define a hash map with specific types for all keys and values:
|
|
@@ -1361,7 +1368,7 @@ TODO: pluggable concurrency engines (Async?)
|
|
|
1361
1368
|
|
|
1362
1369
|
### `#stream`
|
|
1363
1370
|
|
|
1364
|
-
Turn an Array definition into an enumerator that yields each element wrapped in
|
|
1371
|
+
Turn an Array definition into an enumerator that yields each element wrapped in a valid or invalid `Result`.
|
|
1365
1372
|
|
|
1366
1373
|
See [`Types::Stream`](#typesstream) below for more.
|
|
1367
1374
|
|
|
@@ -1421,7 +1428,7 @@ Row = Types::Tuple[Types::String.present, Types:Lax::Integer]
|
|
|
1421
1428
|
Stream = Types::Stream[Row]
|
|
1422
1429
|
|
|
1423
1430
|
data = CSV.new(File.new('./big-file.csv')).each # An Enumerator
|
|
1424
|
-
# stream is an Enumerator that yields rows wrapped in
|
|
1431
|
+
# stream is an Enumerator that yields rows wrapped in a valid or invalid Result
|
|
1425
1432
|
stream = Stream.parse(data)
|
|
1426
1433
|
stream.each.with_index(1) do |result, line|
|
|
1427
1434
|
if result.valid?
|
|
@@ -1816,7 +1823,7 @@ CreateIfJoe = IsJoe >> CreateUser
|
|
|
1816
1823
|
Use `#around` in a pipeline definition to add a middleware step that wraps all other steps registered.
|
|
1817
1824
|
|
|
1818
1825
|
```ruby
|
|
1819
|
-
# The #around interface is #call(Step, Result
|
|
1826
|
+
# The #around interface is #call(Step, Result) => Result
|
|
1820
1827
|
StepLogger = proc do |step, result|
|
|
1821
1828
|
Logger.info "Processing step #{step}"
|
|
1822
1829
|
step.call(result)
|
|
@@ -2170,12 +2177,14 @@ Things to know:
|
|
|
2170
2177
|
Every Plumb type exposes the following one-method interface:
|
|
2171
2178
|
|
|
2172
2179
|
```
|
|
2173
|
-
#call(Result
|
|
2180
|
+
#call(Result) => Result
|
|
2174
2181
|
```
|
|
2175
2182
|
|
|
2183
|
+
The incoming result is always valid (invalid results halt a pipeline before reaching later steps). Return a valid result to pass a value on, or an invalid one to halt.
|
|
2184
|
+
|
|
2176
2185
|
As long as an object implements this interface, it can be composed into Plumb workflows.
|
|
2177
2186
|
|
|
2178
|
-
|
|
2187
|
+
`Plumb::Result` has helper methods `#valid(value) => Result` and `#invalid(value, errors:) => Result` to facilitate returning valid or invalid results from your own steps. Both return a new `Result` and leave the receiver untouched.
|
|
2179
2188
|
|
|
2180
2189
|
### Compose procs or lambdas directly
|
|
2181
2190
|
|
|
@@ -2243,7 +2252,7 @@ LoudGreeting = Greeting.default('no greeting').invoke(:upcase)
|
|
|
2243
2252
|
|
|
2244
2253
|
### A custom `#call` class
|
|
2245
2254
|
|
|
2246
|
-
Or write a custom class that responds to `#call(Result
|
|
2255
|
+
Or write a custom class that responds to `#call(Result) => Result`
|
|
2247
2256
|
|
|
2248
2257
|
```ruby
|
|
2249
2258
|
class Greeting
|
|
@@ -2252,8 +2261,8 @@ class Greeting
|
|
|
2252
2261
|
end
|
|
2253
2262
|
|
|
2254
2263
|
# The Plumb step interface
|
|
2255
|
-
# @param result [Plumb::Result
|
|
2256
|
-
# @return [Plumb::Result
|
|
2264
|
+
# @param result [Plumb::Result] a valid result
|
|
2265
|
+
# @return [Plumb::Result]
|
|
2257
2266
|
def call(result)
|
|
2258
2267
|
result.valid("#{@gr} #{result.value}")
|
|
2259
2268
|
end
|
data/lib/plumb/and.rb
CHANGED
|
@@ -29,11 +29,20 @@ module Plumb
|
|
|
29
29
|
#
|
|
30
30
|
# Conjunction.build, not Intersection.new — `left.output_type` need not preserve
|
|
31
31
|
# values (a record drops undeclared keys, a Static replaces it), and only the
|
|
32
|
-
# classifier may decide.
|
|
33
|
-
#
|
|
32
|
+
# classifier may decide.
|
|
33
|
+
#
|
|
34
|
+
# A left that IS its own output type (eg. a recursive type) can't be met without
|
|
35
|
+
# recursing forever, so fall back to the right alone: every output passed it
|
|
36
|
+
# unchanged, so it's a sound over-approximation. Unless the right is the Any top,
|
|
37
|
+
# which would opt the chain out of #>> checks; then stay opaque (self).
|
|
34
38
|
@output_type = if Plumb::Subtyping.value_preserving?(right)
|
|
35
39
|
lo = left.output_type
|
|
36
|
-
lo.equal?(left)
|
|
40
|
+
if lo.equal?(left)
|
|
41
|
+
ro = right.output_type
|
|
42
|
+
ro.is_a?(AnyClass) ? self : ro
|
|
43
|
+
else
|
|
44
|
+
Conjunction.build(lo, right)
|
|
45
|
+
end
|
|
37
46
|
else
|
|
38
47
|
right.output_type
|
|
39
48
|
end
|
data/lib/plumb/hash_map.rb
CHANGED
|
@@ -35,6 +35,15 @@ module Plumb
|
|
|
35
35
|
super
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# A map whose key and value types both admit Any is the "any Hash" type, so it
|
|
39
|
+
# claims anything whose base types are all Hashes (eg. `transform(::Hash)`).
|
|
40
|
+
def supertype_of?(other)
|
|
41
|
+
return false unless children.all? { |c| Plumb::Subtyping.subtype?(Types::Any, c) }
|
|
42
|
+
|
|
43
|
+
bases = Plumb.resolve_base_types(other)
|
|
44
|
+
bases.any? && bases.all? { |k| k.is_a?(::Class) && k <= ::Hash }
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
# A HashMap re-maps each key and value through its key/value types, so it
|
|
39
48
|
# preserves the value only when both do (a coercing key or value would change
|
|
40
49
|
# the hash).
|
|
@@ -74,6 +83,19 @@ module Plumb
|
|
|
74
83
|
FilteredHashMap.new(@key_type, @value_type)
|
|
75
84
|
end
|
|
76
85
|
|
|
86
|
+
# Symbolize keys (via Types::SymbolizedHash), then validate against this map.
|
|
87
|
+
# Only the key type is checked up front: a map whose keys can't be Symbols
|
|
88
|
+
# (eg. `Hash[String, X]`) would reject every symbolized key.
|
|
89
|
+
# @see HashClass#symbolized
|
|
90
|
+
# @raise [Plumb::TypeError] when the key type doesn't accept Symbols.
|
|
91
|
+
def symbolized
|
|
92
|
+
unless Plumb::Subtyping.subtype?(Types::Symbol, Plumb::Subtyping.accepted_type(@key_type))
|
|
93
|
+
raise Plumb::TypeError, "cannot symbolize #{inspect}: its key type doesn't accept Symbols"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Types::SymbolizedHash / self
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}]"
|
|
78
100
|
|
|
79
101
|
# Same key/value types as a HashMap (so it inherits #initialize, #children
|
data/lib/plumb/subtyping.rb
CHANGED
|
@@ -72,8 +72,7 @@ module Plumb
|
|
|
72
72
|
ub = unwrap_transparent(b)
|
|
73
73
|
return subtype?(ua, ub) unless ua.equal?(a) && ub.equal?(b)
|
|
74
74
|
|
|
75
|
-
return true if b.is_a?(AnyClass)
|
|
76
|
-
return false if a.is_a?(AnyClass) # Top <= X only when X is Top (handled above)
|
|
75
|
+
return true if b.is_a?(AnyClass) # X <= Top
|
|
77
76
|
|
|
78
77
|
# A value-converting type (Function, or any custom type that opts in) is
|
|
79
78
|
# identified for subtyping by what it *produces*, not what it consumes, so we
|
|
@@ -108,6 +107,10 @@ module Plumb
|
|
|
108
107
|
return b.children.all? { |bb| subtype?(a, bb) } if b.is_a?(Intersection) # a <= (b1 ∧ b2)
|
|
109
108
|
return a.children.any? { |aa| subtype?(aa, b) } if a.is_a?(Intersection) # (a1 ∧ a2) <= b
|
|
110
109
|
|
|
110
|
+
# Top <= X only when X is Top-equivalent. Checked after the join/meet rules
|
|
111
|
+
# so a union with a Top branch (`X | Any`) still admits Top.
|
|
112
|
+
return false if a.is_a?(AnyClass)
|
|
113
|
+
|
|
111
114
|
# `a` decides via its #subtype_of? leaf; if it can't (it doesn't know about
|
|
112
115
|
# `b`), `b` may claim `a` via #supertype_of? — the mirror hook for
|
|
113
116
|
# supertype-driven relations like Interface duck-typing.
|
data/lib/plumb/types.rb
CHANGED
|
@@ -196,8 +196,8 @@ module Plumb
|
|
|
196
196
|
# @example Mixed types preserved
|
|
197
197
|
# SymbolizedHash.parse({ 'count' => 1, 'active' => true }) # => { count: 1, active: true }
|
|
198
198
|
SymbolizedHash = Hash[
|
|
199
|
-
# String keys are converted to symbols
|
|
200
|
-
(Symbol | String.transform(::Symbol, :to_sym)),
|
|
199
|
+
# String keys are converted to symbols; any other key passes through unchanged
|
|
200
|
+
(Symbol | String.transform(::Symbol, :to_sym) | Any),
|
|
201
201
|
# Hash values are recursively symbolized, other types pass through unchanged
|
|
202
202
|
Any.defer { SymbolizedHash } | Any
|
|
203
203
|
]
|
data/lib/plumb/version.rb
CHANGED