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.
@@ -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
@@ -12,17 +13,26 @@ module Plumb
12
13
  DEFAULT = 'default'
13
14
  ANY_OF = 'anyOf'
14
15
  ALL_OF = 'allOf'
16
+ NOT = 'not'
15
17
  ENUM = 'enum'
16
18
  CONST = 'const'
17
19
  ITEMS = 'items'
18
20
  PATTERN = 'pattern'
19
21
  MINIMUM = 'minimum'
20
22
  MAXIMUM = 'maximum'
23
+ MIN_ITEMS = 'minItems'
24
+ MAX_ITEMS = 'maxItems'
25
+ MIN_LENGTH = 'minLength'
26
+ MAX_LENGTH = 'maxLength'
27
+ ENVELOPE = {
28
+ '$schema' => 'https://json-schema.org/draft-08/schema#'
29
+ }.freeze
21
30
 
22
- def self.call(node)
23
- {
24
- '$schema' => 'https://json-schema.org/draft-08/schema#'
25
- }.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)
26
36
  end
27
37
 
28
38
  private def stringify_keys(hash) = hash.transform_keys(&:to_s)
@@ -32,7 +42,7 @@ module Plumb
32
42
  end
33
43
 
34
44
  on(:pipeline) do |node, props|
35
- visit(node.type, props)
45
+ visit_children(node, props)
36
46
  end
37
47
 
38
48
  on(:step) do |node, props|
@@ -53,9 +63,12 @@ module Plumb
53
63
  )
54
64
  end
55
65
 
66
+ on(:data) do |node, props|
67
+ visit_name :hash, node._schema, props
68
+ end
69
+
56
70
  on(:and) do |node, props|
57
- left = visit(node.left)
58
- right = visit(node.right)
71
+ left, right = node.children.map { |c| visit(c) }
59
72
  type = right[TYPE] || left[TYPE]
60
73
  props = props.merge(left).merge(right)
61
74
  props = props.merge(TYPE => type) if type
@@ -64,11 +77,10 @@ module Plumb
64
77
 
65
78
  # A "default" value is usually an "or" of expected_value | (undefined >> static_value)
66
79
  on(:or) do |node, props|
67
- left = visit(node.left)
68
- right = visit(node.right)
69
- any_of = [left, right].uniq
80
+ left, right = node.children.map { |c| visit(c) }
81
+ any_of = [left, right].uniq.filter(&:any?)
70
82
  if any_of.size == 1
71
- props.merge(left)
83
+ props.merge(any_of.first)
72
84
  elsif any_of.size == 2 && (defidx = any_of.index { |p| p.key?(DEFAULT) })
73
85
  val = any_of[defidx.zero? ? 1 : 0]
74
86
  props.merge(val).merge(DEFAULT => any_of[defidx][DEFAULT])
@@ -78,22 +90,23 @@ module Plumb
78
90
  end
79
91
 
80
92
  on(:not) do |node, props|
81
- props.merge('not' => visit(node.step))
93
+ props.merge(NOT => visit_children(node))
82
94
  end
83
95
 
84
96
  on(:value) do |node, props|
85
- props = case node.value
97
+ value = node.children.first
98
+ props = case value
86
99
  when ::String, ::Symbol, ::Numeric
87
- props.merge(CONST => node.value)
100
+ props.merge(CONST => value)
88
101
  else
89
102
  props
90
103
  end
91
104
 
92
- visit(node.value, props)
105
+ visit(value, props)
93
106
  end
94
107
 
95
108
  on(:transform) do |node, props|
96
- visit(node.target_type, props)
109
+ visit_children(node, props)
97
110
  end
98
111
 
99
112
  on(:undefined) do |_node, props|
@@ -103,36 +116,76 @@ module Plumb
103
116
  on(:static) do |node, props|
104
117
  # Set const AND default
105
118
  # to emulate static values
106
- props = case node.value
119
+ value = node.children.first
120
+ props = case value
107
121
  when ::String, ::Symbol, ::Numeric
108
- props.merge(CONST => node.value, DEFAULT => node.value)
122
+ props.merge(CONST => value, DEFAULT => value)
109
123
  else
110
124
  props
111
125
  end
112
126
 
113
- visit(node.value, props)
127
+ visit(value, props)
114
128
  end
115
129
 
116
- on(:rules) do |node, props|
117
- node.rules.reduce(props) do |acc, rule|
118
- acc.merge(visit(rule))
130
+ on(:policy) do |node, props|
131
+ props = visit_children(node, props)
132
+ method_name = :"visit_#{node.policy_name}_policy"
133
+ if respond_to?(method_name)
134
+ send(method_name, node, props)
135
+ else
136
+ props
119
137
  end
120
138
  end
121
139
 
122
- on(:rule_included_in) do |node, props|
123
- props.merge(ENUM => node.arg_value)
140
+ on(:options_policy) do |node, props|
141
+ props.merge(ENUM => node.arg)
142
+ end
143
+
144
+ on(:size_policy) do |node, props|
145
+ opts = {}
146
+ case props[TYPE]
147
+ when 'array'
148
+ case node.arg
149
+ when Range
150
+ opts[MIN_ITEMS] = node.arg.min if node.arg.begin
151
+ opts[MAX_ITEMS] = node.arg.max if node.arg.end
152
+ when Numeric
153
+ opts[MIN_ITEMS] = node.arg
154
+ opts[MAX_ITEMS] = node.arg
155
+ end
156
+ when 'string'
157
+ case node.arg
158
+ when Range
159
+ opts[MIN_LENGTH] = node.arg.min if node.arg.begin
160
+ opts[MAX_LENGTH] = node.arg.max if node.arg.end
161
+ when Numeric
162
+ opts[MIN_LENGTH] = node.arg
163
+ opts[MAX_LENGTH] = node.arg
164
+ end
165
+ end
166
+
167
+ props.merge(opts)
168
+ end
169
+
170
+ on(:excluded_from_policy) do |node, props|
171
+ props.merge(NOT => { ENUM => node.arg })
172
+ end
173
+
174
+ on(Proc) do |_node, props|
175
+ props
124
176
  end
125
177
 
126
178
  on(:match) do |node, props|
127
179
  # Set const if primitive
128
- props = case node.matcher
180
+ matcher = node.children.first
181
+ props = case matcher
129
182
  when ::String, ::Symbol, ::Numeric
130
- props.merge(CONST => node.matcher)
183
+ props.merge(CONST => matcher)
131
184
  else
132
185
  props
133
186
  end
134
187
 
135
- visit(node.matcher, props)
188
+ visit(matcher, props)
136
189
  end
137
190
 
138
191
  on(:boolean) do |_node, props|
@@ -185,6 +238,14 @@ module Plumb
185
238
  props.merge(opts)
186
239
  end
187
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
+
188
249
  on(::Hash) do |_node, props|
189
250
  props.merge(TYPE => 'object')
190
251
  end
@@ -202,7 +263,7 @@ module Plumb
202
263
  {
203
264
  TYPE => 'object',
204
265
  'patternProperties' => {
205
- '.*' => visit(node.value_type)
266
+ '.*' => visit(node.children[1])
206
267
  }
207
268
  }
208
269
  end
@@ -211,27 +272,27 @@ module Plumb
211
272
  {
212
273
  TYPE => 'object',
213
274
  'patternProperties' => {
214
- '.*' => visit(node.value_type)
275
+ '.*' => visit(node.children[1])
215
276
  }
216
277
  }
217
278
  end
218
279
 
219
280
  on(:build) do |node, props|
220
- visit(node.type, props)
281
+ visit_children(node, props)
221
282
  end
222
283
 
223
284
  on(:array) do |node, _props|
224
- items = visit(node.element_type)
225
- { TYPE => 'array', ITEMS => items }
285
+ items_props = visit_children(node)
286
+ { TYPE => 'array', ITEMS => items_props }
226
287
  end
227
288
 
228
289
  on(:stream) do |node, _props|
229
- items = visit(node.element_type)
230
- { TYPE => 'array', ITEMS => items }
290
+ items_props = visit_children(node)
291
+ { TYPE => 'array', ITEMS => items_props }
231
292
  end
232
293
 
233
294
  on(:tuple) do |node, _props|
234
- items = node.types.map { |t| visit(t) }
295
+ items = node.children.map { |t| visit(t) }
235
296
  { TYPE => 'array', 'prefixItems' => items }
236
297
  end
237
298
 
@@ -243,7 +304,7 @@ module Plumb
243
304
  }
244
305
 
245
306
  key = node.key.to_s
246
- children = node.types.map { |c| visit(c) }
307
+ children = node.children.map { |c| visit(c) }
247
308
  key_enum = children.map { |c| c[PROPERTIES][key][CONST] }
248
309
  key_type = children.map { |c| c[PROPERTIES][key][TYPE] }
249
310
  required << key
@@ -1,18 +1,20 @@
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
- def initialize(matcher = Undefined, error: nil)
11
+ def initialize(matcher = Undefined, error: nil, label: nil)
12
12
  raise TypeError 'matcher must respond to #===' unless matcher.respond_to?(:===)
13
13
 
14
14
  @matcher = matcher
15
15
  @error = error.nil? ? build_error(matcher) : (error % matcher)
16
+ @label = matcher.is_a?(Class) ? matcher.inspect : "Match(#{label || @matcher.inspect})"
17
+ @children = [matcher].freeze
16
18
  freeze
17
19
  end
18
20
 
@@ -22,9 +24,7 @@ module Plumb
22
24
 
23
25
  private
24
26
 
25
- def _inspect
26
- @matcher.inspect
27
- end
27
+ def _inspect = @label
28
28
 
29
29
  def build_error(matcher)
30
30
  case matcher
@@ -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,22 +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
- props.merge(static: node.value)
58
+ value = node.children[0]
59
+ type = value.is_a?(Class) ? value : value.class
60
+ props.merge(static: value, type:)
81
61
  end
82
62
 
83
- on(:rules) do |node, props|
84
- node.rules.reduce(props) do |acc, rule|
85
- acc.merge(rule.name => rule.arg_value)
86
- end
63
+ on(:policy) do |node, props|
64
+ props = visit(node.children[0], props)
65
+ props = props.merge(node.policy_name => node.arg) unless node.arg == Plumb::Undefined
66
+ props
87
67
  end
88
68
 
89
69
  on(:boolean) do |_node, props|
@@ -98,10 +78,6 @@ module Plumb
98
78
  props.merge(type: Hash)
99
79
  end
100
80
 
101
- on(:build) do |node, props|
102
- visit(node.type, props)
103
- end
104
-
105
81
  on(:array) do |_node, props|
106
82
  props.merge(type: Array)
107
83
  end
@@ -117,5 +93,9 @@ module Plumb
117
93
  on(:tagged_hash) do |_node, props|
118
94
  props.merge(type: Hash)
119
95
  end
96
+
97
+ on(Proc) do |_node, props|
98
+ props
99
+ end
120
100
  end
121
101
  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?
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumb/policies'
4
+
5
+ module Plumb
6
+ # A policy registry for Plumb
7
+ # It holds and gets registered policies.
8
+ # Policies are callable objects that act as factories for type compositions.
9
+ class Policies
10
+ UnknownPolicyError = Class.new(StandardError)
11
+ MethodAlreadyDefinedError = Class.new(StandardError)
12
+
13
+ def initialize
14
+ @policies = {}
15
+ end
16
+
17
+ # Register a policy for all or specific outpyt types.
18
+ # Example for a policy that works for all types:
19
+ # #register(Object, :my_policy, ->(node, arg) { ... })
20
+ # Example for a policy that works for a specific type:
21
+ # #register(String, :my_policy, ->(node, arg) { ... })
22
+ # Example for a policy that works for a specific interface:
23
+ # #register(:size, :my_policy, ->(node, arg) { ... })
24
+ #
25
+ # The policy callable takes the step it is applied to, a policy argument (if any) and a policy block (if any).
26
+ # Example for a policy #default(default_value = Undefined) { 'some-default-value' }
27
+ # policy = proc do |type, default_value = Undefined, &block|
28
+ # type | (Plumb::Types::Undefined >> Plumb::Types::Static[default_value])
29
+ # end
30
+ #
31
+ # @param for_type [Class, Symbol] the type the policy is for.
32
+ # @param name [Symbol] the name of the policy.
33
+ # @param policy [Proc] the policy to register.
34
+ def register(for_type, name, policy)
35
+ @policies[name] ||= {}
36
+ @policies[name][for_type] = policy
37
+ end
38
+
39
+ # Get a policy for a given type.
40
+ # @param types [Array<Class>] the types
41
+ # @param name [Symbol] the policy name
42
+ # @return [#call] the policy callable
43
+ # @raise [UnknownPolicyError] if the policy is not registered for the given types
44
+ def get(types, name)
45
+ if (pol = resolve_shared_policy(types, name))
46
+ pol
47
+ elsif (pol = @policies.dig(name, Object))
48
+ raise UnknownPolicyError, "Unknown policy #{name} for #{types.inspect}" unless pol
49
+
50
+ pol
51
+ else
52
+ raise UnknownPolicyError, "Unknown or incompatible policy #{name} for #{types.inspect}"
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def resolve_shared_policy(types, name)
59
+ pols = types.map do |type|
60
+ resolve_policy(type, name)
61
+ end.uniq
62
+ pols.size == 1 ? pols.first : nil
63
+ end
64
+
65
+ def resolve_policy(type, name)
66
+ policies = @policies[name]
67
+ return nil unless policies
68
+
69
+ # { Object => policy1, String => policy2, size: policy3 }
70
+ #
71
+ policies.find do |for_type, _pol|
72
+ case for_type
73
+ when Symbol # :size
74
+ type.instance_methods.include?(for_type)
75
+ when Class # String
76
+ for_type == type
77
+ end
78
+ end&.last
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumb/composable'
4
+
5
+ module Plumb
6
+ # Wrap a policy composition ("step") in a Policy object.
7
+ # So that visitors such as JSONSchema and Metadata visitors
8
+ # can define dedicated handlers for policies, if they need to.
9
+ class Policy
10
+ include Composable
11
+
12
+ attr_reader :policy_name, :arg, :children
13
+
14
+ # @param policy_name [Symbol]
15
+ # @param arg [Object, nil] the argument to the policy, if any.
16
+ # @param step [Step] the step composition wrapped by this policy.
17
+ def initialize(policy_name, arg, step)
18
+ @policy_name = policy_name
19
+ @arg = arg
20
+ @step = step
21
+ @children = [step].freeze
22
+ freeze
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(self.class) &&
27
+ policy_name == other.policy_name &&
28
+ arg == other.arg
29
+ end
30
+
31
+ # The standard Step interface.
32
+ # @param result [Result::Valid]
33
+ # @return [Result::Valid, Result::Invalid]
34
+ def call(result) = @step.call(result)
35
+
36
+ private def _inspect = @step.inspect
37
+ end
38
+ end