plumb 0.0.2 → 0.0.4

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