defmodule Newt do @moduledoc """ A low-cost abstraction implementing the NewType pattern in Elixir There is no way (currently) to define zero-cost NewTypes in Elixir (like you can in Rust, Haskell, F#, etc.) because Elixir is dynamically typed. However, this library provides a low-cost abstraction that allows you to define NewTypes in Elixir with minimal boilerplate. """ @spec __using__(keyword(type: term())) :: Macro.t() defmacro __using__(opts) do opts = Keyword.validate!(opts, [:type]) typespec = Keyword.fetch!(opts, :type) module_name = "Type_#{UUID.uuid4(:hex)}" |> String.to_atom() type_name = __CALLER__.module quote location: :keep do use TypedStruct @behaviour Newt @opaque t() :: unquote(module_name).t() typedstruct enforce: true, opaque: true, module: unquote(module_name) do field(:value, unquote(typespec)) end @impl true # N.B. This clause of validate should never *actually* be called, but # having it prevents a dialyzer warning with the case statement in new/1. def validate( Newt.StupidPlaceholderValueThatWouldBeRidiculousToEverUseInYourProgramSoDoNotDoItOK ) do {:error, "418 - I'm a teapot"} end def validate(value) do {:ok, value} end defoverridable validate: 1 @spec new(unquote(typespec)) :: {:ok, t()} | {:error, String.t()} def new(value) do case validate(value) do {:ok, value} -> {:ok, %unquote(module_name){value: value}} {:error, reason} -> {:error, reason} end end @spec unwrap(t() | {:error, String.t()}) :: unquote(typespec) def unwrap(%{value: value} = type) when is_struct(type, unquote(module_name)) do {:ok, value} end def unwrap({:error, reason}) do {:error, reason} end def unwrap(value) do {:error, "#{inspect(value)} is not a valid #{inspect(unquote(type_name))}"} end defguard is_type(value) when is_struct(value, unquote(module_name)) defmacro __using__(_opts \\ []) do quote do require unquote(__MODULE__) end end defimpl Inspect, for: unquote(module_name) do import Inspect.Algebra def inspect(%{value: value}, opts) do concat([ "#", to_doc(unquote(type_name), opts), string("<"), to_doc(value, opts), string(">") ]) end end defimpl String.Chars, for: unquote(module_name) do def to_string(%{value: value}) do to_string(value) end end defimpl Jason.Encoder, for: unquote(module_name) do def encode(%{value: value}, opts) do Jason.Encoder.encode(value, opts) end end end end @spec newtype(atom(), term()) :: Macro.t() defmacro newtype(type_name, typespec) do quote do defmodule unquote(type_name) do use Newt, type: unquote(typespec) end end end @spec newtype(atom(), term(), do: Macro.t()) :: Macro.t() defmacro newtype(type_name, typespec, do: block) do quote do defmodule unquote(type_name) do use Newt, type: unquote(typespec) @impl true def validate(var!(value)) do unquote(block) end end end end @callback validate(value :: any) :: {:ok, any} | {:error, String.t()} end