plumb 0.0.3 → 0.0.5

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +609 -57
  3. data/bench/compare_parametric_schema.rb +102 -0
  4. data/bench/compare_parametric_struct.rb +68 -0
  5. data/bench/parametric_schema.rb +229 -0
  6. data/bench/plumb_hash.rb +109 -0
  7. data/examples/concurrent_downloads.rb +3 -3
  8. data/examples/env_config.rb +2 -2
  9. data/examples/event_registry.rb +127 -0
  10. data/examples/weekdays.rb +1 -1
  11. data/lib/plumb/and.rb +4 -3
  12. data/lib/plumb/any_class.rb +4 -4
  13. data/lib/plumb/array_class.rb +8 -5
  14. data/lib/plumb/attributes.rb +268 -0
  15. data/lib/plumb/build.rb +4 -3
  16. data/lib/plumb/composable.rb +381 -0
  17. data/lib/plumb/decorator.rb +57 -0
  18. data/lib/plumb/deferred.rb +1 -1
  19. data/lib/plumb/hash_class.rb +19 -8
  20. data/lib/plumb/hash_map.rb +8 -6
  21. data/lib/plumb/interface_class.rb +6 -2
  22. data/lib/plumb/json_schema_visitor.rb +59 -32
  23. data/lib/plumb/match_class.rb +5 -4
  24. data/lib/plumb/metadata.rb +5 -1
  25. data/lib/plumb/metadata_visitor.rb +13 -42
  26. data/lib/plumb/not.rb +4 -3
  27. data/lib/plumb/or.rb +10 -4
  28. data/lib/plumb/pipeline.rb +27 -7
  29. data/lib/plumb/policy.rb +10 -3
  30. data/lib/plumb/schema.rb +11 -10
  31. data/lib/plumb/static_class.rb +4 -3
  32. data/lib/plumb/step.rb +4 -3
  33. data/lib/plumb/stream_class.rb +8 -7
  34. data/lib/plumb/tagged_hash.rb +11 -11
  35. data/lib/plumb/transform.rb +4 -3
  36. data/lib/plumb/tuple_class.rb +8 -8
  37. data/lib/plumb/type_registry.rb +5 -2
  38. data/lib/plumb/types.rb +30 -1
  39. data/lib/plumb/value_class.rb +4 -3
  40. data/lib/plumb/version.rb +1 -1
  41. data/lib/plumb/visitor_handlers.rb +6 -0
  42. data/lib/plumb.rb +11 -5
  43. metadata +10 -3
  44. data/lib/plumb/steppable.rb +0 -229
@@ -1,29 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class TaggedHash
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :key, :types
9
+ attr_reader :key, :children
10
10
 
11
- def initialize(hash_type, key, types)
11
+ def initialize(hash_type, key, children)
12
12
  @hash_type = hash_type
13
13
  @key = Key.wrap(key)
14
- @types = types
14
+ @children = children
15
15
 
16
- raise ArgumentError, 'all types must be HashClass' if @types.size.zero? || @types.any? do |t|
16
+ raise ArgumentError, 'all types must be HashClass' if @children.size.zero? || @children.any? do |t|
17
17
  !t.is_a?(HashClass)
18
18
  end
19
- raise ArgumentError, "all types must define key #{@key}" unless @types.all? { |t| !!t.at_key(@key) }
19
+ raise ArgumentError, "all types must define key #{@key}" unless @children.all? { |t| !!t.at_key(@key) }
20
20
 
21
21
  # types are assumed to have literal values for the index field :key
22
- @index = @types.each.with_object({}) do |t, memo|
22
+ @index = @children.each.with_object({}) do |t, memo|
23
23
  key_type = t.at_key(@key)
24
- raise TypeError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
24
+ raise ParseError, "key type at :#{@key} #{key_type} must be a Match type" unless key_type.is_a?(MatchClass)
25
25
 
26
- memo[key_type.matcher] = t
26
+ memo[key_type.children[0]] = t
27
27
  end
28
28
 
29
29
  freeze
@@ -41,6 +41,6 @@ module Plumb
41
41
 
42
42
  private
43
43
 
44
- def _inspect = "TaggedHash[#{@key.inspect}, #{@types.map(&:inspect).join(', ')}]"
44
+ def _inspect = "TaggedHash[#{@key.inspect}, #{@children.map(&:inspect).join(', ')}]"
45
45
  end
46
46
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class Transform
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :target_type
9
+ attr_reader :children
10
10
 
11
11
  def initialize(target_type, callable)
12
12
  @target_type = target_type
13
13
  @callable = callable || Plumb::NOOP
14
+ @children = [target_type].freeze
14
15
  freeze
15
16
  end
16
17
 
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class TupleClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :types
9
+ attr_reader :children
10
10
 
11
- def initialize(*types)
12
- @types = types.map { |t| Steppable.wrap(t) }
11
+ def initialize(*children)
12
+ @children = children.map { |t| Composable.wrap(t) }.freeze
13
13
  freeze
14
14
  end
15
15
 
@@ -21,10 +21,10 @@ module Plumb
21
21
 
22
22
  def call(result)
23
23
  return result.invalid(errors: 'must be an Array') unless result.value.is_a?(::Array)
24
- return result.invalid(errors: 'must have the same size') unless result.value.size == @types.size
24
+ return result.invalid(errors: 'must have the same size') unless result.value.size == @children.size
25
25
 
26
26
  errors = {}
27
- values = @types.map.with_index do |type, idx|
27
+ values = @children.map.with_index do |type, idx|
28
28
  val = result.value[idx]
29
29
  r = type.resolve(val)
30
30
  errors[idx] = ["expected #{type.inspect}, got #{val.inspect}", r.errors].flatten unless r.valid?
@@ -39,7 +39,7 @@ module Plumb
39
39
  private
40
40
 
41
41
  def _inspect
42
- "Tuple[#{@types.map(&:inspect).join(', ')}]"
42
+ "Tuple[#{@children.map(&:inspect).join(', ')}]"
43
43
  end
44
44
  end
45
45
  end
@@ -7,7 +7,7 @@ module Plumb
7
7
  case obj
8
8
  when Module
9
9
  obj.extend TypeRegistry
10
- when Steppable
10
+ when Composable
11
11
  anc = [name, const_name].join('::')
12
12
  obj.freeze.name.set(anc)
13
13
  end
@@ -17,16 +17,19 @@ module Plumb
17
17
  host.extend TypeRegistry
18
18
  constants(false).each do |const_name|
19
19
  const = const_get(const_name)
20
+
20
21
  anc = [host.name, const_name].join('::')
21
22
  case const
22
23
  when Module
24
+ next if const.is_a?(Class)
25
+
23
26
  child_mod = Module.new
24
27
  child_mod.define_singleton_method(:name) do
25
28
  anc
26
29
  end
27
30
  child_mod.send(:include, const)
28
31
  host.const_set(const_name, child_mod)
29
- when Steppable
32
+ when Composable
30
33
  type = const.dup
31
34
  type.freeze.name.set(anc)
32
35
  host.const_set(const_name, type)
data/lib/plumb/types.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bigdecimal'
4
+ require 'uri'
5
+ require 'date'
4
6
 
5
7
  module Plumb
6
8
  # Define core policies
@@ -101,7 +103,19 @@ module Plumb
101
103
  Types::Static[value]
102
104
  end
103
105
 
104
- type | (Types::Undefined >> val_type)
106
+ (Types::Undefined >> val_type) | type
107
+ end
108
+
109
+ # Wrap a step execution in a rescue block.
110
+ # Expect a specific exception class, and return an invalid result if it is raised.
111
+ # Usage:
112
+ # type = Types::String.build(Date, :parse).policy(:rescue, Date::Error)
113
+ policy :rescue do |type, exception_class|
114
+ Step.new(nil, 'Rescue') do |result|
115
+ type.call(result)
116
+ rescue exception_class => e
117
+ result.invalid(errors: e.message)
118
+ end
105
119
  end
106
120
 
107
121
  # Split a string into an array. Default separator is /\s*,\s*/
@@ -145,6 +159,17 @@ module Plumb
145
159
  Tuple = TupleClass.new
146
160
  Hash = HashClass.new
147
161
  Interface = InterfaceClass.new
162
+ Email = String[URI::MailTo::EMAIL_REGEXP].as_node(:email)
163
+ Date = Any[::Date]
164
+
165
+ module UUID
166
+ 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
+ end
168
+
169
+ class Data
170
+ extend Composable
171
+ include Plumb::Attributes
172
+ end
148
173
 
149
174
  module Lax
150
175
  NUMBER_EXPR = /^\d{1,3}(?:,\d{3})*(?:\.\d+)?$/
@@ -185,6 +210,10 @@ module Plumb
185
210
  Boolean = True | False
186
211
 
187
212
  Nil = Nil | (String[BLANK_STRING] >> nil)
213
+
214
+ # Accept a Date, or a string that can be parsed into a Date
215
+ # via Date.parse
216
+ Date = Date | (String >> Any.build(::Date, :parse).policy(:rescue, ::Date::Error))
188
217
  end
189
218
  end
190
219
  end
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
 
5
5
  module Plumb
6
6
  class ValueClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :value
9
+ attr_reader :children
10
10
 
11
11
  def initialize(value = Undefined)
12
12
  @value = value
13
+ @children = [value].freeze
13
14
  freeze
14
15
  end
15
16
 
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.3'
4
+ VERSION = '0.0.5'
5
5
  end
@@ -39,5 +39,11 @@ module Plumb
39
39
  def on_missing_handler(node, _props, method_name)
40
40
  raise "No handler for #{node.inspect} with :#{method_name}"
41
41
  end
42
+
43
+ def visit_children(node, props = BLANK_HASH)
44
+ node.children.reduce(props) do |acc, child|
45
+ visit(child, acc)
46
+ end
47
+ end
42
48
  end
43
49
  end
data/lib/plumb.rb CHANGED
@@ -10,7 +10,7 @@ module Plumb
10
10
  end
11
11
 
12
12
  # Register a policy with the given name and block.
13
- # Optionally define a method on the Steppable method to call the policy.
13
+ # Optionally define a method on the Composable method to call the policy.
14
14
  # Example:
15
15
  # Plumb.policy(:multiply_by, for_type: Integer, helper: true) do |step, factor, &block|
16
16
  # step.transform(Integer) { |number| number * factor }
@@ -39,11 +39,11 @@ module Plumb
39
39
 
40
40
  return self unless helper
41
41
 
42
- if Steppable.instance_methods.include?(name)
43
- raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Steppable"
42
+ if Composable.instance_methods.include?(name)
43
+ raise Policies::MethodAlreadyDefinedError, "Method #{name} is already defined on Composable"
44
44
  end
45
45
 
46
- Steppable.define_method(name) do |arg = Undefined, &bl|
46
+ Composable.define_method(name) do |arg = Undefined, &bl|
47
47
  if arg == Undefined
48
48
  policy(name, &bl)
49
49
  else
@@ -53,11 +53,15 @@ module Plumb
53
53
 
54
54
  self
55
55
  end
56
+
57
+ def self.decorate(type, &block)
58
+ Decorator.call(type, &block)
59
+ end
56
60
  end
57
61
 
58
62
  require 'plumb/result'
59
63
  require 'plumb/type_registry'
60
- require 'plumb/steppable'
64
+ require 'plumb/composable'
61
65
  require 'plumb/any_class'
62
66
  require 'plumb/step'
63
67
  require 'plumb/and'
@@ -72,6 +76,8 @@ require 'plumb/array_class'
72
76
  require 'plumb/stream_class'
73
77
  require 'plumb/hash_class'
74
78
  require 'plumb/interface_class'
79
+ require 'plumb/attributes'
75
80
  require 'plumb/types'
76
81
  require 'plumb/json_schema_visitor'
77
82
  require 'plumb/schema'
83
+ require 'plumb/decorator'
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.3
4
+ version: 0.0.5
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-07-18 00:00:00.000000000 Z
11
+ date: 2024-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -50,17 +50,25 @@ files:
50
50
  - LICENSE.txt
51
51
  - README.md
52
52
  - Rakefile
53
+ - bench/compare_parametric_schema.rb
54
+ - bench/compare_parametric_struct.rb
55
+ - bench/parametric_schema.rb
56
+ - bench/plumb_hash.rb
53
57
  - examples/command_objects.rb
54
58
  - examples/concurrent_downloads.rb
55
59
  - examples/csv_stream.rb
56
60
  - examples/env_config.rb
61
+ - examples/event_registry.rb
57
62
  - examples/programmers.csv
58
63
  - examples/weekdays.rb
59
64
  - lib/plumb.rb
60
65
  - lib/plumb/and.rb
61
66
  - lib/plumb/any_class.rb
62
67
  - lib/plumb/array_class.rb
68
+ - lib/plumb/attributes.rb
63
69
  - lib/plumb/build.rb
70
+ - lib/plumb/composable.rb
71
+ - lib/plumb/decorator.rb
64
72
  - lib/plumb/deferred.rb
65
73
  - lib/plumb/hash_class.rb
66
74
  - lib/plumb/hash_map.rb
@@ -79,7 +87,6 @@ files:
79
87
  - lib/plumb/schema.rb
80
88
  - lib/plumb/static_class.rb
81
89
  - lib/plumb/step.rb
82
- - lib/plumb/steppable.rb
83
90
  - lib/plumb/stream_class.rb
84
91
  - lib/plumb/tagged_hash.rb
85
92
  - lib/plumb/transform.rb
@@ -1,229 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'plumb/metadata_visitor'
4
-
5
- module Plumb
6
- class UndefinedClass
7
- def inspect
8
- %(Undefined)
9
- end
10
-
11
- def to_s = inspect
12
- def node_name = :undefined
13
- def empty? = true
14
- end
15
-
16
- TypeError = Class.new(::TypeError)
17
- Undefined = UndefinedClass.new.freeze
18
-
19
- BLANK_STRING = ''
20
- BLANK_ARRAY = [].freeze
21
- BLANK_HASH = {}.freeze
22
- BLANK_RESULT = Result.wrap(Undefined)
23
- NOOP = ->(result) { result }
24
-
25
- module Callable
26
- def metadata
27
- MetadataVisitor.call(self)
28
- end
29
-
30
- def resolve(value = Undefined)
31
- call(Result.wrap(value))
32
- end
33
-
34
- def parse(value = Undefined)
35
- result = resolve(value)
36
- raise TypeError, result.errors if result.invalid?
37
-
38
- result.value
39
- end
40
-
41
- def call(result)
42
- raise NotImplementedError, "Implement #call(Result) => Result in #{self.class}"
43
- end
44
- end
45
-
46
- module Steppable
47
- include Callable
48
-
49
- def self.included(base)
50
- nname = base.name.split('::').last
51
- nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
52
- nname.downcase!
53
- nname.gsub!(/_class$/, '')
54
- nname = nname.to_sym
55
- base.define_method(:node_name) { nname }
56
- end
57
-
58
- def self.wrap(callable)
59
- if callable.is_a?(Steppable)
60
- callable
61
- elsif callable.is_a?(::Hash)
62
- HashClass.new(schema: callable)
63
- elsif callable.respond_to?(:call)
64
- Step.new(callable)
65
- else
66
- MatchClass.new(callable)
67
- end
68
- end
69
-
70
- attr_reader :name
71
-
72
- class Name
73
- def initialize(name)
74
- @name = name
75
- end
76
-
77
- def to_s = @name
78
-
79
- def set(n)
80
- @name = n
81
- self
82
- end
83
- end
84
-
85
- def freeze
86
- return self if frozen?
87
-
88
- @name = Name.new(_inspect)
89
- super
90
- end
91
-
92
- private def _inspect = self.class.name
93
-
94
- def inspect = name.to_s
95
-
96
- def node_name = self.class.name.split('::').last.to_sym
97
-
98
- def defer(definition = nil, &block)
99
- Deferred.new(definition || block)
100
- end
101
-
102
- def >>(other)
103
- And.new(self, Steppable.wrap(other))
104
- end
105
-
106
- def |(other)
107
- Or.new(self, Steppable.wrap(other))
108
- end
109
-
110
- def transform(target_type, callable = nil, &block)
111
- self >> Transform.new(target_type, callable || block)
112
- end
113
-
114
- def check(errors = 'did not pass the check', &block)
115
- self >> MatchClass.new(block, error: errors, label: errors)
116
- end
117
-
118
- def meta(data = {})
119
- self >> Metadata.new(data)
120
- end
121
-
122
- def not(other = self)
123
- Not.new(other)
124
- end
125
-
126
- def invalid(errors: nil)
127
- Not.new(self, errors:)
128
- end
129
-
130
- def value(val)
131
- self >> ValueClass.new(val)
132
- end
133
-
134
- def match(*args)
135
- self >> MatchClass.new(*args)
136
- end
137
-
138
- def [](val) = match(val)
139
-
140
- class Node
141
- include Steppable
142
-
143
- attr_reader :node_name, :type, :attributes
144
-
145
- def initialize(node_name, type, attributes = BLANK_HASH)
146
- @node_name = node_name
147
- @type = type
148
- @attributes = attributes
149
- freeze
150
- end
151
-
152
- def call(result) = type.call(result)
153
- end
154
-
155
- def as_node(node_name, metadata = BLANK_HASH)
156
- Node.new(node_name, self, metadata)
157
- end
158
-
159
- # Register a policy for this step.
160
- # Mode 1.a: #policy(:name, arg) a single policy with an argument
161
- # Mode 1.b: #policy(:name) a single policy without an argument
162
- # Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
163
- # The latter mode will be expanded to multiple #policy calls.
164
- # @return [Step]
165
- def policy(*args, &blk)
166
- case args
167
- in [::Symbol => name, *rest] # #policy(:name, arg)
168
- types = Array(metadata[:type]).uniq
169
-
170
- bargs = [self]
171
- bargs << rest.first if rest.any?
172
- block = Plumb.policies.get(types, name)
173
- pol = block.call(*bargs, &blk)
174
-
175
- Policy.new(name, rest.first, pol)
176
- in [::Hash => opts] # #policy(p1: value, p2: value)
177
- opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
178
- else
179
- raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
180
- end
181
- end
182
-
183
- def ===(other)
184
- case other
185
- when Steppable
186
- other == self
187
- else
188
- resolve(other).valid?
189
- end
190
- end
191
-
192
- def build(cns, factory_method = :new, &block)
193
- self >> Build.new(cns, factory_method:, &block)
194
- end
195
-
196
- def pipeline(&block)
197
- Pipeline.new(self, &block)
198
- end
199
-
200
- def to_s
201
- inspect
202
- end
203
-
204
- # Build a step that will invoke one or more methods on the value.
205
- # Ex 1: Types::String.invoke(:downcase)
206
- # Ex 2: Types::Array.invoke(:[], 1)
207
- # Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
208
- # @return [Step]
209
- def invoke(*args, &block)
210
- case args
211
- in [::Symbol => method_name, *rest]
212
- self >> Step.new(
213
- ->(result) { result.valid(result.value.public_send(method_name, *rest, &block)) },
214
- [method_name.inspect, rest.inspect].join(' ')
215
- )
216
- in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) }
217
- methods.reduce(self) { |step, method| step.invoke(method) }
218
- else
219
- raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}"
220
- end
221
- end
222
- end
223
- end
224
-
225
- require 'plumb/deferred'
226
- require 'plumb/transform'
227
- require 'plumb/policy'
228
- require 'plumb/build'
229
- require 'plumb/metadata'