plumb 0.0.3 → 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.
@@ -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
 
@@ -31,7 +33,7 @@ module Plumb
31
33
  in [::Hash => hash]
32
34
  self.class.new(schema: _schema.merge(wrap_keys_and_values(hash)), inclusive: @inclusive)
33
35
  in [key_type, value_type]
34
- HashMap.new(Steppable.wrap(key_type), Steppable.wrap(value_type))
36
+ HashMap.new(Composable.wrap(key_type), Composable.wrap(value_type))
35
37
  else
36
38
  raise ::ArgumentError, "unexpected value to Types::Hash#schema #{args.inspect}"
37
39
  end
@@ -44,9 +46,14 @@ module Plumb
44
46
  # we need to keep the right-side key, because even if the key name is the same,
45
47
  # it's optional flag might have changed
46
48
  def +(other)
47
- raise ArgumentError, "expected a HashClass, got #{other.class}" unless other.is_a?(HashClass)
48
-
49
- 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)
50
57
  end
51
58
 
52
59
  def &(other)
@@ -97,7 +104,7 @@ module Plumb
97
104
  end
98
105
 
99
106
  def call(result)
100
- 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)
101
108
  return result unless _schema.any?
102
109
 
103
110
  input = result.value
@@ -121,6 +128,10 @@ module Plumb
121
128
  errors.any? ? result.invalid(output, errors:) : result.valid(output)
122
129
  end
123
130
 
131
+ def ==(other)
132
+ other.is_a?(self.class) && other._schema == _schema
133
+ end
134
+
124
135
  private
125
136
 
126
137
  def _inspect
@@ -138,7 +149,7 @@ module Plumb
138
149
  when Callable
139
150
  hash
140
151
  else #  leaf values
141
- Steppable.wrap(hash)
152
+ Composable.wrap(hash)
142
153
  end
143
154
  end
144
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) }
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
3
4
  require 'plumb/visitor_handlers'
4
5
 
5
6
  module Plumb
@@ -23,11 +24,15 @@ module Plumb
23
24
  MAX_ITEMS = 'maxItems'
24
25
  MIN_LENGTH = 'minLength'
25
26
  MAX_LENGTH = 'maxLength'
27
+ ENVELOPE = {
28
+ '$schema' => 'https://json-schema.org/draft-08/schema#'
29
+ }.freeze
26
30
 
27
- def self.call(node)
28
- {
29
- '$schema' => 'https://json-schema.org/draft-08/schema#'
30
- }.merge(new.visit(node))
31
+ def self.call(node, root: true)
32
+ data = new.visit(node)
33
+ return data unless root
34
+
35
+ ENVELOPE.merge(data)
31
36
  end
32
37
 
33
38
  private def stringify_keys(hash) = hash.transform_keys(&:to_s)
@@ -37,7 +42,7 @@ module Plumb
37
42
  end
38
43
 
39
44
  on(:pipeline) do |node, props|
40
- visit(node.type, props)
45
+ visit_children(node, props)
41
46
  end
42
47
 
43
48
  on(:step) do |node, props|
@@ -58,9 +63,12 @@ module Plumb
58
63
  )
59
64
  end
60
65
 
66
+ on(:data) do |node, props|
67
+ visit_name :hash, node._schema, props
68
+ end
69
+
61
70
  on(:and) do |node, props|
62
- left = visit(node.left)
63
- right = visit(node.right)
71
+ left, right = node.children.map { |c| visit(c) }
64
72
  type = right[TYPE] || left[TYPE]
65
73
  props = props.merge(left).merge(right)
66
74
  props = props.merge(TYPE => type) if type
@@ -69,11 +77,10 @@ module Plumb
69
77
 
70
78
  # A "default" value is usually an "or" of expected_value | (undefined >> static_value)
71
79
  on(:or) do |node, props|
72
- left = visit(node.left)
73
- right = visit(node.right)
74
- any_of = [left, right].uniq
80
+ left, right = node.children.map { |c| visit(c) }
81
+ any_of = [left, right].uniq.filter(&:any?)
75
82
  if any_of.size == 1
76
- props.merge(left)
83
+ props.merge(any_of.first)
77
84
  elsif any_of.size == 2 && (defidx = any_of.index { |p| p.key?(DEFAULT) })
78
85
  val = any_of[defidx.zero? ? 1 : 0]
79
86
  props.merge(val).merge(DEFAULT => any_of[defidx][DEFAULT])
@@ -83,22 +90,23 @@ module Plumb
83
90
  end
84
91
 
85
92
  on(:not) do |node, props|
86
- props.merge(NOT => visit(node.step))
93
+ props.merge(NOT => visit_children(node))
87
94
  end
88
95
 
89
96
  on(:value) do |node, props|
90
- props = case node.value
97
+ value = node.children.first
98
+ props = case value
91
99
  when ::String, ::Symbol, ::Numeric
92
- props.merge(CONST => node.value)
100
+ props.merge(CONST => value)
93
101
  else
94
102
  props
95
103
  end
96
104
 
97
- visit(node.value, props)
105
+ visit(value, props)
98
106
  end
99
107
 
100
108
  on(:transform) do |node, props|
101
- visit(node.target_type, props)
109
+ visit_children(node, props)
102
110
  end
103
111
 
104
112
  on(:undefined) do |_node, props|
@@ -108,18 +116,19 @@ module Plumb
108
116
  on(:static) do |node, props|
109
117
  # Set const AND default
110
118
  # to emulate static values
111
- props = case node.value
119
+ value = node.children.first
120
+ props = case value
112
121
  when ::String, ::Symbol, ::Numeric
113
- props.merge(CONST => node.value, DEFAULT => node.value)
122
+ props.merge(CONST => value, DEFAULT => value)
114
123
  else
115
124
  props
116
125
  end
117
126
 
118
- visit(node.value, props)
127
+ visit(value, props)
119
128
  end
120
129
 
121
130
  on(:policy) do |node, props|
122
- props = visit(node.step, props)
131
+ props = visit_children(node, props)
123
132
  method_name = :"visit_#{node.policy_name}_policy"
124
133
  if respond_to?(method_name)
125
134
  send(method_name, node, props)
@@ -168,14 +177,15 @@ module Plumb
168
177
 
169
178
  on(:match) do |node, props|
170
179
  # Set const if primitive
171
- props = case node.matcher
180
+ matcher = node.children.first
181
+ props = case matcher
172
182
  when ::String, ::Symbol, ::Numeric
173
- props.merge(CONST => node.matcher)
183
+ props.merge(CONST => matcher)
174
184
  else
175
185
  props
176
186
  end
177
187
 
178
- visit(node.matcher, props)
188
+ visit(matcher, props)
179
189
  end
180
190
 
181
191
  on(:boolean) do |_node, props|
@@ -228,6 +238,14 @@ module Plumb
228
238
  props.merge(opts)
229
239
  end
230
240
 
241
+ on(::Time) do |_node, props|
242
+ props.merge(TYPE => 'string', 'format' => 'date-time')
243
+ end
244
+
245
+ on(::Date) do |_node, props|
246
+ props.merge(TYPE => 'string', 'format' => 'date')
247
+ end
248
+
231
249
  on(::Hash) do |_node, props|
232
250
  props.merge(TYPE => 'object')
233
251
  end
@@ -245,7 +263,7 @@ module Plumb
245
263
  {
246
264
  TYPE => 'object',
247
265
  'patternProperties' => {
248
- '.*' => visit(node.value_type)
266
+ '.*' => visit(node.children[1])
249
267
  }
250
268
  }
251
269
  end
@@ -254,27 +272,27 @@ module Plumb
254
272
  {
255
273
  TYPE => 'object',
256
274
  'patternProperties' => {
257
- '.*' => visit(node.value_type)
275
+ '.*' => visit(node.children[1])
258
276
  }
259
277
  }
260
278
  end
261
279
 
262
280
  on(:build) do |node, props|
263
- visit(node.type, props)
281
+ visit_children(node, props)
264
282
  end
265
283
 
266
284
  on(:array) do |node, _props|
267
- items = visit(node.element_type)
268
- { TYPE => 'array', ITEMS => items }
285
+ items_props = visit_children(node)
286
+ { TYPE => 'array', ITEMS => items_props }
269
287
  end
270
288
 
271
289
  on(:stream) do |node, _props|
272
- items = visit(node.element_type)
273
- { TYPE => 'array', ITEMS => items }
290
+ items_props = visit_children(node)
291
+ { TYPE => 'array', ITEMS => items_props }
274
292
  end
275
293
 
276
294
  on(:tuple) do |node, _props|
277
- items = node.types.map { |t| visit(t) }
295
+ items = node.children.map { |t| visit(t) }
278
296
  { TYPE => 'array', 'prefixItems' => items }
279
297
  end
280
298
 
@@ -286,7 +304,7 @@ module Plumb
286
304
  }
287
305
 
288
306
  key = node.key.to_s
289
- children = node.types.map { |c| visit(c) }
307
+ children = node.children.map { |c| visit(c) }
290
308
  key_enum = children.map { |c| c[PROPERTIES][key][CONST] }
291
309
  key_type = children.map { |c| c[PROPERTIES][key][TYPE] }
292
310
  required << key
@@ -1,12 +1,12 @@
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 MatchClass
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :matcher
9
+ attr_reader :children
10
10
 
11
11
  def initialize(matcher = Undefined, error: nil, label: nil)
12
12
  raise TypeError 'matcher must respond to #===' unless matcher.respond_to?(:===)
@@ -14,6 +14,7 @@ module Plumb
14
14
  @matcher = matcher
15
15
  @error = error.nil? ? build_error(matcher) : (error % matcher)
16
16
  @label = matcher.is_a?(Class) ? matcher.inspect : "Match(#{label || @matcher.inspect})"
17
+ @children = [matcher].freeze
17
18
  freeze
18
19
  end
19
20
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Plumb
4
4
  class Metadata
5
- include Steppable
5
+ include Composable
6
6
 
7
7
  attr_reader :metadata
8
8
 
@@ -11,6 +11,10 @@ module Plumb
11
11
  freeze
12
12
  end
13
13
 
14
+ def ==(other)
15
+ other.is_a?(self.class) && @metadata == other.metadata
16
+ end
17
+
14
18
  def call(result) = result
15
19
 
16
20
  private def _inspect = "Metadata[#{@metadata.inspect}]"
@@ -10,23 +10,14 @@ module Plumb
10
10
  new.visit(node)
11
11
  end
12
12
 
13
- def on_missing_handler(node, props, method_name)
13
+ def on_missing_handler(node, props, _method_name)
14
14
  return props.merge(type: node) if node.instance_of?(Class)
15
15
 
16
- puts "Missing handler for #{node.inspect} with props #{node.inspect} and method_name :#{method_name}"
17
- props
18
- end
19
-
20
- on(:undefined) do |_node, props|
21
- props
22
- end
16
+ return props unless node.respond_to?(:children)
23
17
 
24
- on(:any) do |_node, props|
25
- props
26
- end
27
-
28
- on(:pipeline) do |node, props|
29
- visit(node.type, props)
18
+ node.children.reduce(props) do |acc, child|
19
+ visit(child, acc)
20
+ end
30
21
  end
31
22
 
32
23
  on(:step) do |node, props|
@@ -42,17 +33,12 @@ module Plumb
42
33
  props.merge(match: node, type:)
43
34
  end
44
35
 
45
- on(:match) do |node, props|
46
- visit(node.matcher, props)
47
- end
48
-
49
36
  on(:hash) do |_node, props|
50
37
  props.merge(type: Hash)
51
38
  end
52
39
 
53
40
  on(:and) do |node, props|
54
- left = visit(node.left)
55
- right = visit(node.right)
41
+ left, right = node.children.map { |child| visit(child) }
56
42
  type = right[:type] || left[:type]
57
43
  props = props.merge(left).merge(right)
58
44
  props = props.merge(type:) if type
@@ -60,7 +46,7 @@ module Plumb
60
46
  end
61
47
 
62
48
  on(:or) do |node, props|
63
- child_metas = [visit(node.left), visit(node.right)]
49
+ child_metas = node.children.map { |child| visit(child) }
64
50
  types = child_metas.map { |child| child[:type] }.flatten.compact
65
51
  types = types.first if types.size == 1
66
52
  child_metas.reduce(props) do |acc, child|
@@ -68,27 +54,16 @@ module Plumb
68
54
  end.merge(type: types)
69
55
  end
70
56
 
71
- on(:value) do |node, props|
72
- visit(node.value, props)
73
- end
74
-
75
- on(:transform) do |node, props|
76
- props.merge(type: node.target_type)
77
- end
78
-
79
57
  on(:static) do |node, props|
80
- type = node.value.is_a?(Class) ? node.value : node.value.class
81
- props.merge(static: node.value, type:)
58
+ value = node.children[0]
59
+ type = value.is_a?(Class) ? value : value.class
60
+ props.merge(static: value, type:)
82
61
  end
83
62
 
84
63
  on(:policy) do |node, props|
85
- visit(node.step, props).merge(node.policy_name => node.arg)
86
- end
87
-
88
- on(:rules) do |node, props|
89
- node.rules.reduce(props) do |acc, rule|
90
- acc.merge(rule.name => rule.arg_value)
91
- end
64
+ props = visit(node.children[0], props)
65
+ props = props.merge(node.policy_name => node.arg) unless node.arg == Plumb::Undefined
66
+ props
92
67
  end
93
68
 
94
69
  on(:boolean) do |_node, props|
@@ -103,10 +78,6 @@ module Plumb
103
78
  props.merge(type: Hash)
104
79
  end
105
80
 
106
- on(:build) do |node, props|
107
- visit(node.type, props)
108
- end
109
-
110
81
  on(:array) do |_node, props|
111
82
  props.merge(type: Array)
112
83
  end
data/lib/plumb/not.rb CHANGED
@@ -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 Not
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :step
9
+ attr_reader :children, :errors
10
10
 
11
11
  def initialize(step, errors: nil)
12
12
  @step = step
13
13
  @errors = errors
14
+ @children = [step].freeze
14
15
  freeze
15
16
  end
16
17
 
data/lib/plumb/or.rb CHANGED
@@ -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 Or
7
- include Steppable
7
+ include Composable
8
8
 
9
- attr_reader :left, :right
9
+ attr_reader :children
10
10
 
11
11
  def initialize(left, right)
12
12
  @left = left
13
13
  @right = right
14
+ @children = [left, right].freeze
14
15
  freeze
15
16
  end
16
17
 
@@ -23,7 +24,12 @@ module Plumb
23
24
  return left_result if left_result.valid?
24
25
 
25
26
  right_result = @right.call(result)
26
- right_result.valid? ? right_result : result.invalid(errors: [left_result.errors, right_result.errors].flatten)
27
+ if right_result.valid?
28
+ right_result
29
+ else
30
+ right_result.invalid(errors: [left_result.errors,
31
+ right_result.errors].flatten)
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,13 +1,13 @@
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 Pipeline
7
- include Steppable
7
+ include Composable
8
8
 
9
9
  class AroundStep
10
- include Steppable
10
+ include Composable
11
11
 
12
12
  def initialize(step, block)
13
13
  @step = step
@@ -19,10 +19,11 @@ module Plumb
19
19
  end
20
20
  end
21
21
 
22
- attr_reader :type
22
+ attr_reader :children
23
23
 
24
24
  def initialize(type = Types::Any, &setup)
25
25
  @type = type
26
+ @children = [type].freeze
26
27
  @around_blocks = []
27
28
  return unless block_given?
28
29
 
@@ -38,7 +39,7 @@ module Plumb
38
39
  callable ||= block
39
40
  unless is_a_step?(callable)
40
41
  raise ArgumentError,
41
- "#step expects an interface #call(Result) Result, but got #{callable.inspect}"
42
+ "#step expects an interface #call(Result) Result, but got #{callable.inspect}"
42
43
  end
43
44
 
44
45
  callable = @around_blocks.reduce(callable) { |cl, bl| AroundStep.new(cl, bl) } if @around_blocks.any?
data/lib/plumb/policy.rb CHANGED
@@ -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
  # Wrap a policy composition ("step") in a Policy object.
7
7
  # So that visitors such as JSONSchema and Metadata visitors
8
8
  # can define dedicated handlers for policies, if they need to.
9
9
  class Policy
10
- include Steppable
10
+ include Composable
11
11
 
12
- attr_reader :policy_name, :arg, :step
12
+ attr_reader :policy_name, :arg, :children
13
13
 
14
14
  # @param policy_name [Symbol]
15
15
  # @param arg [Object, nil] the argument to the policy, if any.
@@ -18,9 +18,16 @@ module Plumb
18
18
  @policy_name = policy_name
19
19
  @arg = arg
20
20
  @step = step
21
+ @children = [step].freeze
21
22
  freeze
22
23
  end
23
24
 
25
+ def ==(other)
26
+ other.is_a?(self.class) &&
27
+ policy_name == other.policy_name &&
28
+ arg == other.arg
29
+ end
30
+
24
31
  # The standard Step interface.
25
32
  # @param result [Result::Valid]
26
33
  # @return [Result::Valid, Result::Invalid]