plumb 0.0.2 → 0.0.4

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.
@@ -10,6 +10,7 @@ module Plumb
10
10
 
11
11
  def to_s = inspect
12
12
  def node_name = :undefined
13
+ def empty? = true
13
14
  end
14
15
 
15
16
  TypeError = Class.new(::TypeError)
@@ -22,10 +23,6 @@ module Plumb
22
23
  NOOP = ->(result) { result }
23
24
 
24
25
  module Callable
25
- def metadata
26
- MetadataVisitor.call(self)
27
- end
28
-
29
26
  def resolve(value = Undefined)
30
27
  call(Result.wrap(value))
31
28
  end
@@ -42,9 +39,15 @@ module Plumb
42
39
  end
43
40
  end
44
41
 
45
- module Steppable
46
- include Callable
42
+ # This module gets included by Composable,
43
+ # but only when Composable is `included` in classes, not `extended`.
44
+ # The rule of this module is to assign a name to constants that point to Composable instances.
45
+ module Naming
46
+ attr_reader :name
47
47
 
48
+ # When including this module,
49
+ # define a #node_name method on the Composable instance
50
+ # #node_name is used by Visitors to determine the type of node.
48
51
  def self.included(base)
49
52
  nname = base.name.split('::').last
50
53
  nname.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
@@ -54,20 +57,6 @@ module Plumb
54
57
  base.define_method(:node_name) { nname }
55
58
  end
56
59
 
57
- def self.wrap(callable)
58
- if callable.is_a?(Steppable)
59
- callable
60
- elsif callable.is_a?(::Hash)
61
- HashClass.new(schema: callable)
62
- elsif callable.respond_to?(:call)
63
- Step.new(callable)
64
- else
65
- MatchClass.new(callable)
66
- end
67
- end
68
-
69
- attr_reader :name
70
-
71
60
  class Name
72
61
  def initialize(name)
73
62
  @name = name
@@ -93,17 +82,42 @@ module Plumb
93
82
  def inspect = name.to_s
94
83
 
95
84
  def node_name = self.class.name.split('::').last.to_sym
85
+ end
86
+
87
+ #  Composable mixes in composition methods to classes.
88
+ # such as #>>, #|, #not, and others.
89
+ # Any Composable class can participate in Plumb compositions.
90
+ module Composable
91
+ include Callable
92
+
93
+ # This only runs when including Composable,
94
+ # not extending classes with it.
95
+ def self.included(base)
96
+ base.send(:include, Naming)
97
+ end
98
+
99
+ def self.wrap(callable)
100
+ if callable.is_a?(Composable)
101
+ callable
102
+ elsif callable.is_a?(::Hash)
103
+ HashClass.new(schema: callable)
104
+ elsif callable.respond_to?(:call)
105
+ Step.new(callable)
106
+ else
107
+ MatchClass.new(callable)
108
+ end
109
+ end
96
110
 
97
111
  def defer(definition = nil, &block)
98
112
  Deferred.new(definition || block)
99
113
  end
100
114
 
101
115
  def >>(other)
102
- And.new(self, Steppable.wrap(other))
116
+ And.new(self, Composable.wrap(other))
103
117
  end
104
118
 
105
119
  def |(other)
106
- Or.new(self, Steppable.wrap(other))
120
+ Or.new(self, Composable.wrap(other))
107
121
  end
108
122
 
109
123
  def transform(target_type, callable = nil, &block)
@@ -111,11 +125,15 @@ module Plumb
111
125
  end
112
126
 
113
127
  def check(errors = 'did not pass the check', &block)
114
- self >> MatchClass.new(block, error: errors)
128
+ self >> MatchClass.new(block, error: errors, label: errors)
115
129
  end
116
130
 
117
- def meta(data = {})
118
- self >> Metadata.new(data)
131
+ def metadata(data = Undefined)
132
+ if data == Undefined
133
+ MetadataVisitor.call(self)
134
+ else
135
+ self >> Metadata.new(data)
136
+ end
119
137
  end
120
138
 
121
139
  def not(other = self)
@@ -136,24 +154,8 @@ module Plumb
136
154
 
137
155
  def [](val) = match(val)
138
156
 
139
- DefaultProc = proc do |callable|
140
- proc do |result|
141
- result.valid(callable.call)
142
- end
143
- end
144
-
145
- def default(val = Undefined, &block)
146
- val_type = if val == Undefined
147
- DefaultProc.call(block)
148
- else
149
- Types::Static[val]
150
- end
151
-
152
- self | (Types::Undefined >> val_type)
153
- end
154
-
155
157
  class Node
156
- include Steppable
158
+ include Composable
157
159
 
158
160
  attr_reader :node_name, :type, :attributes
159
161
 
@@ -171,40 +173,49 @@ module Plumb
171
173
  Node.new(node_name, self, metadata)
172
174
  end
173
175
 
174
- def nullable
175
- Types::Nil | self
176
- end
177
-
178
- def present
179
- Types::Present >> self
180
- end
181
-
182
- def options(opts = [])
183
- rule(included_in: opts)
184
- end
185
-
186
- def rule(*args)
187
- specs = case args
188
- in [::Symbol => rule_name, value]
189
- { rule_name => value }
190
- in [::Hash => rules]
191
- rules
192
- else
193
- raise ArgumentError, "expected 1 or 2 arguments, but got #{args.size}"
194
- end
195
-
196
- self >> Rules.new(specs, metadata[:type])
176
+ # Register a policy for this step.
177
+ # Mode 1.a: #policy(:name, arg) a single policy with an argument
178
+ # Mode 1.b: #policy(:name) a single policy without an argument
179
+ # Mode 2: #policy(p1: value, p2: value) multiple policies with arguments
180
+ # The latter mode will be expanded to multiple #policy calls.
181
+ # @return [Step]
182
+ def policy(*args, &blk)
183
+ case args
184
+ in [::Symbol => name, *rest] # #policy(:name, arg)
185
+ types = Array(metadata[:type]).uniq
186
+
187
+ bargs = [self]
188
+ arg = Undefined
189
+ if rest.any?
190
+ bargs << rest.first
191
+ arg = rest.first
192
+ end
193
+ block = Plumb.policies.get(types, name)
194
+ pol = block.call(*bargs, &blk)
195
+
196
+ Policy.new(name, arg, pol)
197
+ in [::Hash => opts] # #policy(p1: value, p2: value)
198
+ opts.reduce(self) { |step, (name, value)| step.policy(name, value) }
199
+ else
200
+ raise ArgumentError, "expected a symbol or hash, got #{args.inspect}"
201
+ end
197
202
  end
198
203
 
199
204
  def ===(other)
200
205
  case other
201
- when Steppable
206
+ when Composable
202
207
  other == self
203
208
  else
204
209
  resolve(other).valid?
205
210
  end
206
211
  end
207
212
 
213
+ def ==(other)
214
+ other.is_a?(self.class) && other.children == children
215
+ end
216
+
217
+ def children = BLANK_ARRAY
218
+
208
219
  def build(cns, factory_method = :new, &block)
209
220
  self >> Build.new(cns, factory_method:, &block)
210
221
  end
@@ -217,7 +228,13 @@ module Plumb
217
228
  inspect
218
229
  end
219
230
 
220
- # Build a step that will invoke onr or more methods on the value.
231
+ # @option root [Boolean] whether to include JSON Schema $schema property
232
+ # @return [Hash]
233
+ def to_json_schema(root: false)
234
+ JSONSchemaVisitor.call(self, root:)
235
+ end
236
+
237
+ # Build a step that will invoke one or more methods on the value.
221
238
  # Ex 1: Types::String.invoke(:downcase)
222
239
  # Ex 2: Types::Array.invoke(:[], 1)
223
240
  # Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
@@ -240,5 +257,6 @@ end
240
257
 
241
258
  require 'plumb/deferred'
242
259
  require 'plumb/transform'
260
+ require 'plumb/policy'
243
261
  require 'plumb/build'
244
262
  require 'plumb/metadata'
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plumb
4
+ # A class to help decorate all or some types in a
5
+ # type composition.
6
+ # Example:
7
+ # Type = Types::Array[Types::String | Types::Integer]
8
+ # Decorated = Plumb::Decorator.(Type) do |type|
9
+ # if type.is_a?(Plumb::ArrayClass)
10
+ # LoggerType.new(type, 'array')
11
+ # else
12
+ # type
13
+ # end
14
+ # end
15
+ class Decorator
16
+ def self.call(type, &block)
17
+ new(block).visit(type)
18
+ end
19
+
20
+ def initialize(block)
21
+ @block = block
22
+ end
23
+
24
+ # @param type [Composable]
25
+ # @return [Composable]
26
+ def visit(type)
27
+ type = case type
28
+ when And
29
+ left, right = visit_children(type)
30
+ And.new(left, right)
31
+ when Or
32
+ left, right = visit_children(type)
33
+ Or.new(left, right)
34
+ when Not
35
+ child = visit_children(type).first
36
+ Not.new(child, errors: type.errors)
37
+ when Policy
38
+ child = visit_children(type).first
39
+ Policy.new(type.policy_name, type.arg, child)
40
+ else
41
+ type
42
+ end
43
+
44
+ decorate(type)
45
+ end
46
+
47
+ private
48
+
49
+ def visit_children(type)
50
+ type.children.map { |child| visit(child) }
51
+ end
52
+
53
+ def decorate(type)
54
+ @block.call(type)
55
+ end
56
+ end
57
+ end
@@ -4,7 +4,7 @@ require 'thread'
4
4
 
5
5
  module Plumb
6
6
  class Deferred
7
- include Steppable
7
+ include Composable
8
8
 
9
9
  def initialize(definition)
10
10
  @lock = Mutex.new
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'plumb/steppable'
3
+ require 'plumb/composable'
4
4
  require 'plumb/key'
5
5
  require 'plumb/static_class'
6
6
  require 'plumb/hash_map'
@@ -8,7 +8,9 @@ require 'plumb/tagged_hash'
8
8
 
9
9
  module Plumb
10
10
  class HashClass
11
- include Steppable
11
+ include Composable
12
+
13
+ NOT_A_HASH = { _: 'must be a Hash' }.freeze
12
14
 
13
15
  attr_reader :_schema
14
16
 
@@ -30,10 +32,8 @@ module Plumb
30
32
  case args
31
33
  in [::Hash => hash]
32
34
  self.class.new(schema: _schema.merge(wrap_keys_and_values(hash)), inclusive: @inclusive)
33
- in [Steppable => key_type, value_type]
34
- HashMap.new(key_type, Steppable.wrap(value_type))
35
- in [Class => key_type, value_type]
36
- HashMap.new(Steppable.wrap(key_type), Steppable.wrap(value_type))
35
+ in [key_type, value_type]
36
+ HashMap.new(Composable.wrap(key_type), Composable.wrap(value_type))
37
37
  else
38
38
  raise ::ArgumentError, "unexpected value to Types::Hash#schema #{args.inspect}"
39
39
  end
@@ -46,9 +46,14 @@ module Plumb
46
46
  # we need to keep the right-side key, because even if the key name is the same,
47
47
  # it's optional flag might have changed
48
48
  def +(other)
49
- raise ArgumentError, "expected a HashClass, got #{other.class}" unless other.is_a?(HashClass)
50
-
51
- self.class.new(schema: merge_rightmost_keys(_schema, other._schema), inclusive: @inclusive)
49
+ other_schema = case other
50
+ when HashClass then other._schema
51
+ when ::Hash then other
52
+ else
53
+ raise ArgumentError, "expected a HashClass or Hash, got #{other.class}"
54
+ end
55
+
56
+ self.class.new(schema: merge_rightmost_keys(_schema, other_schema), inclusive: @inclusive)
52
57
  end
53
58
 
54
59
  def &(other)
@@ -99,7 +104,7 @@ module Plumb
99
104
  end
100
105
 
101
106
  def call(result)
102
- return result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
107
+ return result.invalid(errors: NOT_A_HASH) unless result.value.is_a?(::Hash)
103
108
  return result unless _schema.any?
104
109
 
105
110
  input = result.value
@@ -123,6 +128,10 @@ module Plumb
123
128
  errors.any? ? result.invalid(output, errors:) : result.valid(output)
124
129
  end
125
130
 
131
+ def ==(other)
132
+ other.is_a?(self.class) && other._schema == _schema
133
+ end
134
+
126
135
  private
127
136
 
128
137
  def _inspect
@@ -140,7 +149,7 @@ module Plumb
140
149
  when Callable
141
150
  hash
142
151
  else #  leaf values
143
- Steppable.wrap(hash)
152
+ Composable.wrap(hash)
144
153
  end
145
154
  end
146
155
 
@@ -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 HashMap
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :key_type, :value_type
9
+ attr_reader :children
10
10
 
11
11
  def initialize(key_type, value_type)
12
12
  @key_type = key_type
13
13
  @value_type = value_type
14
+ @children = [key_type, value_type].freeze
14
15
  freeze
15
16
  end
16
17
 
@@ -35,19 +36,20 @@ module Plumb
35
36
  end
36
37
 
37
38
  def filtered
38
- FilteredHashMap.new(key_type, value_type)
39
+ FilteredHashMap.new(@key_type, @value_type)
39
40
  end
40
41
 
41
42
  private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}]"
42
43
 
43
44
  class FilteredHashMap
44
- include Steppable
45
+ include Composable
45
46
 
46
- attr_reader :key_type, :value_type
47
+ attr_reader :children
47
48
 
48
49
  def initialize(key_type, value_type)
49
50
  @key_type = key_type
50
51
  @value_type = value_type
52
+ @children = [key_type, value_type].freeze
51
53
  freeze
52
54
  end
53
55
 
@@ -1,10 +1,10 @@
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 InterfaceClass
7
- include Steppable
7
+ include Composable
8
8
 
9
9
  attr_reader :method_names
10
10
 
@@ -13,6 +13,10 @@ module Plumb
13
13
  freeze
14
14
  end
15
15
 
16
+ def ==(other)
17
+ other.is_a?(self.class) && other.method_names == method_names
18
+ end
19
+
16
20
  def of(*args)
17
21
  case args
18
22
  in Array => symbols if symbols.all? { |s| s.is_a?(::Symbol) }