├── test ├── test_helper.exs └── recursive_match_test.exs ├── images └── screenshot.png ├── .travis.yml ├── .formatter.exs ├── .gitignore ├── LICENSE ├── config └── config.exs ├── mix.exs ├── README.md ├── mix.lock └── lib └── recursive_match.ex /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Apelsinka223/test_match/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - '1.10' 4 | otp_release: 5 | - '21.0' 6 | script: 7 | - "MIX_ENV=test mix do deps.get, test && mix compile && mix coveralls.travis" 8 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | locals_without_parens = [ 2 | match_r: 2, 3 | match_r: 3, 4 | assert_match: 2, 5 | assert_match: 3, 6 | refute_match: 2, 7 | refute_match: 3 8 | ] 9 | 10 | [ 11 | locals_without_parens: locals_without_parens, 12 | export: [ 13 | locals_without_parens: locals_without_parens 14 | ], 15 | inputs: ["*.{ex,exs}"] 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | test_match-*.tar 24 | 25 | /.idea/ 26 | /test_match.iml 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anastasiya Dyachenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :test_match, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:test_match, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TestMatch.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :test_match, 7 | version: "3.0.5", 8 | elixir: "~> 1.10", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | description: description(), 12 | package: package(), 13 | deps: deps(), 14 | test_coverage: [tool: ExCoveralls], 15 | preferred_cli_env: [ 16 | coveralls: :test, 17 | "coveralls.detail": :test, 18 | "coveralls.post": :test, 19 | "coveralls.html": :test, 20 | "coveralls.travis": :test 21 | ] 22 | ] 23 | end 24 | 25 | def application do 26 | [] 27 | end 28 | 29 | defp deps do 30 | [ 31 | {:ex_doc, "~> 0.19", only: :dev}, 32 | {:excoveralls, github: "parroty/excoveralls", only: :test}, 33 | {:inch_ex, "~> 2.0", only: :docs} 34 | ] 35 | end 36 | 37 | defp description() do 38 | "Recursive matching" 39 | end 40 | 41 | defp package() do 42 | [ 43 | files: ["lib", "mix.exs", "README*", "LICENSE*", ".formatter.exs"], 44 | maintainers: ["Anastasiya Dyachenko"], 45 | licenses: ["MIT"], 46 | links: %{"GitHub" => "https://github.com/Apelsinka223/test_match"} 47 | ] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Hex.pm](https://img.shields.io/hexpm/v/test_match.svg)](https://hex.pm/packages/test_match) 3 | [![Downloads](https://img.shields.io/hexpm/dt/test_match.svg)](https://hex.pm/packages/test_match) 4 | [![Build Status](https://travis-ci.org/Apelsinka223/test_match.svg?branch=master)](https://travis-ci.org/Apelsinka223/test_match) 5 | [![Coverage Status](https://coveralls.io/repos/github/Apelsinka223/test_match/badge.svg?branch=master)](https://coveralls.io/github/Apelsinka223/test_match?branch=master) 6 | [![Inline docs](http://inch-ci.org/github/Apelsinka223/test_match.svg?branch=master)](http://inch-ci.org/github/Apelsinka223/test_match) 7 | 8 | # RecursiveMatch 9 | 10 | Module for matching 11 | 12 | ### What difference between `Kernel.match?/2` and `RecursiveMatch.match_r/3`? 13 | When you use `Kernel.match?/2` 14 | * can't use functions as pattern 15 | * can't match not strict equality (only `===`, no `==`) 16 | 17 | `RecursiveMatch.match_r/3` allows you: 18 | * use functions as patterns 19 | * match not strictly (with option `strict: false`) 20 | * ignore order of lists item (with option `ignore_order: true`) 21 | 22 | ### What is for `assert_match/3` and `refute_match/3`? 23 | It is same as `assert RecursiveMatch.match_r`, but with detailed fail message. 24 | ExUnit has no special message for `match_r/3` and even no special message for `match?/2` is not detailed enough, it has no diff in fail message. 25 | 26 | `assert_match/3` provides diff in test fail message 27 | 28 | 29 | 30 | 31 | ## Installation 32 | 33 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 34 | by adding `test_match` to your list of dependencies in `mix.exs`: 35 | 36 | **Requires `elixir ~> 1.5`** 37 | 38 | ```elixir 39 | def deps do 40 | [ 41 | {:test_match, "~> 2.0"} 42 | ] 43 | end 44 | ``` 45 | 46 | ## Usage 47 | ```elixir 48 | defmodule YourModule do 49 | import RecursiveMatch 50 | 51 | def function1 do 52 | ... 53 | end 54 | 55 | def function2 do 56 | ... 57 | match_r 1, 2 58 | match_r a, b 59 | match_r :_, b 60 | match_r function1(), 1 61 | match_r [1, 2], [2, 1], ignore_order: true # true 62 | match_r 1, 1.0, strict: true # false 63 | match_r {1, 2}, {2, 1}, ignore_order: true # false, nope :) 64 | ... 65 | end 66 | end 67 | 68 | ``` 69 | 70 | ```elixir 71 | defmodule YourModuleTest do 72 | use ExUnit.Case 73 | import RecursiveMatch 74 | 75 | test "some test" do 76 | ... 77 | assert_match 1, 2 # false 78 | assert_match :_, b 79 | assert_match a, b 80 | assert_match [1, 2], [2, 1], ignore_order: true 81 | refute_match 1, 1.0 82 | assert_match 1, 1.0, strict: false 83 | refute_match a, c 84 | assert_match YourModule.function1(), 1 85 | ... 86 | end 87 | end 88 | 89 | ``` 90 | ## Options 91 | * `strict`: when `true` compare using `===`, when `false` compare using `==`, default `true` 92 | * `ignore_order`, when `true` - ignore order of items in lists, default `false` 93 | * `message`: Custom message on fail 94 | 95 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 96 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 97 | be found at [https://hexdocs.pm/test_match](https://hexdocs.pm/test_match). 98 | 99 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "excoveralls": {:git, "https://github.com/parroty/excoveralls.git", "55b18af73dafa778e9f05225a58d2f48605c6953", []}, 7 | "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 12 | "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, 13 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 16 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 18 | "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, 19 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 20 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, 21 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, 22 | } 23 | -------------------------------------------------------------------------------- /lib/recursive_match.ex: -------------------------------------------------------------------------------- 1 | defmodule RecursiveMatch do 2 | @moduledoc """ 3 | Recursive matching 4 | """ 5 | 6 | @doc """ 7 | Matches given value with pattern 8 | 9 | Returns `true` or `false` 10 | 11 | ## Parameters 12 | 13 | - pattern: Expected pattern (use `:_` instead of `_`) 14 | 15 | - tested: Tested value 16 | 17 | - options: Options 18 | 19 | * `strict`, when `true` compare using `===`, when `false` compare using `==`, default `true` 20 | 21 | * `ignore_order`, when `true` - ignore order of items in lists, default `false` 22 | 23 | ## Example 24 | 25 | iex> import RecursiveMatch 26 | RecursiveMatch 27 | iex> match_r %{a: 1}, %{a: 1, b: 2} 28 | true 29 | iex> match_r %{a: 1, b: 2}, %{a: 1} 30 | false 31 | """ 32 | @spec match_r(term, term, list | nil) :: boolean 33 | def match_r(pattern, tested, options \\ [strict: true]) 34 | 35 | def match_r(pattern, %{__struct__: _} = tested, options) do 36 | match_r(pattern, Map.from_struct(tested), options) 37 | end 38 | 39 | def match_r(:_, _, _), do: true 40 | 41 | def match_r(%{__struct__: _} = pattern, tested, options) do 42 | match_r(Map.from_struct(pattern), tested, options) 43 | end 44 | 45 | def match_r(pattern, tested, options) when is_tuple(tested) and is_tuple(pattern) do 46 | list_pattern = Tuple.to_list(pattern) 47 | list_tested = Tuple.to_list(tested) 48 | 49 | if Enum.count(list_pattern) == Enum.count(list_tested) do 50 | list_pattern 51 | |> Enum.zip(list_tested) 52 | |> Enum.all?(fn {pattern_item, tested_item} -> 53 | match_r(pattern_item, tested_item, options) 54 | end) 55 | else 56 | false 57 | end 58 | end 59 | 60 | def match_r(pattern, tested, options) when is_list(tested) and is_list(pattern) do 61 | if Enum.count(pattern) == Enum.count(tested) do 62 | if options[:ignore_order] == true do 63 | match_lists_ignore_order(pattern, tested, options) 64 | else 65 | pattern 66 | |> Enum.zip(tested) 67 | |> Enum.all?(fn {pattern_item, tested_item} -> 68 | match_r(pattern_item, tested_item, options) 69 | end) 70 | end 71 | else 72 | false 73 | end 74 | end 75 | 76 | def match_r(pattern, tested, options) when is_map(tested) and is_map(pattern) do 77 | strict = options[:strict] 78 | Enum.all?(pattern, fn 79 | {_key, :_} -> true 80 | 81 | {key, value} when is_map(value) or is_list(value) -> 82 | match_r(value, tested[key], options) 83 | 84 | {key, value} when strict === true -> 85 | Map.has_key?(tested, key) and value === Map.get(tested, key) 86 | 87 | {key, value} -> 88 | Map.has_key?(tested, key) and value == Map.get(tested, key) 89 | end) 90 | end 91 | 92 | def match_r(a, a, _), do: true 93 | def match_r(a, b, options) do 94 | case options[:strict] do 95 | true -> a === b 96 | nil -> a === b 97 | false -> a == b 98 | end 99 | end 100 | 101 | defp match_lists_ignore_order([], [], _), do: true 102 | 103 | defp match_lists_ignore_order([pattern | pattern_tail], tested, options) do 104 | case Enum.find_index(tested, fn t -> match_r(pattern, t, options) end) do 105 | nil -> 106 | false 107 | 108 | index -> 109 | tested_rest = List.delete_at(tested, index) 110 | match_lists_ignore_order(pattern_tail, tested_rest, options) 111 | end 112 | end 113 | 114 | def prepare_right_for_diff(pattern, tested, options) 115 | when is_struct(tested) and is_map(pattern) and not is_struct(pattern), 116 | do: prepare_right_for_diff(pattern, Map.from_struct(tested), options) 117 | 118 | def prepare_right_for_diff(%{__struct__: struct} = pattern, tested, options) 119 | when is_struct(tested) and is_struct(pattern) do 120 | pattern 121 | |> Map.from_struct() 122 | |> Enum.map(fn 123 | {_key, :_} -> 124 | tested 125 | 126 | {key, value} -> 127 | if Map.has_key?(pattern, key) do 128 | {key, prepare_right_for_diff(Map.get(pattern, key), value, options)} 129 | else 130 | nil 131 | end 132 | end) 133 | |> Enum.filter(& &1 && elem(&1, 1)) 134 | |> (& struct(struct, &1)).() 135 | end 136 | 137 | def prepare_right_for_diff(pattern, tested, options) 138 | when is_list(tested) and is_list(pattern) do 139 | if options[:ignore_order] === true do 140 | tested 141 | |> Enum.sort_by(&Enum.find_index(pattern, fn v -> v == &1 end), &<=/2) 142 | |> zip_with_rest(pattern) 143 | |> Enum.map(fn {tested, pattern} -> 144 | prepare_right_for_diff(pattern, tested, options) 145 | end) 146 | |> Enum.filter(& &1 != :zip_nil) 147 | else 148 | tested 149 | |> zip_with_rest(pattern) 150 | |> Enum.map(fn {tested, pattern} -> 151 | prepare_right_for_diff(pattern, tested, options) 152 | end) 153 | |> Enum.filter(& &1 != :zip_nil) 154 | end 155 | end 156 | 157 | def prepare_right_for_diff(pattern, tested, options) 158 | when is_map(tested) and is_map(pattern) do 159 | tested 160 | |> filter_tested(pattern) 161 | |> Enum.map(fn 162 | {_key, :_} -> 163 | :_ 164 | 165 | {key, value} -> 166 | {key, prepare_right_for_diff(Map.get(pattern, key), value, options)} 167 | end) 168 | |> Map.new() 169 | end 170 | 171 | def prepare_right_for_diff(pattern, tested, options) 172 | when is_tuple(pattern) and is_tuple(tested) do 173 | 174 | list_pattern = Tuple.to_list(pattern) 175 | list_tested = Tuple.to_list(tested) 176 | 177 | list_tested 178 | |> zip_with_rest(list_pattern) 179 | |> Enum.map(fn {tested, pattern} -> 180 | prepare_right_for_diff(pattern, tested, options) 181 | end) 182 | |> Enum.filter(& &1 != :zip_nil) 183 | |> List.to_tuple() 184 | end 185 | 186 | def prepare_right_for_diff(_pattern, tested, _options), do: tested 187 | 188 | defp filter_tested(tested, pattern) do 189 | if list_intersection(Map.keys(tested), Map.keys(pattern)) == [] do 190 | tested 191 | else 192 | Map.take(tested, Map.keys(pattern)) 193 | end 194 | end 195 | 196 | defp list_intersection(a, b), do: a -- (a -- b) 197 | 198 | def prepare_left_for_diff(pattern, tested, options) 199 | when is_struct(pattern) and is_map(tested) and not is_struct(tested), 200 | do: prepare_left_for_diff(Map.from_struct(pattern), tested, options) 201 | 202 | def prepare_left_for_diff(%{__struct__: struct} = pattern, tested, options) 203 | when is_struct(tested) and is_struct(pattern) do 204 | pattern 205 | |> Map.from_struct 206 | |> Enum.map(fn 207 | {key, :_} -> 208 | {key, Map.get(tested, key)} 209 | 210 | {key, value} -> 211 | {key, prepare_left_for_diff(value, Map.get(tested, key), options)} 212 | end) 213 | |> Map.new() 214 | |> (& struct(struct, &1)).() 215 | end 216 | 217 | def prepare_left_for_diff(pattern, tested, options) 218 | when is_list(tested) and is_list(pattern) do 219 | pattern 220 | |> zip_with_rest(tested) 221 | |> Enum.map(fn {pattern, tested} -> 222 | prepare_left_for_diff(pattern, tested, options) 223 | end) 224 | |> Enum.filter(& &1 != :zip_nil) 225 | end 226 | 227 | def prepare_left_for_diff(pattern, tested, options) 228 | when is_map(tested) and is_map(pattern) do 229 | pattern 230 | |> Enum.map(fn 231 | {key, :_} -> 232 | {key, Map.get(tested, key)} 233 | 234 | {key, value} -> 235 | {key, prepare_left_for_diff(value, Map.get(tested, key), options)} 236 | end) 237 | |> Map.new() 238 | end 239 | 240 | def prepare_left_for_diff(:_, tested, _options), do: tested 241 | def prepare_left_for_diff(pattern, _tested, _options), do: pattern 242 | 243 | defp zip_with_rest(a, b) do 244 | if length(a) > length(b) do 245 | Enum.reduce(a, {[], b}, fn 246 | a_i, {acc, [b_i | b_rest]} -> 247 | {[{a_i, b_i} | acc], b_rest} 248 | 249 | a_i, {acc, []} -> 250 | {[{a_i, :zip_nil} | acc], []} 251 | end) 252 | else 253 | Enum.reduce(b, {[], a}, fn 254 | b_i, {acc, [a_i | a_rest]} -> 255 | {[{a_i, b_i} | acc], a_rest} 256 | 257 | b_i, {acc, []} -> 258 | {[{:zip_nil, b_i} | acc], []} 259 | end) 260 | end 261 | |> elem(0) 262 | |> Enum.reverse() 263 | end 264 | 265 | @doc """ 266 | Matches given value with pattern 267 | 268 | Returns `true` or raises `ExUnit.AssertionError` 269 | 270 | ## Parameters 271 | 272 | - pattern: Expected pattern (use `:_` instead of `_`) 273 | 274 | - tested: Tested value 275 | 276 | - options: Options 277 | 278 | * strict: when `true` compare using `===`, when `false` compare using `==`, default `true` 279 | 280 | * `ignore_order`, when `true` - ignore order of items in lists, default `false` 281 | 282 | * message: Custom message on fail 283 | 284 | ## Example 285 | 286 | The assertion 287 | 288 | assert_match %{a: 1}, %{a: 1, b: 2} 289 | 290 | will match, 291 | 292 | assert_match %{a: 1, b: 2}, %{a: 1} 293 | 294 | will fail with the message: 295 | 296 | match (assert_match) failed 297 | left: %{a: 1, b: 2}, 298 | right: %{a: 1} 299 | """ 300 | 301 | @spec assert_match(term, term, list | nil) :: boolean 302 | defmacro assert_match(left, right, options \\ [strict: true]) do 303 | message = options[:message] || "match (assert_match) failed" 304 | quote do 305 | 306 | right = unquote(right) 307 | left = unquote(left) 308 | message = unquote(message) 309 | options = unquote(options) 310 | 311 | prepared_right = prepare_right_for_diff(left, right, options) 312 | prepared_left = prepare_left_for_diff(left, right, options) 313 | 314 | ExUnit.Assertions.assert match_r(left, right, options), 315 | right: prepared_right, 316 | left: prepared_left, 317 | message: message 318 | end 319 | end 320 | 321 | @doc """ 322 | Matches given value with pattern 323 | 324 | Returns `true` or raises `ExUnit.AssertionError` 325 | 326 | ## Parameters 327 | 328 | - pattern: Expected pattern (use `:_` instead of `_`) 329 | 330 | - tested: Tested value 331 | 332 | - options: Options 333 | 334 | * strict: when `true` compare using `===`, when `false` compare using `==`, default `true` 335 | 336 | * `ignore_order`, when `true` - ignore order of items in lists, default `false` 337 | 338 | * message: Custom message on fail 339 | 340 | 341 | ## Example 342 | 343 | The assertion 344 | 345 | assert_match %{a: 1}, %{a: 1, b: 2} 346 | 347 | will match, 348 | 349 | assert_match %{a: 1, b: 2}, %{a: 1} 350 | 351 | will fail with the message: 352 | 353 | match (refute_match) succeeded, but should have failed 354 | """ 355 | @spec refute_match(term, term, list | nil) :: boolean 356 | defmacro refute_match(left, right, options \\ [strict: true]) do 357 | message = options[:message] || "match (refute_match) succeeded, but should have failed" 358 | quote do 359 | right = unquote(right) 360 | left = unquote(left) 361 | message = unquote(message) 362 | options = unquote(options) 363 | 364 | ExUnit.Assertions.refute match_r(left, right, options), message: message 365 | end 366 | end 367 | 368 | defmacro __using__([]) do 369 | quote do 370 | import unquote(__MODULE__) 371 | end 372 | end 373 | end 374 | -------------------------------------------------------------------------------- /test/recursive_match_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RecursiveMatchTest do 2 | use ExUnit.Case 3 | doctest RecursiveMatch 4 | import RecursiveMatch 5 | 6 | defp fun_a(), do: 1 7 | defp fun_b(), do: 2 8 | defp fun_c(), do: 1 9 | 10 | defp counter(table), do: :ets.update_counter(table, :counter, 1, {1, 0}) 11 | 12 | defmodule TestStruct do 13 | defstruct [:field1, :field2] 14 | end 15 | 16 | describe "match_r/3" do 17 | test "values" do 18 | assert match_r 1, 1 19 | refute match_r 1, 2 20 | end 21 | 22 | test "variables" do 23 | a = 1 24 | b = 2 25 | c = 1 26 | 27 | assert match_r a, c 28 | refute match_r a, b 29 | end 30 | 31 | test "functions" do 32 | assert match_r fun_a(), fun_c() 33 | refute match_r fun_a(), fun_b() 34 | end 35 | 36 | test "functions calls once" do 37 | table = :ets.new(__MODULE__, [:set, :named_table]) 38 | 39 | assert match_r 1, counter(table) 40 | refute match_r 1, counter(table) 41 | 42 | # called 2 times 43 | assert :ets.lookup_element(table, :counter, 2) == 2 44 | end 45 | 46 | test "maps" do 47 | a = %{a: 1, c: %{a: 1}} 48 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 49 | c = %{b: 2} 50 | 51 | assert match_r a, b 52 | refute match_r b, a 53 | refute match_r a, c 54 | refute match_r c, a 55 | assert match_r a.a, b.a 56 | refute match_r %{c: nil}, %{} 57 | end 58 | 59 | test "structs" do 60 | a = %TestStruct{field1: 1, field2: %{a: 1}} 61 | b = %TestStruct{field1: 1, field2: %{a: 1, b: 2}} 62 | c = %TestStruct{field1: 1} 63 | 64 | assert match_r a, b 65 | refute match_r b, a 66 | assert match_r %{field1: 1}, a 67 | refute match_r c, a 68 | refute match_r a, c 69 | assert match_r a.field1, b.field1 70 | end 71 | 72 | test "tuples" do 73 | a = {1, 2} 74 | b = {1, 2} 75 | c = {2, 1} 76 | 77 | assert match_r a, b 78 | assert match_r b, a 79 | refute match_r a, c 80 | refute match_r c, a 81 | 82 | # We cannot ignore tuples order 83 | refute match_r {1, 2}, {2, 1}, ignore_order: true 84 | refute match_r {1, 2}, {2, 1} 85 | assert match_r [{1, 2}, {2, 1}], [{2, 1}, {1, 2}], ignore_order: true 86 | refute match_r [{1, 2}, {2, 1}], [{2, 1}, {1, 2}] 87 | end 88 | 89 | test "lists" do 90 | a = [1, 2] 91 | b = [1, 2] 92 | c = [2, 1] 93 | d = [4] 94 | e = [1] 95 | f = [%{a: 1, b: [2, 1, 3]}, %{a: 2, b: [2, 1]}] 96 | g = [%{b: [1, 2]}, %{b: [1, 2, 3]}] 97 | 98 | assert match_r [:_], ["a"] 99 | refute match_r [:_], ["a", "b"] 100 | assert match_r b, a 101 | refute match_r a, c 102 | refute match_r c, a 103 | refute match_r d, a 104 | refute match_r a, e 105 | refute match_r e, a 106 | assert match_r a, c, ignore_order: true 107 | assert match_r c, a, ignore_order: true 108 | refute match_r a, d, ignore_order: true 109 | refute match_r d, a, ignore_order: true 110 | refute match_r a, e, ignore_order: true 111 | refute match_r e, a, ignore_order: true 112 | assert match_r g, f, ignore_order: true 113 | refute match_r g, f 114 | refute match_r f, g, ignore_order: true 115 | end 116 | 117 | test "keyword lists" do 118 | a = [a: 1] 119 | b = [a: 1] 120 | c = [b: 2, a: 1] 121 | d = [d: 1] 122 | e = [a: 1, b: 2] 123 | 124 | assert match_r a, b 125 | assert match_r b, a 126 | refute match_r a, c 127 | refute match_r c, a 128 | refute match_r d, a 129 | refute match_r a, e 130 | refute match_r e, a 131 | assert match_r c, e, ignore_order: true 132 | assert match_r e, c, ignore_order: true 133 | refute match_r a, c, ignore_order: true 134 | refute match_r c, a, ignore_order: true 135 | end 136 | 137 | test "strict false" do 138 | assert match_r 1, 1.0, strict: false 139 | assert match_r 1.0, 1, strict: false 140 | 141 | a = %{a: 1.0, c: %{a: 1.0}} 142 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 143 | c = %{b: 2} 144 | 145 | assert match_r a, b, strict: false 146 | refute match_r b, a, strict: false 147 | refute match_r a, c, strict: false 148 | refute match_r c, a, strict: false 149 | 150 | d = 1.0 151 | e = 1 152 | f = 2 153 | 154 | assert match_r d, e, strict: false 155 | assert match_r e, d, strict: false 156 | refute match_r d, f, strict: false 157 | end 158 | 159 | test "strict true" do 160 | assert match_r 1.0, 1.0, strict: true 161 | assert match_r 1, 1, strict: true 162 | refute match_r 1, 1.0, strict: true 163 | refute match_r 1.0, 1, strict: true 164 | 165 | a = %{a: 1.0, c: %{a: 1.0}} 166 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 167 | c = %{b: 2} 168 | 169 | refute match_r a, b, strict: true 170 | refute match_r b, a, strict: true 171 | refute match_r a, c, strict: true 172 | refute match_r c, a, strict: true 173 | 174 | d = 1.0 175 | e = 1 176 | f = 2 177 | 178 | refute match_r d, e, strict: true 179 | refute match_r e, d, strict: true 180 | refute match_r d, f, strict: true 181 | end 182 | end 183 | 184 | describe "assert_match?/3" do 185 | test "values" do 186 | assert_match 1, 1 187 | 188 | assert_raise ExUnit.AssertionError, 189 | """ 190 | 191 | 192 | match (assert_match) failed 193 | left: 1 194 | right: 2 195 | """, 196 | fn -> assert_match 1, 2 end 197 | 198 | assert_raise ExUnit.AssertionError, 199 | """ 200 | 201 | 202 | match (assert_match) failed 203 | left: {:_} 204 | right: 2 205 | """, 206 | fn -> assert_match {:_}, 2 end 207 | 208 | assert_raise ExUnit.AssertionError, 209 | """ 210 | 211 | 212 | match (assert_match) failed 213 | left: %{"a" => 1, "b" => 2} 214 | right: %{"a" => 1} 215 | """, 216 | fn -> assert_match %{"a" => :_, "b" => 2}, %{"a" => 1} end 217 | 218 | assert_raise ExUnit.AssertionError, 219 | """ 220 | 221 | 222 | match (assert_match) failed 223 | left: %{a: [1, 2]} 224 | right: %{a: [1, 2, 3]} 225 | """, fn -> assert_match %{a: [1, 2]}, %{a: [2, 1, 3]}, ignore_order: true end 226 | 227 | assert_raise ExUnit.AssertionError, 228 | """ 229 | 230 | 231 | match (assert_match) failed 232 | left: %{a: [1, 2, 3]} 233 | right: %{a: [1, 2]} 234 | """, fn -> assert_match %{a: [1, 2, 3]}, %{a: [2, 1]}, ignore_order: true end 235 | 236 | assert_raise ExUnit.AssertionError, 237 | """ 238 | 239 | 240 | match (assert_match) failed 241 | left: %{a: [1, 2]} 242 | right: %{a: [2, 1, 3]} 243 | """, fn -> assert_match %{a: [1, 2]}, %{a: [2, 1, 3]}, ignore_order: false end 244 | 245 | assert_raise ExUnit.AssertionError, 246 | """ 247 | 248 | 249 | match (assert_match) failed 250 | left: %{a: [1, 2, 3]} 251 | right: %{a: [2, 1]} 252 | """, fn -> assert_match %{a: [1, 2, 3]}, %{a: [2, 1]}, ignore_order: false end 253 | 254 | assert_raise ExUnit.AssertionError, 255 | """ 256 | 257 | 258 | match (assert_match) failed 259 | left: %{field1: 2} 260 | right: %{field1: 1} 261 | """, fn -> assert_match %{field1: 2}, %TestStruct{field1: 1, field2: %{a: 1}} end 262 | 263 | assert_raise ExUnit.AssertionError, 264 | """ 265 | 266 | 267 | match (assert_match) failed 268 | left: %{field1: 2} 269 | right: %{"field2" => 1} 270 | """, fn -> assert_match %{field1: 2}, %{"field2" => 1} end 271 | 272 | assert_raise ExUnit.AssertionError, 273 | """ 274 | 275 | 276 | match (assert_match) failed 277 | left: {:ok, %{field1: 2}} 278 | right: {:ok, %{field1: 1}} 279 | """, 280 | fn -> 281 | assert_match {:ok, %{field1: 2}}, 282 | { 283 | :ok, 284 | %TestStruct{ 285 | field1: 1, 286 | field2: %{ 287 | a: 1 288 | } 289 | } 290 | } end 291 | 292 | assert_raise ExUnit.AssertionError, 293 | """ 294 | 295 | 296 | match (assert_match) failed 297 | left: %{field1: 2} 298 | right: %{field2: ~D[2020-01-01]} 299 | """, fn -> assert_match %{field1: 2}, %{field2: ~D[2020-01-01]} end 300 | 301 | assert_raise ExUnit.AssertionError, 302 | """ 303 | 304 | 305 | match (assert_match) failed 306 | left: %{field1: ~D[2020-01-01]} 307 | right: %{field2: 1} 308 | """, fn -> assert_match %{field1: ~D[2020-01-01]}, %{field2: 1} end 309 | 310 | end 311 | 312 | test "variables" do 313 | a = 1 314 | b = 2 315 | c = 1 316 | 317 | assert_match a, c 318 | 319 | assert_raise ExUnit.AssertionError, 320 | """ 321 | 322 | 323 | match (assert_match) failed 324 | left: 1 325 | right: 2 326 | """, 327 | fn -> assert_match a, b end 328 | end 329 | 330 | test "functions" do 331 | assert_match fun_a(), fun_c() 332 | 333 | assert_raise ExUnit.AssertionError, 334 | """ 335 | 336 | 337 | match (assert_match) failed 338 | left: 1 339 | right: 2 340 | """, 341 | fn -> assert_match fun_a(), fun_b() end 342 | end 343 | 344 | test "functions calls once" do 345 | table = :ets.new(__MODULE__, [:set, :named_table]) 346 | 347 | assert_match 1, counter(table) 348 | 349 | assert_raise ExUnit.AssertionError, 350 | """ 351 | 352 | 353 | match (assert_match) failed 354 | left: 1 355 | right: 2 356 | """, 357 | fn -> assert_match 1, counter(table) end 358 | 359 | refute_match 1, counter(table) 360 | 361 | # called 3 times 362 | assert :ets.lookup_element(table, :counter, 2) == 3 363 | end 364 | 365 | test "strict false" do 366 | assert_match 1, 1.0, strict: false 367 | assert_match 1.0, 1, strict: false 368 | 369 | a = %{a: 1.0, c: %{a: 1.0}} 370 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 371 | c = %{b: 2} 372 | 373 | assert_match a, b, exactly: false 374 | 375 | assert_raise ExUnit.AssertionError, 376 | """ 377 | 378 | 379 | match (assert_match) failed 380 | left: %{a: 1, b: 2, c: %{a: 1, b: 2}} 381 | right: %{a: 1.0, c: %{a: 1.0}} 382 | """, 383 | fn -> assert_match b, a, strict: false end 384 | 385 | assert_raise ExUnit.AssertionError, 386 | """ 387 | 388 | 389 | match (assert_match) failed 390 | left: %{a: 1.0, c: %{a: 1.0}} 391 | right: %{b: 2} 392 | """, 393 | fn -> assert_match a, c, strict: false end 394 | 395 | assert_raise ExUnit.AssertionError, 396 | """ 397 | 398 | 399 | match (assert_match) failed 400 | left: %{b: 2} 401 | right: %{a: 1.0, c: %{a: 1.0}} 402 | """, fn -> assert_match c, a, strict: false end 403 | 404 | d = 1.0 405 | e = 1 406 | f = 2 407 | 408 | assert_match d, e, strict: false 409 | assert_match e, d, strict: false 410 | 411 | assert_raise ExUnit.AssertionError, 412 | """ 413 | 414 | 415 | match (assert_match) failed 416 | left: 1.0 417 | right: 2 418 | """, 419 | fn -> assert_match d, f, strict: false end 420 | end 421 | 422 | test "strict true" do 423 | assert_raise ExUnit.AssertionError, 424 | """ 425 | 426 | 427 | match (assert_match) failed 428 | left: 1 429 | right: 1.0 430 | """, 431 | fn -> assert_match 1, 1.0, strict: true end 432 | 433 | assert_raise ExUnit.AssertionError, 434 | """ 435 | 436 | 437 | match (assert_match) failed 438 | left: 1.0 439 | right: 1 440 | """, 441 | fn -> assert_match 1.0, 1, strict: true end 442 | 443 | a = %{a: 1.0, c: %{a: 1.0}} 444 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 445 | c = %{b: 2} 446 | 447 | assert_raise ExUnit.AssertionError, 448 | """ 449 | 450 | 451 | match (assert_match) failed 452 | left: %{a: 1.0, c: %{a: 1.0}} 453 | right: %{a: 1, c: %{a: 1}} 454 | """, 455 | fn -> assert_match a, b, strict: true end 456 | 457 | assert_raise ExUnit.AssertionError, 458 | """ 459 | 460 | 461 | match (assert_match) failed 462 | left: %{a: 1, b: 2, c: %{a: 1, b: 2}} 463 | right: %{a: 1.0, c: %{a: 1.0}} 464 | """, 465 | fn -> assert_match b, a, strict: true end 466 | 467 | assert_raise ExUnit.AssertionError, 468 | """ 469 | 470 | 471 | match (assert_match) failed 472 | left: %{a: 1.0, c: %{a: 1.0}} 473 | right: %{b: 2} 474 | """, 475 | fn -> assert_match a, c, strict: true end 476 | 477 | assert_raise ExUnit.AssertionError, 478 | """ 479 | 480 | 481 | match (assert_match) failed 482 | left: %{b: 2} 483 | right: %{a: 1.0, c: %{a: 1.0}} 484 | """, 485 | fn -> assert_match c, a, strict: true end 486 | 487 | d = 1.0 488 | e = 1 489 | f = 2 490 | 491 | assert_raise ExUnit.AssertionError, 492 | """ 493 | 494 | 495 | match (assert_match) failed 496 | left: 1.0 497 | right: 1 498 | """, 499 | fn -> assert_match d, e, strict: true end 500 | 501 | assert_raise ExUnit.AssertionError, 502 | """ 503 | 504 | 505 | match (assert_match) failed 506 | left: 1 507 | right: 1.0 508 | """, 509 | fn -> assert_match e, d, strict: true end 510 | 511 | assert_raise ExUnit.AssertionError, 512 | """ 513 | 514 | 515 | match (assert_match) failed 516 | left: 1.0 517 | right: 2 518 | """, 519 | fn -> assert_match d, f, strict: true end 520 | end 521 | 522 | test "with message" do 523 | assert_raise ExUnit.AssertionError, 524 | """ 525 | 526 | 527 | test message 528 | left: 1 529 | right: 2 530 | """, 531 | fn -> assert_match 1, 2, message: "test message" end 532 | 533 | a = %{a: 1.0, c: %{a: 1.0}} 534 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 535 | c = %{b: 2} 536 | 537 | assert_raise ExUnit.AssertionError, fn -> assert_match a, b, strict: true end 538 | assert_raise ExUnit.AssertionError, fn -> assert_match b, a, strict: true end 539 | assert_raise ExUnit.AssertionError, fn -> assert_match a, c, strict: true end 540 | assert_raise ExUnit.AssertionError, fn -> assert_match c, a, strict: true end 541 | 542 | d = 1.0 543 | e = 1 544 | f = 2 545 | 546 | assert_raise ExUnit.AssertionError, fn -> assert_match d, e, strict: true end 547 | assert_raise ExUnit.AssertionError, fn -> assert_match e, d, strict: true end 548 | assert_raise ExUnit.AssertionError, fn -> assert_match d, f, strict: true end 549 | end 550 | end 551 | 552 | describe "refute_match?/2" do 553 | test "values" do 554 | refute_match 1, 2 555 | 556 | assert_raise ExUnit.AssertionError, 557 | "\n\nmatch (refute_match) succeeded, but should have failed\n", 558 | fn -> refute_match 1, 1 end 559 | end 560 | 561 | test "variables" do 562 | a = 1 563 | b = 1 564 | c = 2 565 | refute_match a, c 566 | assert_raise ExUnit.AssertionError, fn -> refute_match a, b end 567 | end 568 | 569 | test "functions" do 570 | refute_match fun_a(), fun_b() 571 | assert_raise ExUnit.AssertionError, fn -> refute_match fun_a(), fun_c() end 572 | end 573 | 574 | test "strict false" do 575 | assert_raise ExUnit.AssertionError, fn -> refute_match 1, 1.0, strict: false end 576 | assert_raise ExUnit.AssertionError, fn -> refute_match 1.0, 1, strict: false end 577 | 578 | a = %{a: 1.0, c: %{a: 1.0}} 579 | b = %{b: 2} 580 | c = %{a: 1.0} 581 | 582 | refute_match a, b, strict: false 583 | refute_match b, a, strict: false 584 | refute_match a, c, strict: false 585 | assert_raise ExUnit.AssertionError, fn -> refute_match c, a, strict: false end 586 | 587 | d = 1.0 588 | e = 1 589 | f = 1.0 590 | 591 | assert_raise ExUnit.AssertionError, fn -> refute_match d, e, strict: false end 592 | assert_raise ExUnit.AssertionError, fn -> refute_match e, d, strict: false end 593 | assert_raise ExUnit.AssertionError, fn -> refute_match d, f, strict: false end 594 | end 595 | 596 | test "strict true" do 597 | refute_match 1, 1.0, strict: true 598 | refute_match 1.0, 1, strict: true 599 | 600 | a = %{a: 1.0, c: %{a: 1.0}} 601 | b = %{a: 1, b: 2, c: %{a: 1, b: 2}} 602 | c = %{a: 1.0} 603 | 604 | refute_match a, b, strict: true 605 | refute_match b, a, strict: true 606 | refute_match a, c, strict: true 607 | assert_raise ExUnit.AssertionError, fn -> refute_match c, a, strict: true end 608 | 609 | d = 1.0 610 | e = 1 611 | f = 1.0 612 | 613 | refute_match d, e, strict: true 614 | refute_match e, d, strict: true 615 | assert_raise ExUnit.AssertionError, fn -> refute_match d, f, strict: true end 616 | end 617 | end 618 | end 619 | --------------------------------------------------------------------------------