plumb 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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'