Current section
Files
Jump to
Current section
Files
lib/combo/url_param.ex
defprotocol Combo.URLParam do
@moduledoc ~S"""
A protocol that converts data structures into URL parameters.
This protocol is mainly used by `Combo.VerifiedRoutes`. For example, when
you write:
~p"/user/#{@user}/edit"
It knows how to convert `@user` to a string thanks to this protocol.
By default, Combo implements this protocol for integers, binaries, atoms,
and structs. For structs, a key `:id` is assumed, but you may provide a
specific implementation.
The term `nil` cannot be converted to URL parameter.
## Custom implementation
In order to customize the parameter for any struct, one can simply implement
this protocol. For example for a `Date` struct:
defimpl Combo.URLParam, for: Date do
def to_param(date) do
Date.to_string(date)
end
end
However, for convenience, this protocol can also be derivable. For example:
defmodule User do
@derive Combo.URLParam
defstruct [:id, :username]
end
By default, the derived implementation will use the `:id` key. In case the
user does not contain an `:id` key, the key can be specified with an option:
defmodule User do
@derive {Combo.URLParam, key: :username}
defstruct [:username]
end
Then, it will use `:username` in URLs.
When using Ecto, you must call `@derive` before the `schema` call:
@derive {Combo.URLParam, key: :username}
schema "users" do
"""
@fallback_to_any true
@spec to_param(term) :: String.t()
def to_param(term)
end
defimpl Combo.URLParam, for: Integer do
def to_param(int), do: Integer.to_string(int)
end
defimpl Combo.URLParam, for: Float do
def to_param(float), do: Float.to_string(float)
end
defimpl Combo.URLParam, for: BitString do
def to_param(bin) when is_binary(bin), do: bin
end
defimpl Combo.URLParam, for: Atom do
def to_param(nil) do
raise ArgumentError, "cannot convert nil to param"
end
def to_param(atom) do
Atom.to_string(atom)
end
end
defimpl Combo.URLParam, for: Map do
def to_param(map) do
raise ArgumentError,
"maps cannot be converted to_param. A struct was expected, got: #{inspect(map)}"
end
end
defimpl Combo.URLParam, for: Any do
defmacro __deriving__(module, struct, options) do
key = Keyword.get(options, :key, :id)
unless Map.has_key?(struct, key) do
raise ArgumentError,
"cannot derive Combo.URLParam for struct #{inspect(module)} " <>
"because it does not have key #{inspect(key)}. Please pass " <>
"the :key option when deriving"
end
quote do
defimpl Combo.URLParam, for: unquote(module) do
def to_param(%{unquote(key) => nil}) do
raise ArgumentError,
"cannot convert #{inspect(unquote(module))} to param, " <>
"key #{inspect(unquote(key))} contains a nil value"
end
def to_param(%{unquote(key) => key}) when is_integer(key), do: Integer.to_string(key)
def to_param(%{unquote(key) => key}) when is_binary(key), do: key
def to_param(%{unquote(key) => key}), do: Combo.URLParam.to_param(key)
end
end
end
def to_param(%{id: nil}) do
raise ArgumentError, "cannot convert struct to param, key :id contains a nil value"
end
def to_param(%{id: id}) when is_integer(id), do: Integer.to_string(id)
def to_param(%{id: id}) when is_binary(id), do: id
def to_param(%{id: id}), do: Combo.URLParam.to_param(id)
def to_param(map) when is_map(map) do
raise ArgumentError,
"structs expect an :id key when converting to_param or a custom implementation " <>
"of the Combo.URLParam protocol (read Combo.URLParam docs for more information), " <>
"got: #{inspect(map)}"
end
def to_param(data) do
raise Protocol.UndefinedError, protocol: @protocol, value: data
end
end