plumb 0.0.1 → 0.0.2
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 +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
|
|