-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement assert_matches
with expanded expressions support in match assertions
#5024
Conversation
43dd4e4
to
9608a34
Compare
@macobo suggested it would be good to have a way to enforce exact set of keys within a potentially nested map pattern. I think it's feasible with an extra pass in the macro, and the syntax for such pattern would look roughly like: assert_matches %{
"results" => [%{"dimensions" => ["some phrase"], "metrics" => [1, 1, 100.0]}],
"meta" => ^strict_map(%{
"imports_included" => false,
"imports_skip_reason" => "unsupported_query",
"imports_warning" =>
~r/Imported stats are not included in the results because query parameters are not supported/
}),
"query" =>
^exactly(response_query(site, %{
"metrics" => ["visitors", "events", "conversion_rate"],
"dimensions" => ["event:props:search_query"],
"filters" => [
["is", "event:goal", ["WP Search Queries"]]
],
"include" => %{"imports" => true}
}))
} = json_response(conn, 200) the idea here is that ^strict_map pins are matched first (with a check boiling down to |
I have added some basic support for enforcing keys with ^strict_map operator in ef3c33d (along with a fix for an unrelated case of properly handling normal pins and bindings). It's now possible to do something like: assert_matches ^strict_map(%{
"id" => ^any(:integer),
"name" => "Some segment",
"type" => ^"#{unquote(type)}",
"segment_data" => %{"filters" => [["is", "visit:entry_page", ["/blog"]]]},
"owner_id" => ^user.id,
"inserted_at" => ^any(:string),
"updated_at" => ^any(:string)
}) = response The error is not ideal yet though ("updated_at" was commented out for this one): More importantly though, the current implementation does not allow nesting ^strict_map's, so it's not possible to do patterns like (the inner one using ^exactly would work though): assert_matches ^strict_map(%{
"id" => ^any(:integer),
"name" => "Some segment",
"type" => ^"#{unquote(type)}",
"segment_data" => ^strict_map(%{"filters" => [["is", "visit:entry_page", ["/blog"]]]}),
"owner_id" => ^user.id,
"inserted_at" => ^any(:string),
"updated_at" => ^any(:string)
}) = response This can be made to work but I'll shelve it for now and come back to it later. It requires a separate match operation per each |
ef3c33d
to
7788262
Compare
7788262
to
45dc56b
Compare
Alright, I think I got it functionally at least, the code's still not pretty: assert_matches ^strict_map(%{
"id" => ^any(:integer),
"name" => "Some segment",
"type" => ^"#{unquote(type)}",
"segment_data" =>
^strict_map(%{"filters" => [["is", "visit:entry_page", ["/blog"]]]}),
"owner_id" => ^user.id,
"inserted_at" => ^any(:string),
# "updated_at" => ^any(:string)
}) = response The report: |
Also, added some additional built-in checks for common data types and date/time string formats, because why not: assert_matches ^strict_map(%{
"id" => ^any(:pos_integer),
"name" => "Some segment",
"type" => ^"#{unquote(type)}",
"segment_data" =>
^strict_map(%{"filters" => [["is", "visit:entry_page", ["/blog"]]]}),
"owner_id" => ^user.id,
"inserted_at" => ^any(:iso8601_naive_datetime),
"updated_at" => ^any(:iso8601_naive_datetime)
}) = response |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great and I appreciate the extra functionality! 💪
Changes
This is a proposal for implementing a more expressive match assertion mechanism. It was prompted by the original idea by @macobo in #5019.
The idea here is that the pin (^) operator does not only rebind existing binding in the scope but also allows embedding basically any other expression to match against the given part of the pattern. Normal pattern matching is also supported and both can be mixed. The only caveat so far is that when normal patterns fail, only they are listed in the error even if there are potentially failing expressions. However, once the normal pattern is fixed, they surface.
Currently, the following expressions can be pinned:
any(:atom)
any(:string)
any(:binary)
any(:integer)
any(:float)
any(:boolean)
any(:map)
any(:list)
any
with a one argument predicate function accepting value and returning a boolean, like:any(:integer, & &1 > 20)
any(:string, ~r/regex pattern/)
checking that value is a string and matches a pattern~r/regex pattern
&is_float/1
or&(&1 < 40 or &1 > 300)
exact(expression)
where expression is compared using equality, so that can enforce full equality inside a pattern, like:exact(%{foo: 2})
which will fail if the value is something like%{foo: 2, other: "something}
exact
- this allows "interpolating" values from schemas and maps without rebinding likeuser.id
(instead of having to rebind touser_id
first)Although the macro seems to be relatively feature complete, more testing against some real world scenarios must be done before merging.