plumb 0.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26a0c552d1280339792c3acf480302056f203a6ac3a1a19a548d977b72bb581f
4
- data.tar.gz: 325d6d8e267e107c6a2a7c28dddf696ed8198595fe0bc13af76b53648531dd1a
3
+ metadata.gz: 3c32b91fda7581018e0febe58953ab0256b9850d9848cfe9ba468ef81d6003e3
4
+ data.tar.gz: 340f1681daffc709e724254a9ee031f1fd0bc12c2cdd80e53f47ce1384e5827c
5
5
  SHA512:
6
- metadata.gz: aff0c3be749f252774c753074d87fe1b6f169a0ef3f118013b54100a3d0482283745b664f90a39c44b9af664dbd5d0d9cda9f63b86867236e178642bb026933d
7
- data.tar.gz: 6e8383986cf0bd2ee8e429adb13dd0440bf634536c3db745910fab84a6aac67b7130a6e90e59edca539cc9f22b661937c2c9feb9fa9f854467c086731189b37b
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::Valid` or `Result::Invalid`
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 a Result::Invalid with
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::Valid with Date object
856
- type.resolve('2024-') # => Result::Invalid with error message
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 by calling `#to_sym` on them, and returning a new Hash.
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
- `SymbolizedHash` is a `Symbol => Any` map. You can use it as a _transform_.
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 `Result::Valid` or `Result::Invalid`.
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[Result::Valid] or [Result::Invalid]
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::Valid) => Result::Valid | Result::Invalid
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::Valid) => Result::Valid | Result::Invalid
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
- 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.
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::Valid) => Result::Valid | Result::Invalid`
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::Valid]
2256
- # @return [Plumb::Result::Valid, Plumb::Result::Invalid]
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. The `lo.equal?(left)` guard is the fixpoint: a left that
33
- # IS its own output type would otherwise recurse forever.
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) ? self : Conjunction.build(lo, right)
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
@@ -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
@@ -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) # X <= Top
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, existing symbols are preserved
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plumb
4
- VERSION = '0.3.0'
4
+ VERSION = '0.3.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis