plumb 0.0.6 → 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: 308e76909c6466b0a6c2cc9443498a267186344b9508b8f485975479e0ff165a
4
- data.tar.gz: 8498e5a4619437b8f91b3baae4b2d208c27031a5617dba174d52893cd4e3a54a
3
+ metadata.gz: 82cbbcfabbe2240d11a1957c7d06f59e314d8013c1459ecdd6339404f0c3208e
4
+ data.tar.gz: a17828a33c296ba50bffbb629a806f732bc3a9f73d974617e4526bfd74065d42
5
5
  SHA512:
6
- metadata.gz: d41ebdf232099770d04abc85f81ead1e8dc1d4f55eb1bc9265484401cfd0418e984d7cf97a67a6ef452d67f05c3f92e66e3e3fe64f11622acbb89e5c223c73b1
7
- data.tar.gz: 5e2749e954fae81753d63d6d27b95a53f239b5ac6ad776755646d794fe7819b56087f48bd99394aeab2f40c64d45606cc413a1475512f6943279d51a7dd7d2b2
6
+ metadata.gz: 28ce9c69dcfa1f5d1129745301164400211c1df5707b851a2457f4361b59ed0ad73472851ae2993267f3d74389a6477ded4a60ce3eaed70472c041fad03df7bd
7
+ data.tar.gz: 45e8f649c646b3236c6e7e5c3c86ed7493b88fc544585e5dc168675c69660b7ca217b5508a834b2dfa1820f97ca87b4edd9b988d17effb82697da28273545e2d
data/README.md CHANGED
@@ -455,6 +455,71 @@ All scalar types support this:
455
455
  ten = Types::Integer.value(10)
456
456
  ```
457
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
+
458
523
  #### `#metadata`
459
524
 
460
525
  Add metadata to a type
@@ -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
@@ -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
@@ -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|
@@ -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/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.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.6
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-09-04 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