plumb 0.0.5 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
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