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 +4 -4
- data/README.md +100 -5
- data/bench/plumb_hash.rb +1 -11
- data/examples/command_objects.rb +0 -3
- data/examples/concurrent_downloads.rb +2 -5
- data/examples/event_registry.rb +1 -4
- data/examples/weekdays.rb +1 -1
- data/lib/plumb/array_class.rb +8 -8
- data/lib/plumb/attributes.rb +6 -1
- data/lib/plumb/composable.rb +67 -17
- data/lib/plumb/hash_class.rb +3 -12
- data/lib/plumb/json_schema_visitor.rb +12 -0
- data/lib/plumb/pipeline.rb +2 -2
- data/lib/plumb/types.rb +18 -0
- data/lib/plumb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82cbbcfabbe2240d11a1957c7d06f59e314d8013c1459ecdd6339404f0c3208e
|
4
|
+
data.tar.gz: a17828a33c296ba50bffbb629a806f732bc3a9f73d974617e4526bfd74065d42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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
|
-
#
|
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
|
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
|
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
|
-
|
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) \
|
data/examples/command_objects.rb
CHANGED
@@ -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::
|
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.
|
data/examples/event_registry.rb
CHANGED
@@ -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.
|
data/lib/plumb/array_class.rb
CHANGED
@@ -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
|
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(
|
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 =
|
67
|
+
element_result = result.dup
|
68
68
|
errors = {}
|
69
|
-
values =
|
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(
|
81
|
+
def map_array_elements(result)
|
82
82
|
errors = {}
|
83
83
|
|
84
|
-
values =
|
85
|
-
|
86
|
-
|
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
|
data/lib/plumb/attributes.rb
CHANGED
@@ -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|
|
data/lib/plumb/composable.rb
CHANGED
@@ -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
|
data/lib/plumb/hash_class.rb
CHANGED
@@ -109,7 +109,7 @@ module Plumb
|
|
109
109
|
|
110
110
|
input = result.value
|
111
111
|
errors = {}
|
112
|
-
field_result =
|
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
|
-
|
143
|
-
|
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
|
data/lib/plumb/pipeline.rb
CHANGED
@@ -37,14 +37,14 @@ module Plumb
|
|
37
37
|
|
38
38
|
attr_reader :children
|
39
39
|
|
40
|
-
def initialize(type
|
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
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.
|
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-
|
11
|
+
date: 2024-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bigdecimal
|