plumb 0.0.3 → 0.0.4

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