{"id":27019245,"url":"https://github.com/jameslavin/recursive_selective_match","last_synced_at":"2025-09-04T07:06:18.585Z","repository":{"id":57542124,"uuid":"131514074","full_name":"JamesLavin/recursive_selective_match","owner":"JamesLavin","description":"Library for testing nested Elixir data structures and ignoring irrelevant data elements and data structure subtrees","archived":false,"fork":false,"pushed_at":"2020-10-13T18:32:22.000Z","size":418,"stargazers_count":11,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-08-14T21:14:29.374Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JamesLavin.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-04-29T17:16:18.000Z","updated_at":"2025-03-04T18:12:55.000Z","dependencies_parsed_at":"2022-09-09T04:12:03.056Z","dependency_job_id":null,"html_url":"https://github.com/JamesLavin/recursive_selective_match","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/JamesLavin/recursive_selective_match","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JamesLavin%2Frecursive_selective_match","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JamesLavin%2Frecursive_selective_match/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JamesLavin%2Frecursive_selective_match/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JamesLavin%2Frecursive_selective_match/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JamesLavin","download_url":"https://codeload.github.com/JamesLavin/recursive_selective_match/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JamesLavin%2Frecursive_selective_match/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273568336,"owners_count":25128763,"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","status":"online","status_checked_at":"2025-09-04T02:00:08.968Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2025-04-04T17:19:59.941Z","updated_at":"2025-09-04T07:06:18.537Z","avatar_url":"https://github.com/JamesLavin.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RecursiveSelectiveMatch\n\n`RecursiveSelectiveMatch` is an Elixir library application enabling testing of deeply nested Elixir data structures. It includes several powerful features:\n\n1. It selectively ignores irrelevant data elements and data structure subtrees you wish to exclude from your matching (like primary \u0026 foreign key IDs, timestamps, and 3rd-party IDs), so you can specify what must match and ignore everything else\n\n2. By default, it allows testing actual structs with expected maps, but you can enable :strict_struct_matching\n\n3. By default, it requires that keys be of the same type, but you can ignore differences between string and atom keys by enabling :standardize_keys\n\n4. Rather than testing only values, you can also test values' datatypes using any of the following:\n\n   - :anything\n   - :any_iso8601_date (a string, like \"2018-07-04\"; rejects most invalid dates)\n   - :any_iso8601_time (a string, like \"12:56:11\"; rejects invalid times)\n   - :any_iso8601_datetime (a string, like \"2018-07-04 12:56:11\" or \"2018-07-04T12:56:11\"; rejects most invalid dates/times)\n   - :any_date (the Elixir Date representation)\n   - :any_time (the Elixir Time representation)\n   - :any_datetime (the Elixir DateTime -- with timezone -- representation)\n   - :any_naive_datetime (the Elixir NaiveDateTime representation)\n   - :any_utc_datetime (the Elixir UTCDateTime representation)\n   - :any_list\n   - :any_map\n   - :any_tuple\n   - :any_integer (also: :any_pos_integer \u0026 :any_non_neg_integer)\n   - :any_float (also: :any_pos_float \u0026 :any_non_neg_float)\n   - :any_number (also: :any_pos_number \u0026 :any_non_neg_number)\n   - :any_binary\n   - :any_bitstring\n   - :any_atom\n   - :any_boolean\n   - :any_struct\n   - :any_pid\n   - :any_port\n   - :any_reference\n\n5. Rather than test only values, you can test against arbitrary anonymous functions, for example: `fname: \u0026(Regex.match?(~r/[A-Z][a-z]{2,}/,\u00261))`\n\n6. You can test multiple criteria for a single value using a `{:multi, [...]}` tuple\n\n`RecursiveSelectiveMatch` currently provides two functions:\n\n    1. `matches?(expected, actual, opts \\\\ %{})`\n    2. `includes?(expected, actual_list, opts \\\\ %{})`.\n\nMost of this documentation covers `matches?(expected, actual, opts \\\\ %{})`, which is for matching entire data structures.\n\n`includes?(expected, actual_list, opts \\\\ %{})` is similar but used to test whether `expected` matches _any list item_ inside the list `actual_list`.\n\nFor example, imagine you want to test a function that returns a nested data structure like this:\n\n    %{\n      players: [\n        %Person{id: 1187, fname: \"Robert\", lname: \"Parrish\", position: :center, jersey_num: \"00\"},\n        %Person{id: 979, fname: \"Kevin\", lname: \"McHale\", position: :forward, jersey_num: \"32\"},\n        %Person{id: 1033, fname: \"Larry\", lname: \"Bird\", position: :forward, jersey_num: \"33\"},\n      ],\n      team: %{name: \"Celtics\",\n              nba_id: 13,\n              greatest_player: %Person{id: 4, fname: \"Bill\", lname: \"Russell\", position: :center, jersey_num: \"6\", born: ~D[1934-02-12]},\n              plays_at: %{arena: %{name: \"Boston Garden\",\n                                   location: %{\"city\" =\u003e \"Boston\", \"state\" =\u003e \"MA\"}}}},\n      formatted_data_fetched_at: ~N[2018-04-17 11:14:53],\n      data_fetched_at: \"2018-04-17 11:14:53\"\n    }\n\nImagine further that each time you call this function, some details vary. Maybe each time you\ncall the function, you get a random team, not always the NBA's greatest team of all time (only\nteam with 17 championships... #boston_strong!) and you don't care about specific ids or the\ndata_fetched_at time stamp or maybe even details about the players or team. But you want to\ntest that the structure of the data is correct and possibly confirm some of the values.\n\nWith `RecursiveSelectiveMatch`, you can create a generic test by specifying an _expected_ data structure,\nlike this:\n\n    %{\n      players: :any_list,\n      team: %{name: :any_binary,\n              nba_id: :any_integer,\n              greatest_player: :any_struct,\n              plays_at: %{arena: %{name: :any_binary,\n                                   location: %{\"city\" =\u003e :any_binary,\n                                               \"state\" =\u003e :any_binary}}}},\n      formatted_data_fetched_at: :any_naive_datetime,\n      data_fetched_at: :any_binary\n    }\n\nIf you assign the actual data structure (in this case a map) to the variable `actual` and the\nexpected data structure to the variable `expected`, you can test whether they match using:\n\n    defmodule MyTest do\n      use ExUnit.Case\n\n      alias RecursiveSelectiveMatch, as: RSM\n\n      test \"actual matches expected\" do\n        expected = %{ players: :any_list, ... }\n\n        actual = %{ ... }\n\n        assert RSM.matches?(expected, actual)\n      end\n    end\n\nPlease note that the order matters. The first parameter is for _expected_ and the second is for _actual_. This successfully matches (you can see the test in [test/recursive_selective_match_test.exs](test/recursive_selective_match_test.exs)).\n\nAlternatively, you can pass in any function as a matcher. The above can be rewritten as the\nfollowing (notice that both approaches can be used interchangeably):\n\n    %{\n      players: \u0026is_list/1,\n      team: %{name: \u0026is_binary/1,\n              nba_id: \u0026is_integer/1,\n              greatest_player: :any_struct,\n              plays_at: %{arena: %{name: \u0026is_binary/1,\n                                   location: %{\"city\" =\u003e \u0026is_binary/1,\n                                               \"state\" =\u003e \u0026is_binary/1}}}},\n      data_fetched_at: \u0026is_binary/1\n    }\n\nEven better, you can pass in a one-argument anonymous function and it will pass the actual\nvalue in for testing. The following expectation will also pass with the example above:\n\n    %{\n      players: \u0026(length(\u00261) == 3),\n      team: %{name: \u0026(\u00261 in [\"Bucks\",\"Celtics\", \"76ers\", \"Lakers\", \"Rockets\", \"Warriors\"]),\n              nba_id: \u0026(\u00261 \u003e= 1 \u0026\u0026 \u00261 \u003c= 30),\n              greatest_player: %Person{id: \u0026(\u00261 \u003e= 0 \u0026\u0026 \u00261 \u003c= 99),\n                                       fname: \u0026(Regex.match?(~r/[A-Z][a-z]{2,}/,\u00261)),\n                                       lname: \u0026(Regex.match?(~r/[A-Z][a-z]{2,}/,\u00261)),\n                                       position: \u0026(\u00261 in [:center, :guard, :forward]),\n                                       jersey_num: \u0026(Regex.match?(~r/\\d{1,2}/,\u00261))},\n              plays_at: %{arena: %{name: \u0026(String.length(\u00261) \u003e 3),\n                                   location: %{\"city\" =\u003e \u0026is_binary/1,\n                                               \"state\" =\u003e \u0026(Regex.match?(~r/[A-Z]{2}/, \u00261))}}}},\n      data_fetched_at: \u0026(Regex.match?(~r/2018-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}/, \u00261))\n    }\n\n`RecursiveSelectiveMatch` currently works (at least sort of) with Elixir maps, lists,\ntuples, and structs (which it begins comparing based on struct type and then treats as maps).\n\nYou can also specify multiple expectations for a single value using a `{:multi, ...}` tuple.\nThe following will check that: 1) there are exactly three items in the `:players` list; and, 2) every player has an `lname` field which is a string of at least four bytes:\n\n    %{players:\n       {:multi, [\u0026(length(\u00261) == 3),\n                 \u0026(Enum.all?(\u00261, fn(player) -\u003e (player.lname |\u003e byte_size()) \u003e= 4 end))\n                ]\n       }\n     }\n\nAfter adding `RecursiveSelectiveMatch` to your project as a dependency, you can pass\nan expected and an actual data structure to `RecursiveSelectiveMatch.matches?()` as follows.\nIf every element in `expected` also exists in `actual`, `matches?()` should return `true`.\nIf any element of `expected` is not in `actual`, `matches?()` should return `false`.\n\nBy default, when `matches?()` returns `false`, it should also display a message indicating\nwhat data structure or element failed to match. It will not display all missing data\nstructures or elements but only the first it finds.\n\n`RecursiveSelectiveMatch.matches?()` take an optional third argument, which is a map of\noptions:\n\n- _To disable warnings_: You can disable the default behavior of displaying the reason for any match failure by passing an options map (as a third argument) containing `%{suppress_warnings: true}`.\n\n- _To treat string \u0026 atom keys as equivalent when evaluating maps_: You can override the default behavior of requiring that maps' expected and actual keys be of the same type and instead ignore differences between string and atom keys in maps by passing an options map (as a third argument) containing `%{standardize_keys: true}`.\n\n- _To prevent expected maps from matching actual structs_: If you expect a map and attempt to match it against an actual struct, by default `RecursiveSelectiveMatch` treats the struct as a map for matching purposes. You can override this default behavior and prevent expected maps from matching actual structs by passing an options map (as a third argument) containing `%{strict_struct_matching: true}`, which will prevent ordinary maps from matching structs.\n\n- _To require that lists match exactly (i.e., all expected list elements are present \u0026 in the expected order)_: The default behavior is to consider lists to match if all expected list elements are found in the actual list. If you want to consider lists to match only if the lists are identical, you can pass an options map (as a third argument) containing `%{exact_lists: true}`. This will cause lists to match only if they match exactly.\n\n- _To require that actual lists contain all expected list elements but ignore order_: The default behavior is to consider lists to match if all expected list elements are found in the actual list. If you want to consider lists to match only if all expected list items are present and no additional list items are present in the actual list (and you don't care about the ordering of these elements), you can pass an options map (as a third argument) containing `%{full_lists: true}`. This will cause lists to match only if all expected list elements are present and no unexpected list elements are present.\n\nIf you wanted to change the earlier example by overriding all three default options, just add\na third argument, like this:\n\n    defmodule MyTest do\n      use ExUnit.Case\n\n      alias RecursiveSelectiveMatch, as: RSM\n\n      assert RSM.matches?(expected,\n                          actual,\n                          %{suppress_warnings: true,\n                            standardize_keys: true,\n                            strict_struct_matching: true})\n    end\n\n`RecursiveSelectiveMatch` module originally printed failure messages. I've rewritten it to log error messages,\nbut you can override this to keep the original behavior by passing `io_errors: true` inside\nthe opts map.\n\nYou can test that the correct error messages are generated (and prevent those error messages from\nleaking through) by using ExUnit's `capture_log()`:\n\n    defmodule MyTest do\n      use ExUnit.Case\n      import ExUnit.CaptureLog\n\n      alias RecursiveSelectiveMatch, as: RSM\n\n      expected = {:a, :b, :c}\n      actual = {:a, :b, :d}\n      assert capture_log(fn -\u003e RSM.matches?(expected, actual) end) =~\n        \"[error] :d does not match :c\"\n      assert capture_log(fn -\u003e RSM.matches?(expected, actual) end) =~\n        \"[error] {:a, :b, :d} does not match {:a, :b, :c}\"\n    end\n\nIf you don't care about the error messages and just want to ensure that the test fails when the actual data structure doesn't match the expected data structure, you can instead use ExUnit's `refute` and pass `%{suppress_warnings: true}` in the opts hash:\n\n    defmodule MyTest do\n      use ExUnit.Case\n\n      alias RecursiveSelectiveMatch, as: RSM\n\n      expected = {:a, :b, :c}\n      actual = {:a, :b, :d}\n      refute RSM.matches?(expected, actual, %{suppress_warnings: true})\n    end\n\n`RecursiveSelectiveMatch` is a clean reimplementation and extension of `SelectiveRecursiveMatch`, a\nlibrary I wrote at [Teladoc](https://www.teladoc.com/) to solve the same problem. I have reimplemented it to\nwrite cleaner code on my second attempt. (As Fred Brooks wrote, \"plan to throw\none away; you will, anyhow.\") While I wrote this library on my own time and have added\nfeatures not present in the original, my inspiration to create this and the time spent\nbuilding my initial implementation both came from Teladoc, so thank you, Teladoc! Thanks\nalso to [CareDox](https://caredox.com/), where I work now and have begun extending this library.\n\n## Changelog\n\nTo see how `RecursiveSelectiveMatch` has changed over time, please see the [CHANGELOG](CHANGELOG.md).\n\n## Installation\n\n`RecursiveSelectiveMatch` is [available in Hex](https://hex.pm/packages/recursive_selective_match) and can be installed\nby adding `recursive_selective_match` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:recursive_selective_match, \"~\u003e 0.2.6\"}\n  ]\nend\n```\n\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\nand published on [HexDocs](https://hexdocs.pm). Docs can also\nbe found at [https://hexdocs.pm/recursive_selective_match](https://hexdocs.pm/recursive_selective_match).\n\n## TODO\n\nI have not yet reimplemented several features of my original `SelectiveRecursiveMatch` but plan to do so:\n\n- `:debug_mode`: Option to display every step in the `RecursiveSelectiveMatch` process\n\nI want :debug_mode to intelligently display all levels of information for the first failing path it encounters but not display any information for dead-ends it encounters that are not actually failing paths. These can be different if, for example, we're searching through a list of items for one that matches, in which case we would want to ignore items that don't match until we fail to match the expected item against the very last item in the corresponding actual list.\n\nI also hope to allow you to use your expected data structures as a template for generating concrete data structures for testing purposes.\n\nI want to add an option to require that list elements be in the order specified in the expected list. (By default, the order of list items is ignored.)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjameslavin%2Frecursive_selective_match","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjameslavin%2Frecursive_selective_match","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjameslavin%2Frecursive_selective_match/lists"}