plumb 0.0.5 → 0.0.7

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: 896f2ec4a63cde86dd22aaec579696e19c980b09c09e4a4d9d4690f9505f742d
4
- data.tar.gz: 3296480dd0026e8050b624e3e1d65a020fde376a87c4ba269c6481b57c40d19e
3
+ metadata.gz: 82cbbcfabbe2240d11a1957c7d06f59e314d8013c1459ecdd6339404f0c3208e
4
+ data.tar.gz: a17828a33c296ba50bffbb629a806f732bc3a9f73d974617e4526bfd74065d42
5
5
  SHA512:
6
- metadata.gz: 76ecebbe1dc408630107170a213d0145af08bc8d9cec3ef28d66f8aa1eb3fee09352ae89c00aed1eb11b3a52709100f65e6f0a2a43b4a38ed777709f096f9198
7
- data.tar.gz: 0cdbf9c24900fdbcd8393dc4f1d260063b303cc44cf84caa86bdb4d34a6edf77e1f75e431481121b9004c7fbac695f3021b4a7be965c34c1d4b96146f17c2c4c
6
+ metadata.gz: 28ce9c69dcfa1f5d1129745301164400211c1df5707b851a2457f4361b59ed0ad73472851ae2993267f3d74389a6477ded4a60ce3eaed70472c041fad03df7bd
7
+ data.tar.gz: 45e8f649c646b3236c6e7e5c3c86ed7493b88fc544585e5dc168675c69660b7ca217b5508a834b2dfa1820f97ca87b4edd9b988d17effb82697da28273545e2d
data/README.md CHANGED
@@ -135,7 +135,7 @@ joe = User.parse({ name: 'Joe', email: 'joe@email.com', age: 20}) # returns vali
135
135
  Users.parse([joe]) # returns valid array of user hashes
136
136
  ```
137
137
 
138
- More about [Types::Array](#typeshash) and [Types::Array](#typesarray). There's also [tuples](#typestuple), [hash maps](#hash-maps) and [data structs](#typesdata), and it's possible to create your own composite types.
138
+ More about [Types::Hash](#typeshash) and [Types::Array](#typesarray). There's also [tuples](#typestuple), [hash maps](#hash-maps), [data structs](#typesdata) and [streams](#typesstream), and it's possible to create your own composite types.
139
139
 
140
140
  ### Type composition
141
141
 
@@ -235,6 +235,10 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
235
235
  * `Types::UUID::V4`
236
236
  * `Types::Email`
237
237
  * `Types::Date`
238
+ * `Types::Time`
239
+ * `Types::URI::Generic`
240
+ * `Types::URI::HTTP`
241
+ * `Types::URI::File`
238
242
  * `Types::Lax::Integer`
239
243
  * `Types::Lax::String`
240
244
  * `Types::Lax::Symbol`
@@ -243,6 +247,10 @@ You can see more use cases in [the examples directory](https://github.com/ismasa
243
247
  * `Types::Forms::True`
244
248
  * `Types::Forms::False`
245
249
  * `Types::Forms::Date`
250
+ * `Types::Forms::Time`
251
+ * `Types::Forms::URI::Generic`
252
+ * `Types::Forms::URI::HTTP`
253
+ * `Types::Forms::URI::File`
246
254
 
247
255
  TODO: datetime, others.
248
256
 
@@ -447,6 +455,71 @@ All scalar types support this:
447
455
  ten = Types::Integer.value(10)
448
456
  ```
449
457
 
458
+ #### `#static`
459
+
460
+ A type that always returns a valid, static value, regardless of input.
461
+
462
+ ```ruby
463
+ ten = Types::Integer.static(10)
464
+ ten.parse(10) # => 10
465
+ ten.parse(100) # => 10
466
+ ten.parse('hello') # => 10
467
+ ten.parse() # => 10
468
+ ten.metadata[:type] # => Integer
469
+ ```
470
+
471
+ Useful for data structures where some fields shouldn't change. Example:
472
+
473
+ ```ruby
474
+ CreateUserEvent = Types::Hash[
475
+ type: Types::String.static('CreateUser'),
476
+ name: String,
477
+ age: Integer
478
+ ]
479
+ ```
480
+
481
+ Note that the value must be of the same type as the starting step's target type.
482
+
483
+ ```ruby
484
+ Types::Integer.static('nope') # raises ArgumentError
485
+ ```
486
+
487
+ This usage is similar as using `Types::Static['hello']`directly.
488
+
489
+ This helper is shorthand for the following composition:
490
+
491
+ ```ruby
492
+ Types::Static[value] >> step
493
+ ```
494
+
495
+ This means that validations and coercions in the original step are still applied to the static value.
496
+
497
+ ```ruby
498
+ ten = Types::Integer[100..].static(10)
499
+ ten.parse # => Plumb::ParseError "Must be within 100..."
500
+ ```
501
+
502
+ So, normally you'd only use this attached to primitive types without further processing (but your use case may vary).
503
+
504
+ #### `#generate`
505
+
506
+ Passing a proc will evaluate the proc on every invocation. Use this for generated values.
507
+
508
+ ```ruby
509
+ random_number = Types::Numeric.static { rand }
510
+ random_number.parse # 0.32332
511
+ random_number.parse('foo') # 0.54322 etc
512
+ ```
513
+
514
+ Note that the type of generated value must match the initial step's type, validated at invocation.
515
+
516
+ ```ruby
517
+ random_number = Types::String.static { rand } # this won't raise an error here
518
+ random_number.parse # raises Plumb::ParseError because `rand` is not a String
519
+ ```
520
+
521
+ You can also pass any `#call() => Object` interface as a generator, instead of a proc.
522
+
450
523
  #### `#metadata`
451
524
 
452
525
  Add metadata to a type
@@ -824,13 +897,15 @@ Images = Types::Array[ImageDownload].concurrent
824
897
  Images.parse(['https://images.com/1.png', 'https://images.com/2.png'])
825
898
  ```
826
899
 
900
+ See the [concurrent downloads example](https://github.com/ismasan/plumb/blob/main/examples/concurrent_downloads.rb).
901
+
827
902
  TODO: pluggable concurrency engines (Async?)
828
903
 
829
904
  #### `#stream`
830
905
 
831
906
  Turn an Array definition into an enumerator that yields each element wrapped in `Result::Valid` or `Result::Invalid`.
832
907
 
833
- See `Types::Stream` below for more.
908
+ See [`Types::Stream`](#typesstream) below for more.
834
909
 
835
910
  #### `#filtered`
836
911
 
@@ -899,6 +974,8 @@ stream.each.with_index(1) do |result, line|
899
974
  end
900
975
  ```
901
976
 
977
+ See a more complete the [CSV Stream example](https://github.com/ismasan/plumb/blob/main/examples/csv_stream.rb)
978
+
902
979
  #### `Types::Stream#filtered`
903
980
 
904
981
  Use `#filtered` to turn a `Types::Stream` into a stream that only yields valid elements.
@@ -1053,7 +1130,25 @@ Note that this does NOT work with union'd or piped structs.
1053
1130
  attribute :company, Company | Person do
1054
1131
  ```
1055
1132
 
1133
+ #### Shorthand array syntax
1134
+
1135
+ ```ruby
1136
+ attribute :things, [] # Same as attribute :things, Types::Array
1137
+ attribute :numbers, [Integer] # Same as attribute :numbers, Types::Array[Integer]
1138
+ attribute :people, [Person] # same as attribute :people, Types::Array[Person]
1139
+ attribute :friends, [Person] do # same as attribute :friends, Types::Array[Person] do...
1140
+ attribute :phone_number, Integer
1141
+ end
1142
+ ```
1143
+
1144
+ Note that, if you want to match an attribute value against a literal array, you need to use `#value`
1145
+
1146
+ ```ruby
1147
+ attribute :one_two_three, Types::Array.value[[1, 2, 3]])
1148
+ ```
1149
+
1056
1150
  #### Optional Attributes
1151
+
1057
1152
  Using `attribute?` allows for optional attributes. If the attribute is not present, these attribute values will be `nil`
1058
1153
 
1059
1154
  ```ruby
@@ -1149,7 +1244,7 @@ CreateUser = User.pipeline do |pl|
1149
1244
  end
1150
1245
  end
1151
1246
 
1152
- # User normally as any other Plumb step
1247
+ # Use normally as any other Plumb step
1153
1248
  result = CreateUser.resolve(name: 'Joe', age: 40)
1154
1249
  # result.valid?
1155
1250
  # result.errors
@@ -1189,7 +1284,7 @@ end
1189
1284
  Note that order matters: an _around_ step will only wrap steps registered _after it_.
1190
1285
 
1191
1286
  ```ruby
1192
- # This step will not be wrapper by StepLogger
1287
+ # This step will not be wrapped by StepLogger
1193
1288
  pl.step Step1
1194
1289
 
1195
1290
  pl.around StepLogger
@@ -1227,7 +1322,7 @@ end
1227
1322
  ```ruby
1228
1323
  class LoggedPipeline < Plumb::Pipeline
1229
1324
  # class-level midleware will be inherited by sub-classes
1230
- around StepLogged
1325
+ around StepLogger
1231
1326
  end
1232
1327
 
1233
1328
  # Subclass inherits class-level middleware stack,
data/bench/plumb_hash.rb CHANGED
@@ -12,22 +12,12 @@ module PlumbHash
12
12
  BLANK_STRING = ''
13
13
  MONEY_EXP = /(\W{1}|\w{3})?[\d+,.]/
14
14
 
15
- PARSE_DATE = proc do |result|
16
- date = ::Date.parse(result.value)
17
- result.valid(date)
18
- rescue ::Date::Error
19
- result.invalid(errors: 'invalid date')
20
- end
21
-
22
15
  PARSE_MONEY = proc do |result|
23
16
  value = Monetize.parse!(result.value.to_s.gsub(',', ''))
24
17
  result.valid(value)
25
18
  end
26
19
 
27
- Date = Any[::Date] \
28
- | (String[MONEY_EXP] >> PARSE_DATE)
29
-
30
- BlankStringOrDate = Forms::Nil | Date
20
+ BlankStringOrDate = Forms::Nil | Forms::Date
31
21
 
32
22
  Money = Any[::Money] \
33
23
  | (String.present >> PARSE_MONEY) \
@@ -26,9 +26,6 @@ module Types
26
26
  # Turn integers into Money objects (requires the money gem)
27
27
  Amount = Integer.build(Money)
28
28
 
29
- # A naive email check
30
- Email = String[/\w+@\w+\.\w+/]
31
-
32
29
  # A valid customer type
33
30
  Customer = Hash[
34
31
  name: String.present,
@@ -12,9 +12,6 @@ require 'digest/md5'
12
12
  module Types
13
13
  include Plumb::Types
14
14
 
15
- # Turn a string into an URI
16
- URL = String[/^https?:/].build(::URI, :parse)
17
-
18
15
  # a Struct to hold image data
19
16
  Image = ::Data.define(:url, :io)
20
17
 
@@ -24,7 +21,7 @@ module Types
24
21
  # required by all Plumb steps.
25
22
  # URI => Image
26
23
  Download = Plumb::Step.new do |result|
27
- io = URI.open(result.value)
24
+ io = ::URI.open(result.value)
28
25
  result.valid(Image.new(result.value.to_s, io))
29
26
  end
30
27
 
@@ -81,7 +78,7 @@ cache = Types::Cache.new('./examples/data/downloads')
81
78
  # 1). Take a valid URL string.
82
79
  # 2). Attempt reading the file from the cache. Return that if it exists.
83
80
  # 3). Otherwise, download the file from the internet and write it to the cache.
84
- IdempotentDownload = Types::URL >> (cache.read | (Types::Download >> cache.write))
81
+ IdempotentDownload = Types::Forms::URI::HTTP >> (cache.read | (Types::Download >> cache.write))
85
82
 
86
83
  # An array of downloadable images,
87
84
  # marked as concurrent so that all IO operations are run in threads.
@@ -14,9 +14,6 @@ module Types
14
14
  # Turn an ISO8601 string into a Time object
15
15
  ISOTime = String.build(::Time, :parse).policy(:rescue, ArgumentError)
16
16
 
17
- # A type that can be a Time object or an ISO8601 string >> Time
18
- Time = Any[::Time] | ISOTime
19
-
20
17
  # A UUID string, or generate a new one
21
18
  AutoUUID = UUID::V4.default { SecureRandom.uuid }
22
19
  end
@@ -60,7 +57,7 @@ class Event < Types::Data
60
57
  attribute :id, Types::AutoUUID
61
58
  attribute :stream_id, Types::String.present
62
59
  attribute :type, Types::String
63
- attribute(:created_at, Types::Time.default { ::Time.now })
60
+ attribute(:created_at, Types::Forms::Time.default { ::Time.now })
64
61
  attribute? :causation_id, Types::UUID::V4
65
62
  attribute? :correlation_id, Types::UUID::V4
66
63
  attribute :payload, Types::Static[nil]
data/examples/weekdays.rb CHANGED
@@ -4,7 +4,7 @@ require 'bundler'
4
4
  Bundler.setup(:examples)
5
5
  require 'plumb'
6
6
 
7
- # bundle exec examples/weekdays.rb
7
+ # bundle exec ruby examples/weekdays.rb
8
8
  #
9
9
  # Data types to represent and parse an array of days of the week.
10
10
  # Input data can be an array of day names or numbers, ex.
@@ -45,7 +45,7 @@ module Plumb
45
45
  def call(result)
46
46
  return result.invalid(errors: 'is not an Array') unless ::Array === result.value
47
47
 
48
- values, errors = map_array_elements(result.value)
48
+ values, errors = map_array_elements(result)
49
49
  return result.valid(values) unless errors.any?
50
50
 
51
51
  result.invalid(values, errors:)
@@ -59,14 +59,14 @@ module Plumb
59
59
  %(Array[#{element_type}])
60
60
  end
61
61
 
62
- def map_array_elements(list)
62
+ def map_array_elements(result)
63
63
  # Reuse the same result object for each element
64
64
  # to decrease object allocation.
65
65
  # Steps might return the same result instance, so we map the values directly
66
66
  # separate from the errors.
67
- element_result = BLANK_RESULT.dup
67
+ element_result = result.dup
68
68
  errors = {}
69
- values = list.map.with_index do |e, idx|
69
+ values = result.value.map.with_index do |e, idx|
70
70
  re = element_type.call(element_result.reset(e))
71
71
  errors[idx] = re.errors unless re.valid?
72
72
  re.value
@@ -78,12 +78,12 @@ module Plumb
78
78
  class ConcurrentArrayClass < self
79
79
  private
80
80
 
81
- def map_array_elements(list)
81
+ def map_array_elements(result)
82
82
  errors = {}
83
83
 
84
- values = list
85
- .map { |e| Concurrent::Future.execute { element_type.resolve(e) } }
86
- .map.with_index do |f, idx|
84
+ values = result.value
85
+ .map { |e| Concurrent::Future.execute { element_type.resolve(e) } }
86
+ .map.with_index do |f, idx|
87
87
  re = f.value
88
88
  errors[idx] = f.reason if f.rejected?
89
89
  re.value
@@ -160,10 +160,12 @@ module Plumb
160
160
 
161
161
  @errors = BLANK_HASH
162
162
  result = self.class._schema.resolve(attrs.to_h)
163
- @attributes = result.value
163
+ @attributes = prepare_attributes(result.value)
164
164
  @errors = result.errors unless result.valid?
165
165
  end
166
166
 
167
+ def prepare_attributes(attrs) = attrs
168
+
167
169
  module ClassMethods
168
170
  def _schema
169
171
  @_schema ||= HashClass.new
@@ -205,12 +207,15 @@ module Plumb
205
207
  # attribute(:name, String)
206
208
  # attribute(:friends, Types::Array) { attribute(:name, String) }
207
209
  # attribute(:friends, Types::Array) # same as Types::Array[Types::Any]
210
+ # attribute(:friends, []) # same as Types::Array[Types::Any]
208
211
  # attribute(:friends, Types::Array[Person])
212
+ # attribute(:friends, [Person])
209
213
  #
210
214
  def attribute(name, type = Types::Any, &block)
211
215
  key = Key.wrap(name)
212
216
  name = key.to_sym
213
217
  type = Composable.wrap(type)
218
+
214
219
  if block_given? # :foo, Array[Data] or :foo, Struct
215
220
  type = __plumb_struct_class__ if type == Types::Any
216
221
  type = Plumb.decorate(type) do |node|
@@ -84,6 +84,26 @@ module Plumb
84
84
  def node_name = self.class.name.split('::').last.to_sym
85
85
  end
86
86
 
87
+ # Override #=== and #== for Composable instances.
88
+ # but only when included in classes, not extended.
89
+ module Equality
90
+ # `#===` equality. So that Plumb steps can be used in case statements and pattern matching.
91
+ # @param other [Object]
92
+ # @return [Boolean]
93
+ def ===(other)
94
+ case other
95
+ when Composable
96
+ other == self
97
+ else
98
+ resolve(other).valid?
99
+ end
100
+ end
101
+
102
+ def ==(other)
103
+ other.is_a?(self.class) && other.respond_to?(:children) && other.children == children
104
+ end
105
+ end
106
+
87
107
  #  Composable mixes in composition methods to classes.
88
108
  # such as #>>, #|, #not, and others.
89
109
  # Any Composable class can participate in Plumb compositions.
@@ -95,11 +115,13 @@ module Plumb
95
115
  # not extending classes with it.
96
116
  def self.included(base)
97
117
  base.send(:include, Naming)
118
+ base.send(:include, Equality)
98
119
  end
99
120
 
100
121
  # Wrap an object in a Composable instance.
101
122
  # Anything that includes Composable is a noop.
102
123
  # A Hash is assumed to be a HashClass schema.
124
+ # An Array with zero or 1 element is assumed to be an ArrayClass.
103
125
  # Any `#call(Result) => Result` interface is wrapped in a Step.
104
126
  # Anything else is assumed to be something you want to match against via `#===`.
105
127
  #
@@ -115,6 +137,16 @@ module Plumb
115
137
  callable
116
138
  elsif callable.is_a?(::Hash)
117
139
  HashClass.new(schema: callable)
140
+ elsif callable.is_a?(::Array)
141
+ element_type = case callable.size
142
+ when 0
143
+ Types::Any
144
+ when 1
145
+ callable.first
146
+ else
147
+ raise ArgumentError, '[element_type] syntax allows a single element type'
148
+ end
149
+ Types::Array[element_type]
118
150
  elsif callable.respond_to?(:call)
119
151
  Step.new(callable)
120
152
  else
@@ -293,22 +325,6 @@ module Plumb
293
325
  end
294
326
  end
295
327
 
296
- # `#===` equality. So that Plumb steps can be used in case statements and pattern matching.
297
- # @param other [Object]
298
- # @return [Boolean]
299
- def ===(other)
300
- case other
301
- when Composable
302
- other == self
303
- else
304
- resolve(other).valid?
305
- end
306
- end
307
-
308
- def ==(other)
309
- other.is_a?(self.class) && other.respond_to?(:children) && other.children == children
310
- end
311
-
312
328
  # Visitors expect a #node_name and #children interface.
313
329
  # @return [Array<Composable>]
314
330
  def children = BLANK_ARRAY
@@ -330,6 +346,40 @@ module Plumb
330
346
  self >> Build.new(cns, factory_method:, &block)
331
347
  end
332
348
 
349
+ # Always return a static value, regardless of the input.
350
+ # @example
351
+ # type = Types::Integer.static(10)
352
+ # type.parse(10) # => 10
353
+ # type.parse(100) # => 10
354
+ # type.parse # => 10
355
+ #
356
+ # @param value [Object]
357
+ # @return [And]
358
+ def static(value)
359
+ my_type = Array(metadata[:type]).first
360
+ unless my_type.nil? || value.instance_of?(my_type)
361
+ raise ArgumentError,
362
+ "can't set a static #{value.class} value for a #{my_type} step"
363
+ end
364
+
365
+ StaticClass.new(value) >> self
366
+ end
367
+
368
+ # Return the output of a block or #call interface, regardless of input.
369
+ # The block will be called to get the value, on every invocation.
370
+ # @example
371
+ # now = Types::Integer.generate { Time.now.to_i }
372
+ #
373
+ # @param generator [#call, nil] a callable that will be applied to the value, or nil if block
374
+ # @param block [Proc] a block that will be applied to the value, or nil if callable
375
+ # @return [And]
376
+ def generate(generator = nil, &block)
377
+ generator ||= block
378
+ raise ArgumentError, 'expected a generator' unless generator.respond_to?(:call)
379
+
380
+ Step.new(->(r) { r.valid(generator.call) }, 'generator') >> self
381
+ end
382
+
333
383
  # Build a Plumb::Pipeline with this object as the starting step.
334
384
  # @example
335
385
  # pipe = Types::Data[name: String].pipeline do |pl|
@@ -340,7 +390,7 @@ module Plumb
340
390
  #
341
391
  # @return [Pipeline]
342
392
  def pipeline(&block)
343
- Pipeline.new(self, &block)
393
+ Pipeline.new(type: self, &block)
344
394
  end
345
395
 
346
396
  def to_s
@@ -109,7 +109,7 @@ module Plumb
109
109
 
110
110
  input = result.value
111
111
  errors = {}
112
- field_result = BLANK_RESULT.dup
112
+ field_result = result.dup
113
113
  initial = {}
114
114
  initial = initial.merge(input) if @inclusive
115
115
  output = _schema.each.with_object(initial) do |(key, field), ret|
@@ -139,17 +139,8 @@ module Plumb
139
139
  end
140
140
 
141
141
  def wrap_keys_and_values(hash)
142
- case hash
143
- when ::Array
144
- hash.map { |e| wrap_keys_and_values(e) }
145
- when ::Hash
146
- hash.each.with_object({}) do |(k, v), ret|
147
- ret[Key.wrap(k)] = wrap_keys_and_values(v)
148
- end
149
- when Callable
150
- hash
151
- else #  leaf values
152
- Composable.wrap(hash)
142
+ hash.each.with_object({}) do |(k, v), ret|
143
+ ret[Key.wrap(k)] = Composable.wrap(v)
153
144
  end
154
145
  end
155
146
 
@@ -255,6 +255,18 @@ module Plumb
255
255
  props.merge(TYPE => 'string', FORMAT => 'date')
256
256
  end
257
257
 
258
+ on(::URI::Generic) do |_node, props|
259
+ props.merge(TYPE => 'string', FORMAT => 'uri')
260
+ end
261
+
262
+ on(::URI::HTTP) do |_node, props|
263
+ props.merge(TYPE => 'string', FORMAT => 'uri')
264
+ end
265
+
266
+ on(::URI::File) do |_node, props|
267
+ props.merge(TYPE => 'string', FORMAT => 'uri')
268
+ end
269
+
258
270
  on(::Hash) do |_node, props|
259
271
  props.merge(TYPE => 'object')
260
272
  end
@@ -37,14 +37,14 @@ module Plumb
37
37
 
38
38
  attr_reader :children
39
39
 
40
- def initialize(type = Types::Any, &setup)
40
+ def initialize(type: Types::Any, freeze_after: true, &setup)
41
41
  @type = type
42
42
  @children = [type].freeze
43
43
  @around_blocks = self.class.around_blocks.dup
44
44
  return unless block_given?
45
45
 
46
46
  configure(&setup)
47
- freeze
47
+ freeze if freeze_after
48
48
  end
49
49
 
50
50
  def call(result)
data/lib/plumb/types.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require 'bigdecimal'
4
4
  require 'uri'
5
5
  require 'date'
6
+ require 'time'
6
7
 
7
8
  module Plumb
8
9
  # Define core policies
@@ -161,11 +162,18 @@ module Plumb
161
162
  Interface = InterfaceClass.new
162
163
  Email = String[URI::MailTo::EMAIL_REGEXP].as_node(:email)
163
164
  Date = Any[::Date]
165
+ Time = Any[::Time]
164
166
 
165
167
  module UUID
166
168
  V4 = String[/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i].as_node(:uuid)
167
169
  end
168
170
 
171
+ module URI
172
+ Generic = Any[::URI::Generic]
173
+ HTTP = Any[::URI::HTTP]
174
+ File = Any[::URI::File]
175
+ end
176
+
169
177
  class Data
170
178
  extend Composable
171
179
  include Plumb::Attributes
@@ -214,6 +222,16 @@ module Plumb
214
222
  # Accept a Date, or a string that can be parsed into a Date
215
223
  # via Date.parse
216
224
  Date = Date | (String >> Any.build(::Date, :parse).policy(:rescue, ::Date::Error))
225
+ Time = Time | (String >> Any.build(::Time, :parse).policy(:rescue, ::ArgumentError))
226
+
227
+ # Turn strings into different URI types
228
+ module URI
229
+ # URI.parse is very permisive - a blank string is valid.
230
+ # We want to ensure that a generic URI at least starts with a scheme as per RFC 3986
231
+ Generic = Types::URI::Generic | (String[/^([a-z][a-z0-9+\-.]*)/].build(::URI, :parse))
232
+ HTTP = Generic[::URI::HTTP]
233
+ File = Generic[::URI::File]
234
+ end
217
235
  end
218
236
  end
219
237
  end
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.0.5'
4
+ VERSION = '0.0.7'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plumb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-30 00:00:00.000000000 Z
11
+ date: 2024-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal