plumb 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,46 +12,55 @@ module Plumb
12
12
  DEFAULT = 'default'
13
13
  ANY_OF = 'anyOf'
14
14
  ALL_OF = 'allOf'
15
+ NOT = 'not'
15
16
  ENUM = 'enum'
16
17
  CONST = 'const'
17
18
  ITEMS = 'items'
18
19
  PATTERN = 'pattern'
19
20
  MINIMUM = 'minimum'
20
21
  MAXIMUM = 'maximum'
22
+ MIN_ITEMS = 'minItems'
23
+ MAX_ITEMS = 'maxItems'
24
+ MIN_LENGTH = 'minLength'
25
+ MAX_LENGTH = 'maxLength'
21
26
 
22
- def self.call(type)
23
- {
24
- '$schema' => 'https://json-schema.org/draft-08/schema#',
25
- }.merge(new.visit(type))
27
+ def self.call(node)
28
+ {
29
+ '$schema' => 'https://json-schema.org/draft-08/schema#'
30
+ }.merge(new.visit(node))
26
31
  end
27
32
 
28
33
  private def stringify_keys(hash) = hash.transform_keys(&:to_s)
29
34
 
30
- on(:any) do |type, props|
35
+ on(:any) do |_node, props|
31
36
  props
32
37
  end
33
38
 
34
- on(:pipeline) do |type, props|
35
- visit(type.type, props)
39
+ on(:pipeline) do |node, props|
40
+ visit(node.type, props)
36
41
  end
37
42
 
38
- on(:step) do |type, props|
39
- props.merge(stringify_keys(type._metadata))
43
+ on(:step) do |node, props|
44
+ props.merge(stringify_keys(node._metadata))
40
45
  end
41
46
 
42
- on(:hash) do |type, props|
47
+ on(:interface) do |_node, props|
48
+ props
49
+ end
50
+
51
+ on(:hash) do |node, props|
43
52
  props.merge(
44
53
  TYPE => 'object',
45
- PROPERTIES => type._schema.each_with_object({}) do |(key, value), hash|
54
+ PROPERTIES => node._schema.each_with_object({}) do |(key, value), hash|
46
55
  hash[key.to_s] = visit(value)
47
56
  end,
48
- REQUIRED => type._schema.select { |key, value| !key.optional? }.keys.map(&:to_s)
57
+ REQUIRED => node._schema.reject { |key, _value| key.optional? }.keys.map(&:to_s)
49
58
  )
50
59
  end
51
60
 
52
- on(:and) do |type, props|
53
- left = visit(type.left)
54
- right = visit(type.right)
61
+ on(:and) do |node, props|
62
+ left = visit(node.left)
63
+ right = visit(node.right)
55
64
  type = right[TYPE] || left[TYPE]
56
65
  props = props.merge(left).merge(right)
57
66
  props = props.merge(TYPE => type) if type
@@ -59,148 +68,225 @@ module Plumb
59
68
  end
60
69
 
61
70
  # A "default" value is usually an "or" of expected_value | (undefined >> static_value)
62
- on(:or) do |type, props|
63
- left = visit(type.left)
64
- right = visit(type.right)
71
+ on(:or) do |node, props|
72
+ left = visit(node.left)
73
+ right = visit(node.right)
65
74
  any_of = [left, right].uniq
66
75
  if any_of.size == 1
67
76
  props.merge(left)
68
77
  elsif any_of.size == 2 && (defidx = any_of.index { |p| p.key?(DEFAULT) })
69
- val = any_of[defidx == 0 ? 1 : 0]
78
+ val = any_of[defidx.zero? ? 1 : 0]
70
79
  props.merge(val).merge(DEFAULT => any_of[defidx][DEFAULT])
71
80
  else
72
81
  props.merge(ANY_OF => any_of)
73
82
  end
74
83
  end
75
84
 
76
- on(:value) do |type, props|
77
- props = case type.value
78
- when ::String, ::Symbol, ::Numeric
79
- props.merge(CONST => type.value)
80
- else
81
- props
82
- end
85
+ on(:not) do |node, props|
86
+ props.merge(NOT => visit(node.step))
87
+ end
88
+
89
+ on(:value) do |node, props|
90
+ props = case node.value
91
+ when ::String, ::Symbol, ::Numeric
92
+ props.merge(CONST => node.value)
93
+ else
94
+ props
95
+ end
83
96
 
84
- visit(type.value, props)
97
+ visit(node.value, props)
85
98
  end
86
99
 
87
- on(:transform) do |type, props|
88
- visit(type.target_type, props)
100
+ on(:transform) do |node, props|
101
+ visit(node.target_type, props)
89
102
  end
90
103
 
91
- on(:undefined) do |type, props|
104
+ on(:undefined) do |_node, props|
92
105
  props
93
106
  end
94
107
 
95
- on(:static) do |type, props|
96
- props = case type.value
97
- when ::String, ::Symbol, ::Numeric
98
- props.merge(CONST => type.value, DEFAULT => type.value)
108
+ on(:static) do |node, props|
109
+ # Set const AND default
110
+ # to emulate static values
111
+ props = case node.value
112
+ when ::String, ::Symbol, ::Numeric
113
+ props.merge(CONST => node.value, DEFAULT => node.value)
114
+ else
115
+ props
116
+ end
117
+
118
+ visit(node.value, props)
119
+ end
120
+
121
+ on(:policy) do |node, props|
122
+ props = visit(node.step, props)
123
+ method_name = :"visit_#{node.policy_name}_policy"
124
+ if respond_to?(method_name)
125
+ send(method_name, node, props)
99
126
  else
100
127
  props
101
128
  end
129
+ end
102
130
 
103
- visit(type.value, props)
131
+ on(:options_policy) do |node, props|
132
+ props.merge(ENUM => node.arg)
104
133
  end
105
134
 
106
- on(:rules) do |type, props|
107
- type.rules.reduce(props) do |acc, rule|
108
- acc.merge(visit(rule))
135
+ on(:size_policy) do |node, props|
136
+ opts = {}
137
+ case props[TYPE]
138
+ when 'array'
139
+ case node.arg
140
+ when Range
141
+ opts[MIN_ITEMS] = node.arg.min if node.arg.begin
142
+ opts[MAX_ITEMS] = node.arg.max if node.arg.end
143
+ when Numeric
144
+ opts[MIN_ITEMS] = node.arg
145
+ opts[MAX_ITEMS] = node.arg
146
+ end
147
+ when 'string'
148
+ case node.arg
149
+ when Range
150
+ opts[MIN_LENGTH] = node.arg.min if node.arg.begin
151
+ opts[MAX_LENGTH] = node.arg.max if node.arg.end
152
+ when Numeric
153
+ opts[MIN_LENGTH] = node.arg
154
+ opts[MAX_LENGTH] = node.arg
155
+ end
109
156
  end
157
+
158
+ props.merge(opts)
110
159
  end
111
160
 
112
- on(:rule_included_in) do |type, props|
113
- props.merge(ENUM => type.arg_value)
161
+ on(:excluded_from_policy) do |node, props|
162
+ props.merge(NOT => { ENUM => node.arg })
114
163
  end
115
164
 
116
- on(:match) do |type, props|
117
- visit(type.matcher, props)
165
+ on(Proc) do |_node, props|
166
+ props
118
167
  end
119
168
 
120
- on(:boolean) do |type, props|
169
+ on(:match) do |node, props|
170
+ # Set const if primitive
171
+ props = case node.matcher
172
+ when ::String, ::Symbol, ::Numeric
173
+ props.merge(CONST => node.matcher)
174
+ else
175
+ props
176
+ end
177
+
178
+ visit(node.matcher, props)
179
+ end
180
+
181
+ on(:boolean) do |_node, props|
121
182
  props.merge(TYPE => 'boolean')
122
183
  end
123
184
 
124
- on(::String) do |type, props|
185
+ on(::String) do |_node, props|
125
186
  props.merge(TYPE => 'string')
126
187
  end
127
188
 
128
- on(::Integer) do |type, props|
189
+ on(::Integer) do |_node, props|
129
190
  props.merge(TYPE => 'integer')
130
191
  end
131
192
 
132
- on(::Numeric) do |type, props|
193
+ on(::Numeric) do |_node, props|
133
194
  props.merge(TYPE => 'number')
134
195
  end
135
196
 
136
- on(::BigDecimal) do |type, props|
197
+ on(::BigDecimal) do |_node, props|
137
198
  props.merge(TYPE => 'number')
138
199
  end
139
200
 
140
- on(::Float) do |type, props|
201
+ on(::Float) do |_node, props|
141
202
  props.merge(TYPE => 'number')
142
203
  end
143
204
 
144
- on(::TrueClass) do |type, props|
205
+ on(::TrueClass) do |_node, props|
145
206
  props.merge(TYPE => 'boolean')
146
207
  end
147
208
 
148
- on(::NilClass) do |type, props|
209
+ on(::NilClass) do |_node, props|
149
210
  props.merge(TYPE => 'null')
150
211
  end
151
212
 
152
- on(::FalseClass) do |type, props|
213
+ on(::FalseClass) do |_node, props|
153
214
  props.merge(TYPE => 'boolean')
154
215
  end
155
216
 
156
- on(::Regexp) do |type, props|
157
- props.merge(PATTERN => type.source)
217
+ on(::Regexp) do |node, props|
218
+ props.merge(PATTERN => node.source, TYPE => props[TYPE] || 'string')
158
219
  end
159
220
 
160
- on(::Range) do |type, props|
161
- opts = {}
162
- opts[MINIMUM] = type.min if type.begin
163
- opts[MAXIMUM] = type.max if type.end
221
+ on(::Range) do |node, props|
222
+ element = node.begin || node.end
223
+ opts = visit(element.class)
224
+ if element.is_a?(::Numeric)
225
+ opts[MINIMUM] = node.min if node.begin
226
+ opts[MAXIMUM] = node.max if node.end
227
+ end
164
228
  props.merge(opts)
165
229
  end
166
230
 
167
- on(:metadata) do |type, props|
168
- # TODO: here we should filter out the metadata that is not relevant for JSON Schema
169
- props.merge(stringify_keys(type.metadata))
231
+ on(::Hash) do |_node, props|
232
+ props.merge(TYPE => 'object')
233
+ end
234
+
235
+ on(::Array) do |_node, props|
236
+ props.merge(TYPE => 'array')
170
237
  end
171
238
 
172
- on(:hash_map) do |type, props|
239
+ on(:metadata) do |node, props|
240
+ #  TODO: here we should filter out the metadata that is not relevant for JSON Schema
241
+ props.merge(stringify_keys(node.metadata))
242
+ end
243
+
244
+ on(:hash_map) do |node, _props|
173
245
  {
174
246
  TYPE => 'object',
175
247
  'patternProperties' => {
176
- '.*' => visit(type.value_type)
248
+ '.*' => visit(node.value_type)
177
249
  }
178
250
  }
179
251
  end
180
252
 
181
- on(:build) do |type, props|
182
- visit(type.type, props)
253
+ on(:filtered_hash_map) do |node, _props|
254
+ {
255
+ TYPE => 'object',
256
+ 'patternProperties' => {
257
+ '.*' => visit(node.value_type)
258
+ }
259
+ }
260
+ end
261
+
262
+ on(:build) do |node, props|
263
+ visit(node.type, props)
264
+ end
265
+
266
+ on(:array) do |node, _props|
267
+ items = visit(node.element_type)
268
+ { TYPE => 'array', ITEMS => items }
183
269
  end
184
270
 
185
- on(:array) do |type, props|
186
- items = visit(type.element_type)
271
+ on(:stream) do |node, _props|
272
+ items = visit(node.element_type)
187
273
  { TYPE => 'array', ITEMS => items }
188
274
  end
189
275
 
190
- on(:tuple) do |type, props|
191
- items = type.types.map { |t| visit(t) }
276
+ on(:tuple) do |node, _props|
277
+ items = node.types.map { |t| visit(t) }
192
278
  { TYPE => 'array', 'prefixItems' => items }
193
279
  end
194
280
 
195
- on(:tagged_hash) do |type, props|
281
+ on(:tagged_hash) do |node, _props|
196
282
  required = Set.new
197
283
  result = {
198
284
  TYPE => 'object',
199
285
  PROPERTIES => {}
200
286
  }
201
287
 
202
- key = type.key.to_s
203
- children = type.types.map { |c| visit(c) }
288
+ key = node.key.to_s
289
+ children = node.types.map { |c| visit(c) }
204
290
  key_enum = children.map { |c| c[PROPERTIES][key][CONST] }
205
291
  key_type = children.map { |c| c[PROPERTIES][key][TYPE] }
206
292
  required << key
@@ -8,22 +8,24 @@ module Plumb
8
8
 
9
9
  attr_reader :matcher
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
- end
17
-
18
- def inspect
19
- %(#{name}[#{@matcher.inspect}])
16
+ @label = matcher.is_a?(Class) ? matcher.inspect : "Match(#{label || @matcher.inspect})"
17
+ freeze
20
18
  end
21
19
 
22
20
  def call(result)
23
21
  @matcher === result.value ? result : result.invalid(errors: @error)
24
22
  end
25
23
 
26
- private def build_error(matcher)
24
+ private
25
+
26
+ def _inspect = @label
27
+
28
+ def build_error(matcher)
27
29
  case matcher
28
30
  when Class # A class primitive, ex. String, Integer, etc.
29
31
  "Must be a #{matcher}"
@@ -8,8 +8,11 @@ module Plumb
8
8
 
9
9
  def initialize(metadata)
10
10
  @metadata = metadata
11
+ freeze
11
12
  end
12
13
 
13
14
  def call(result) = result
15
+
16
+ private def _inspect = "Metadata[#{@metadata.inspect}]"
14
17
  end
15
18
  end
@@ -6,60 +6,61 @@ module Plumb
6
6
  class MetadataVisitor
7
7
  include VisitorHandlers
8
8
 
9
- def self.call(type)
10
- new.visit(type)
9
+ def self.call(node)
10
+ new.visit(node)
11
11
  end
12
12
 
13
- def on_missing_handler(type, props, method_name)
14
- return props.merge(type: type) if type.class == Class
13
+ def on_missing_handler(node, props, method_name)
14
+ return props.merge(type: node) if node.instance_of?(Class)
15
15
 
16
- puts "Missing handler for #{type.inspect} with props #{props.inspect} and method_name :#{method_name}"
16
+ puts "Missing handler for #{node.inspect} with props #{node.inspect} and method_name :#{method_name}"
17
17
  props
18
18
  end
19
19
 
20
- on(:undefined) do |type, props|
20
+ on(:undefined) do |_node, props|
21
21
  props
22
22
  end
23
23
 
24
- on(:any) do |type, props|
24
+ on(:any) do |_node, props|
25
25
  props
26
26
  end
27
27
 
28
- on(:pipeline) do |type, props|
29
- visit(type.type, props)
28
+ on(:pipeline) do |node, props|
29
+ visit(node.type, props)
30
30
  end
31
31
 
32
- on(:step) do |type, props|
33
- props.merge(type._metadata)
32
+ on(:step) do |node, props|
33
+ props.merge(node._metadata)
34
34
  end
35
35
 
36
- on(::Regexp) do |type, props|
37
- props.merge(pattern: type)
36
+ on(::Regexp) do |node, props|
37
+ props.merge(pattern: node, type: props[:type] || String)
38
38
  end
39
39
 
40
- on(::Range) do |type, props|
41
- props.merge(match: type)
40
+ on(::Range) do |node, props|
41
+ type = props[:type] || (node.begin || node.end).class
42
+ props.merge(match: node, type:)
42
43
  end
43
44
 
44
- on(:match) do |type, props|
45
- visit(type.matcher, props)
45
+ on(:match) do |node, props|
46
+ visit(node.matcher, props)
46
47
  end
47
48
 
48
- on(:hash) do |type, props|
49
+ on(:hash) do |_node, props|
49
50
  props.merge(type: Hash)
50
51
  end
51
52
 
52
- on(:and) do |type, props|
53
- left = visit(type.left)
54
- right = visit(type.right)
53
+ on(:and) do |node, props|
54
+ left = visit(node.left)
55
+ right = visit(node.right)
55
56
  type = right[:type] || left[:type]
56
57
  props = props.merge(left).merge(right)
57
58
  props = props.merge(type:) if type
58
59
  props
59
60
  end
60
61
 
61
- on(:or) do |type, props|
62
- child_metas = [visit(type.left), visit(type.right)]
62
+ on(:or) do |node, props|
63
+ child_metas = [visit(node.left), visit(node.right)]
63
64
  types = child_metas.map { |child| child[:type] }.flatten.compact
64
65
  types = types.first if types.size == 1
65
66
  child_metas.reduce(props) do |acc, child|
@@ -67,50 +68,63 @@ module Plumb
67
68
  end.merge(type: types)
68
69
  end
69
70
 
70
- on(:value) do |type, props|
71
- visit(type.value, props)
71
+ on(:value) do |node, props|
72
+ visit(node.value, props)
72
73
  end
73
74
 
74
- on(:transform) do |type, props|
75
- props.merge(type: type.target_type)
75
+ on(:transform) do |node, props|
76
+ props.merge(type: node.target_type)
76
77
  end
77
78
 
78
- on(:static) do |type, props|
79
- props.merge(static: type.value)
79
+ on(:static) do |node, props|
80
+ type = node.value.is_a?(Class) ? node.value : node.value.class
81
+ props.merge(static: node.value, type:)
80
82
  end
81
83
 
82
- on(:rules) do |type, props|
83
- type.rules.reduce(props) do |acc, rule|
84
+ 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|
84
90
  acc.merge(rule.name => rule.arg_value)
85
91
  end
86
92
  end
87
93
 
88
- on(:boolean) do |type, props|
94
+ on(:boolean) do |_node, props|
89
95
  props.merge(type: 'boolean')
90
96
  end
91
97
 
92
- on(:metadata) do |type, props|
93
- props.merge(type.metadata)
98
+ on(:metadata) do |node, props|
99
+ props.merge(node.metadata)
94
100
  end
95
101
 
96
- on(:hash_map) do |type, props|
102
+ on(:hash_map) do |_node, props|
97
103
  props.merge(type: Hash)
98
104
  end
99
105
 
100
- on(:build) do |type, props|
101
- visit(type.type, props)
106
+ on(:build) do |node, props|
107
+ visit(node.type, props)
102
108
  end
103
109
 
104
- on(:array) do |type, props|
110
+ on(:array) do |_node, props|
105
111
  props.merge(type: Array)
106
112
  end
107
113
 
108
- on(:tuple) do |type, props|
114
+ on(:stream) do |_node, props|
115
+ props.merge(type: Enumerator)
116
+ end
117
+
118
+ on(:tuple) do |_node, props|
109
119
  props.merge(type: Array)
110
120
  end
111
121
 
112
- on(:tagged_hash) do |type, props|
122
+ on(:tagged_hash) do |_node, props|
113
123
  props.merge(type: Hash)
114
124
  end
125
+
126
+ on(Proc) do |_node, props|
127
+ props
128
+ end
115
129
  end
116
130
  end
@@ -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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'plumb/steppable'
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 Steppable
11
+
12
+ attr_reader :policy_name, :arg, :step
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
+ freeze
22
+ end
23
+
24
+ # The standard Step interface.
25
+ # @param result [Result::Valid]
26
+ # @return [Result::Valid, Result::Invalid]
27
+ def call(result) = @step.call(result)
28
+
29
+ private def _inspect = @step.inspect
30
+ end
31
+ end