Better support for Ecto (#21)

* use schema associations to derive prefixes

* expand documention for ecto usage

* 0.5.0
This commit is contained in:
Sloane 2023-07-16 11:01:51 -04:00 committed by GitHub
parent 1a1f2a7008
commit c9eb03b44e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 67 deletions

1
.gitignore vendored
View file

@ -24,3 +24,4 @@ typeid-*.tar
# Temporary files, for example, from tests.
/tmp/
/.elixir-tools

View file

@ -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

View file

@ -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
```

View file

@ -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
View 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

View file

@ -1,7 +1,7 @@
defmodule TypeID.MixProject do
use Mix.Project
@version "0.4.0"
@version "0.5.0"
def project do
[