dry-matcher 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|