matchers 0.1.0.pre.test
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.
Potentially problematic release.
This version of matchers might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/lib/matcher/assertions.rb +21 -0
- data/lib/matcher/base.rb +189 -0
- data/lib/matcher/builder.rb +74 -0
- data/lib/matcher/chain.rb +60 -0
- data/lib/matcher/debug.rb +48 -0
- data/lib/matcher/errors/and_error.rb +86 -0
- data/lib/matcher/errors/boolean_collector.rb +51 -0
- data/lib/matcher/errors/element_error.rb +24 -0
- data/lib/matcher/errors/empty_error.rb +23 -0
- data/lib/matcher/errors/error.rb +39 -0
- data/lib/matcher/errors/error_collector.rb +99 -0
- data/lib/matcher/errors/nested_error.rb +96 -0
- data/lib/matcher/errors/or_error.rb +86 -0
- data/lib/matcher/expression_cache.rb +57 -0
- data/lib/matcher/expression_labeler.rb +91 -0
- data/lib/matcher/expressions/array_expression.rb +45 -0
- data/lib/matcher/expressions/block.rb +153 -0
- data/lib/matcher/expressions/call.rb +338 -0
- data/lib/matcher/expressions/call_error.rb +45 -0
- data/lib/matcher/expressions/constant.rb +53 -0
- data/lib/matcher/expressions/expression.rb +147 -0
- data/lib/matcher/expressions/expression_building.rb +258 -0
- data/lib/matcher/expressions/expression_walker.rb +73 -0
- data/lib/matcher/expressions/hash_expression.rb +59 -0
- data/lib/matcher/expressions/proc_expression.rb +92 -0
- data/lib/matcher/expressions/range_expression.rb +58 -0
- data/lib/matcher/expressions/recorder.rb +86 -0
- data/lib/matcher/expressions/rescue_last_error_expression.rb +44 -0
- data/lib/matcher/expressions/set_expression.rb +45 -0
- data/lib/matcher/expressions/string_expression.rb +53 -0
- data/lib/matcher/expressions/symbol_proc.rb +53 -0
- data/lib/matcher/expressions/variable.rb +85 -0
- data/lib/matcher/hash_stack.rb +53 -0
- data/lib/matcher/list.rb +102 -0
- data/lib/matcher/markers/optional.rb +80 -0
- data/lib/matcher/markers/others.rb +28 -0
- data/lib/matcher/matcher_cache.rb +18 -0
- data/lib/matcher/matchers/all_matcher.rb +60 -0
- data/lib/matcher/matchers/always_matcher.rb +28 -0
- data/lib/matcher/matchers/any_matcher.rb +70 -0
- data/lib/matcher/matchers/array_matcher.rb +35 -0
- data/lib/matcher/matchers/block_matcher.rb +59 -0
- data/lib/matcher/matchers/boolean_matcher.rb +35 -0
- data/lib/matcher/matchers/dig_matcher.rb +146 -0
- data/lib/matcher/matchers/each_matcher.rb +52 -0
- data/lib/matcher/matchers/each_pair_matcher.rb +119 -0
- data/lib/matcher/matchers/equal_matcher.rb +197 -0
- data/lib/matcher/matchers/equal_set_matcher.rb +99 -0
- data/lib/matcher/matchers/expression_matcher.rb +73 -0
- data/lib/matcher/matchers/filter_matcher.rb +111 -0
- data/lib/matcher/matchers/hash_matcher.rb +223 -0
- data/lib/matcher/matchers/imply_matcher.rb +81 -0
- data/lib/matcher/matchers/imply_some_matcher.rb +112 -0
- data/lib/matcher/matchers/index_by_matcher.rb +175 -0
- data/lib/matcher/matchers/inline_matcher.rb +99 -0
- data/lib/matcher/matchers/keys_matcher.rb +121 -0
- data/lib/matcher/matchers/kind_of_matcher.rb +35 -0
- data/lib/matcher/matchers/lazy_all_matcher.rb +68 -0
- data/lib/matcher/matchers/lazy_any_matcher.rb +68 -0
- data/lib/matcher/matchers/let_matcher.rb +73 -0
- data/lib/matcher/matchers/map_matcher.rb +129 -0
- data/lib/matcher/matchers/matcher_building.rb +5 -0
- data/lib/matcher/matchers/negated_array_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_each_matcher.rb +36 -0
- data/lib/matcher/matchers/negated_each_pair_matcher.rb +38 -0
- data/lib/matcher/matchers/negated_imply_some_matcher.rb +46 -0
- data/lib/matcher/matchers/negated_matcher.rb +23 -0
- data/lib/matcher/matchers/negated_project_matcher.rb +31 -0
- data/lib/matcher/matchers/never_matcher.rb +29 -0
- data/lib/matcher/matchers/one_matcher.rb +70 -0
- data/lib/matcher/matchers/optional_matcher.rb +38 -0
- data/lib/matcher/matchers/parse_float_matcher.rb +86 -0
- data/lib/matcher/matchers/parse_integer_matcher.rb +98 -0
- data/lib/matcher/matchers/parse_iso8601_matcher.rb +92 -0
- data/lib/matcher/matchers/parse_json_matcher.rb +95 -0
- data/lib/matcher/matchers/project_matcher.rb +68 -0
- data/lib/matcher/matchers/raises_matcher.rb +124 -0
- data/lib/matcher/matchers/range_matcher.rb +47 -0
- data/lib/matcher/matchers/reference_matcher.rb +111 -0
- data/lib/matcher/matchers/reference_matcher_collection.rb +57 -0
- data/lib/matcher/matchers/regexp_matcher.rb +84 -0
- data/lib/matcher/messages/expected_phrasing.rb +342 -0
- data/lib/matcher/messages/message.rb +102 -0
- data/lib/matcher/messages/message_builder.rb +35 -0
- data/lib/matcher/messages/message_rules.rb +223 -0
- data/lib/matcher/messages/namespaced_message_builder.rb +19 -0
- data/lib/matcher/messages/phrasing.rb +57 -0
- data/lib/matcher/messages/standard_message_builder.rb +105 -0
- data/lib/matcher/once_before.rb +18 -0
- data/lib/matcher/optional_chain.rb +24 -0
- data/lib/matcher/patterns/ast_mapping.rb +42 -0
- data/lib/matcher/patterns/capture_hole.rb +33 -0
- data/lib/matcher/patterns/constant_hole.rb +14 -0
- data/lib/matcher/patterns/hole.rb +30 -0
- data/lib/matcher/patterns/method_hole.rb +58 -0
- data/lib/matcher/patterns/pattern.rb +92 -0
- data/lib/matcher/patterns/pattern_building.rb +39 -0
- data/lib/matcher/patterns/pattern_capture.rb +11 -0
- data/lib/matcher/patterns/pattern_match.rb +29 -0
- data/lib/matcher/patterns/variable_hole.rb +14 -0
- data/lib/matcher/reporter.rb +98 -0
- data/lib/matcher/rules/message_factory.rb +25 -0
- data/lib/matcher/rules/message_rule.rb +18 -0
- data/lib/matcher/rules/message_rule_context.rb +24 -0
- data/lib/matcher/rules/rule_builder.rb +29 -0
- data/lib/matcher/rules/rule_set.rb +57 -0
- data/lib/matcher/rules/transform_builder.rb +24 -0
- data/lib/matcher/rules/transform_mapping.rb +5 -0
- data/lib/matcher/rules/transform_rule.rb +21 -0
- data/lib/matcher/state.rb +40 -0
- data/lib/matcher/testing/error_builder.rb +62 -0
- data/lib/matcher/testing/error_checker.rb +496 -0
- data/lib/matcher/testing/error_testing.rb +37 -0
- data/lib/matcher/testing/pattern_testing.rb +11 -0
- data/lib/matcher/testing/pattern_testing_scope.rb +34 -0
- data/lib/matcher/testing.rb +102 -0
- data/lib/matcher/undefined.rb +10 -0
- data/lib/matcher/utils/mapping_utils.rb +61 -0
- data/lib/matcher/utils.rb +72 -0
- data/lib/matcher/version.rb +5 -0
- data/lib/matcher.rb +337 -0
- metadata +167 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class NamespacedMessageBuilder < MessageBuilder
|
|
5
|
+
def initialize(negated, actual, namespace)
|
|
6
|
+
super(negated, actual)
|
|
7
|
+
|
|
8
|
+
@namespace = namespace
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def message(key, *, **)
|
|
14
|
+
key = [@namespace, key]
|
|
15
|
+
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Phrasing
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
attr_reader :path, :message
|
|
8
|
+
def_delegator :@message, :actual
|
|
9
|
+
def_delegator :@message, :negated
|
|
10
|
+
|
|
11
|
+
def self.namespace(value)
|
|
12
|
+
@namespace = value
|
|
13
|
+
yield
|
|
14
|
+
ensure
|
|
15
|
+
@namespace = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.define(key, &block)
|
|
19
|
+
key = [@namespace, key] if @namespace
|
|
20
|
+
|
|
21
|
+
dict[key] = block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.dict
|
|
25
|
+
@dict ||= {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.phrasing
|
|
29
|
+
->(path, message) { new(path, message).apply }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(path, message)
|
|
33
|
+
@path = path
|
|
34
|
+
@message = message
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def apply
|
|
38
|
+
phrase(@message.key, *@message.args, **@message.kwargs)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def phrase(key, *, **)
|
|
42
|
+
block = self.class.dict[key]
|
|
43
|
+
|
|
44
|
+
if block
|
|
45
|
+
instance_exec(*, **, &block)
|
|
46
|
+
else
|
|
47
|
+
"got #{actual.inspect} but found no message for #{'*NOT* ' if negated}#{key.inspect}"#{" (negated)" if negated}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def phrase_negated(key, *, **)
|
|
52
|
+
message = Message.new(key, !negated, actual, *, **)
|
|
53
|
+
|
|
54
|
+
self.class.new(@path, message).apply
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class StandardMessageBuilder < MessageBuilder
|
|
5
|
+
def namespace(namespace)
|
|
6
|
+
NamespacedMessageBuilder.new(@negated, @actual, namespace)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def truthy
|
|
10
|
+
message(:truthy)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def same(object)
|
|
14
|
+
message(:same, object)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def equal(value)
|
|
18
|
+
message(:equal, value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def less_than(operand)
|
|
22
|
+
message(:less_than, operand)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def greater_than(operand)
|
|
26
|
+
message(:greater_than, operand)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def less_than_or_equal(operand)
|
|
30
|
+
message(:less_than_or_equal, operand)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def greater_than_or_equal(operand)
|
|
34
|
+
message(:greater_than_or_equal, operand)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def comparable_to(operand)
|
|
38
|
+
message(:comparable_to, operand)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def between(min, max, exclude_end: false)
|
|
42
|
+
message(:between, min, max, exclude_end:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def length_of(exp, act)
|
|
46
|
+
message(:length_of, exp, act)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def having_key(key)
|
|
50
|
+
message(:having_key, key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def having_index(index)
|
|
54
|
+
message(:having_index, index)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def exist
|
|
58
|
+
message(:exist)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def in(collection)
|
|
62
|
+
message(:in, collection)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def including(item)
|
|
66
|
+
message(:including, item)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def duplicate(original_index)
|
|
70
|
+
message(:duplicate, original_index)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def duplicate_by(expression, value, original_index)
|
|
74
|
+
message(:duplicate_by, expression, value, original_index)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def matching(pattern)
|
|
78
|
+
message(:matching, pattern)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def valid_format(format)
|
|
82
|
+
message(:valid_format, format)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def instance_of(klass)
|
|
86
|
+
message(:instance_of, klass)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def kind_of(klass)
|
|
90
|
+
message(:kind_of, klass)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def responding_to(method)
|
|
94
|
+
message(:responding_to, method)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def predicate(name)
|
|
98
|
+
message(:predicate, name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def described_by(description)
|
|
102
|
+
message(:described_by, description)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
module OnceBefore
|
|
5
|
+
def once_before(method, &)
|
|
6
|
+
original_method = "_before_once_#{method}"
|
|
7
|
+
alias_method(original_method, method)
|
|
8
|
+
|
|
9
|
+
define_method(method) do |*args, **kwargs, &block|
|
|
10
|
+
instance_exec(&)
|
|
11
|
+
send(original_method, *args, **kwargs, &block)
|
|
12
|
+
ensure
|
|
13
|
+
self.class.alias_method(method, original_method)
|
|
14
|
+
self.class.undef_method(original_method)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class OptionalChain
|
|
5
|
+
include NoExpression
|
|
6
|
+
include NoKey
|
|
7
|
+
extend Forwardable
|
|
8
|
+
|
|
9
|
+
def initialize(chain, fallback)
|
|
10
|
+
@chain = chain
|
|
11
|
+
@fallback = fallback
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def_delegator :@chain, :^
|
|
15
|
+
|
|
16
|
+
def ~
|
|
17
|
+
OptionalChain.new(~@chain, @fallback)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def fallback
|
|
21
|
+
@chain ^ @fallback
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class AstMapping
|
|
5
|
+
RECEIVER = 0
|
|
6
|
+
ARGS = 1
|
|
7
|
+
KWARGS = 2
|
|
8
|
+
|
|
9
|
+
def initialize(path = List.empty)
|
|
10
|
+
@path = path
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :path
|
|
14
|
+
|
|
15
|
+
def receiver
|
|
16
|
+
@receiver ||= AstMapping.new(@path << RECEIVER)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def args
|
|
20
|
+
@args ||= Args.new(@path << ARGS, [])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def kwargs
|
|
24
|
+
@kwargs ||= Args.new(@path << KWARGS, {})
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Args
|
|
28
|
+
extend Forwardable
|
|
29
|
+
|
|
30
|
+
def initialize(path, data)
|
|
31
|
+
@path = path
|
|
32
|
+
@data = data
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :path, :data
|
|
36
|
+
|
|
37
|
+
def [](index)
|
|
38
|
+
@data[index] ||= AstMapping.new(@path << index)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class CaptureHole < Hole
|
|
5
|
+
def initialize(key, pattern)
|
|
6
|
+
super(key)
|
|
7
|
+
|
|
8
|
+
@pattern = pattern
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :pattern
|
|
12
|
+
|
|
13
|
+
def match?(_expression)
|
|
14
|
+
yield @pattern
|
|
15
|
+
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ==(other)
|
|
20
|
+
super && @pattern == other.pattern
|
|
21
|
+
end
|
|
22
|
+
alias eql? ==
|
|
23
|
+
|
|
24
|
+
def hash
|
|
25
|
+
@hash ||= [self.class, @key, @pattern].hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_s
|
|
29
|
+
"capture(#{@key.inspect}, #{@pattern})"
|
|
30
|
+
end
|
|
31
|
+
alias inspect to_s
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Hole
|
|
5
|
+
def initialize(key, filter = nil)
|
|
6
|
+
@key = key
|
|
7
|
+
@filter = filter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :key
|
|
11
|
+
|
|
12
|
+
def match?(expression)
|
|
13
|
+
!@filter || @filter.call(expression)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def ==(other)
|
|
17
|
+
other.instance_of?(self.class) && key == other.key
|
|
18
|
+
end
|
|
19
|
+
alias eql? ==
|
|
20
|
+
|
|
21
|
+
def hash
|
|
22
|
+
[self.class, @key].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_s
|
|
26
|
+
"hole(#{@key.inspect})"
|
|
27
|
+
end
|
|
28
|
+
alias inspect to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class MethodHole < Hole
|
|
5
|
+
def initialize(key, receiver, method, args, kwargs)
|
|
6
|
+
super(key)
|
|
7
|
+
|
|
8
|
+
@receiver = receiver
|
|
9
|
+
@method = method
|
|
10
|
+
@args = args
|
|
11
|
+
@kwargs = kwargs
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :receiver, :method, :args, :kwargs
|
|
15
|
+
|
|
16
|
+
def match?(expression)
|
|
17
|
+
return false if !expression.is_a?(Call) || !match_method?(expression.method)
|
|
18
|
+
|
|
19
|
+
yield Call.new(@receiver, expression.method, @args, @kwargs)
|
|
20
|
+
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def ==(other)
|
|
25
|
+
return true if other.equal?(self)
|
|
26
|
+
|
|
27
|
+
super &&
|
|
28
|
+
@receiver == other.receiver &&
|
|
29
|
+
@method == other.method &&
|
|
30
|
+
@args.eql?(other.receiver) &&
|
|
31
|
+
@kwargs.eql?(other.receiver)
|
|
32
|
+
end
|
|
33
|
+
alias eql? ==
|
|
34
|
+
|
|
35
|
+
def hash
|
|
36
|
+
@hash ||= [self.class, @key, @receiver, @method, @args, @kwargs].hash
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_s
|
|
40
|
+
args = @args.map(&:inspect)
|
|
41
|
+
kwargs = @kwargs.map { "#{_1}: #{_2.inspect}" }
|
|
42
|
+
list = [@key.inspect, @receiver, @method.inspect].concat(args, kwargs)
|
|
43
|
+
|
|
44
|
+
"method_hole(#{list.join(', ')})"
|
|
45
|
+
end
|
|
46
|
+
alias inspect to_s
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def match_method?(method)
|
|
51
|
+
if @method.is_a?(Array)
|
|
52
|
+
@method.include?(method)
|
|
53
|
+
else
|
|
54
|
+
@method === method
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Pattern
|
|
5
|
+
class PatternBuilder
|
|
6
|
+
include PatternBuilding
|
|
7
|
+
|
|
8
|
+
def initialize(build_session: Matcher.build_session)
|
|
9
|
+
ExpressionBuilding.init(self, build_session)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build(&)
|
|
14
|
+
Matcher.with_build_session do |build_session|
|
|
15
|
+
builder = PatternBuilder.new(build_session:)
|
|
16
|
+
result = builder.instance_exec(&)
|
|
17
|
+
builder.pattern_of(result)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
extend Forwardable
|
|
22
|
+
|
|
23
|
+
def initialize(expression)
|
|
24
|
+
@expression = expression
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :expression
|
|
28
|
+
|
|
29
|
+
def_delegator :@expression, :to_s
|
|
30
|
+
def_delegator :@expression, :inspect
|
|
31
|
+
|
|
32
|
+
def match(expression, mapping = AstMapping.new)
|
|
33
|
+
result = PatternMatch.new
|
|
34
|
+
|
|
35
|
+
catch(:mismatch) do
|
|
36
|
+
match_helper(expression, @expression, mapping, result)
|
|
37
|
+
|
|
38
|
+
result.capture(:root, expression, mapping) unless result.include?(:root)
|
|
39
|
+
|
|
40
|
+
return result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def match_helper(expression, pattern, mapping, result)
|
|
49
|
+
if (hole = get_hole(pattern))
|
|
50
|
+
key = hole.key
|
|
51
|
+
capture = result[key]
|
|
52
|
+
|
|
53
|
+
if capture
|
|
54
|
+
throw(:mismatch) if capture.expression != expression
|
|
55
|
+
elsif !hole.match?(expression) { |p| match_helper(expression, p, mapping, result) }
|
|
56
|
+
throw(:mismatch)
|
|
57
|
+
else
|
|
58
|
+
result.capture(key, expression, mapping)
|
|
59
|
+
end
|
|
60
|
+
elsif expression.is_a?(Call)
|
|
61
|
+
throw(:mismatch) unless similar_call?(expression, pattern)
|
|
62
|
+
|
|
63
|
+
match_helper(expression.receiver, pattern.receiver, mapping.receiver, result)
|
|
64
|
+
|
|
65
|
+
expression.args.each_index do |i|
|
|
66
|
+
match_helper(expression.args[i], pattern.args[i], mapping.args[i], result)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
expression.kwargs.each_key do |k|
|
|
70
|
+
match_helper(expression.kwargs[k], pattern.kwargs[k], mapping.kwargs[k], result)
|
|
71
|
+
end
|
|
72
|
+
elsif expression != pattern
|
|
73
|
+
throw(:mismatch)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def get_hole(expression)
|
|
78
|
+
return unless expression.is_a?(Constant)
|
|
79
|
+
|
|
80
|
+
constant = expression.value
|
|
81
|
+
constant if constant.is_a?(Hole)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def similar_call?(expression, pattern)
|
|
85
|
+
expression.class == pattern.class &&
|
|
86
|
+
expression.method == pattern.method &&
|
|
87
|
+
expression.args.length == pattern.args.length &&
|
|
88
|
+
expression.kwargs.size == pattern.kwargs.size &&
|
|
89
|
+
expression.kwargs.keys.sort == pattern.kwargs.keys.sort
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
module PatternBuilding
|
|
5
|
+
include ExpressionBuilding
|
|
6
|
+
|
|
7
|
+
def pattern_of(value)
|
|
8
|
+
Pattern.new(expression_of(value))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def capture(key, pattern)
|
|
12
|
+
pattern = expression_of(pattern)
|
|
13
|
+
hole = CaptureHole.new(key, pattern)
|
|
14
|
+
|
|
15
|
+
expr(hole)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def hole(key, &filter)
|
|
19
|
+
expr(Hole.new(key, filter))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def var(key)
|
|
23
|
+
expr(VariableHole.new(key))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def const(key)
|
|
27
|
+
expr(ConstantHole.new(key))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def method_hole(key, receiver, method, *args, **kwargs)
|
|
31
|
+
receiver = expression_of(receiver)
|
|
32
|
+
args = args.map { expression_of(_1) }
|
|
33
|
+
kwargs = kwargs.transform_values { expression_of(_1) }
|
|
34
|
+
hole = MethodHole.new(key, receiver, method, args, kwargs)
|
|
35
|
+
|
|
36
|
+
expr(hole)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class PatternMatch
|
|
5
|
+
def initialize
|
|
6
|
+
@captures = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def [](key)
|
|
10
|
+
@captures[key]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def include?(key)
|
|
14
|
+
@captures.include?(key)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def capture(key, expression, mapping)
|
|
18
|
+
@captures[key] = PatternCapture.new(expression, mapping)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def value_paths
|
|
22
|
+
@captures.transform_values(&:value_path)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def expressions
|
|
26
|
+
@captures.transform_values(&:expression)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Matcher
|
|
4
|
+
class Reporter
|
|
5
|
+
##
|
|
6
|
+
# Formats an error tree as a human-readable string
|
|
7
|
+
# @example
|
|
8
|
+
# errors = Matcher.build { Integer }.match("foo")
|
|
9
|
+
# puts Reporter.report(errors)
|
|
10
|
+
# # > root: expected a kind of Integer but got "foo"
|
|
11
|
+
# @param error [Error] the error tree from {Base#match}
|
|
12
|
+
# @return [String]
|
|
13
|
+
def self.report(error)
|
|
14
|
+
new.report(error)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize
|
|
18
|
+
@level = 0
|
|
19
|
+
@continue_line = false
|
|
20
|
+
@path_stack = ['root']
|
|
21
|
+
@phrasing = ExpectedPhrasing.phrasing
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def report(error)
|
|
25
|
+
@io = StringIO.new
|
|
26
|
+
|
|
27
|
+
report_error(error)
|
|
28
|
+
|
|
29
|
+
string = @io.string
|
|
30
|
+
@io.close
|
|
31
|
+
@io = nil
|
|
32
|
+
|
|
33
|
+
string
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def report_error(error)
|
|
39
|
+
case error
|
|
40
|
+
when EmptyError
|
|
41
|
+
report_empty
|
|
42
|
+
when ElementError
|
|
43
|
+
report_element(error)
|
|
44
|
+
when NestedError
|
|
45
|
+
report_nested(error)
|
|
46
|
+
when AndError
|
|
47
|
+
report_and(error)
|
|
48
|
+
when OrError
|
|
49
|
+
report_or(error)
|
|
50
|
+
else
|
|
51
|
+
raise "Illegal error: #{error.inspect}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def report_empty
|
|
56
|
+
'no error'
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def report_element(element)
|
|
60
|
+
message = element.message
|
|
61
|
+
message = @phrasing.call(@path_stack.last, message) if message.is_a?(Message)
|
|
62
|
+
|
|
63
|
+
line("#{@path_stack.last}: #{message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def report_nested(nested)
|
|
67
|
+
path = NestedError.key_to_s(nested.key, @path_stack.last)
|
|
68
|
+
|
|
69
|
+
@path_stack.push(path)
|
|
70
|
+
report_error(nested.child)
|
|
71
|
+
@path_stack.pop
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def report_and(error)
|
|
75
|
+
error.children.each { report_error(_1) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def report_or(error)
|
|
79
|
+
line('expected at least one error to be absent:')
|
|
80
|
+
|
|
81
|
+
error.children.each do |n|
|
|
82
|
+
line('- ', newline: false)
|
|
83
|
+
|
|
84
|
+
@level += 1
|
|
85
|
+
report_error(n)
|
|
86
|
+
@level -= 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def line(message, newline: true)
|
|
91
|
+
message = ' ' * @level + message unless @continue_line
|
|
92
|
+
message += "\n" if newline
|
|
93
|
+
|
|
94
|
+
@continue_line = !newline
|
|
95
|
+
@io.print(message)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|