Skip to content

Latest commit

 

History

History
124 lines (94 loc) · 4.46 KB

README.md

File metadata and controls

124 lines (94 loc) · 4.46 KB

Ham

CI Module Version Hex Docs Total Download License Last Updated

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.

Installation

Add :ham:

def deps do
  [
    {:ham, "~> 0.1", only: :test}
  ]
end

Using Ham

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.

Why use Ham for my application code when I have Dialyzer?

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.

Protocol types

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.

Limitations

  • 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.

License

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.