plumb 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/README.md +558 -118
- data/examples/command_objects.rb +207 -0
- data/examples/concurrent_downloads.rb +107 -0
- data/examples/csv_stream.rb +97 -0
- data/examples/env_config.rb +122 -0
- data/examples/programmers.csv +201 -0
- data/examples/weekdays.rb +66 -0
- data/lib/plumb/array_class.rb +25 -19
- data/lib/plumb/build.rb +3 -0
- data/lib/plumb/hash_class.rb +42 -13
- data/lib/plumb/hash_map.rb +34 -0
- data/lib/plumb/interface_class.rb +6 -4
- data/lib/plumb/json_schema_visitor.rb +157 -71
- data/lib/plumb/match_class.rb +8 -6
- data/lib/plumb/metadata.rb +3 -0
- data/lib/plumb/metadata_visitor.rb +54 -40
- data/lib/plumb/policies.rb +81 -0
- data/lib/plumb/policy.rb +31 -0
- data/lib/plumb/schema.rb +39 -43
- data/lib/plumb/static_class.rb +4 -4
- data/lib/plumb/step.rb +6 -1
- data/lib/plumb/steppable.rb +47 -60
- data/lib/plumb/stream_class.rb +61 -0
- data/lib/plumb/tagged_hash.rb +12 -3
- data/lib/plumb/transform.rb +6 -1
- data/lib/plumb/tuple_class.rb +8 -5
- data/lib/plumb/types.rb +119 -69
- data/lib/plumb/value_class.rb +5 -2
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +19 -10
- data/lib/plumb.rb +53 -1
- metadata +14 -6
- data/lib/plumb/rules.rb +0 -103
@@ -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(
|
23
|
-
{
|
24
|
-
'$schema' => 'https://json-schema.org/draft-08/schema#'
|
25
|
-
}.merge(new.visit(
|
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 |
|
35
|
+
on(:any) do |_node, props|
|
31
36
|
props
|
32
37
|
end
|
33
38
|
|
34
|
-
on(:pipeline) do |
|
35
|
-
visit(
|
39
|
+
on(:pipeline) do |node, props|
|
40
|
+
visit(node.type, props)
|
36
41
|
end
|
37
42
|
|
38
|
-
on(:step) do |
|
39
|
-
props.merge(stringify_keys(
|
43
|
+
on(:step) do |node, props|
|
44
|
+
props.merge(stringify_keys(node._metadata))
|
40
45
|
end
|
41
46
|
|
42
|
-
on(:
|
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 =>
|
54
|
+
PROPERTIES => node._schema.each_with_object({}) do |(key, value), hash|
|
46
55
|
hash[key.to_s] = visit(value)
|
47
56
|
end,
|
48
|
-
REQUIRED =>
|
57
|
+
REQUIRED => node._schema.reject { |key, _value| key.optional? }.keys.map(&:to_s)
|
49
58
|
)
|
50
59
|
end
|
51
60
|
|
52
|
-
on(:and) do |
|
53
|
-
left = visit(
|
54
|
-
right = visit(
|
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 |
|
63
|
-
left = visit(
|
64
|
-
right = visit(
|
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
|
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(:
|
77
|
-
props
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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(
|
97
|
+
visit(node.value, props)
|
85
98
|
end
|
86
99
|
|
87
|
-
on(:transform) do |
|
88
|
-
visit(
|
100
|
+
on(:transform) do |node, props|
|
101
|
+
visit(node.target_type, props)
|
89
102
|
end
|
90
103
|
|
91
|
-
on(:undefined) do |
|
104
|
+
on(:undefined) do |_node, props|
|
92
105
|
props
|
93
106
|
end
|
94
107
|
|
95
|
-
on(:static) do |
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
-
|
131
|
+
on(:options_policy) do |node, props|
|
132
|
+
props.merge(ENUM => node.arg)
|
104
133
|
end
|
105
134
|
|
106
|
-
on(:
|
107
|
-
|
108
|
-
|
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(:
|
113
|
-
props.merge(ENUM =>
|
161
|
+
on(:excluded_from_policy) do |node, props|
|
162
|
+
props.merge(NOT => { ENUM => node.arg })
|
114
163
|
end
|
115
164
|
|
116
|
-
on(
|
117
|
-
|
165
|
+
on(Proc) do |_node, props|
|
166
|
+
props
|
118
167
|
end
|
119
168
|
|
120
|
-
on(:
|
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 |
|
185
|
+
on(::String) do |_node, props|
|
125
186
|
props.merge(TYPE => 'string')
|
126
187
|
end
|
127
188
|
|
128
|
-
on(::Integer) do |
|
189
|
+
on(::Integer) do |_node, props|
|
129
190
|
props.merge(TYPE => 'integer')
|
130
191
|
end
|
131
192
|
|
132
|
-
on(::Numeric) do |
|
193
|
+
on(::Numeric) do |_node, props|
|
133
194
|
props.merge(TYPE => 'number')
|
134
195
|
end
|
135
196
|
|
136
|
-
on(::BigDecimal) do |
|
197
|
+
on(::BigDecimal) do |_node, props|
|
137
198
|
props.merge(TYPE => 'number')
|
138
199
|
end
|
139
200
|
|
140
|
-
on(::Float) do |
|
201
|
+
on(::Float) do |_node, props|
|
141
202
|
props.merge(TYPE => 'number')
|
142
203
|
end
|
143
204
|
|
144
|
-
on(::TrueClass) do |
|
205
|
+
on(::TrueClass) do |_node, props|
|
145
206
|
props.merge(TYPE => 'boolean')
|
146
207
|
end
|
147
208
|
|
148
|
-
on(::NilClass) do |
|
209
|
+
on(::NilClass) do |_node, props|
|
149
210
|
props.merge(TYPE => 'null')
|
150
211
|
end
|
151
212
|
|
152
|
-
on(::FalseClass) do |
|
213
|
+
on(::FalseClass) do |_node, props|
|
153
214
|
props.merge(TYPE => 'boolean')
|
154
215
|
end
|
155
216
|
|
156
|
-
on(::Regexp) do |
|
157
|
-
props.merge(PATTERN =>
|
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 |
|
161
|
-
|
162
|
-
opts
|
163
|
-
|
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(
|
168
|
-
|
169
|
-
|
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(:
|
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(
|
248
|
+
'.*' => visit(node.value_type)
|
177
249
|
}
|
178
250
|
}
|
179
251
|
end
|
180
252
|
|
181
|
-
on(:
|
182
|
-
|
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(:
|
186
|
-
items = visit(
|
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 |
|
191
|
-
items =
|
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 |
|
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 =
|
203
|
-
children
|
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
|
data/lib/plumb/match_class.rb
CHANGED
@@ -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
|
-
|
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
|
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}"
|
data/lib/plumb/metadata.rb
CHANGED
@@ -6,60 +6,61 @@ module Plumb
|
|
6
6
|
class MetadataVisitor
|
7
7
|
include VisitorHandlers
|
8
8
|
|
9
|
-
def self.call(
|
10
|
-
new.visit(
|
9
|
+
def self.call(node)
|
10
|
+
new.visit(node)
|
11
11
|
end
|
12
12
|
|
13
|
-
def on_missing_handler(
|
14
|
-
return props.merge(type:
|
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 #{
|
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 |
|
20
|
+
on(:undefined) do |_node, props|
|
21
21
|
props
|
22
22
|
end
|
23
23
|
|
24
|
-
on(:any) do |
|
24
|
+
on(:any) do |_node, props|
|
25
25
|
props
|
26
26
|
end
|
27
27
|
|
28
|
-
on(:pipeline) do |
|
29
|
-
visit(
|
28
|
+
on(:pipeline) do |node, props|
|
29
|
+
visit(node.type, props)
|
30
30
|
end
|
31
31
|
|
32
|
-
on(:step) do |
|
33
|
-
props.merge(
|
32
|
+
on(:step) do |node, props|
|
33
|
+
props.merge(node._metadata)
|
34
34
|
end
|
35
35
|
|
36
|
-
on(::Regexp) do |
|
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 |
|
41
|
-
props
|
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 |
|
45
|
-
visit(
|
45
|
+
on(:match) do |node, props|
|
46
|
+
visit(node.matcher, props)
|
46
47
|
end
|
47
48
|
|
48
|
-
on(:hash) do |
|
49
|
+
on(:hash) do |_node, props|
|
49
50
|
props.merge(type: Hash)
|
50
51
|
end
|
51
52
|
|
52
|
-
on(:and) do |
|
53
|
-
left = visit(
|
54
|
-
right = visit(
|
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 |
|
62
|
-
child_metas = [visit(
|
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 |
|
71
|
-
visit(
|
71
|
+
on(:value) do |node, props|
|
72
|
+
visit(node.value, props)
|
72
73
|
end
|
73
74
|
|
74
|
-
on(:transform) do |
|
75
|
-
props.merge(type:
|
75
|
+
on(:transform) do |node, props|
|
76
|
+
props.merge(type: node.target_type)
|
76
77
|
end
|
77
78
|
|
78
|
-
on(:static) do |
|
79
|
-
|
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(:
|
83
|
-
|
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 |
|
94
|
+
on(:boolean) do |_node, props|
|
89
95
|
props.merge(type: 'boolean')
|
90
96
|
end
|
91
97
|
|
92
|
-
on(:metadata) do |
|
93
|
-
props.merge(
|
98
|
+
on(:metadata) do |node, props|
|
99
|
+
props.merge(node.metadata)
|
94
100
|
end
|
95
101
|
|
96
|
-
on(:hash_map) do |
|
102
|
+
on(:hash_map) do |_node, props|
|
97
103
|
props.merge(type: Hash)
|
98
104
|
end
|
99
105
|
|
100
|
-
on(:build) do |
|
101
|
-
visit(
|
106
|
+
on(:build) do |node, props|
|
107
|
+
visit(node.type, props)
|
102
108
|
end
|
103
109
|
|
104
|
-
on(:array) do |
|
110
|
+
on(:array) do |_node, props|
|
105
111
|
props.merge(type: Array)
|
106
112
|
end
|
107
113
|
|
108
|
-
on(:
|
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 |
|
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
|
data/lib/plumb/policy.rb
ADDED
@@ -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
|