├── bin ├── .gitkeep ├── setup └── console ├── .yardopts ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug-report.md ├── SUPPORT.md └── workflows │ ├── rubocop.yml │ ├── repo-sync-preview.yml │ └── ci.yml ├── .rspec ├── lib ├── dry-matcher.rb └── dry │ ├── matcher │ ├── version.rb │ ├── either_matcher.rb │ ├── match.rb │ ├── case.rb │ ├── maybe_matcher.rb │ ├── evaluator.rb │ └── result_matcher.rb │ └── matcher.rb ├── .gitignore ├── .rubocop.yml ├── Rakefile ├── Gemfile ├── spec ├── support │ ├── warnings.rb │ ├── coverage.rb │ └── rspec.rb ├── integration │ ├── class_enhancement_spec.rb │ ├── dry_monads_do_spec.rb │ ├── matcher_spec.rb │ ├── maybe_matcher_spec.rb │ └── result_matcher_spec.rb ├── unit │ └── case_spec.rb └── spec_helper.rb ├── Gemfile.devtools ├── repo-sync.yml ├── README.md ├── docsite └── source │ ├── maybe-matcher.html.md │ ├── class-enhancement.html.md │ ├── result-matcher.html.md │ └── index.html.md ├── LICENSE ├── CONTRIBUTING.md ├── dry-matcher.gemspec ├── CODE_OF_CONDUCT.md └── CHANGELOG.md /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup=markdown 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: hanami 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | --warnings 5 | -------------------------------------------------------------------------------- /lib/dry-matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/matcher" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /.yardoc 3 | /doc 4 | /coverage 5 | /spec/examples.txt 6 | /pkg 7 | tmp/ 8 | .bundle 9 | /.rubocop-* 10 | -------------------------------------------------------------------------------- /lib/dry/matcher/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | class Matcher 5 | VERSION = "1.0.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from hanakai-rb/repo-sync 2 | 3 | inherit_from: 4 | - https://raw.githubusercontent.com/hanakai-rb/repo-sync/main/rubocop/rubocop.yml 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Community support 4 | url: https://discourse.hanamirb.org 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | eval_gemfile "Gemfile.devtools" 6 | 7 | gemspec 8 | 9 | group :test do 10 | gem "dry-monads", "~> 1.6" 11 | gem "rspec", "~> 3.8" 12 | end 13 | 14 | group :tools do 15 | gem "yard" 16 | end 17 | -------------------------------------------------------------------------------- /spec/support/warnings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync 4 | 5 | require "warning" 6 | 7 | Warning.ignore(%r{rspec/core}) 8 | Warning.ignore(%r{rspec/mocks}) 9 | Warning.ignore(/codacy/) 10 | Warning[:experimental] = false if Warning.respond_to?(:[]) 11 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | If you need help with any of the Hanami, Dry or Rom libraries, feel free to ask questions on our [discussion forum](https://discourse.hanamirb.org/). This is the best place to seek help. Make sure to search for a potential solution in past threads before posting your question. Thanks! :heart: 4 | -------------------------------------------------------------------------------- /lib/dry/matcher/either_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core/deprecations" 4 | require "dry/matcher/result_matcher" 5 | 6 | module Dry 7 | class Matcher 8 | extend Dry::Core::Deprecations[:"dry-matcher"] 9 | 10 | EitherMatcher = ResultMatcher 11 | 12 | deprecate_constant(:EitherMatcher, message: "Dry::Matcher::ResultMatcher") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/support/coverage.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync 4 | 5 | if ENV["COVERAGE"] == "true" 6 | require "simplecov" 7 | require "simplecov-cobertura" 8 | 9 | SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 10 | 11 | SimpleCov.start do 12 | add_filter "/spec/" 13 | enable_coverage :branch 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "dry-matcher" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | require "pry" 12 | 13 | binding.pry 14 | -------------------------------------------------------------------------------- /Gemfile.devtools: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync 4 | 5 | gem "rake", ">= 12.3.3" 6 | 7 | group :test do 8 | gem "simplecov", require: false, platforms: :ruby 9 | gem "simplecov-cobertura", require: false, platforms: :ruby 10 | gem "rexml", require: false 11 | 12 | gem "warning" 13 | end 14 | 15 | group :tools do 16 | gem "rubocop" 17 | end 18 | -------------------------------------------------------------------------------- /repo-sync.yml: -------------------------------------------------------------------------------- 1 | name: 2 | gem: dry-matcher 3 | constant: Dry::Matcher 4 | github_org: dry-rb 5 | gemspec: 6 | authors: ["Hanakai team"] 7 | email: ["info@hanakai.org"] 8 | summary: "Flexible, expressive pattern matching for Ruby" 9 | homepage: "https://dry-rb.org/gems/dry-matcher" 10 | required_ruby_version: ">= 3.1.0" 11 | development_dependencies: 12 | - bundler 13 | - rake 14 | - rspec 15 | runtime_dependencies: 16 | - [dry-core, "~> 1.0", "< 2"] 17 | -------------------------------------------------------------------------------- /lib/dry/matcher/match.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/matcher" 4 | 5 | # rubocop:disable Style/CaseEquality 6 | module Dry 7 | class Matcher 8 | PatternMatch = proc do |value, patterns| 9 | if patterns.empty? 10 | value 11 | elsif value.is_a?(::Array) && patterns.any? { |p| p === value[0] } 12 | value 13 | elsif patterns.any? { |p| p === value } 14 | # rubocop:enable Lint/DuplicateBranch 15 | value 16 | else 17 | Undefined 18 | end 19 | end 20 | end 21 | end 22 | # rubocop:enable Style/CaseEquality 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [rubygem]: https://rubygems.org/gems/dry-matcher 4 | [actions]: https://github.com/dry-rb/dry-matcher/actions 5 | 6 | # dry-matcher [![Gem Version](https://badge.fury.io/rb/dry-matcher.svg)][rubygem] [![CI Status](https://github.com/dry-rb/dry-matcher/workflows/CI/badge.svg)][actions] 7 | 8 | ## Links 9 | 10 | - [User documentation](https://dry-rb.org/gems/dry-matcher) 11 | - [API documentation](http://rubydoc.info/gems/dry-matcher) 12 | - [Forum](https://discourse.dry-rb.org) 13 | 14 | ## License 15 | 16 | See `LICENSE` file. 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: See CONTRIBUTING.md for more information 4 | title: '' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## To Reproduce 15 | 16 | Provide detailed steps to reproduce, **an executable script would be best**. 17 | 18 | ## Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## My environment 23 | 24 | - Affects my production application: **YES/NO** 25 | - Ruby version: ... 26 | - OS: ... 27 | -------------------------------------------------------------------------------- /docsite/source/maybe-matcher.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Maybe matcher 3 | layout: gem-single 4 | name: dry-matcher 5 | --- 6 | 7 | dry-matcher provides a ready-to-use `MaybeMatcher` for working with `Maybe` from [dry-monads](/gems/dry-monads) or any other compatible gems. 8 | 9 | ```ruby 10 | require "dry/monads" 11 | require "dry/matcher/maybe_matcher" 12 | 13 | value = Dry::Monads::Maybe("success!") 14 | 15 | result = Dry::Matcher::MaybeMatcher.(value) do |m| 16 | m.some(Integer) do |i| 17 | "Got int: #{i}" 18 | end 19 | 20 | m.some do |v| 21 | "Yay: #{v}" 22 | end 23 | 24 | m.none do 25 | "Boo: none" 26 | end 27 | end 28 | 29 | result # => "Yay: success!" 30 | ``` 31 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync 4 | 5 | name: RuboCop 6 | 7 | on: 8 | push: 9 | branches: ["main", "release-*", "ci/*"] 10 | tags: ["v*"] 11 | pull_request: 12 | branches: ["main", "release-*"] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | env: 21 | BUNDLE_ONLY: tools 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Set up Ruby 3.4 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: 3.4 30 | bundler-cache: true 31 | 32 | - name: Run RuboCop 33 | run: bundle exec rubocop --parallel 34 | -------------------------------------------------------------------------------- /docsite/source/class-enhancement.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Class enhancement 3 | layout: gem-single 4 | name: dry-matcher 5 | --- 6 | 7 | You can offer a match block API from your own methods using `Dry::Matcher.for`: 8 | 9 | ```ruby 10 | require "dry-matcher" 11 | 12 | # First, build a matcher or use an existing one (like dry-matcher's ResultMatcher) 13 | MyMatcher = Dry::Matcher.new(...) 14 | 15 | # Offer it from your class with `Dry::Matcher.for` 16 | class MyOperation 17 | include Dry::Matcher.for(:call, with: MyMatcher) 18 | 19 | def call 20 | # return a value here 21 | end 22 | end 23 | 24 | # And now `MyOperation#call` offers the matcher block API 25 | operation = MyOperation.new 26 | 27 | operation.() do |m| 28 | # Use the matcher's API here 29 | end 30 | ``` 31 | -------------------------------------------------------------------------------- /docsite/source/result-matcher.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Result matcher 3 | layout: gem-single 4 | name: dry-matcher 5 | --- 6 | 7 | dry-matcher provides a ready-to-use `ResultMatcher` for working with `Result` or `Try` monads from [dry-monads](/gems/dry-monads) or any other compatible gems. 8 | 9 | ```ruby 10 | require "dry/monads" 11 | require "dry/matcher/result_matcher" 12 | 13 | value = Dry::Monads::Success("success!") 14 | 15 | result = Dry::Matcher::ResultMatcher.(value) do |m| 16 | m.success(Integer) do |i| 17 | "Got int: #{i}" 18 | end 19 | 20 | m.success do |v| 21 | "Yay: #{v}" 22 | end 23 | 24 | m.failure :not_found do |_err, reason| 25 | "Nope: #{reason}" 26 | end 27 | 28 | m.failure do |v| 29 | "Boo: #{v}" 30 | end 31 | end 32 | 33 | result # => "Yay: success!" 34 | ``` 35 | -------------------------------------------------------------------------------- /spec/support/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync 4 | 5 | RSpec.configure do |config| 6 | # When no filter given, search and run focused tests 7 | config.filter_run_when_matching :focus 8 | 9 | # Disables rspec monkey patches (no reason for their existence tbh) 10 | config.disable_monkey_patching! 11 | 12 | # Run ruby in verbose mode 13 | config.warnings = true 14 | 15 | # Collect all failing expectations automatically, 16 | # without calling aggregate_failures everywhere 17 | config.define_derived_metadata do |meta| 18 | meta[:aggregate_failures] = true 19 | end 20 | 21 | if ENV['CI'] 22 | # No focused specs should be committed. This ensures 23 | # builds fail when this happens. 24 | config.before(:each, :focus) do 25 | raise StandardError, "You've committed a focused spec!" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2025 Hanakai team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/repo-sync-preview.yml: -------------------------------------------------------------------------------- 1 | name: Repo-sync preview 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["CI", "RuboCop"] 6 | types: [completed] 7 | branches: 8 | - "ci/repo-sync-preview-*" 9 | 10 | jobs: 11 | report: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Dispatch status to repo-sync 15 | uses: actions/github-script@v7 16 | with: 17 | github-token: ${{ secrets.REPO_SYNC_DISPATCH_TOKEN }} 18 | script: | 19 | await github.rest.actions.createWorkflowDispatch({ 20 | owner: "hanakai-rb", 21 | repo: "repo-sync", 22 | workflow_id: "aggregate-preview-status.yml", 23 | ref: "main", 24 | inputs: { 25 | pr_number: "${{ github.event.workflow_run.head_branch }}".replace("ci/repo-sync-preview-", ""), 26 | repo_name: "${{ github.repository }}", 27 | workflow_name: "${{ github.event.workflow_run.name }}", 28 | status: "${{ github.event.workflow_run.conclusion }}", 29 | run_url: "${{ github.event.workflow_run.html_url }}" 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /lib/dry/matcher/case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | class Matcher 5 | # {Case} object contains logic for pattern matching and resolving result 6 | # from matched pattern 7 | class Case 8 | DEFAULT_RESOLVE = -> result { result } 9 | 10 | # @param match [#call] callable used to test given pattern against value 11 | # @param resolve [#call] callable used to resolve value into a result 12 | def initialize(match: Undefined, resolve: DEFAULT_RESOLVE, &block) 13 | @match = block || proc do |value, patterns| 14 | if match.(value, *patterns) 15 | resolve.(value) 16 | else 17 | Undefined 18 | end 19 | end 20 | end 21 | 22 | # @param [Object] value Value to match 23 | # @param [Array] patterns Optional list of patterns to match against 24 | # @yieldparam [Object] v Resolved value if match succeeds 25 | # @return [Object,Dry::Core::Constants::Undefined] Either the yield result 26 | # or Undefined if match wasn't successful 27 | def call(value, patterns = EMPTY_ARRAY, &block) 28 | Undefined.map(@match.(value, patterns), &block) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issue Guidelines 2 | 3 | ## Reporting bugs 4 | 5 | If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated. 6 | 7 | ## Reporting feature requests 8 | 9 | Report a feature request **only after discussing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature. 10 | 11 | ## Reporting questions, support requests, ideas, concerns etc. 12 | 13 | **PLEASE DON'T** - use [discourse.dry-rb.org](https://discourse.dry-rb.org) instead. 14 | 15 | # Pull Request Guidelines 16 | 17 | A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc. 18 | 19 | Other requirements: 20 | 21 | 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue. 22 | 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 23 | 3) Add API documentation if it's a new feature 24 | 4) Update API documentation if it changes an existing feature 25 | 5) Bonus points for sending a PR which updates user documentation in the `docsite` directory 26 | 27 | # Asking for help 28 | 29 | If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org). 30 | -------------------------------------------------------------------------------- /dry-matcher.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml. 4 | 5 | lib = File.expand_path("lib", __dir__) 6 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 7 | require "dry/matcher/version" 8 | 9 | Gem::Specification.new do |spec| 10 | spec.name = "dry-matcher" 11 | spec.authors = ["Hanakai team"] 12 | spec.email = ["info@hanakai.org"] 13 | spec.license = "MIT" 14 | spec.version = Dry::Matcher::VERSION.dup 15 | 16 | spec.summary = "Flexible, expressive pattern matching for Ruby" 17 | spec.description = spec.summary 18 | spec.homepage = "https://dry-rb.org/gems/dry-matcher" 19 | spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-matcher.gemspec", "lib/**/*"] 20 | spec.bindir = "bin" 21 | spec.executables = [] 22 | spec.require_paths = ["lib"] 23 | 24 | spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"] 25 | 26 | spec.metadata["allowed_push_host"] = "https://rubygems.org" 27 | spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-matcher/blob/main/CHANGELOG.md" 28 | spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-matcher" 29 | spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-matcher/issues" 30 | spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami" 31 | 32 | spec.required_ruby_version = ">= 3.1.0" 33 | 34 | spec.add_runtime_dependency "dry-core", "~> 1.0", "< 2" 35 | spec.add_development_dependency "bundler" 36 | spec.add_development_dependency "rake" 37 | spec.add_development_dependency "rspec" 38 | end 39 | 40 | -------------------------------------------------------------------------------- /spec/integration/class_enhancement_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "dry/matcher/result_matcher" 5 | 6 | RSpec.describe "Class enhancement with Dry::Matcher.for" do 7 | let(:operation) do 8 | Class.new do 9 | include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) 10 | 11 | def call(bool) 12 | bool ? Dry::Monads::Success("a success") : Dry::Monads::Failure("a failure") 13 | end 14 | end.new 15 | end 16 | 17 | describe "match blocks" do 18 | subject(:match) do 19 | operation.call(input) do |m| 20 | m.success do |v| 21 | "Matched success: #{v}" 22 | end 23 | 24 | m.failure do |v| 25 | "Matched failure: #{v}" 26 | end 27 | end 28 | end 29 | 30 | context "successful result" do 31 | let(:input) { true } 32 | 33 | it "matches on success" do 34 | expect(match).to eq "Matched success: a success" 35 | end 36 | end 37 | 38 | context "failed result" do 39 | let(:input) { false } 40 | 41 | it "matches on failure" do 42 | expect(match).to eq "Matched failure: a failure" 43 | end 44 | end 45 | end 46 | 47 | describe "without match blocks" do 48 | subject(:result) { operation.call(input) } 49 | 50 | context "successful result" do 51 | let(:input) { true } 52 | 53 | it "returns the result" do 54 | expect(result).to eq Dry::Monads::Success("a success") 55 | end 56 | end 57 | 58 | context "failed result" do 59 | let(:input) { false } 60 | 61 | it "returns the result" do 62 | expect(result).to eq Dry::Monads::Failure("a failure") 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/dry/matcher/maybe_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/matcher" 4 | require "dry/matcher/match" 5 | 6 | module Dry 7 | class Matcher 8 | # Built-in {Matcher} ready to use with `Maybe` monad from 9 | # [dry-monads](/gems/dry-monads) or any other compatible gems. 10 | # 11 | # Provides {Case}s for two matchers: 12 | # * `:some` matches `Dry::Monads::Maybe::Some` 13 | # * `:none` matches `Dry::Monads::Maybe::None` 14 | # 15 | # @return [Dry::Matcher] 16 | # 17 | # @example Usage with `dry-monads` 18 | # require 'dry/monads' 19 | # require 'dry/matcher/maybe_matcher' 20 | # 21 | # value = Dry::Monads::Maybe.new('there is a value!') 22 | # 23 | # Dry::Matcher::MaybeMatcher.(value) do |m| 24 | # m.some do |v| 25 | # "Yay: #{v}" 26 | # end 27 | # 28 | # m.none do 29 | # "Boo: none" 30 | # end 31 | # end #=> "Yay: there is a value!" 32 | # 33 | # 34 | # @example Usage with specific types 35 | # value = Dry::Monads::Maybe.new([200, :ok]) 36 | # 37 | # Dry::Matcher::MaybeMatcher.(value) do |m| 38 | # m.some(200, :ok) do |code, value| 39 | # "Yay: #{value}" 40 | # end 41 | # 42 | # m.none do 43 | # "Boo: none" 44 | # end 45 | # end #=> "Yay: :ok" 46 | # 47 | MaybeMatcher = Dry::Matcher.new( 48 | some: Case.new { |maybe, patterns| 49 | if maybe.none? 50 | Undefined 51 | else 52 | Dry::Matcher::PatternMatch.(maybe.value!, patterns) 53 | end 54 | }, 55 | none: Case.new { |maybe| 56 | if maybe.some? 57 | Undefined 58 | end 59 | } 60 | ) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/unit/case_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Dry::Matcher::Case do 4 | let(:undefined) { Dry::Core::Constants::Undefined } 5 | 6 | describe "old interface" do 7 | describe "matching" do 8 | subject(:kase) { described_class.new(match: -> value { value.even? }) } 9 | 10 | it "calls the match proc with the value" do 11 | expect(kase.call(2) { :matched }).to be(:matched) 12 | expect(kase.call(1) { raise }).to be(undefined) 13 | end 14 | end 15 | 16 | describe "resolving" do 17 | it "calls the resolve proc with the value" do 18 | kase = described_class.new(match: -> * { true }, resolve: -> value { value.to_s }) 19 | 20 | expect(kase.call(123) { |result| result }).to eq "123" 21 | end 22 | 23 | kase = described_class.new(match: -> * { true }) 24 | it "defaults to passing through the value" do 25 | expect(kase.call(123) { |result| result }).to eq 123 26 | end 27 | end 28 | end 29 | 30 | describe "#call" do 31 | describe "using patterns" do 32 | let(:kase) do 33 | described_class.new do |value, patterns| 34 | if patterns.include?(value) 35 | value 36 | else 37 | undefined 38 | end 39 | end 40 | end 41 | 42 | it "uses patterns to match given value" do 43 | expect(kase.call(3, [1, 2, 3], &:to_s)).to eql("3") 44 | expect(kase.call(4, [1, 2, 3]) { raise }).to be(undefined) 45 | end 46 | end 47 | 48 | describe "extracting values" do 49 | let(:kase) do 50 | described_class.new do |(code, value), patterns| 51 | if patterns.include?(code) 52 | value 53 | else 54 | undefined 55 | end 56 | end 57 | end 58 | 59 | it "transforms value by dropping result code" do 60 | expect(kase.call([:found, 100], %i[found not_found], &:to_s)).to eql("100") 61 | expect(kase.call([:else, 100], %i[found not_found]) { raise }).to be(undefined) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "support/coverage" 4 | require_relative "support/warnings" 5 | 6 | begin 7 | require "byebug" 8 | rescue LoadError; 9 | end 10 | require "dry-matcher" 11 | 12 | Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each { |f| require f } 13 | 14 | RSpec.configure do |config| 15 | config.expect_with :rspec do |expectations| 16 | # This option will default to `true` in RSpec 4. 17 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 18 | end 19 | 20 | config.mock_with :rspec do |mocks| 21 | # This option will default to `true` in RSpec 4. 22 | mocks.verify_partial_doubles = true 23 | end 24 | 25 | # Allows RSpec to persist some state between runs in order to support 26 | # the `--only-failures` and `--next-failure` CLI options. We recommend 27 | # you configure your source control system to ignore this file. 28 | config.example_status_persistence_file_path = "spec/examples.txt" 29 | 30 | # This setting enables warnings. It's recommended, but in some cases may 31 | # be too noisy due to issues in dependencies. 32 | config.warnings = true 33 | 34 | # Many RSpec users commonly either run the entire suite or an individual 35 | # file, and it's useful to allow more verbose output when running an 36 | # individual spec file. 37 | if config.files_to_run.one? 38 | # Use the documentation formatter for detailed output, unless a formatter 39 | # has already been configured (e.g. via a command-line flag). 40 | config.default_formatter = "doc" 41 | end 42 | 43 | # Run specs in random order to surface order dependencies. If you find an 44 | # order dependency and want to debug it, you can fix the order by providing 45 | # the seed, which is printed after each run. 46 | # --seed 1234 47 | config.order = :random 48 | 49 | # Seed global randomization in this process using the `--seed` CLI option. 50 | # Setting this allows you to use `--seed` to deterministically reproduce 51 | # test failures related to randomization by passing the same `--seed` value 52 | # as the one that triggered the failure. 53 | Kernel.srand config.seed 54 | end 55 | -------------------------------------------------------------------------------- /lib/dry/matcher/evaluator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Dry 4 | class Matcher 5 | NonExhaustiveMatchError = Class.new(StandardError) 6 | 7 | # {Evaluator} is used in {Dry::Matcher#call Dry::Matcher#call} block to handle different {Case}s 8 | class Evaluator 9 | # @param [Object] result 10 | # @param [Hash{Symbol => Case}] cases 11 | def initialize(result, cases) 12 | @cases = cases 13 | @result = result 14 | 15 | @unhandled_cases = @cases.keys.map(&:to_sym) 16 | end 17 | 18 | def call 19 | yield self 20 | 21 | ensure_exhaustive_match 22 | 23 | @output if defined? @output 24 | end 25 | 26 | # Checks whether `cases` given to {#initialize} contains one called `name` 27 | # @param [String] name 28 | # @param [Boolean] include_private 29 | # @return [Boolean] 30 | def respond_to_missing?(name, _include_private = false) 31 | @cases.key?(name) 32 | end 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 48 | def method_missing(name, *args, &block) 49 | kase = @cases.fetch(name) { return super } 50 | 51 | @unhandled_cases.delete name 52 | 53 | unless defined? @output 54 | kase.(@result, args) do |result| 55 | @output = yield(result) 56 | end 57 | end 58 | end 59 | 60 | private 61 | 62 | def ensure_exhaustive_match 63 | if @unhandled_cases.any? 64 | ::Kernel.raise NonExhaustiveMatchError, 65 | "cases +#{@unhandled_cases.join(", ")}+ not handled" 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/integration/dry_monads_do_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | require "dry/matcher/result_matcher" 5 | 6 | RSpec.describe "Integration with dry-monads Do notation" do 7 | shared_examples "class using both dry-matcher and dry-monads Do notation" do 8 | it "supports yielding via Do notation as well as final result matching block" do 9 | matched_success = nil 10 | matched_failure = nil 11 | 12 | operation.(name: "Jane", email: "jane@example.com") do |m| 13 | m.success { |v| matched_success = v } 14 | m.failure {} 15 | end 16 | 17 | operation.(name: "Jo") do |m| 18 | m.success {} 19 | m.failure { |v| matched_failure = v } 20 | end 21 | 22 | expect(matched_success).to eq "Hello, Jane" 23 | expect(matched_failure).to eq :no_email 24 | end 25 | end 26 | 27 | describe "yielding" do 28 | let(:operation) do 29 | Class.new do 30 | include Dry::Monads::Result::Mixin 31 | include Dry::Monads::Do 32 | include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) 33 | 34 | def call(user) 35 | user = yield validate(user) 36 | greeting = yield greet(user) 37 | 38 | Success(greeting) 39 | end 40 | 41 | private 42 | 43 | def validate(user) 44 | user[:email] ? Success(user) : Failure(:no_email) 45 | end 46 | 47 | def greet(user) 48 | Success("Hello, #{user[:name]}") 49 | end 50 | end.new 51 | end 52 | 53 | it_behaves_like "class using both dry-matcher and dry-monads Do notation" 54 | end 55 | 56 | describe "calling bind block explicitly" do 57 | let(:operation) do 58 | Class.new do 59 | include Dry::Monads::Result::Mixin 60 | include Dry::Monads::Do 61 | include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) 62 | 63 | def call(user, &bind) 64 | user = bind.(validate(user)) 65 | greeting = bind.(greet(user)) 66 | 67 | Success(greeting) 68 | end 69 | 70 | private 71 | 72 | def validate(user) 73 | user[:email] ? Success(user) : Failure(:no_email) 74 | end 75 | 76 | def greet(user) 77 | Success("Hello, #{user[:name]}") 78 | end 79 | end.new 80 | end 81 | 82 | it_behaves_like "class using both dry-matcher and dry-monads Do notation" 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /docsite/source/index.html.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Expressive match API for operating on computation results 4 | layout: gem-single 5 | type: gem 6 | name: dry-matcher 7 | sections: 8 | - class-enhancement 9 | - result-matcher 10 | --- 11 | 12 | dry-matcher offers flexible, expressive pattern matching for Ruby. 13 | 14 | You can build your own matcher or use the out-of-the-box support for matching on [dry-monads](/gems/dry-monads) `Result` values. 15 | 16 | ## Building a matcher 17 | 18 | To build your own matcher, create a series of "case" objects with their own resolving logic. First argument of the case block is the value to match, second argument is the list of patterns (see below). The block must either return the result or `Dry::Matcher::Undefined` if the value has no match. The latter signals dry-matcher to try the next case. 19 | 20 | ```ruby 21 | require "dry-matcher" 22 | 23 | # Match `[:ok, some_value]` for success 24 | success_case = Dry::Matcher::Case.new do |(code, value), _| 25 | if code.equal?(:ok) 26 | value 27 | else 28 | # this is a constant from dry/core/constants 29 | Dry::Matcher::Undefined 30 | end 31 | end 32 | 33 | # Match `[:err, some_error_code, some_value]` for failure 34 | failure_case = Dry::Matcher::Case.new do |(code, value), patterns| 35 | if code.equal?(:err) && (patterns.empty? || patterns.include?(value)) 36 | value 37 | else 38 | Dry::Matcher::Undefined 39 | end 40 | end 41 | 42 | # Build the matcher 43 | matcher = Dry::Matcher.new(success: success_case, failure: failure_case) 44 | ``` 45 | 46 | Then use these cases as part of an API to match on results: 47 | 48 | ```ruby 49 | my_success = [:ok, "success!"] 50 | 51 | result = matcher.(my_success) do |m| 52 | m.success do |v| 53 | "Yay: #{v}" 54 | end 55 | 56 | # :not_found and :lost are patterns 57 | m.failure :not_found, :lost do |v| 58 | "Oops, not found: #{v}" 59 | end 60 | 61 | m.failure do |v| 62 | "Boo: #{v}" 63 | end 64 | end 65 | 66 | result # => "Yay: success!" 67 | ``` 68 | 69 | Cases are executed in order. The first match wins and halts subsequent matching. 70 | 71 | ```ruby 72 | my_failure = [:err, :not_found, "missing!"] 73 | 74 | result = matcher.(my_failure) do |m| 75 | m.success do |v| 76 | "Yay: #{v}" 77 | end 78 | 79 | m.failure :not_found do |v| 80 | "Oops, not found: #{v}" 81 | end 82 | 83 | m.failure do |v| 84 | "Boo: #{v}" 85 | end 86 | end 87 | 88 | result # => "Oops, not found: missing!" 89 | ``` 90 | -------------------------------------------------------------------------------- /spec/integration/matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/monads" 4 | 5 | RSpec.describe Dry::Matcher do 6 | context "with match cases provided" do 7 | let(:success_case) do 8 | Dry::Matcher::Case.new( 9 | match: -> result { result.success? }, 10 | resolve: -> result { result.value! } 11 | ) 12 | end 13 | 14 | let(:failure_case) do 15 | Dry::Matcher::Case.new( 16 | match: -> result { result.failure? }, 17 | resolve: -> result { result.failure } 18 | ) 19 | end 20 | 21 | let(:matcher) do 22 | Dry::Matcher.new( 23 | success: success_case, 24 | failure: failure_case 25 | ) 26 | end 27 | 28 | def call_match(input) 29 | matcher.(input) do |m| 30 | m.success do |v| 31 | "Success: #{v}" 32 | end 33 | 34 | m.failure do |v| 35 | "Failure: #{v}" 36 | end 37 | end 38 | end 39 | 40 | it "matches on success" do 41 | input = Dry::Monads::Success("Yes!") 42 | expect(call_match(input)).to eq "Success: Yes!" 43 | end 44 | 45 | it "matches on failure" do 46 | input = Dry::Monads::Failure("No!") 47 | expect(call_match(input)).to eq "Failure: No!" 48 | end 49 | 50 | it "requires an exhaustive match" do 51 | input = Dry::Monads::Success("Yes!") 52 | 53 | expect { 54 | matcher.(input) do |m| 55 | m.success { |v| "Success: #{v}" } 56 | end 57 | }.to raise_error Dry::Matcher::NonExhaustiveMatchError 58 | end 59 | 60 | context "with patterns" do 61 | let(:success_case) do 62 | Dry::Matcher::Case.new( 63 | match: -> result { result.first == :ok }, 64 | resolve: -> result { result.last } 65 | ) 66 | end 67 | 68 | let(:failure_case) do 69 | Dry::Matcher::Case.new( 70 | match: -> result, failure_type { 71 | result.length == 3 && result[0] == :failure && result[1] == failure_type 72 | }, 73 | resolve: -> result { result.last } 74 | ) 75 | end 76 | 77 | def call_match(input) 78 | matcher.(input) do |m| 79 | m.success 80 | 81 | m.failure :my_error do |v| 82 | "Pattern-matched failure: #{v}" 83 | end 84 | end 85 | end 86 | 87 | it "matches using the provided pattern" do 88 | input = [:failure, :my_error, "No!"] 89 | expect(call_match(input)).to eq "Pattern-matched failure: No!" 90 | end 91 | 92 | it "doesn't match if the pattern doesn't match" do 93 | input = [:failure, :non_matching_error, "No!"] 94 | expect(call_match(input)).to be_nil 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/dry/matcher/result_matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/matcher" 4 | require "dry/matcher/match" 5 | 6 | module Dry 7 | class Matcher 8 | # Built-in {Matcher} ready to use with `Result` or `Try` monads from 9 | # [dry-monads](/gems/dry-monads) or any other compatible gems. 10 | # 11 | # Provides {Case}s for two matchers: 12 | # * `:success` matches `Dry::Monads::Result::Success` 13 | # and `Dry::Monads::Try::Value` (or any other monad that responds to 14 | # `#to_result` returning result monad that is `#success?`) 15 | # * `:failure` matches `Dry::Monads::Result::Failure` and 16 | # `Dry::Monads::Try::Error` (or any other monad that responds to 17 | # `#to_result` returning result monad that is `#failure?`) 18 | # 19 | # @return [Dry::Matcher] 20 | # 21 | # @example Usage with `dry-monads` 22 | # require 'dry/monads' 23 | # require 'dry/matcher/result_matcher' 24 | # 25 | # value = Dry::Monads::Result::Success.new('success!') 26 | # 27 | # Dry::Matcher::ResultMatcher.(value) do |m| 28 | # m.success do |v| 29 | # "Yay: #{v}" 30 | # end 31 | # 32 | # m.failure do |v| 33 | # "Boo: #{v}" 34 | # end 35 | # end #=> "Yay: success!" 36 | # 37 | # @example Usage with custom monad 38 | # require 'dry/matcher/result_matcher' 39 | # 40 | # class CustomBooleanMonad 41 | # def initialize(value); @value = value; end 42 | # attr_reader :value 43 | # alias_method :success?, :value 44 | # def failure?; !success?; end 45 | # def to_result; self; end 46 | # end 47 | # 48 | # value = CustomBooleanMonad.new(nil) 49 | # 50 | # Dry::Matcher::ResultMatcher.(value) do |m| 51 | # m.success { |v| "#{v.inspect} is truthy" } 52 | # m.failure { |v| "#{v.inspect} is falsey" } 53 | # end # => "nil is falsey" 54 | # 55 | # @example Usage with error codes 56 | # value = Dry::Monads::Result::Failure.new([:invalid, :reasons]) 57 | # 58 | # Dry::Matcher::ResultMatcher.(value) do |m| 59 | # m.success do |v| 60 | # "Yay: #{v}" 61 | # end 62 | # 63 | # m.failure(:not_found) do 64 | # "No such thing" 65 | # end 66 | # 67 | # m.failure(:invalid) do |_code, errors| 68 | # "Cannot be done: #{errors.inspect}" 69 | # end 70 | # end #=> "Cannot be done: :reasons" 71 | # 72 | ResultMatcher = Dry::Matcher.new( 73 | success: Case.new { |result, patterns| 74 | result = result.to_result 75 | 76 | if result.success? 77 | Dry::Matcher::PatternMatch.(result.value!, patterns) 78 | else 79 | Undefined 80 | end 81 | }, 82 | failure: Case.new { |result, patterns| 83 | result = result.to_result 84 | 85 | if result.failure? 86 | Dry::Matcher::PatternMatch.(result.failure, patterns) 87 | else 88 | Undefined 89 | end 90 | } 91 | ) 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/dry/matcher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/core/constants" 4 | require "dry/matcher/case" 5 | require "dry/matcher/evaluator" 6 | 7 | module Dry 8 | # @see http://dry-rb.org/gems/dry-matcher 9 | class Matcher 10 | include Core::Constants 11 | 12 | RUBY2_KEYWORDS = respond_to?(:ruby2_keywords, true) 13 | 14 | # Generates a module containing pattern matching for methods listed in 15 | # `match_methods` argument with behavior defined by `with` matcher 16 | # 17 | # @param [] match_methods 18 | # @param [Dry::Matcher] with 19 | # @return [Module] 20 | # 21 | # @example Usage with `dry-monads` 22 | # class MonadicOperation 23 | # include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher) 24 | # 25 | # def call 26 | # Dry::Monads::Result::Success.new('Success') 27 | # end 28 | # end 29 | # 30 | # operation = MonadicOperation.new 31 | # 32 | # operation.call do |m| 33 | # m.success { |v| "#{v} was successful!"} 34 | # m.failure { |v| "#{v} has failed!"} 35 | # end #=> "Success was successful" 36 | def self.for(*match_methods, with:) 37 | matcher = with 38 | 39 | matchers_mod = Module.new do 40 | match_methods.each do |match_method| 41 | define_method(match_method) do |*args, &block| 42 | result = super(*args) 43 | 44 | if block 45 | matcher.(result, &block) 46 | else 47 | result 48 | end 49 | end 50 | ruby2_keywords(match_method) if RUBY2_KEYWORDS 51 | end 52 | end 53 | 54 | Module.new do 55 | const_set :Matchers, matchers_mod 56 | 57 | def self.included(klass) 58 | klass.prepend const_get(:Matchers) 59 | end 60 | end 61 | end 62 | 63 | # @return [Hash{Symbol => Case}] 64 | attr_reader :cases 65 | 66 | # @param [Hash{Symbol => Case}] cases 67 | def initialize(cases = {}) 68 | @cases = cases 69 | end 70 | 71 | # Evaluate {#cases}' matchers and returns a result of block given to 72 | # corresponding case matcher 73 | # 74 | # @param [Object] result value that would be tested for matches with given {#cases} 75 | # @param [Object] block 76 | # @yieldparam [Evaluator] m 77 | # @return [Object] value returned from the block given to method called 78 | # after matched pattern 79 | # 80 | # @example Usage with `dry-monads` 81 | # require 'dry/monads' 82 | # require 'dry/matcher/result_matcher' 83 | # 84 | # value = Dry::Monads::Result::Failure.new('failure!') 85 | # 86 | # Dry::Matcher::ResultMatcher.(value) do |m| 87 | # m.success { |v| "Yay: #{v}" } 88 | # m.failure { |v| "Boo: #{v}" } 89 | # end #=> "Boo: failure!" 90 | def call(result, &block) 91 | Evaluator.new(result, cases).call(&block) 92 | end 93 | 94 | # Shortcut for Dry::Matcher.for(..., with: matcher) 95 | # 96 | # @param [Array[Symbol]] 97 | # 98 | # @return [Module] 99 | def for(*methods) 100 | self.class.for(*methods, with: self) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file is synced from hanakai-rb/repo-sync 2 | 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: ["main", "release-*", "ci/*"] 8 | tags: ["v*"] 9 | pull_request: 10 | branches: ["main", "release-*"] 11 | schedule: 12 | - cron: "30 4 * * *" 13 | 14 | jobs: 15 | tests: 16 | name: Tests (Ruby ${{ matrix.ruby }}) 17 | permissions: 18 | pull-requests: write 19 | runs-on: ubuntu-latest 20 | continue-on-error: ${{ matrix.optional || false }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | ruby: 25 | - "3.4" 26 | - "3.3" 27 | - "3.2" 28 | include: 29 | - ruby: "3.4" 30 | coverage: "true" 31 | - ruby: "jruby" 32 | optional: true 33 | - ruby: "4.0.0-preview2" 34 | optional: true 35 | env: 36 | COVERAGE: ${{ matrix.coverage }} 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | - name: Install package dependencies 41 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 42 | - name: Set up Ruby 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{ matrix.ruby }} 46 | bundler-cache: true 47 | - name: Run all tests 48 | id: test 49 | run: | 50 | status=0 51 | bundle exec rake || status=$? 52 | if [ ${status} -ne 0 ] && [ "${{ matrix.optional }}" == "true" ]; then 53 | echo "::warning::Optional matrix job failed." 54 | echo "optional_fail=true" >> "${GITHUB_OUTPUT}" 55 | echo "optional_fail_status=${status}" >> "${GITHUB_OUTPUT}" 56 | exit 0 # Ignore error here to keep the green checkmark 57 | fi 58 | exit ${status} 59 | - name: Add comment for optional failures 60 | uses: thollander/actions-comment-pull-request@v3 61 | if: ${{ matrix.optional && github.event.pull_request }} 62 | with: 63 | comment-tag: "${{ matrix.ruby }}-optional-failure-notice" 64 | message: | 65 | ℹ️ Optional job failed: Ruby ${{ matrix.ruby }} 66 | mode: ${{ steps.test.outputs.optional_fail == 'true' && 'upsert' || 'delete' }} 67 | 68 | workflow-keepalive: 69 | if: github.event_name == 'schedule' 70 | runs-on: ubuntu-latest 71 | permissions: 72 | actions: write 73 | steps: 74 | - uses: liskin/gh-workflow-keepalive@v1 75 | 76 | release: 77 | runs-on: ubuntu-latest 78 | if: github.ref_type == 'tag' 79 | needs: tests 80 | env: 81 | GITHUB_LOGIN: dry-bot 82 | GITHUB_TOKEN: ${{secrets.GH_PAT}} 83 | steps: 84 | - uses: actions/checkout@v3 85 | - name: Install package dependencies 86 | run: "[ -e $APT_DEPS ] || sudo apt-get install -y --no-install-recommends $APT_DEPS" 87 | - name: Set up Ruby 88 | uses: ruby/setup-ruby@v1 89 | with: 90 | ruby-version: 3.3 91 | - name: Install dependencies 92 | run: gem install ossy --no-document 93 | - name: Trigger release workflow 94 | run: | 95 | tag=$(echo $GITHUB_REF | cut -d / -f 3) 96 | ossy gh w dry-rb/devtools release --payload "{\"tag\":\"$tag\",\"sha\":\"${{github.sha}}\",\"tag_creator\":\"$GITHUB_ACTOR\",\"repo\":\"$GITHUB_REPOSITORY\",\"repo_name\":\"${{github.event.repository.name}}\"}" 97 | 98 | 99 | -------------------------------------------------------------------------------- /spec/integration/maybe_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | require "dry/monads" 5 | require "dry/matcher/maybe_matcher" 6 | 7 | RSpec.describe "Dry::Matcher::MaybeMatcher" do 8 | extend Dry::Monads[:maybe] 9 | include Dry::Monads[:maybe] 10 | 11 | before { Object.send(:remove_const, :Operation) if defined? Operation } 12 | 13 | def self.prepare_expectations(matches) 14 | matches.each do |value, matched| 15 | context "Matching #{value}" do 16 | let(:result) { value } 17 | 18 | it { is_expected.to eql(matched) } 19 | end 20 | end 21 | end 22 | 23 | describe "external matching" do 24 | subject do 25 | Dry::Matcher::MaybeMatcher.(result) do |m| 26 | m.some do |v| 27 | "Matched some: #{v}" 28 | end 29 | 30 | m.none do 31 | "Matched none" 32 | end 33 | end 34 | end 35 | 36 | prepare_expectations( 37 | Some("a success") => "Matched some: a success", 38 | None() => "Matched none" 39 | ) 40 | end 41 | 42 | context "multiple branch matching" do 43 | subject do 44 | Dry::Matcher::MaybeMatcher.(result) do |on| 45 | on.some(:a) { "Matched specific success: :a" } 46 | on.some(:b) { "Matched specific success: :b" } 47 | on.some { |v| "Matched general success: #{v}" } 48 | on.none { "Matched none" } 49 | end 50 | end 51 | 52 | prepare_expectations( 53 | Some(:a) => "Matched specific success: :a", 54 | Some(:b) => "Matched specific success: :b", 55 | Some("a success") => "Matched general success: a success", 56 | None() => "Matched none" 57 | ) 58 | end 59 | 60 | context "using ===" do 61 | subject do 62 | Dry::Matcher::MaybeMatcher.(result) do |on| 63 | on.some(/done/) { |s| "Matched string by pattern: #{s.inspect}" } 64 | on.some(String) { |s| "Matched string success: #{s.inspect}" } 65 | on.some(Integer) { |n| "Matched integer success: #{n}" } 66 | on.some(Date, Time) { |t| "Matched date success: #{t.strftime("%Y-%m-%d")}" } 67 | on.some { |v| "Matched general success: #{v}" } 68 | on.none { "Matched none" } 69 | end 70 | end 71 | 72 | prepare_expectations( 73 | Some("nicely done") => 'Matched string by pattern: "nicely done"', 74 | Some("yay") => 'Matched string success: "yay"', 75 | Some(3) => "Matched integer success: 3", 76 | Some(Date.new(2019, 7, 13)) => "Matched date success: 2019-07-13", 77 | Some(Time.new(2019, 7, 13)) => "Matched date success: 2019-07-13", 78 | None() => "Matched none" 79 | ) 80 | end 81 | 82 | context "matching tuples using codes" do 83 | subject do 84 | Dry::Matcher::MaybeMatcher.(result) do |on| 85 | on.some(:created) { |code, s| "Matched #{code.inspect} by code: #{s.inspect}" } 86 | on.some(:updated) { |_, s, v| "Matched :updated by code: #{s.inspect}, #{v.inspect}" } 87 | on.some(:deleted) { |_, s| "Matched :deleted by code: #{s.inspect}" } 88 | on.some(Symbol) { |sym, s| "Matched #{sym.inspect} by Symbol: #{s.inspect}" } 89 | on.some(200...300) { |status, _, body| "Matched #{status} body: #{body}" } 90 | on.some { |v| "Matched general success: #{v.inspect}" } 91 | on.none { "Matched none" } 92 | end 93 | end 94 | 95 | prepare_expectations( 96 | Some([:created, 5]) => "Matched :created by code: 5", 97 | Some([:updated, 6, 7]) => "Matched :updated by code: 6, 7", 98 | Some([:deleted, 8, 9]) => "Matched :deleted by code: 8", 99 | Some([:else, 10, 11]) => "Matched :else by Symbol: 10", 100 | Some([201, {}, "done!"]) => "Matched 201 body: done!", 101 | Some(["complete"]) => 'Matched general success: ["complete"]', 102 | None() => "Matched none" 103 | ) 104 | end 105 | 106 | context "with .for" do 107 | let(:operation) do 108 | class Operation 109 | include Dry::Matcher::MaybeMatcher.for(:perform) 110 | 111 | def perform(value) 112 | value 113 | end 114 | end 115 | Operation.new 116 | end 117 | 118 | context "using with methods" do 119 | def match(value) 120 | operation.perform(value) do |m| 121 | m.some { |v| "success: #{v}" } 122 | m.none { "none" } 123 | end 124 | end 125 | 126 | it "builds a wrapping module" do 127 | expect(match(Some(:foo))).to eql("success: foo") 128 | expect(match(None())).to eql("none") 129 | end 130 | end 131 | 132 | context "with keyword arguments" do 133 | let(:operation) do 134 | class Operation 135 | include Dry::Matcher::MaybeMatcher.for(:perform) 136 | 137 | def perform(value:) 138 | value 139 | end 140 | end 141 | Operation.new 142 | end 143 | 144 | it "works without a warning" do 145 | result = operation.perform(value: Some(1)) do |m| 146 | m.some { |v| v } 147 | m.none { raise } 148 | end 149 | 150 | expect(result).to be(1) 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | team at dry-rb.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /spec/integration/result_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | require "dry/monads" 5 | require "dry/matcher/result_matcher" 6 | 7 | RSpec.describe "Dry::Matcher::ResultMatcher" do 8 | extend Dry::Monads[:result, :try] 9 | include Dry::Monads[:result, :try] 10 | 11 | before { Object.send(:remove_const, :Operation) if defined? Operation } 12 | 13 | def self.prepare_expectations(matches) 14 | matches.each do |value, matched| 15 | context "Matching #{value}" do 16 | let(:result) { value } 17 | 18 | it { is_expected.to eql(matched) } 19 | end 20 | end 21 | end 22 | 23 | describe "external matching" do 24 | subject do 25 | Dry::Matcher::ResultMatcher.(result) do |m| 26 | m.success do |v| 27 | "Matched success: #{v}" 28 | end 29 | 30 | m.failure do |v| 31 | "Matched failure: #{v}" 32 | end 33 | end 34 | end 35 | 36 | prepare_expectations( 37 | Success("a success") => "Matched success: a success", 38 | Failure("a failure") => "Matched failure: a failure", 39 | Try(StandardError) { "a success" } => "Matched success: a success", 40 | Try(StandardError) { raise("a failure") } => "Matched failure: a failure" 41 | ) 42 | end 43 | 44 | context "multiple branch matching" do 45 | subject do 46 | Dry::Matcher::ResultMatcher.(result) do |on| 47 | on.success(:a) { "Matched specific success: :a" } 48 | on.success(:b) { "Matched specific success: :b" } 49 | on.success { |v| "Matched general success: #{v}" } 50 | on.failure(:a) { "Matched specific failure: :a" } 51 | on.failure(:b) { "Matched specific failure: :b" } 52 | on.failure { |v| "Matched general failure: #{v}" } 53 | end 54 | end 55 | 56 | prepare_expectations( 57 | Success(:a) => "Matched specific success: :a", 58 | Success(:b) => "Matched specific success: :b", 59 | Success("a success") => "Matched general success: a success", 60 | Failure(:a) => "Matched specific failure: :a", 61 | Failure(:b) => "Matched specific failure: :b", 62 | Failure("a failure") => "Matched general failure: a failure" 63 | ) 64 | end 65 | 66 | context "using ===" do 67 | subject do 68 | Dry::Matcher::ResultMatcher.(result) do |on| 69 | on.success(/done/) { |s| "Matched string by pattern: #{s.inspect}" } 70 | on.success(String) { |s| "Matched string success: #{s.inspect}" } 71 | on.success(Integer) { |n| "Matched integer success: #{n}" } 72 | on.success(Date, Time) { |t| "Matched date success: #{t.strftime("%Y-%m-%d")}" } 73 | on.success { |v| "Matched general success: #{v}" } 74 | on.failure(Integer) { |n| "Matched integer failure: #{n}" } 75 | on.failure { |v| "Matched general failure: #{v}" } 76 | end 77 | end 78 | 79 | prepare_expectations( 80 | Success("nicely done") => 'Matched string by pattern: "nicely done"', 81 | Success("yay") => 'Matched string success: "yay"', 82 | Success(3) => "Matched integer success: 3", 83 | Failure(3) => "Matched integer failure: 3", 84 | Success(Date.new(2019, 7, 13)) => "Matched date success: 2019-07-13", 85 | Success(Time.new(2019, 7, 13)) => "Matched date success: 2019-07-13" 86 | ) 87 | end 88 | 89 | context "matching tuples using codes" do 90 | subject do 91 | Dry::Matcher::ResultMatcher.(result) do |on| 92 | on.success(:created) { |code, s| "Matched #{code.inspect} by code: #{s.inspect}" } 93 | on.success(:updated) { |_, s, v| "Matched :updated by code: #{s.inspect}, #{v.inspect}" } 94 | on.success(:deleted) { |_, s| "Matched :deleted by code: #{s.inspect}" } 95 | on.success(Symbol) { |sym, s| "Matched #{sym.inspect} by Symbol: #{s.inspect}" } 96 | on.success(200...300) { |status, _, body| "Matched #{status} body: #{body}" } 97 | on.success { |v| "Matched general success: #{v.inspect}" } 98 | on.failure(:not_found) { |_, e| "Matched not found with #{e.inspect}" } 99 | on.failure("not_found") { |e| "Matched not found by string: #{e.inspect}" } 100 | on.failure { |v| "Matched general failure: #{v.inspect}" } 101 | end 102 | end 103 | 104 | prepare_expectations( 105 | Success([:created, 5]) => "Matched :created by code: 5", 106 | Success([:updated, 6, 7]) => "Matched :updated by code: 6, 7", 107 | Success([:deleted, 8, 9]) => "Matched :deleted by code: 8", 108 | Success([:else, 10, 11]) => "Matched :else by Symbol: 10", 109 | Success([201, {}, "done!"]) => "Matched 201 body: done!", 110 | Success(["complete"]) => 'Matched general success: ["complete"]', 111 | Failure(%i[not_found for_a_reason]) => "Matched not found with :for_a_reason", 112 | Failure(:other) => "Matched general failure: :other" 113 | ) 114 | end 115 | 116 | context "with .for" do 117 | let(:operation) do 118 | class Operation 119 | include Dry::Matcher::ResultMatcher.for(:perform) 120 | 121 | def perform(value) 122 | value 123 | end 124 | end 125 | Operation.new 126 | end 127 | 128 | context "using with methods" do 129 | def match(value) 130 | operation.perform(value) do |m| 131 | m.success { |v| "success: #{v}" } 132 | m.failure { |e| "failure: #{e}" } 133 | end 134 | end 135 | 136 | it "builds a wrapping module" do 137 | expect(match(Success(:foo))).to eql("success: foo") 138 | expect(match(Failure(:bar))).to eql("failure: bar") 139 | end 140 | end 141 | 142 | context "with keyword arguments" do 143 | let(:operation) do 144 | class Operation 145 | include Dry::Matcher::ResultMatcher.for(:perform) 146 | 147 | def perform(value:) 148 | value 149 | end 150 | end 151 | Operation.new 152 | end 153 | 154 | it "works without a warning" do 155 | result = operation.perform(value: Success(1)) do |m| 156 | m.success { |v| v } 157 | m.failure { raise } 158 | end 159 | 160 | expect(result).to be(1) 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Break Versioning](https://www.taoensso.com/break-versioning). 7 | 8 | ## [Unreleased] 9 | 10 | 11 | ## [1.0.0] - 2023-01-01 12 | 13 | 14 | ### Changed 15 | 16 | - Update dry-core dependency (via #34) (@pnomolos) 17 | 18 | [Compare v0.10.0...v1.0.0](https://github.com/dry-rb/dry-matcher/compare/v0.10.0...v1.0.0) 19 | 20 | ## [0.10.0] - 2022-11-16 21 | 22 | 23 | ### Added 24 | 25 | - MaybeMatcher for matching against Maybe values from dry-monads (@gabfssilva) in #33 26 | 27 | ### Changed 28 | 29 | - This version is compatible with recently released dry-rb dependencies (@flash-gordon) 30 | 31 | [Compare v0.9.0...v0.10.0](https://github.com/dry-rb/dry-matcher/compare/v0.9.0...v0.10.0) 32 | 33 | ## [0.9.0] - 2021-03-05 34 | 35 | 36 | ### Changed 37 | 38 | - Matcher evaluator is now a standard `Object` descendant (see #32) (@solnic) 39 | 40 | [Compare v0.8.3...v0.9.0](https://github.com/dry-rb/dry-matcher/compare/v0.8.3...v0.9.0) 41 | 42 | ## 0.8.3 2020-01-07 43 | 44 | 45 | ### Fixed 46 | 47 | - Delegation warnings about keyword arguments (flash-gordon) 48 | 49 | 50 | [Compare v0.8.2...v0.8.3](https://github.com/dry-rb/dry-matcher/compare/v0.8.2...v0.8.3) 51 | 52 | ## [0.8.2] - 2019-09-06 53 | 54 | 55 | ### Fixed 56 | 57 | - Minimal dry-core version set to 0.4.8 (flash-gordon) 58 | 59 | 60 | [Compare v0.8.1...v0.8.2](https://github.com/dry-rb/dry-matcher/compare/v0.8.1...v0.8.2) 61 | 62 | ## [0.8.1] - 2019-08-13 63 | 64 | 65 | ### Added 66 | 67 | - `Dry::Matcher#for` is a shortcut for `Dry::Matcher.for(..., with: matcher)` (flash-gordon) 68 | 69 | ```ruby 70 | require 'dry/matcher/result_matcher' 71 | 72 | class CreateUser 73 | include Dry::Matcher::ResultMatcher.for(:call) 74 | 75 | def call(...) 76 | # code returning an instance of Dry::Monads::Result 77 | end 78 | end 79 | ``` 80 | 81 | 82 | [Compare v0.8.0...v0.8.1](https://github.com/dry-rb/dry-matcher/compare/v0.8.0...v0.8.1) 83 | 84 | ## [0.8.0] - 2019-07-30 85 | 86 | 87 | ### Added 88 | 89 | - API for cases was changed to work with a single block instead of `match`/`resolve` combination (flash-gordon in [#23](https://github.com/dry-rb/dry-matcher/pull/23)): 90 | ```ruby 91 | Dry::Matcher::Case.new do |value, patterns| 92 | if patterns.include?(value) 93 | # value will be passed to the block 94 | value 95 | else 96 | # Undefined stands for no match 97 | Dry::Matcher::Undefined 98 | end 99 | end 100 | ``` 101 | - `ResultMatcher` now uses patterns for matching and matches against the first element if an array is passed (flash-gordon in [#24](https://github.com/dry-rb/dry-matcher/pull/24) and [#22](https://github.com/dry-rb/dry-matcher/pull/22) and michaelherold in [#21](https://github.com/dry-rb/dry-matcher/pull/21)) 102 | 103 | ```ruby 104 | value = Dry::Monads::Result::Failure.new([:invalid, :reasons]) 105 | 106 | Dry::Matcher::ResultMatcher.(value) do |m| 107 | m.success do |v| 108 | "Yay: #{v}" 109 | end 110 | 111 | m.failure(:not_found) do 112 | "No such thing" 113 | end 114 | 115 | m.failure(:invalid) do |_code, errors| 116 | "Cannot be done: #{errors.inspect}" 117 | end 118 | end #=> "Cannot be done: :reasons" 119 | ``` 120 | 121 | ### Changed 122 | 123 | - [BREAKING] Support for Ruby 2.3 was dropped as it's EOL 124 | 125 | [Compare v0.7.0...v0.8.0](https://github.com/dry-rb/dry-matcher/compare/v0.7.0...v0.8.0) 126 | 127 | ## [0.7.0] - 2018-01-11 128 | 129 | 130 | ### Changed 131 | 132 | - `EitherMatcher` was renamed to `ResultMatcher` according to match the rename of `Either` to `Result` in dry-monads 0.4.0. The previous name is still there for backward compatibility, we'll deprecate and drop it in furure releases (flash-gordon in [#13](https://github.com/dry-rb/dry-matcher/pull/13)) 133 | 134 | [Compare v0.6.0...v0.7.0](https://github.com/dry-rb/dry-matcher/compare/v0.6.0...v0.7.0) 135 | 136 | ## [0.6.0] - 2016-12-19 137 | 138 | 139 | ### Added 140 | 141 | - API documentation for most methods (alsemyonov in [#8](https://github.com/dry-rb/dry-matcher/pull/8)) 142 | 143 | ### Fixed 144 | 145 | - Fixed handling of calls to non-existent cases within a matcher block (timriley) 146 | 147 | ### Changed 148 | 149 | - 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)) 150 | - `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)) 151 | 152 | [Compare v0.5.0...v0.6.0](https://github.com/dry-rb/dry-matcher/compare/v0.5.0...v0.6.0) 153 | 154 | ## [0.5.0] - 2016-06-30 155 | 156 | 157 | ### Added 158 | 159 | - Added support for building custom matchers, with an any number of match cases, each offering their own matching and resolving logic. This is now the primary API for dry-matcher. (timriley) 160 | 161 | ```ruby 162 | # Match `[:ok, some_value]` for success 163 | success_case = Dry::Matcher::Case.new( 164 | match: -> value { value.first == :ok }, 165 | resolve: -> value { value.last } 166 | ) 167 | 168 | # Match `[:err, some_error_code, some_value]` for failure 169 | failure_case = Dry::Matcher::Case.new( 170 | match: -> value, *pattern { 171 | value[0] == :err && (pattern.any? ? pattern.include?(value[1]) : true) 172 | }, 173 | resolve: -> value { value.last } 174 | ) 175 | 176 | # Build the matcher 177 | matcher = Dry::Matcher.new(success: success_case, failure: failure_case) 178 | 179 | # Then put it to use 180 | my_success = [:ok, "success!"] 181 | 182 | result = matcher.(my_success) do |m| 183 | m.success do |v| 184 | "Yay: #{v}" 185 | end 186 | 187 | m.failure :not_found do |v| 188 | "Oops, not found: #{v}" 189 | end 190 | 191 | m.failure do |v| 192 | "Boo: #{v}" 193 | end 194 | end 195 | 196 | result # => "Yay: success!" 197 | ``` 198 | 199 | ### Changed 200 | 201 | - Renamed to `dry-matcher`, since this is now a flexible, general purpose pattern matching API. All components are now available under the `Dry::Matcher` namespace. (timriley) 202 | - `Dry::Matcher.for` requires a matcher object to be passed when being included in a class: 203 | 204 | ```ruby 205 | MyMatcher = Dry::Matcher.new(...) 206 | 207 | class MyOperation 208 | include Dry::Matcher.for(:call, with: MyMatcher) 209 | 210 | def call 211 | end 212 | end 213 | ``` 214 | - The previous `Dry::ResultMatcher.match` behaviour (for matching `Either` monads) has been moved to `Dry::Matcher::EitherMatcher.call` 215 | 216 | [Compare v0.4.0...v0.5.0](https://github.com/dry-rb/dry-matcher/compare/v0.4.0...v0.5.0) 217 | 218 | ## [0.4.0] - 2016-06-08 219 | 220 | 221 | ### Added 222 | 223 | - Support convertible result objects responding to `#to_either` (ttdonovan) 224 | 225 | ### Changed 226 | 227 | - Expect monads from [dry-monads](https://github.com/dry-rb/dry-monads) instead of [Kleisli](https://github.com/txus/kleisli) (ttdonovan) 228 | 229 | [Compare v0.3.0...v0.4.0](https://github.com/dry-rb/dry-matcher/compare/v0.3.0...v0.4.0) 230 | 231 | ## [0.3.0] - 2016-03-23 232 | 233 | 234 | ### Changed 235 | 236 | - Renamed to `dry-result_matcher`. Match results using `Dry::ResultMatcher.match` or extend your own classes with `Dry::ResultMatcher.for`. 237 | 238 | [Compare v0.2.0...v0.3.0](https://github.com/dry-rb/dry-matcher/compare/v0.2.0...v0.3.0) 239 | 240 | ## [0.2.0] - 2016-02-10 241 | 242 | 243 | ### Added 244 | 245 | - Added `EitherResultMatcher.for(*methods)` to return a module wrapping the specified methods (returning an `Either`) with the match block API. Example usage, in a class with a `#call` method: `include EitherResultMatcher.for(:call)`. 246 | 247 | 248 | [Compare v0.1.0...v0.2.0](https://github.com/dry-rb/dry-matcher/compare/v0.1.0...v0.2.0) 249 | 250 | ## [0.1.0] - 2015-12-03 251 | 252 | Initial release. 253 | --------------------------------------------------------------------------------