plumb 0.0.1 → 0.0.3

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.
@@ -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