plumb 0.0.2 → 0.0.4

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