dry-matcher 0.5.0 → 0.6.0
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/CHANGELOG.md +23 -0
- data/Gemfile +2 -1
- data/Rakefile +4 -0
- data/lib/dry/matcher.rb +44 -0
- data/lib/dry/matcher/case.rb +19 -1
- data/lib/dry/matcher/either_matcher.rb +46 -0
- data/lib/dry/matcher/evaluator.rb +38 -1
- data/lib/dry/matcher/version.rb +1 -1
- data/spec/examples.txt +16 -12
- data/spec/integration/matcher_spec.rb +19 -0
- data/spec/spec_helper.rb +4 -7
- data/spec/unit/case_spec.rb +22 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d10419e259ad298f3f7406069c3abedaf6d0b3a
|
|
4
|
+
data.tar.gz: 73c64df0c8dcdbfa5729e1788af55c59c464d1ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7fe5b016af423a72b690fea03bc18f310fca34d580665345096a283cf937ee26c27bac3f0de69d2a279308700b1ddc85e478e70bb8518a194ae79ae2ac692f3b
|
|
7
|
+
data.tar.gz: 669f02bcb69d4f0c57fb38bdd0534561cf1924c4e2ecdf7cc4bf40fe928e7af585f3b5ee8c7d91799475e3b37565f609b1620c1d192538da3fb96987d42c797b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
# 0.6.0 / 2016-12-19
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
|
|
5
|
+
- API documentation for most methods (alsemyonov in [#8](https://github.com/dry-rb/dry-matcher/pull/8))
|
|
6
|
+
|
|
7
|
+
## Changed
|
|
8
|
+
|
|
9
|
+
- Matches must now be exhaustive - when matching against a value, at least one match block must be provided for each of a matcher's cases (timriley in [#7](https://github.com/dry-rb/dry-matcher/pull/7))
|
|
10
|
+
- `Dry::Matcher::Case` objects can now be created without a `resolve:` proc. In this case, a default will be provided that passes the result value through (timriley in [#9](https://github.com/dry-rb/dry-matcher/pull/9))
|
|
11
|
+
|
|
12
|
+
[Compare v0.5.1...v0.6.0](https://github.com/dry-rb/dry-result_matcher/compare/v0.5.1...v0.6.0)
|
|
13
|
+
|
|
14
|
+
# 0.5.1 / 2016-07-06
|
|
15
|
+
|
|
16
|
+
## Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed handling of calls to non-existent cases within a matcher block (timriley)
|
|
19
|
+
|
|
20
|
+
[Compare v0.5.0...v0.5.1](https://github.com/dry-rb/dry-result_matcher/compare/v0.5.0...v0.5.1)
|
|
21
|
+
|
|
1
22
|
# 0.5.0 / 2016-06-30
|
|
2
23
|
|
|
3
24
|
## Added
|
|
@@ -59,6 +80,8 @@
|
|
|
59
80
|
```
|
|
60
81
|
- The previous `Dry::ResultMatcher.match` behaviour (for matching `Either` monads) has been moved to `Dry::Matcher::EitherMatcher.call`
|
|
61
82
|
|
|
83
|
+
[Compare v0.4.0...v0.5.0](https://github.com/dry-rb/dry-result_matcher/compare/v0.4.0...v0.5.0)
|
|
84
|
+
|
|
62
85
|
# 0.4.0 / 2016-06-08
|
|
63
86
|
|
|
64
87
|
## Added
|
data/Gemfile
CHANGED
data/Rakefile
CHANGED
data/lib/dry/matcher.rb
CHANGED
|
@@ -2,7 +2,30 @@ require "dry/matcher/case"
|
|
|
2
2
|
require "dry/matcher/evaluator"
|
|
3
3
|
|
|
4
4
|
module Dry
|
|
5
|
+
# @see http://dry-rb.org/gems/dry-matcher
|
|
5
6
|
class Matcher
|
|
7
|
+
# Generates a module containing pattern matching for methods listed in
|
|
8
|
+
# `match_methods` argument with behavior defined by `with` matcher
|
|
9
|
+
#
|
|
10
|
+
# @param [<Symbol>] match_methods
|
|
11
|
+
# @param [Dry::Matcher] with
|
|
12
|
+
# @return [Module]
|
|
13
|
+
#
|
|
14
|
+
# @example Usage with `dry-monads`
|
|
15
|
+
# class MonadicOperation
|
|
16
|
+
# include Dry::Matcher.for(:call, with: Dry::Matcher::EitherMatcher)
|
|
17
|
+
#
|
|
18
|
+
# def call
|
|
19
|
+
# Dry::Monads::Either::Right.new('Success')
|
|
20
|
+
# end
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# operation = MonadicOperation.new
|
|
24
|
+
#
|
|
25
|
+
# operation.call do |m|
|
|
26
|
+
# m.success { |v| "#{v} was successful!"}
|
|
27
|
+
# m.failure { |v| "#{v} has failed!"}
|
|
28
|
+
# end #=> "Success was successful"
|
|
6
29
|
def self.for(*match_methods, with:)
|
|
7
30
|
matcher = with
|
|
8
31
|
|
|
@@ -29,12 +52,33 @@ module Dry
|
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
54
|
|
|
55
|
+
# @return [Hash{Symbol => Case}]
|
|
32
56
|
attr_reader :cases
|
|
33
57
|
|
|
58
|
+
# @param [Hash{Symbol => Case}] cases
|
|
34
59
|
def initialize(cases = {})
|
|
35
60
|
@cases = cases
|
|
36
61
|
end
|
|
37
62
|
|
|
63
|
+
# Evaluate {#cases}' matchers and returns a result of block given to
|
|
64
|
+
# corresponding case matcher
|
|
65
|
+
#
|
|
66
|
+
# @param [Object] result value that would be tested for matches with given {#cases}
|
|
67
|
+
# @param [Object] block
|
|
68
|
+
# @yieldparam [Evaluator] m
|
|
69
|
+
# @return [Object] value returned from the block given to method called
|
|
70
|
+
# after matched pattern
|
|
71
|
+
#
|
|
72
|
+
# @example Usage with `dry-monads`
|
|
73
|
+
# require 'dry-monads'
|
|
74
|
+
# require 'dry/matcher/either_matcher'
|
|
75
|
+
#
|
|
76
|
+
# value = Dry::Monads::Either::Left.new('failure!')
|
|
77
|
+
#
|
|
78
|
+
# Dry::Matcher::EitherMatcher.(value) do |m|
|
|
79
|
+
# m.success { |v| "Yay: #{v}" }
|
|
80
|
+
# m.failure { |v| "Boo: #{v}" }
|
|
81
|
+
# end #=> "Boo: failure!"
|
|
38
82
|
def call(result, &block)
|
|
39
83
|
Evaluator.new(result, cases).call(&block)
|
|
40
84
|
end
|
data/lib/dry/matcher/case.rb
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
module Dry
|
|
2
2
|
class Matcher
|
|
3
|
+
# {Case} object contains logic for pattern matching and resolving result
|
|
4
|
+
# from matched pattern
|
|
3
5
|
class Case
|
|
4
|
-
|
|
6
|
+
DEFAULT_RESOLVE = -> result { result }
|
|
7
|
+
|
|
8
|
+
# @param match [#call] callable used to test given pattern against value
|
|
9
|
+
# @param resolve [#call] callable used to resolve value into a result
|
|
10
|
+
def initialize(match:, resolve: DEFAULT_RESOLVE)
|
|
5
11
|
@match = match
|
|
6
12
|
@resolve = resolve
|
|
7
13
|
end
|
|
8
14
|
|
|
15
|
+
# Tests whether `value` (with optional `*pattern`) matches pattern using
|
|
16
|
+
# callable given to {#initialize} as `match:` argument
|
|
17
|
+
#
|
|
18
|
+
# @param [Object] value
|
|
19
|
+
# @param [<Object>] pattern optional pattern given after the `value` to
|
|
20
|
+
# `match:` callable
|
|
21
|
+
# @return [Boolean]
|
|
9
22
|
def matches?(value, *pattern)
|
|
10
23
|
@match.(value, *pattern)
|
|
11
24
|
end
|
|
12
25
|
|
|
26
|
+
# Resolves result from `value` using callable given to {#initialize}
|
|
27
|
+
# as `resolve:` argument
|
|
28
|
+
#
|
|
29
|
+
# @param [Object] value
|
|
30
|
+
# @return [Object] result resolved from given `value`
|
|
13
31
|
def resolve(value)
|
|
14
32
|
@resolve.(value)
|
|
15
33
|
end
|
|
@@ -2,6 +2,52 @@ require "dry/matcher"
|
|
|
2
2
|
|
|
3
3
|
module Dry
|
|
4
4
|
class Matcher
|
|
5
|
+
# Built-in {Matcher} ready to use with `Either` or `Try` monads from
|
|
6
|
+
# [dry-monads](/gems/dry-monads) or any other compatible gems.
|
|
7
|
+
#
|
|
8
|
+
# Provides {Case}s for two matchers:
|
|
9
|
+
# * `:success` matches `Dry::Monads::Either::Right`
|
|
10
|
+
# and `Dry::Monads::Try::Success` (or any other monad that responds to
|
|
11
|
+
# `#to_either` returning either monad that is `#right?`)
|
|
12
|
+
# * `:failure` matches `Dry::Monads::Either::Left` and
|
|
13
|
+
# `Dry::Monads::Try::Failure` (or any other monad that responds to
|
|
14
|
+
# `#to_either` returning either monad that is `#left?`)
|
|
15
|
+
#
|
|
16
|
+
# @return [Dry::Matcher]
|
|
17
|
+
#
|
|
18
|
+
# @example Usage with `dry-monads`
|
|
19
|
+
# require 'dry-monads'
|
|
20
|
+
# require 'dry/matcher/either_matcher'
|
|
21
|
+
#
|
|
22
|
+
# value = Dry::Monads::Either::Right.new('success!')
|
|
23
|
+
#
|
|
24
|
+
# Dry::Matcher::EitherMatcher.(value) do |m|
|
|
25
|
+
# m.success do |v|
|
|
26
|
+
# "Yay: #{v}"
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# m.failure do |v|
|
|
30
|
+
# "Boo: #{v}"
|
|
31
|
+
# end
|
|
32
|
+
# end #=> "Yay: success!"
|
|
33
|
+
#
|
|
34
|
+
# @example Usage with custom monad
|
|
35
|
+
# require 'dry/matcher/either_matcher'
|
|
36
|
+
#
|
|
37
|
+
# class CustomBooleanMonad
|
|
38
|
+
# def initialize(value); @value = value; end
|
|
39
|
+
# attr_reader :value
|
|
40
|
+
# alias_method :right?, :value
|
|
41
|
+
# def left?; !right?; end
|
|
42
|
+
# def to_either; self; end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# value = CustomBooleanMonad.new(nil)
|
|
46
|
+
#
|
|
47
|
+
# Dry::Matcher::EitherMatcher.(value) do |m|
|
|
48
|
+
# m.success { |v| "#{v.inspect} is truthy" }
|
|
49
|
+
# m.failure { |v| "#{v.inspect} is falsey" }
|
|
50
|
+
# end # => "nil is falsey"
|
|
5
51
|
EitherMatcher = Dry::Matcher.new(
|
|
6
52
|
success: Case.new(
|
|
7
53
|
match: -> result, *pattern {
|
|
@@ -1,30 +1,67 @@
|
|
|
1
1
|
module Dry
|
|
2
2
|
class Matcher
|
|
3
|
+
NonExhaustiveMatchError = Class.new(StandardError)
|
|
4
|
+
|
|
5
|
+
# {Evaluator} is used in {Dry::Matcher#call Dry::Matcher#call} block to handle different {Case}s
|
|
3
6
|
class Evaluator < BasicObject
|
|
7
|
+
# @param [Object] result
|
|
8
|
+
# @param [Hash{Symbol => Case}] cases
|
|
4
9
|
def initialize(result, cases)
|
|
5
10
|
@cases = cases
|
|
6
11
|
@result = result
|
|
12
|
+
|
|
13
|
+
@unhandled_cases = @cases.keys.map(&:to_sym)
|
|
7
14
|
@matched = false
|
|
8
15
|
@output = nil
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
def call
|
|
12
19
|
yield self
|
|
20
|
+
|
|
21
|
+
ensure_exhaustive_match
|
|
22
|
+
|
|
13
23
|
@output
|
|
14
24
|
end
|
|
15
25
|
|
|
26
|
+
# Checks whether `cases` given to {#initialize} contains one called `name`
|
|
27
|
+
# @param [String] name
|
|
28
|
+
# @param [Boolean] include_private
|
|
29
|
+
# @return [Boolean]
|
|
16
30
|
def respond_to_missing?(name, include_private = false)
|
|
17
|
-
@cases.key?(name)
|
|
31
|
+
@cases.key?(name)
|
|
18
32
|
end
|
|
19
33
|
|
|
34
|
+
# Handles method `name` called after one of the keys in `cases` hash given
|
|
35
|
+
# to {#initialize}
|
|
36
|
+
#
|
|
37
|
+
# @param [String] name name of the case given to {#initialize} in `cases`
|
|
38
|
+
# argument
|
|
39
|
+
# @param [Array] args pattern that would be tested for match and used to
|
|
40
|
+
# resolve result
|
|
41
|
+
# @param [#call] block callable that will processes resolved value
|
|
42
|
+
# from matched pattern
|
|
43
|
+
# @yieldparam [Object] v resolved value
|
|
44
|
+
# @return [Object] result of calling `block` on value resolved from `args`
|
|
45
|
+
# if `args` pattern was matched by the given case called `name`
|
|
46
|
+
# @raise [NoMethodError] if there was no case called `name` given to
|
|
47
|
+
# {#initialize} in `cases` hash
|
|
20
48
|
def method_missing(name, *args, &block)
|
|
21
49
|
return super unless @cases.key?(name)
|
|
22
50
|
|
|
51
|
+
@unhandled_cases.delete name
|
|
23
52
|
handle_case @cases[name], *args, &block
|
|
24
53
|
end
|
|
25
54
|
|
|
26
55
|
private
|
|
27
56
|
|
|
57
|
+
def ensure_exhaustive_match
|
|
58
|
+
if @unhandled_cases.any?
|
|
59
|
+
::Kernel.raise NonExhaustiveMatchError, "cases +#{@unhandled_cases.join(', ')}+ not handled"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @param [Case] kase
|
|
64
|
+
# @param [Array] pattern
|
|
28
65
|
def handle_case(kase, *pattern)
|
|
29
66
|
return @output if @matched
|
|
30
67
|
|
data/lib/dry/matcher/version.rb
CHANGED
data/spec/examples.txt
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
example_id | status | run_time |
|
|
2
2
|
----------------------------------------------------- | ------ | --------------- |
|
|
3
|
-
./spec/integration/class_enhancement_spec.rb[1:1:1:1] | passed | 0.
|
|
4
|
-
./spec/integration/class_enhancement_spec.rb[1:1:2:1] | passed | 0.
|
|
5
|
-
./spec/integration/class_enhancement_spec.rb[1:2:1:1] | passed | 0.
|
|
6
|
-
./spec/integration/class_enhancement_spec.rb[1:2:2:1] | passed | 0.
|
|
7
|
-
./spec/integration/either_matcher_spec.rb[1:1:1:1] | passed | 0.
|
|
8
|
-
./spec/integration/either_matcher_spec.rb[1:1:2:1] | passed | 0.
|
|
9
|
-
./spec/integration/either_matcher_spec.rb[1:1:3:1:1] | passed | 0.
|
|
10
|
-
./spec/integration/either_matcher_spec.rb[1:1:3:2:1] | passed | 0.
|
|
11
|
-
./spec/integration/matcher_spec.rb[1:1:1] | passed | 0.
|
|
12
|
-
./spec/integration/matcher_spec.rb[1:1:2] | passed | 0.
|
|
13
|
-
./spec/integration/matcher_spec.rb[1:1:3
|
|
14
|
-
./spec/integration/matcher_spec.rb[1:1:
|
|
3
|
+
./spec/integration/class_enhancement_spec.rb[1:1:1:1] | passed | 0.00015 seconds |
|
|
4
|
+
./spec/integration/class_enhancement_spec.rb[1:1:2:1] | passed | 0.00014 seconds |
|
|
5
|
+
./spec/integration/class_enhancement_spec.rb[1:2:1:1] | passed | 0.00088 seconds |
|
|
6
|
+
./spec/integration/class_enhancement_spec.rb[1:2:2:1] | passed | 0.00013 seconds |
|
|
7
|
+
./spec/integration/either_matcher_spec.rb[1:1:1:1] | passed | 0.00011 seconds |
|
|
8
|
+
./spec/integration/either_matcher_spec.rb[1:1:2:1] | passed | 0.0001 seconds |
|
|
9
|
+
./spec/integration/either_matcher_spec.rb[1:1:3:1:1] | passed | 0.00016 seconds |
|
|
10
|
+
./spec/integration/either_matcher_spec.rb[1:1:3:2:1] | passed | 0.00015 seconds |
|
|
11
|
+
./spec/integration/matcher_spec.rb[1:1:1] | passed | 0.00016 seconds |
|
|
12
|
+
./spec/integration/matcher_spec.rb[1:1:2] | passed | 0.00015 seconds |
|
|
13
|
+
./spec/integration/matcher_spec.rb[1:1:3] | passed | 0.00159 seconds |
|
|
14
|
+
./spec/integration/matcher_spec.rb[1:1:4:1] | passed | 0.00018 seconds |
|
|
15
|
+
./spec/integration/matcher_spec.rb[1:1:4:2] | passed | 0.00185 seconds |
|
|
16
|
+
./spec/unit/case_spec.rb[1:1:1] | passed | 0.00054 seconds |
|
|
17
|
+
./spec/unit/case_spec.rb[1:2:1] | passed | 0.00048 seconds |
|
|
18
|
+
./spec/unit/case_spec.rb[1:2:2] | passed | 0.00008 seconds |
|
|
@@ -45,7 +45,24 @@ RSpec.describe Dry::Matcher do
|
|
|
45
45
|
expect(call_match(input)).to eq "Failure: No!"
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
it "requires an exhaustive match" do
|
|
49
|
+
input = Dry::Monads::Right("Yes!")
|
|
50
|
+
|
|
51
|
+
expect {
|
|
52
|
+
matcher.(input) do |m|
|
|
53
|
+
m.success { |v| "Success: #{v}" }
|
|
54
|
+
end
|
|
55
|
+
}.to raise_error Dry::Matcher::NonExhaustiveMatchError
|
|
56
|
+
end
|
|
57
|
+
|
|
48
58
|
context "with patterns" do
|
|
59
|
+
let(:success_case) {
|
|
60
|
+
Dry::Matcher::Case.new(
|
|
61
|
+
match: -> result { result.first == :ok },
|
|
62
|
+
resolve: -> result { result.last },
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
let(:failure_case) {
|
|
50
67
|
Dry::Matcher::Case.new(
|
|
51
68
|
match: -> result, failure_type {
|
|
@@ -57,6 +74,8 @@ RSpec.describe Dry::Matcher do
|
|
|
57
74
|
|
|
58
75
|
def call_match(input)
|
|
59
76
|
matcher.(input) do |m|
|
|
77
|
+
m.success
|
|
78
|
+
|
|
60
79
|
m.failure :my_error do |v|
|
|
61
80
|
"Pattern-matched failure: #{v}"
|
|
62
81
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
if RUBY_ENGINE == "ruby"
|
|
2
|
-
require "
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
SimpleCov.start do
|
|
6
|
-
add_filter "/spec/"
|
|
7
|
-
end
|
|
1
|
+
if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.3"
|
|
2
|
+
require "simplecov"
|
|
3
|
+
SimpleCov.start
|
|
8
4
|
end
|
|
9
5
|
|
|
6
|
+
|
|
10
7
|
begin
|
|
11
8
|
require "byebug"
|
|
12
9
|
rescue LoadError; end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
RSpec.describe Dry::Matcher::Case do
|
|
2
|
+
describe "#matches?" do
|
|
3
|
+
it "calls the match proc with the value" do
|
|
4
|
+
kase = described_class.new(match: -> value { value.even? })
|
|
5
|
+
expect(kase.matches?(2)).to be true
|
|
6
|
+
expect(kase.matches?(1)).to be false
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe "#resolve" do
|
|
11
|
+
it "calls the resolve proc with the value" do
|
|
12
|
+
kase = described_class.new(match: -> * { true }, resolve: resolve = -> value { value.to_s })
|
|
13
|
+
|
|
14
|
+
expect(kase.resolve(123)).to eq "123"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "defaults to passing through the value" do
|
|
18
|
+
kase = described_class.new(match: -> * { true })
|
|
19
|
+
expect(kase.resolve(123)).to eq 123
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dry-matcher
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tim Riley
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2016-
|
|
11
|
+
date: 2016-12-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -103,6 +103,7 @@ files:
|
|
|
103
103
|
- spec/integration/either_matcher_spec.rb
|
|
104
104
|
- spec/integration/matcher_spec.rb
|
|
105
105
|
- spec/spec_helper.rb
|
|
106
|
+
- spec/unit/case_spec.rb
|
|
106
107
|
homepage: http://dry-rb.org/gems/dry-matcher
|
|
107
108
|
licenses:
|
|
108
109
|
- MIT
|