plumb 0.0.1 → 0.0.2
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 +291 -19
- data/examples/command_objects.rb +207 -0
- data/examples/concurrent_downloads.rb +107 -0
- data/examples/csv_stream.rb +97 -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 +44 -13
- data/lib/plumb/hash_map.rb +34 -0
- data/lib/plumb/interface_class.rb +6 -4
- data/lib/plumb/json_schema_visitor.rb +117 -74
- data/lib/plumb/match_class.rb +8 -5
- data/lib/plumb/metadata.rb +3 -0
- data/lib/plumb/metadata_visitor.rb +45 -40
- data/lib/plumb/rules.rb +6 -7
- data/lib/plumb/schema.rb +37 -41
- data/lib/plumb/static_class.rb +4 -4
- data/lib/plumb/step.rb +6 -1
- data/lib/plumb/steppable.rb +36 -34
- 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 +19 -60
- data/lib/plumb/value_class.rb +5 -2
- data/lib/plumb/version.rb +1 -1
- data/lib/plumb/visitor_handlers.rb +13 -9
- data/lib/plumb.rb +1 -0
- metadata +8 -2
data/lib/plumb/hash_map.rb
CHANGED
@@ -15,6 +15,8 @@ module Plumb
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def call(result)
|
18
|
+
return result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
|
19
|
+
|
18
20
|
failed = result.value.lazy.filter_map do |key, value|
|
19
21
|
key_r = @key_type.resolve(key)
|
20
22
|
value_r = @value_type.resolve(value)
|
@@ -31,5 +33,37 @@ module Plumb
|
|
31
33
|
rescue StopIteration
|
32
34
|
result
|
33
35
|
end
|
36
|
+
|
37
|
+
def filtered
|
38
|
+
FilteredHashMap.new(key_type, value_type)
|
39
|
+
end
|
40
|
+
|
41
|
+
private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}]"
|
42
|
+
|
43
|
+
class FilteredHashMap
|
44
|
+
include Steppable
|
45
|
+
|
46
|
+
attr_reader :key_type, :value_type
|
47
|
+
|
48
|
+
def initialize(key_type, value_type)
|
49
|
+
@key_type = key_type
|
50
|
+
@value_type = value_type
|
51
|
+
freeze
|
52
|
+
end
|
53
|
+
|
54
|
+
def call(result)
|
55
|
+
result.invalid(errors: 'must be a Hash') unless result.value.is_a?(::Hash)
|
56
|
+
|
57
|
+
hash = result.value.each.with_object({}) do |(key, value), memo|
|
58
|
+
key_r = @key_type.resolve(key)
|
59
|
+
value_r = @value_type.resolve(value)
|
60
|
+
memo[key_r.value] = value_r.value if key_r.valid? && value_r.valid?
|
61
|
+
end
|
62
|
+
|
63
|
+
result.valid(hash)
|
64
|
+
end
|
65
|
+
|
66
|
+
private def _inspect = "HashMap[#{@key_type.inspect}, #{@value_type.inspect}].filtered"
|
67
|
+
end
|
34
68
|
end
|
35
69
|
end
|
@@ -16,10 +16,10 @@ module Plumb
|
|
16
16
|
def of(*args)
|
17
17
|
case args
|
18
18
|
in Array => symbols if symbols.all? { |s| s.is_a?(::Symbol) }
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
self.class.new(symbols)
|
20
|
+
else
|
21
|
+
raise ::ArgumentError, "unexpected value to Types::Interface#of #{args.inspect}"
|
22
|
+
end
|
23
23
|
end
|
24
24
|
|
25
25
|
alias [] of
|
@@ -31,5 +31,7 @@ module Plumb
|
|
31
31
|
|
32
32
|
result
|
33
33
|
end
|
34
|
+
|
35
|
+
private def _inspect = "Interface[#{method_names.join(', ')}]"
|
34
36
|
end
|
35
37
|
end
|
@@ -19,39 +19,43 @@ module Plumb
|
|
19
19
|
MINIMUM = 'minimum'
|
20
20
|
MAXIMUM = 'maximum'
|
21
21
|
|
22
|
-
def self.call(
|
23
|
-
{
|
24
|
-
'$schema' => 'https://json-schema.org/draft-08/schema#'
|
25
|
-
}.merge(new.visit(
|
22
|
+
def self.call(node)
|
23
|
+
{
|
24
|
+
'$schema' => 'https://json-schema.org/draft-08/schema#'
|
25
|
+
}.merge(new.visit(node))
|
26
26
|
end
|
27
27
|
|
28
28
|
private def stringify_keys(hash) = hash.transform_keys(&:to_s)
|
29
29
|
|
30
|
-
on(:any) do |
|
30
|
+
on(:any) do |_node, props|
|
31
31
|
props
|
32
32
|
end
|
33
33
|
|
34
|
-
on(:pipeline) do |
|
35
|
-
visit(
|
34
|
+
on(:pipeline) do |node, props|
|
35
|
+
visit(node.type, props)
|
36
|
+
end
|
37
|
+
|
38
|
+
on(:step) do |node, props|
|
39
|
+
props.merge(stringify_keys(node._metadata))
|
36
40
|
end
|
37
41
|
|
38
|
-
on(:
|
39
|
-
props
|
42
|
+
on(:interface) do |_node, props|
|
43
|
+
props
|
40
44
|
end
|
41
45
|
|
42
|
-
on(:hash) do |
|
46
|
+
on(:hash) do |node, props|
|
43
47
|
props.merge(
|
44
48
|
TYPE => 'object',
|
45
|
-
PROPERTIES =>
|
49
|
+
PROPERTIES => node._schema.each_with_object({}) do |(key, value), hash|
|
46
50
|
hash[key.to_s] = visit(value)
|
47
51
|
end,
|
48
|
-
REQUIRED =>
|
52
|
+
REQUIRED => node._schema.reject { |key, _value| key.optional? }.keys.map(&:to_s)
|
49
53
|
)
|
50
54
|
end
|
51
55
|
|
52
|
-
on(:and) do |
|
53
|
-
left = visit(
|
54
|
-
right = visit(
|
56
|
+
on(:and) do |node, props|
|
57
|
+
left = visit(node.left)
|
58
|
+
right = visit(node.right)
|
55
59
|
type = right[TYPE] || left[TYPE]
|
56
60
|
props = props.merge(left).merge(right)
|
57
61
|
props = props.merge(TYPE => type) if type
|
@@ -59,148 +63,187 @@ module Plumb
|
|
59
63
|
end
|
60
64
|
|
61
65
|
# A "default" value is usually an "or" of expected_value | (undefined >> static_value)
|
62
|
-
on(:or) do |
|
63
|
-
left = visit(
|
64
|
-
right = visit(
|
66
|
+
on(:or) do |node, props|
|
67
|
+
left = visit(node.left)
|
68
|
+
right = visit(node.right)
|
65
69
|
any_of = [left, right].uniq
|
66
70
|
if any_of.size == 1
|
67
71
|
props.merge(left)
|
68
72
|
elsif any_of.size == 2 && (defidx = any_of.index { |p| p.key?(DEFAULT) })
|
69
|
-
val = any_of[defidx
|
73
|
+
val = any_of[defidx.zero? ? 1 : 0]
|
70
74
|
props.merge(val).merge(DEFAULT => any_of[defidx][DEFAULT])
|
71
75
|
else
|
72
76
|
props.merge(ANY_OF => any_of)
|
73
77
|
end
|
74
78
|
end
|
75
79
|
|
76
|
-
on(:
|
77
|
-
props
|
78
|
-
|
79
|
-
props.merge(CONST => type.value)
|
80
|
-
else
|
81
|
-
props
|
82
|
-
end
|
80
|
+
on(:not) do |node, props|
|
81
|
+
props.merge('not' => visit(node.step))
|
82
|
+
end
|
83
83
|
|
84
|
-
|
84
|
+
on(:value) do |node, props|
|
85
|
+
props = case node.value
|
86
|
+
when ::String, ::Symbol, ::Numeric
|
87
|
+
props.merge(CONST => node.value)
|
88
|
+
else
|
89
|
+
props
|
90
|
+
end
|
91
|
+
|
92
|
+
visit(node.value, props)
|
85
93
|
end
|
86
94
|
|
87
|
-
on(:transform) do |
|
88
|
-
visit(
|
95
|
+
on(:transform) do |node, props|
|
96
|
+
visit(node.target_type, props)
|
89
97
|
end
|
90
98
|
|
91
|
-
on(:undefined) do |
|
99
|
+
on(:undefined) do |_node, props|
|
92
100
|
props
|
93
101
|
end
|
94
102
|
|
95
|
-
on(:static) do |
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
103
|
+
on(:static) do |node, props|
|
104
|
+
# Set const AND default
|
105
|
+
# to emulate static values
|
106
|
+
props = case node.value
|
107
|
+
when ::String, ::Symbol, ::Numeric
|
108
|
+
props.merge(CONST => node.value, DEFAULT => node.value)
|
109
|
+
else
|
110
|
+
props
|
111
|
+
end
|
112
|
+
|
113
|
+
visit(node.value, props)
|
104
114
|
end
|
105
115
|
|
106
|
-
on(:rules) do |
|
107
|
-
|
116
|
+
on(:rules) do |node, props|
|
117
|
+
node.rules.reduce(props) do |acc, rule|
|
108
118
|
acc.merge(visit(rule))
|
109
119
|
end
|
110
120
|
end
|
111
121
|
|
112
|
-
on(:rule_included_in) do |
|
113
|
-
props.merge(ENUM =>
|
122
|
+
on(:rule_included_in) do |node, props|
|
123
|
+
props.merge(ENUM => node.arg_value)
|
114
124
|
end
|
115
125
|
|
116
|
-
on(:match) do |
|
117
|
-
|
126
|
+
on(:match) do |node, props|
|
127
|
+
# Set const if primitive
|
128
|
+
props = case node.matcher
|
129
|
+
when ::String, ::Symbol, ::Numeric
|
130
|
+
props.merge(CONST => node.matcher)
|
131
|
+
else
|
132
|
+
props
|
133
|
+
end
|
134
|
+
|
135
|
+
visit(node.matcher, props)
|
118
136
|
end
|
119
137
|
|
120
|
-
on(:boolean) do |
|
138
|
+
on(:boolean) do |_node, props|
|
121
139
|
props.merge(TYPE => 'boolean')
|
122
140
|
end
|
123
141
|
|
124
|
-
on(::String) do |
|
142
|
+
on(::String) do |_node, props|
|
125
143
|
props.merge(TYPE => 'string')
|
126
144
|
end
|
127
145
|
|
128
|
-
on(::Integer) do |
|
146
|
+
on(::Integer) do |_node, props|
|
129
147
|
props.merge(TYPE => 'integer')
|
130
148
|
end
|
131
149
|
|
132
|
-
on(::Numeric) do |
|
150
|
+
on(::Numeric) do |_node, props|
|
133
151
|
props.merge(TYPE => 'number')
|
134
152
|
end
|
135
153
|
|
136
|
-
on(::BigDecimal) do |
|
154
|
+
on(::BigDecimal) do |_node, props|
|
137
155
|
props.merge(TYPE => 'number')
|
138
156
|
end
|
139
157
|
|
140
|
-
on(::Float) do |
|
158
|
+
on(::Float) do |_node, props|
|
141
159
|
props.merge(TYPE => 'number')
|
142
160
|
end
|
143
161
|
|
144
|
-
on(::TrueClass) do |
|
162
|
+
on(::TrueClass) do |_node, props|
|
145
163
|
props.merge(TYPE => 'boolean')
|
146
164
|
end
|
147
165
|
|
148
|
-
on(::NilClass) do |
|
166
|
+
on(::NilClass) do |_node, props|
|
149
167
|
props.merge(TYPE => 'null')
|
150
168
|
end
|
151
169
|
|
152
|
-
on(::FalseClass) do |
|
170
|
+
on(::FalseClass) do |_node, props|
|
153
171
|
props.merge(TYPE => 'boolean')
|
154
172
|
end
|
155
173
|
|
156
|
-
on(::Regexp) do |
|
157
|
-
props.merge(PATTERN =>
|
174
|
+
on(::Regexp) do |node, props|
|
175
|
+
props.merge(PATTERN => node.source, TYPE => props[TYPE] || 'string')
|
158
176
|
end
|
159
177
|
|
160
|
-
on(::Range) do |
|
161
|
-
|
162
|
-
opts
|
163
|
-
|
178
|
+
on(::Range) do |node, props|
|
179
|
+
element = node.begin || node.end
|
180
|
+
opts = visit(element.class)
|
181
|
+
if element.is_a?(::Numeric)
|
182
|
+
opts[MINIMUM] = node.min if node.begin
|
183
|
+
opts[MAXIMUM] = node.max if node.end
|
184
|
+
end
|
164
185
|
props.merge(opts)
|
165
186
|
end
|
166
187
|
|
167
|
-
on(
|
168
|
-
|
169
|
-
props.merge(stringify_keys(type.metadata))
|
188
|
+
on(::Hash) do |_node, props|
|
189
|
+
props.merge(TYPE => 'object')
|
170
190
|
end
|
171
191
|
|
172
|
-
on(
|
192
|
+
on(::Array) do |_node, props|
|
193
|
+
props.merge(TYPE => 'array')
|
194
|
+
end
|
195
|
+
|
196
|
+
on(:metadata) do |node, props|
|
197
|
+
# TODO: here we should filter out the metadata that is not relevant for JSON Schema
|
198
|
+
props.merge(stringify_keys(node.metadata))
|
199
|
+
end
|
200
|
+
|
201
|
+
on(:hash_map) do |node, _props|
|
173
202
|
{
|
174
203
|
TYPE => 'object',
|
175
204
|
'patternProperties' => {
|
176
|
-
'.*' => visit(
|
205
|
+
'.*' => visit(node.value_type)
|
177
206
|
}
|
178
207
|
}
|
179
208
|
end
|
180
209
|
|
181
|
-
on(:
|
182
|
-
|
210
|
+
on(:filtered_hash_map) do |node, _props|
|
211
|
+
{
|
212
|
+
TYPE => 'object',
|
213
|
+
'patternProperties' => {
|
214
|
+
'.*' => visit(node.value_type)
|
215
|
+
}
|
216
|
+
}
|
217
|
+
end
|
218
|
+
|
219
|
+
on(:build) do |node, props|
|
220
|
+
visit(node.type, props)
|
221
|
+
end
|
222
|
+
|
223
|
+
on(:array) do |node, _props|
|
224
|
+
items = visit(node.element_type)
|
225
|
+
{ TYPE => 'array', ITEMS => items }
|
183
226
|
end
|
184
227
|
|
185
|
-
on(:
|
186
|
-
items = visit(
|
228
|
+
on(:stream) do |node, _props|
|
229
|
+
items = visit(node.element_type)
|
187
230
|
{ TYPE => 'array', ITEMS => items }
|
188
231
|
end
|
189
232
|
|
190
|
-
on(:tuple) do |
|
191
|
-
items =
|
233
|
+
on(:tuple) do |node, _props|
|
234
|
+
items = node.types.map { |t| visit(t) }
|
192
235
|
{ TYPE => 'array', 'prefixItems' => items }
|
193
236
|
end
|
194
237
|
|
195
|
-
on(:tagged_hash) do |
|
238
|
+
on(:tagged_hash) do |node, _props|
|
196
239
|
required = Set.new
|
197
240
|
result = {
|
198
241
|
TYPE => 'object',
|
199
242
|
PROPERTIES => {}
|
200
243
|
}
|
201
244
|
|
202
|
-
key =
|
203
|
-
children
|
245
|
+
key = node.key.to_s
|
246
|
+
children = node.types.map { |c| visit(c) }
|
204
247
|
key_enum = children.map { |c| c[PROPERTIES][key][CONST] }
|
205
248
|
key_type = children.map { |c| c[PROPERTIES][key][TYPE] }
|
206
249
|
required << key
|
data/lib/plumb/match_class.rb
CHANGED
@@ -13,17 +13,20 @@ module Plumb
|
|
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
|
+
freeze
|
20
17
|
end
|
21
18
|
|
22
19
|
def call(result)
|
23
20
|
@matcher === result.value ? result : result.invalid(errors: @error)
|
24
21
|
end
|
25
22
|
|
26
|
-
private
|
23
|
+
private
|
24
|
+
|
25
|
+
def _inspect
|
26
|
+
@matcher.inspect
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_error(matcher)
|
27
30
|
case matcher
|
28
31
|
when Class # A class primitive, ex. String, Integer, etc.
|
29
32
|
"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,49 +68,53 @@ 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
|
-
props.merge(static:
|
79
|
+
on(:static) do |node, props|
|
80
|
+
props.merge(static: node.value)
|
80
81
|
end
|
81
82
|
|
82
|
-
on(:rules) do |
|
83
|
-
|
83
|
+
on(:rules) do |node, props|
|
84
|
+
node.rules.reduce(props) do |acc, rule|
|
84
85
|
acc.merge(rule.name => rule.arg_value)
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
88
|
-
on(:boolean) do |
|
89
|
+
on(:boolean) do |_node, props|
|
89
90
|
props.merge(type: 'boolean')
|
90
91
|
end
|
91
92
|
|
92
|
-
on(:metadata) do |
|
93
|
-
props.merge(
|
93
|
+
on(:metadata) do |node, props|
|
94
|
+
props.merge(node.metadata)
|
94
95
|
end
|
95
96
|
|
96
|
-
on(:hash_map) do |
|
97
|
+
on(:hash_map) do |_node, props|
|
97
98
|
props.merge(type: Hash)
|
98
99
|
end
|
99
100
|
|
100
|
-
on(:build) do |
|
101
|
-
visit(
|
101
|
+
on(:build) do |node, props|
|
102
|
+
visit(node.type, props)
|
102
103
|
end
|
103
104
|
|
104
|
-
on(:array) do |
|
105
|
+
on(:array) do |_node, props|
|
105
106
|
props.merge(type: Array)
|
106
107
|
end
|
107
108
|
|
108
|
-
on(:
|
109
|
+
on(:stream) do |_node, props|
|
110
|
+
props.merge(type: Enumerator)
|
111
|
+
end
|
112
|
+
|
113
|
+
on(:tuple) do |_node, props|
|
109
114
|
props.merge(type: Array)
|
110
115
|
end
|
111
116
|
|
112
|
-
on(:tagged_hash) do |
|
117
|
+
on(:tagged_hash) do |_node, props|
|
113
118
|
props.merge(type: Hash)
|
114
119
|
end
|
115
120
|
end
|
data/lib/plumb/rules.rb
CHANGED
@@ -8,12 +8,12 @@ module Plumb
|
|
8
8
|
UndefinedRuleError = Class.new(KeyError)
|
9
9
|
|
10
10
|
class Registry
|
11
|
-
RuleDef = Data.define(:name, :error_tpl, :callable, :
|
11
|
+
RuleDef = Data.define(:name, :error_tpl, :callable, :expects) do
|
12
12
|
def supports?(type)
|
13
13
|
types = [type].flatten # may be an array of types for OR logic
|
14
14
|
case expects
|
15
15
|
when Symbol
|
16
|
-
types.all? { |type| type.public_instance_methods.include?(expects) }
|
16
|
+
types.all? { |type| type && type.public_instance_methods.include?(expects) }
|
17
17
|
when Class then types.all? { |type| type <= expects }
|
18
18
|
when Object then true
|
19
19
|
else raise "Unexpected expects: #{expects}"
|
@@ -29,7 +29,6 @@ module Plumb
|
|
29
29
|
|
30
30
|
def node_name = :"rule_#{rule_def.name}"
|
31
31
|
def name = rule_def.name
|
32
|
-
def metadata_key = rule_def.metadata_key
|
33
32
|
|
34
33
|
def error_for(result)
|
35
34
|
return nil if rule_def.callable.call(result, arg_value)
|
@@ -42,10 +41,10 @@ module Plumb
|
|
42
41
|
@definitions = Hash.new { |h, k| h[k] = Set.new }
|
43
42
|
end
|
44
43
|
|
45
|
-
def define(name, error_tpl, callable = nil,
|
44
|
+
def define(name, error_tpl, callable = nil, expects: Object, &block)
|
46
45
|
name = name.to_sym
|
47
46
|
callable ||= block
|
48
|
-
@definitions[name] << RuleDef.new(name:, error_tpl:, callable:,
|
47
|
+
@definitions[name] << RuleDef.new(name:, error_tpl:, callable:, expects:)
|
49
48
|
end
|
50
49
|
|
51
50
|
# Ex. size: 3, match: /foo/
|
@@ -54,10 +53,10 @@ module Plumb
|
|
54
53
|
rule_defs = @definitions.fetch(name.to_sym) { raise UndefinedRuleError, "no rule defined with :#{name}" }
|
55
54
|
rule_def = rule_defs.find { |rd| rd.supports?(for_type) }
|
56
55
|
unless rule_def
|
57
|
-
raise UnsupportedRuleError, "No :#{name} rule for type #{for_type}" unless for_type.is_a?(Array)
|
56
|
+
raise UnsupportedRuleError, "No :#{name} rule for type #{for_type.inspect}" unless for_type.is_a?(Array)
|
58
57
|
|
59
58
|
raise UnsupportedRuleError,
|
60
|
-
|
59
|
+
"Can't apply :#{name} rule for types #{for_type}. All types must support the same rule implementation"
|
61
60
|
|
62
61
|
end
|
63
62
|
|