mirror of
https://github.com/sloanelybutsurely/typeid-elixir.git
synced 2024-11-28 01:42:54 -05:00
Better support for Ecto (#21)
* use schema associations to derive prefixes * expand documention for ecto usage * 0.5.0
This commit is contained in:
parent
1a1f2a7008
commit
c9eb03b44e
6 changed files with 148 additions and 67 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,3 +24,4 @@ typeid-*.tar
|
|||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
/.elixir-tools
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- `Ecto.ParameterizedType` implementation traverses associations so prefixes only need to be defined on schema primary keys
|
||||
- `Ecto.ParameterizedType` implementation `type` option can be set globally with a `default_type` Application configuration
|
||||
|
||||
## 0.4.0
|
||||
|
||||
- Implements `Jason.Encoder` protocol
|
||||
|
|
18
README.md
18
README.md
|
@ -13,7 +13,7 @@ The package can be installed from [hex](https://hex.pm/packages/typeid_elixir) b
|
|||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:typeid_elixir, "~> 0.4.0"}
|
||||
{:typeid_elixir, "~> 0.5.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
@ -32,7 +32,23 @@ defmodule MyApp.Accounts.User do
|
|||
use Ecto.Schema
|
||||
|
||||
@primary_key {:id, TypeID, autogenerate: true, prefix: "acct", type: :binary_id}
|
||||
@foreign_key_type TypeID
|
||||
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
### Underlying types
|
||||
|
||||
`TypeID`s can be stored as either `:string` or `:binary_id`. `:string` will
|
||||
store the entire TypeID including the prefix. `:binary_id` stores only the
|
||||
UUID portion and requires a `:uuid` or `:binary` column.
|
||||
|
||||
#### Default type
|
||||
|
||||
The type used can be set globally in the application config.
|
||||
|
||||
```elixir
|
||||
config :typeid_elixir,
|
||||
default_type: :binary_id
|
||||
```
|
||||
|
|
|
@ -275,81 +275,22 @@ defmodule TypeID do
|
|||
use Ecto.ParameterizedType
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def init(opts), do: validate_opts!(opts)
|
||||
defdelegate init(opts), to: TypeID.Ecto
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def type(%{type: type}), do: type
|
||||
defdelegate type(params), to: TypeID.Ecto
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def autogenerate(%{prefix: prefix}) do
|
||||
new(prefix)
|
||||
end
|
||||
defdelegate autogenerate(params), to: TypeID.Ecto
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def cast(nil, _params), do: {:ok, nil}
|
||||
def cast(%__MODULE__{prefix: prefix} = tid, %{prefix: prefix}), do: {:ok, tid}
|
||||
|
||||
def cast(str, %{prefix: prefix}) when is_binary(str) do
|
||||
if String.starts_with?(str, prefix) do
|
||||
from_string(str)
|
||||
else
|
||||
with {:ok, uuid} <- Ecto.UUID.cast(str) do
|
||||
from_uuid(prefix, uuid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cast(_, _), do: :error
|
||||
defdelegate cast(data, params), to: TypeID.Ecto
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def dump(nil, _dumper, _params), do: {:ok, nil}
|
||||
|
||||
def dump(%__MODULE__{prefix: prefix} = tid, _, %{prefix: prefix, type: :string}) do
|
||||
{:ok, __MODULE__.to_string(tid)}
|
||||
end
|
||||
|
||||
def dump(%__MODULE__{prefix: prefix} = tid, _, %{prefix: prefix, type: :binary_id}) do
|
||||
{:ok, uuid(tid)}
|
||||
end
|
||||
|
||||
def dump(_, _, _), do: :error
|
||||
defdelegate dump(data, dumper, params), to: TypeID.Ecto
|
||||
|
||||
@impl Ecto.ParameterizedType
|
||||
def load(nil, _, _), do: {:ok, nil}
|
||||
|
||||
def load(str, _, %{type: :string, prefix: prefix}) do
|
||||
with {:ok, %__MODULE__{prefix: ^prefix}} = loaded <- from_string(str) do
|
||||
loaded
|
||||
end
|
||||
end
|
||||
|
||||
def load(<<_::128>> = uuid, _, %{type: :binary_id, prefix: prefix}) do
|
||||
from_uuid_bytes(prefix, uuid)
|
||||
end
|
||||
|
||||
def load(<<_::288>> = uuid, _, %{type: :binary_id, prefix: prefix}) do
|
||||
from_uuid(prefix, uuid)
|
||||
rescue
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
def load(_, _, _), do: :error
|
||||
|
||||
defp validate_opts!(opts) do
|
||||
type = Keyword.get(opts, :type, :string)
|
||||
prefix = Keyword.get(opts, :prefix, "")
|
||||
|
||||
unless prefix && prefix =~ ~r/^[a-z]{0,63}$/ do
|
||||
raise ArgumentError,
|
||||
"must specify `prefix` using only lowercase letters between 0 and 63 characters long."
|
||||
end
|
||||
|
||||
unless type in ~w[string binary_id]a do
|
||||
raise ArgumentError, "`type` must be `:string` or `:binary_id`"
|
||||
end
|
||||
|
||||
%{prefix: prefix, type: type}
|
||||
end
|
||||
defdelegate load(data, loader, params), to: TypeID.Ecto
|
||||
end
|
||||
end
|
||||
|
||||
|
|
118
lib/type_id/ecto.ex
Normal file
118
lib/type_id/ecto.ex
Normal file
|
@ -0,0 +1,118 @@
|
|||
if Code.ensure_loaded?(Ecto.ParameterizedType) do
|
||||
defmodule TypeID.Ecto do
|
||||
@moduledoc false
|
||||
|
||||
@doc false
|
||||
def init(opts), do: validate_opts!(opts)
|
||||
|
||||
@doc false
|
||||
def type(%{type: type}), do: type
|
||||
|
||||
@doc false
|
||||
def autogenerate(params) do
|
||||
params
|
||||
|> find_prefix()
|
||||
|> TypeID.new()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def cast(nil, _params), do: {:ok, nil}
|
||||
|
||||
def cast(%TypeID{prefix: prefix} = tid, params) do
|
||||
if prefix == find_prefix(params) do
|
||||
{:ok, tid}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def cast(str, params) when is_binary(str) do
|
||||
prefix = find_prefix(params)
|
||||
|
||||
if String.starts_with?(str, prefix) do
|
||||
TypeID.from_string(str)
|
||||
else
|
||||
with {:ok, uuid} <- Ecto.UUID.cast(str) do
|
||||
TypeID.from_uuid(prefix, uuid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cast(_, _), do: :error
|
||||
|
||||
@doc false
|
||||
def dump(nil, _dumper, _params), do: {:ok, nil}
|
||||
|
||||
def dump(%TypeID{} = tid, _, %{type: type} = params) do
|
||||
prefix = find_prefix(params)
|
||||
|
||||
case {tid.prefix, type} do
|
||||
{^prefix, :string} -> {:ok, TypeID.to_string(tid)}
|
||||
{^prefix, :binary_id} -> {:ok, TypeID.uuid(tid)}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def dump(_, _, _), do: :error
|
||||
|
||||
@doc false
|
||||
def load(nil, _, _), do: {:ok, nil}
|
||||
|
||||
def load(str, _, %{type: :string} = params) do
|
||||
prefix = find_prefix(params)
|
||||
|
||||
with {:ok, %TypeID{prefix: ^prefix}} = loaded <- TypeID.from_string(str) do
|
||||
loaded
|
||||
end
|
||||
end
|
||||
|
||||
def load(<<_::128>> = uuid, _, %{type: :binary_id} = params) do
|
||||
prefix = find_prefix(params)
|
||||
TypeID.from_uuid_bytes(prefix, uuid)
|
||||
end
|
||||
|
||||
def load(<<_::288>> = uuid, _, %{type: :binary_id} = params) do
|
||||
prefix = find_prefix(params)
|
||||
TypeID.from_uuid(prefix, uuid)
|
||||
rescue
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
def load(_, _, _), do: :error
|
||||
|
||||
defp validate_opts!(opts) do
|
||||
primary_key = Keyword.get(opts, :primary_key, false)
|
||||
schema = Keyword.fetch!(opts, :schema)
|
||||
field = Keyword.fetch!(opts, :field)
|
||||
default_type = Application.get_env(:typeid_elixir, :default_type, :string)
|
||||
type = Keyword.get(opts, :type, default_type)
|
||||
prefix = Keyword.get(opts, :prefix, "")
|
||||
|
||||
if primary_key do
|
||||
unless prefix && prefix =~ ~r/^[a-z]{0,63}$/ do
|
||||
raise ArgumentError,
|
||||
"must specify `prefix` using only lowercase letters between 0 and 63 characters long."
|
||||
end
|
||||
end
|
||||
|
||||
unless type in ~w[string binary_id]a do
|
||||
raise ArgumentError, "`type` must be `:string` or `:binary_id`"
|
||||
end
|
||||
|
||||
if primary_key do
|
||||
%{primary_key: primary_key, schema: schema, field: field, prefix: prefix, type: type}
|
||||
else
|
||||
%{schema: schema, field: field, type: type}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_prefix(%{prefix: prefix}), do: prefix
|
||||
|
||||
defp find_prefix(%{schema: schema, field: field}) do
|
||||
%{related: schema, related_key: field} = schema.__schema__(:association, field)
|
||||
{:parameterized, TypeID, %{prefix: prefix}} = schema.__schema__(:type, field)
|
||||
|
||||
prefix
|
||||
end
|
||||
end
|
||||
end
|
2
mix.exs
2
mix.exs
|
@ -1,7 +1,7 @@
|
|||
defmodule TypeID.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
@version "0.4.0"
|
||||
@version "0.5.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
|
Loading…
Reference in a new issue