plumb 0.0.6 → 0.0.8

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: 308e76909c6466b0a6c2cc9443498a267186344b9508b8f485975479e0ff165a
4
- data.tar.gz: 8498e5a4619437b8f91b3baae4b2d208c27031a5617dba174d52893cd4e3a54a
3
+ metadata.gz: 7e5d0ac088968506a61d63da75f4010b050579c873d79604a25dfe0b133d3726
4
+ data.tar.gz: d559c1a3964544184258043e14d0bf9e3b41ff743a8117eccee09be2c22b084a
5
5
  SHA512:
6
- metadata.gz: d41ebdf232099770d04abc85f81ead1e8dc1d4f55eb1bc9265484401cfd0418e984d7cf97a67a6ef452d67f05c3f92e66e3e3fe64f11622acbb89e5c223c73b1
7
- data.tar.gz: 5e2749e954fae81753d63d6d27b95a53f239b5ac6ad776755646d794fe7819b56087f48bd99394aeab2f40c64d45606cc413a1475512f6943279d51a7dd7d2b2
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).
@@ -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]
@@ -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
@@ -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
- attributes.transform_values do |value|
144
- case value
145
- when ::Array
146
- value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
147
- else
148
- value.respond_to?(:to_h) ? value.to_h : value
149
- end
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 = BLANK_HASH
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
- __plumb_define_attribute_method__(name)
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 __plumb_define_attribute_method__(name)
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
@@ -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
@@ -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|
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
@@ -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
@@ -159,6 +159,7 @@ module Plumb
159
159
  Stream = StreamClass.new
160
160
  Tuple = TupleClass.new
161
161
  Hash = HashClass.new
162
+ Not = Plumb::Not.new
162
163
  Interface = InterfaceClass.new
163
164
  Email = String[URI::MailTo::EMAIL_REGEXP].as_node(:email)
164
165
  Date = Any[::Date]
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.6'
4
+ VERSION = '0.0.8'
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.6
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-09-04 00:00:00.000000000 Z
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.11
118
+ rubygems_version: 3.5.21
119
119
  signing_key:
120
120
  specification_version: 4
121
121
  summary: Data validation and transformation library.