{"id":21502413,"url":"https://github.com/fixrb/matchi","last_synced_at":"2025-07-15T23:30:29.790Z","repository":{"id":27235672,"uuid":"30707259","full_name":"fixrb/matchi","owner":"fixrb","description":"Collection of expectation matchers 🤹","archived":false,"fork":false,"pushed_at":"2024-05-16T21:54:34.000Z","size":925,"stargazers_count":6,"open_issues_count":2,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-09-17T15:19:09.244Z","etag":null,"topics":["matcher","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fixrb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-02-12T14:55:59.000Z","updated_at":"2024-02-06T15:08:06.000Z","dependencies_parsed_at":"2022-07-18T10:39:28.297Z","dependency_job_id":null,"html_url":"https://github.com/fixrb/matchi","commit_stats":null,"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fixrb%2Fmatchi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fixrb%2Fmatchi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fixrb%2Fmatchi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fixrb%2Fmatchi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fixrb","download_url":"https://codeload.github.com/fixrb/matchi/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226077479,"owners_count":17570164,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["matcher","ruby"],"created_at":"2024-11-23T18:14:51.166Z","updated_at":"2025-07-15T23:30:29.564Z","avatar_url":"https://github.com/fixrb.png","language":"Ruby","readme":"# Matchi\n\n[![Version](https://img.shields.io/github/v/tag/fixrb/matchi?label=Version\u0026logo=github)](https://github.com/fixrb/matchi/tags)\n[![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/fixrb/matchi/main)\n[![License](https://img.shields.io/github/license/fixrb/matchi?label=License\u0026logo=github)](https://github.com/fixrb/matchi/raw/main/LICENSE.md)\n\nMatchi is a lightweight, framework-agnostic Ruby library that provides a comprehensive set of expectation matchers for elegant and secure testing. Its design focuses on simplicity, security, and extensibility.\n\n![A Rubyist juggling between Matchi letters](https://github.com/fixrb/matchi/raw/main/img/matchi.png)\n\n## Key Features\n\n- **Framework Agnostic**: Easily integrate with any Ruby testing framework\n- **Security-Focused Design**: Built with robust type checking for most matchers\n- **Simple Integration**: Minimal setup required to get started\n- **Extensible**: Create custom matchers with just a few lines of code\n- **Comprehensive**: Rich set of built-in matchers for common testing scenarios\n- **Well Documented**: Extensive documentation with clear examples and implementation details\n- **Thread Safe**: Immutable matchers design ensures thread safety in concurrent environments\n\n### Security Considerations for Predicate Matchers\n\nWhile most Matchi matchers are designed to resist type spoofing, predicate matchers (`Matchi::Predicate`) rely on Ruby's dynamic method dispatch system and can be vulnerable to method overriding:\n\n```ruby\n# Example of predicate matcher vulnerability:\nmatcher = Matchi::Predicate.new(:be_empty)\narray = []\n\n# Method overriding can defeat the matcher\ndef array.empty?\n  false\nend\n\nmatcher.match? { array } # =\u003e false (Even though array is empty!)\n```\n\nThis limitation is inherent to Ruby's dynamic nature when working with predicate methods. If your tests require strict security guarantees, consider using direct state verification matchers instead of predicate matchers.\n\n## What is a Matchi Matcher?\n\nA Matchi matcher is a simple Ruby object that follows a specific contract:\n\n1. **Core Interface**: Every matcher must implement a `match?` method that:\n   - Accepts a block as its only parameter\n   - Executes that block to get the actual value\n   - Returns a boolean indicating if the actual value matches the expected criteria\n\n2. **Optional Description**: Matchers can implement a `to_s` method that returns a human-readable description of the match criteria\n\nHere's the simplest possible matcher:\n\n```ruby\nmodule Matchi\n  class SimpleEqual\n    def initialize(expected)\n      @expected = expected\n    end\n\n    def match?\n      raise ArgumentError, \"a block must be provided\" unless block_given?\n\n      @expected == yield\n    end\n\n    def to_s\n      \"equal #{@expected.inspect}\"\n    end\n  end\nend\n\n# Usage:\nmatcher = Matchi::SimpleEqual.new(42)\nmatcher.match? { 42 }     # =\u003e true\nmatcher.match? { \"42\" }   # =\u003e false\nmatcher.to_s              # =\u003e \"equal 42\"\n```\n\nThis design provides several benefits:\n- **Lazy Evaluation**: The actual value is only computed when needed via the block\n- **Encapsulation**: Each matcher is a self-contained object with clear responsibilities\n- **Composability**: Matchers can be easily combined and reused\n- **Testability**: The contract is simple and easy to verify\n\n## Installation\n\nAdd to your Gemfile:\n\n```ruby\ngem \"matchi\"\n```\n\nOr install directly:\n\n```ruby\ngem install matchi\n```\n\n## Quick Start\n\n```ruby\nrequire \"matchi\"\n\n# Basic equality matching\nMatchi::Eq.new(\"hello\").match? { \"hello\" } # =\u003e true\n\n# Type checking\nMatchi::BeAKindOf.new(Numeric).match? { 42 }   # =\u003e true\nMatchi::BeAKindOf.new(String).match? { 42 }    # =\u003e false\n\n# State change verification\narray = []\nMatchi::Change.new(array, :length).by(2).match? { array.push(1, 2) } # =\u003e true\n```\n\n## Core Matchers\n\n### Value Comparison\n\n```ruby\n# Exact equality (eql?)\nMatchi::Eq.new(\"test\").match? { \"test\" } # =\u003e true\nMatchi::Eq.new([1, 2, 3]).match? { [1, 2, 3] } # =\u003e true\n\n# Object identity (equal?)\nsymbol = :test\nMatchi::Be.new(symbol).match? { symbol } # =\u003e true\nstring = \"test\"\nMatchi::Be.new(string).match? { string.dup } # =\u003e false\n```\n\n### Type Checking\n\n```ruby\n# Inheritance-aware type checking\nMatchi::BeAKindOf.new(Numeric).match? { 42.0 } # =\u003e true\nMatchi::BeAKindOf.new(Integer).match? { 42.0 } # =\u003e false\n\n# Exact type matching\nMatchi::BeAnInstanceOf.new(Float).match? { 42.0 } # =\u003e true\nMatchi::BeAnInstanceOf.new(Numeric).match? { 42.0 } # =\u003e false\n\n# Using class names as strings\nMatchi::BeAKindOf.new(\"Numeric\").match? { 42.0 } # =\u003e true\nMatchi::BeAnInstanceOf.new(\"Float\").match? { 42.0 } # =\u003e true\n```\n\n### State Changes\n\n```ruby\n# Verify exact changes\ncounter = 0\nMatchi::Change.new(counter, :to_i).by(5).match? { counter += 5 } # =\u003e true\n\n# Verify minimum changes\nMatchi::Change.new(counter, :to_i).by_at_least(2).match? { counter += 3 } # =\u003e true\n\n# Verify maximum changes\nMatchi::Change.new(counter, :to_i).by_at_most(5).match? { counter += 3 } # =\u003e true\n\n# Track value transitions\nstring = \"hello\"\nMatchi::Change.new(string, :to_s).from(\"hello\").to(\"HELLO\").match? { string.upcase! } # =\u003e true\n\n# Simple change detection\narray = []\nMatchi::Change.new(array, :length).match? { array \u003c\u003c 1 } # =\u003e true\n\n# Check final state only\ncounter = 0\nMatchi::Change.new(counter, :to_i).to(5).match? { counter = 5 } # =\u003e true\n```\n\n### Pattern Matching\n\n```ruby\n# Regular expressions\nMatchi::Match.new(/^test/).match? { \"test_string\" } # =\u003e true\nMatchi::Match.new(/^\\d{3}-\\d{2}$/).match? { \"123-45\" } # =\u003e true\n\n# Custom predicates with Satisfy\nMatchi::Satisfy.new { |x| x.positive? \u0026\u0026 x \u003c 10 }.match? { 5 } # =\u003e true\nMatchi::Satisfy.new { |arr| arr.all?(\u0026:even?) }.match? { [2, 4, 6] } # =\u003e true\n\n# Built-in predicates\nMatchi::Predicate.new(:be_empty).match? { [] } # =\u003e true\nMatchi::Predicate.new(:have_key, :name).match? { { name: \"Alice\" } } # =\u003e true\n```\n\n### Exception Handling\n\n```ruby\n# Verify raised exceptions\nMatchi::RaiseException.new(ArgumentError).match? { raise ArgumentError } # =\u003e true\n\n# Works with inheritance\nMatchi::RaiseException.new(StandardError).match? { raise ArgumentError } # =\u003e true\n\n# Using exception class names\nMatchi::RaiseException.new(\"ArgumentError\").match? { raise ArgumentError } # =\u003e true\n```\n\n### Numeric Comparisons\n\n```ruby\n# Delta comparisons\nMatchi::BeWithin.new(0.5).of(3.0).match? { 3.2 } # =\u003e true\nMatchi::BeWithin.new(2).of(10).match? { 9 } # =\u003e true\n```\n\n## Creating Custom Matchers\n\nCreating custom matchers is straightforward:\n\n```ruby\nmodule Matchi\n  class BePositive\n    def match?\n      yield.positive?\n    end\n\n    def to_s\n      \"be positive\"\n    end\n  end\nend\n\nmatcher = Matchi::BePositive.new\nmatcher.match? { 42 }  # =\u003e true\nmatcher.match? { -1 }  # =\u003e false\n```\n\n## Security Best Practices\n\n### Proper Value Comparison Order\n\nOne of the most critical aspects when implementing matchers is the order of comparison between expected and actual values. Always compare values in this order:\n\n```ruby\n# GOOD: Expected value controls the comparison\nexpected_value.eql?(actual_value)\n# BAD: Actual value controls the comparison\nactual_value.eql?(expected_value)\n```\n\n#### Why This Matters\n\nThe order is crucial because the object receiving the comparison method controls how the comparison is performed. When testing, the actual value might come from untrusted or malicious code that could override comparison methods:\n\n```ruby\n# Example of how comparison can be compromised\nclass MaliciousString\n  def eql?(other)\n    true  # Always returns true regardless of actual equality\n  end\n\n  def ==(other)\n    true  # Always returns true regardless of actual equality\n  end\nend\n\nactual = MaliciousString.new\nexpected = \"expected string\"\nactual.eql?(expected)      # =\u003e true (incorrect result!)\nexpected.eql?(actual)      # =\u003e false (correct result)\n```\n\nThis is why Matchi's built-in matchers are implemented with this security consideration in mind. For example, the `Eq` matcher:\n\n```ruby\n# Implementation in Matchi::Eq\ndef match?\n  @expected.eql?(yield) # Expected value controls the comparison\nend\n```\n\n## Extensions\n\n### matchi-fix\n\nThe [matchi-fix gem](https://rubygems.org/gems/matchi-fix) extends Matchi with support for testing against [Fix](https://github.com/fixrb/fix) specifications. It provides a seamless integration between Matchi's matcher interface and Fix's powerful specification system.\n\n```ruby\n# Add to your Gemfile\ngem \"matchi-fix\"\n```\n\nThis extension adds a `Fix` matcher that allows you to verify implementation conformance to Fix test specifications across different testing frameworks like Minitest and RSpec.\n\n## Versioning\n\nMatchi follows [Semantic Versioning 2.0](https://semver.org/).\n\n## License\n\nThe [gem](https://rubygems.org/gems/matchi) is available as open source under the terms of the [MIT License](https://github.com/fixrb/matchi/raw/main/LICENSE.md).\n\n## Sponsors\n\nThis project is sponsored by [Sashité](https://sashite.com/)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffixrb%2Fmatchi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffixrb%2Fmatchi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffixrb%2Fmatchi/lists"}