plumb 0.0.6 → 0.0.8
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 +105 -0
- data/examples/event_registry.rb +1 -4
- data/lib/plumb/array_class.rb +8 -8
- data/lib/plumb/attributes.rb +37 -12
- data/lib/plumb/composable.rb +56 -17
- data/lib/plumb/hash_class.rb +1 -1
- data/lib/plumb/not.rb +9 -3
- data/lib/plumb/pipeline.rb +2 -2
- data/lib/plumb/types.rb +1 -0
- data/lib/plumb/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7e5d0ac088968506a61d63da75f4010b050579c873d79604a25dfe0b133d3726
|
4
|
+
data.tar.gz: d559c1a3964544184258043e14d0bf9e3b41ff743a8117eccee09be2c22b084a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1207b06d90baa9833cf39737f2697fb6daf9423d94ca531a99307de561b70415bb6c9ec7fb2394af6c1f8b3f86015996ebfde5db6dc3fa2aadd5284fb1867cb2
|
7
|
+
data.tar.gz: 19131a0a289b8c5082ceb3a9cd716cdabb2d12466bba76ecc5d20c2efd6db3fbb9c04d13c8706996225189de50d81c5d8325fc5527ecc8395c492430fd17393d
|
data/README.md
CHANGED
@@ -294,6 +294,22 @@ NotEmail.parse('hello') # "hello"
|
|
294
294
|
NotEmail.parse('hello@server.com') # error
|
295
295
|
```
|
296
296
|
|
297
|
+
`#not` can also be given a type as argument, which might read better:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
Types::Any.not(nil)
|
301
|
+
Types::Any.not(Types::Email)
|
302
|
+
```
|
303
|
+
|
304
|
+
Finally, you can use `Types::Not` for the same effect.
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
NotNil = Types::Not[nil]
|
308
|
+
NotNil.parse(1) # 1
|
309
|
+
NotNil.parse('hello') # 'hello'
|
310
|
+
NotNil.parse(nil) # error
|
311
|
+
```
|
312
|
+
|
297
313
|
#### `#options`
|
298
314
|
|
299
315
|
Sets allowed options for value.
|
@@ -455,6 +471,71 @@ All scalar types support this:
|
|
455
471
|
ten = Types::Integer.value(10)
|
456
472
|
```
|
457
473
|
|
474
|
+
#### `#static`
|
475
|
+
|
476
|
+
A type that always returns a valid, static value, regardless of input.
|
477
|
+
|
478
|
+
```ruby
|
479
|
+
ten = Types::Integer.static(10)
|
480
|
+
ten.parse(10) # => 10
|
481
|
+
ten.parse(100) # => 10
|
482
|
+
ten.parse('hello') # => 10
|
483
|
+
ten.parse() # => 10
|
484
|
+
ten.metadata[:type] # => Integer
|
485
|
+
```
|
486
|
+
|
487
|
+
Useful for data structures where some fields shouldn't change. Example:
|
488
|
+
|
489
|
+
```ruby
|
490
|
+
CreateUserEvent = Types::Hash[
|
491
|
+
type: Types::String.static('CreateUser'),
|
492
|
+
name: String,
|
493
|
+
age: Integer
|
494
|
+
]
|
495
|
+
```
|
496
|
+
|
497
|
+
Note that the value must be of the same type as the starting step's target type.
|
498
|
+
|
499
|
+
```ruby
|
500
|
+
Types::Integer.static('nope') # raises ArgumentError
|
501
|
+
```
|
502
|
+
|
503
|
+
This usage is similar as using `Types::Static['hello']`directly.
|
504
|
+
|
505
|
+
This helper is shorthand for the following composition:
|
506
|
+
|
507
|
+
```ruby
|
508
|
+
Types::Static[value] >> step
|
509
|
+
```
|
510
|
+
|
511
|
+
This means that validations and coercions in the original step are still applied to the static value.
|
512
|
+
|
513
|
+
```ruby
|
514
|
+
ten = Types::Integer[100..].static(10)
|
515
|
+
ten.parse # => Plumb::ParseError "Must be within 100..."
|
516
|
+
```
|
517
|
+
|
518
|
+
So, normally you'd only use this attached to primitive types without further processing (but your use case may vary).
|
519
|
+
|
520
|
+
#### `#generate`
|
521
|
+
|
522
|
+
Passing a proc will evaluate the proc on every invocation. Use this for generated values.
|
523
|
+
|
524
|
+
```ruby
|
525
|
+
random_number = Types::Numeric.generate { rand }
|
526
|
+
random_number.parse # 0.32332
|
527
|
+
random_number.parse('foo') # 0.54322 etc
|
528
|
+
```
|
529
|
+
|
530
|
+
Note that the type of generated value must match the initial step's type, validated at invocation.
|
531
|
+
|
532
|
+
```ruby
|
533
|
+
random_number = Types::String.generate { rand } # this won't raise an error here
|
534
|
+
random_number.parse # raises Plumb::ParseError because `rand` is not a String
|
535
|
+
```
|
536
|
+
|
537
|
+
You can also pass any `#call() => Object` interface as a generator, instead of a proc.
|
538
|
+
|
458
539
|
#### `#metadata`
|
459
540
|
|
460
541
|
Add metadata to a type
|
@@ -1135,6 +1216,30 @@ Payload = Types::Hash[
|
|
1135
1216
|
]
|
1136
1217
|
```
|
1137
1218
|
|
1219
|
+
#### Attribute writers
|
1220
|
+
|
1221
|
+
By default `Types::Data` classes are inmutable, but you can define attribute writers to allow for mutation using the `writer: true` option.
|
1222
|
+
|
1223
|
+
```ruby
|
1224
|
+
class DBConfig < Types::Data
|
1225
|
+
attribute :host, Types::String.default('localhost'), writer: true
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
class Config < Types::Data
|
1229
|
+
attribute :host, Types::Forms::URI::HTTP, writer: true
|
1230
|
+
attribute :port, Types::Integer.default(80), writer: true
|
1231
|
+
|
1232
|
+
# Nested structs can have writers too
|
1233
|
+
attribute :db, DBConfig.default(DBConfig.new)
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
config = Config.new
|
1237
|
+
config.host = 'http://localhost'
|
1238
|
+
config.db.host = 'db.local'
|
1239
|
+
config.valid? # true
|
1240
|
+
config.errors # {}
|
1241
|
+
```
|
1242
|
+
|
1138
1243
|
#### Recursive struct definitions
|
1139
1244
|
|
1140
1245
|
You can use `#defer`. See [recursive types](#recursive-types).
|
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/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
@@ -117,6 +117,7 @@ module Plumb
|
|
117
117
|
|
118
118
|
def initialize(attrs = {})
|
119
119
|
assign_attributes(attrs)
|
120
|
+
freeze
|
120
121
|
end
|
121
122
|
|
122
123
|
def ==(other)
|
@@ -140,13 +141,18 @@ module Plumb
|
|
140
141
|
|
141
142
|
# @return [Hash]
|
142
143
|
def to_h
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
144
|
+
self.class._schema._schema.keys.each.with_object({}) do |key, memo|
|
145
|
+
key = key.to_sym
|
146
|
+
value = attributes[key]
|
147
|
+
val = case value
|
148
|
+
when ::Array
|
149
|
+
value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
|
150
|
+
when ::NilClass
|
151
|
+
nil
|
152
|
+
else
|
153
|
+
value.respond_to?(:to_h) ? value.to_h : value
|
154
|
+
end
|
155
|
+
memo[key] = val
|
150
156
|
end
|
151
157
|
end
|
152
158
|
|
@@ -158,12 +164,14 @@ module Plumb
|
|
158
164
|
def assign_attributes(attrs = BLANK_HASH)
|
159
165
|
raise ArgumentError, 'Must be a Hash of attributes' unless attrs.respond_to?(:to_h)
|
160
166
|
|
161
|
-
@errors =
|
167
|
+
@errors = {}
|
162
168
|
result = self.class._schema.resolve(attrs.to_h)
|
163
|
-
@attributes = result.value
|
169
|
+
@attributes = prepare_attributes(result.value)
|
164
170
|
@errors = result.errors unless result.valid?
|
165
171
|
end
|
166
172
|
|
173
|
+
def prepare_attributes(attrs) = attrs
|
174
|
+
|
167
175
|
module ClassMethods
|
168
176
|
def _schema
|
169
177
|
@_schema ||= HashClass.new
|
@@ -209,7 +217,7 @@ module Plumb
|
|
209
217
|
# attribute(:friends, Types::Array[Person])
|
210
218
|
# attribute(:friends, [Person])
|
211
219
|
#
|
212
|
-
def attribute(name, type = Types::Any, &block)
|
220
|
+
def attribute(name, type = Types::Any, writer: false, &block)
|
213
221
|
key = Key.wrap(name)
|
214
222
|
name = key.to_sym
|
215
223
|
type = Composable.wrap(type)
|
@@ -232,13 +240,30 @@ module Plumb
|
|
232
240
|
end
|
233
241
|
|
234
242
|
@_schema = _schema + { key => type }
|
235
|
-
|
243
|
+
__plumb_define_attribute_reader_method__(name)
|
244
|
+
return name unless writer
|
245
|
+
|
246
|
+
__plumb_define_attribute_writer_method__(name)
|
236
247
|
end
|
237
248
|
|
238
|
-
def
|
249
|
+
def __plumb_define_attribute_reader_method__(name)
|
239
250
|
define_method(name) { @attributes[name] }
|
240
251
|
end
|
241
252
|
|
253
|
+
def __plumb_define_attribute_writer_method__(name)
|
254
|
+
define_method("#{name}=") do |value|
|
255
|
+
type = self.class._schema.at_key(name)
|
256
|
+
result = type.resolve(value)
|
257
|
+
@attributes[name] = result.value
|
258
|
+
if result.valid?
|
259
|
+
@errors.delete(name)
|
260
|
+
else
|
261
|
+
@errors.merge!(name => result.errors)
|
262
|
+
end
|
263
|
+
result.value
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
242
267
|
def attribute?(name, *args, &block)
|
243
268
|
attribute(Key.new(name, optional: true), *args, &block)
|
244
269
|
end
|
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,6 +115,7 @@ 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.
|
@@ -304,22 +325,6 @@ module Plumb
|
|
304
325
|
end
|
305
326
|
end
|
306
327
|
|
307
|
-
# `#===` equality. So that Plumb steps can be used in case statements and pattern matching.
|
308
|
-
# @param other [Object]
|
309
|
-
# @return [Boolean]
|
310
|
-
def ===(other)
|
311
|
-
case other
|
312
|
-
when Composable
|
313
|
-
other == self
|
314
|
-
else
|
315
|
-
resolve(other).valid?
|
316
|
-
end
|
317
|
-
end
|
318
|
-
|
319
|
-
def ==(other)
|
320
|
-
other.is_a?(self.class) && other.respond_to?(:children) && other.children == children
|
321
|
-
end
|
322
|
-
|
323
328
|
# Visitors expect a #node_name and #children interface.
|
324
329
|
# @return [Array<Composable>]
|
325
330
|
def children = BLANK_ARRAY
|
@@ -341,6 +346,40 @@ module Plumb
|
|
341
346
|
self >> Build.new(cns, factory_method:, &block)
|
342
347
|
end
|
343
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
|
+
|
344
383
|
# Build a Plumb::Pipeline with this object as the starting step.
|
345
384
|
# @example
|
346
385
|
# pipe = Types::Data[name: String].pipeline do |pl|
|
@@ -351,7 +390,7 @@ module Plumb
|
|
351
390
|
#
|
352
391
|
# @return [Pipeline]
|
353
392
|
def pipeline(&block)
|
354
|
-
Pipeline.new(self, &block)
|
393
|
+
Pipeline.new(type: self, &block)
|
355
394
|
end
|
356
395
|
|
357
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|
|
data/lib/plumb/not.rb
CHANGED
@@ -8,13 +8,19 @@ module Plumb
|
|
8
8
|
|
9
9
|
attr_reader :children, :errors
|
10
10
|
|
11
|
-
def initialize(step, errors: nil)
|
12
|
-
@step = step
|
13
|
-
@errors = errors
|
11
|
+
def initialize(step = nil, errors: nil)
|
12
|
+
@step = Composable.wrap(step)
|
13
|
+
@errors = errors || "must not be #{step.inspect}"
|
14
14
|
@children = [step].freeze
|
15
15
|
freeze
|
16
16
|
end
|
17
17
|
|
18
|
+
# @param step [Object]
|
19
|
+
# @return [Not]
|
20
|
+
def [](step)
|
21
|
+
self.class.new(step)
|
22
|
+
end
|
23
|
+
|
18
24
|
private def _inspect
|
19
25
|
%(Not(#{@step.inspect}))
|
20
26
|
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
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.8
|
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-10-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bigdecimal
|
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
115
|
- !ruby/object:Gem::Version
|
116
116
|
version: '0'
|
117
117
|
requirements: []
|
118
|
-
rubygems_version: 3.5.
|
118
|
+
rubygems_version: 3.5.21
|
119
119
|
signing_key:
|
120
120
|
specification_version: 4
|
121
121
|
summary: Data validation and transformation library.
|