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