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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7b7b475f9c2a102c0da7f6cf3422b8553e0f5d2f
4
- data.tar.gz: 5c618057052448e4986233291fcadbe794c4b687
3
+ metadata.gz: 6d10419e259ad298f3f7406069c3abedaf6d0b3a
4
+ data.tar.gz: 73c64df0c8dcdbfa5729e1788af55c59c464d1ca
5
5
  SHA512:
6
- metadata.gz: 93a241b0c7fd705e703f6f3b021cbcf19b3ef1f88eeb45605dfcb815daf670ab547e9f306178c8fa8859c0d190de3c13c99d4d6a9a319ee09f84328d744630b5
7
- data.tar.gz: 0e3cc0b1df1587600fe864336d84fcbecf94432c363a279c0f0382c96dbd0542ddf0b1d9c5ba63d321e1bc450e44724e13db5dcb561f9ec227dbd154e33f1dda
6
+ metadata.gz: 7fe5b016af423a72b690fea03bc18f310fca34d580665345096a283cf937ee26c27bac3f0de69d2a279308700b1ddc85e478e70bb8518a194ae79ae2ac692f3b
7
+ data.tar.gz: 669f02bcb69d4f0c57fb38bdd0534561cf1924c4e2ecdf7cc4bf40fe928e7af585f3b5ee8c7d91799475e3b37565f609b1620c1d192538da3fb96987d42c797b
@@ -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
@@ -3,7 +3,8 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  group :test do
6
- gem "codeclimate-test-reporter", "~> 0.5.0"
6
+ gem "simplecov"
7
+ gem "codeclimate-test-reporter"
7
8
  gem "dry-monads", "~> 0.0.2"
8
9
  end
9
10
 
data/Rakefile CHANGED
@@ -4,3 +4,7 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new
5
5
 
6
6
  task default: :spec
7
+
8
+ require 'yard'
9
+ require 'yard/rake/yardoc_task'
10
+ YARD::Rake::YardocTask.new(:doc)
@@ -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
@@ -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
- def initialize(match:, resolve:)
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) || super
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
 
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  class Matcher
3
- VERSION = "0.5.0".freeze
3
+ VERSION = "0.6.0".freeze
4
4
  end
5
5
  end
@@ -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.00016 seconds |
4
- ./spec/integration/class_enhancement_spec.rb[1:1:2:1] | passed | 0.00013 seconds |
5
- ./spec/integration/class_enhancement_spec.rb[1:2:1:1] | passed | 0.00022 seconds |
6
- ./spec/integration/class_enhancement_spec.rb[1:2:2:1] | passed | 0.00141 seconds |
7
- ./spec/integration/either_matcher_spec.rb[1:1:1:1] | passed | 0.00008 seconds |
8
- ./spec/integration/either_matcher_spec.rb[1:1:2:1] | passed | 0.00008 seconds |
9
- ./spec/integration/either_matcher_spec.rb[1:1:3:1:1] | passed | 0.00015 seconds |
10
- ./spec/integration/either_matcher_spec.rb[1:1:3:2:1] | passed | 0.00016 seconds |
11
- ./spec/integration/matcher_spec.rb[1:1:1] | passed | 0.00013 seconds |
12
- ./spec/integration/matcher_spec.rb[1:1:2] | passed | 0.00012 seconds |
13
- ./spec/integration/matcher_spec.rb[1:1:3:1] | passed | 0.00015 seconds |
14
- ./spec/integration/matcher_spec.rb[1:1:3:2] | passed | 0.00224 seconds |
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
@@ -1,12 +1,9 @@
1
- if RUBY_ENGINE == "ruby"
2
- require "codeclimate-test-reporter"
3
- CodeClimate::TestReporter.start
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.5.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-06-30 00:00:00.000000000 Z
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