plumb 0.0.6 → 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 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