Ham is a library to validate function arguments and return values against their typespecs. It was originally extracted out from Ham but without the Mox integration. Hammox - Mox = Ham! Thanks @msz for creating Hammox!
The main reason why I wanted to extract it out is so that it can be used as part of Mimic or other testing libraries.
Add :ham
:
def deps do
[
{:ham, "~> 0.1", only: :test}
]
end
One can simply let Ham apply a module function using Ham.apply/2
(function or macro) and it will validate argument and return value:
iex> Ham.apply(URI, :decode, ["https%3A%2F%2Felixir-lang.org"])
"https://elixir-lang.org"
iex> Ham.apply(URI, :char_reserved?, ["a"])
** (Ham.TypeMatchError) 1st argument value "a" does not match 1st parameter's type byte().
Value "a" does not match type 0..255.
iex> Ham.apply(URI.decode("https%3A%2F%2Felixir-lang.org"))
"https://elixir-lang.org"
iex> Ham.apply(URI.char_reserved?("a"))
** (Ham.TypeMatchError) 1st argument value "a" does not match 1st parameter's type byte().
Value "a" does not match type 0..255.
Another way is to pass the args and return value without Ham executing anything:
iex> Ham.validate(URI, :char_reserved?, ["a"], true)
{:error,
%Ham.TypeMatchError{
reasons: [
{:arg_type_mismatch, 0, "a", {:type, {324, 24}, :byte, []}},
{:type_mismatch, "a", {:type, 0, :range, [{:integer, 0, 0}, {:integer, 0, 255}]}}
]
}}
iex> Ham.validate!(URI, :char_reserved?, ["a"], true)
** (Ham.TypeMatchError) 1st argument value "a" does not match 1st parameter's type byte().
Value "a" does not match type 0..255.
Both apply
and validate
accept behaviours
as an option to declare that the module
implements certain behaviours.
For example let's implement a module that does a poor job of implementing Access
defmodule CustomAccess do
@behaviour Access
def fetch(_data, _key), do: :poor
def get_and_update(_data, _key, _function), do: :poor
def pop(data, key), do: :poor
end
Ham.apply(CustomAccess.fetch([], "key"), behaviours: [Access])
** (Ham.TypeMatchError) Returned value :poor does not match type {:ok, Access.value()} | :error.
Value :wrong does not match type {:ok, Access.value()} | :error.
Dialyzer is a powerful static analysis tool that can uncover serious problems. But during tests dialyzer is not as useful. Ham can be used to validate function arguments and return values during tests even if they are randomly generated or loaded from files.
A t()
type defined on a protocol is taken by Ham to mean "a struct
implementing the given protocol". Therefore, trying to pass :atom
for an
Enumerable.t()
will produce an error, even though the type is defined as
term()
:
** (Ham.TypeMatchError)
Returned value :atom does not match type Enumerable.t().
Value :atom does not implement the Enumerable protocol.
-
For anonymous function types in typespecs, only the arity is checked. Parameter types and return types are not checked.
-
For records we are only checking if it's a tuple and that the first value is the name of the record.
Copyright 2019 Michał Szewczak
Copyright 2024 Eduardo Gurgel
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.