Add Ecto support (#14)

* add ecto type support

* Ecto.ParameterizedType, `time: ...` for `new/2`

* 0.3.0
This commit is contained in:
Sloane 2023-07-07 09:00:51 -04:00 committed by GitHub
parent 428a80a633
commit 3d657061f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 138 additions and 15 deletions

View file

@ -1,5 +1,15 @@
# Changelog
## main
- **BREAKING:** `type/1` has been renamed to `prefix/1`
- `Ecto.ParameterizedType` implementation
- `new/2` now accepts an optional keyword list to specify the UUID `time:` in unix milliseconds
```elixir
iex> TypeID.new("test", time: 0)
#TypeID<"test_0000000000fq893mf5039xea5j">
```
## 0.2.2
- Lower required Elixir version

View file

@ -15,7 +15,7 @@ The package can be installed from [hex](https://hex.pm/packages/typeid_elixir) b
```elixir
def deps do
[
{:typeid_elixir, "~> 0.2.2"}
{:typeid_elixir, "~> 0.3.0"}
]
end
```
@ -23,3 +23,18 @@ end
## Spec
The original TypeID spec is defined [here](https://github.com/jetpack-io/typeid).
## Usage with Ecto
`TypeID` implements the `Ecto.ParameterizedType` behaviour so you can use
TypeIDs as fields in your Ecto schemas.
```elixir
defmodule MyApp.Accounts.User do
use Ecto.Schema
@primary_key {:id, TypeID, autogenerate: true, prefix: "acct", type: :binary_id}
# ...
end
```

View file

@ -18,6 +18,9 @@ defmodule TypeID do
@doc """
Generates a new `t:t/0` with the given prefix.
**Optional**: Specify the time of the UUID v7 by passing
`time: unix_millisecond_time` as the second argument.
### Example
iex> TypeID.new("acct")
@ -25,26 +28,27 @@ defmodule TypeID do
"""
@spec new(prefix :: String.t()) :: t()
def new(prefix) do
@spec new(prefix :: String.t(), Keyword.t()) :: t()
def new(prefix, opts \\ []) do
suffix =
UUID.uuid7()
UUID.uuid7(opts)
|> Base32.encode()
%__MODULE__{prefix: prefix, suffix: suffix}
end
@doc """
Returns the type of the given `t:t/0`.
Returns the prefix of the given `t:t/0`.
### Example
iex> tid = TypeID.new("doc")
iex> TypeID.type(tid)
iex> TypeID.prefix(tid)
"doc"
"""
@spec type(tid :: t()) :: String.t()
def type(%__MODULE__{prefix: prefix}) do
@spec prefix(tid :: t()) :: String.t()
def prefix(%__MODULE__{prefix: prefix}) do
prefix
end
@ -242,12 +246,93 @@ defmodule TypeID do
:ok
end
if Code.ensure_loaded?(Ecto.ParameterizedType) do
use Ecto.ParameterizedType
@impl Ecto.ParameterizedType
def init(opts), do: validate_opts!(opts)
@impl Ecto.ParameterizedType
def type(%{type: type}), do: type
@impl Ecto.ParameterizedType
def autogenerate(%{prefix: prefix}) do
new(prefix)
end
@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
@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
@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
end
end
defimpl Inspect, for: TypeID do
import Inspect.Algebra
def inspect(tid, _opts) do
concat(["#TypeID<\"", tid.prefix, "_", tid.suffix, "\">"])
concat(["#TypeID<\"", TypeID.to_string(tid), "\">"])
end
end

View file

@ -1,7 +1,7 @@
defmodule TypeID.MixProject do
use Mix.Project
@version "0.2.2"
@version "0.3.0"
def project do
[
@ -42,6 +42,7 @@ defmodule TypeID.MixProject do
defp deps do
[
{:ecto, "~> 3.10", only: [:dev, :test], optional: true},
{:ex_doc, "~> 0.27", only: :dev, runtime: false},
{:yaml_elixir, "~> 2.9", only: [:dev, :test], runtime: false}
]

View file

@ -1,10 +1,13 @@
%{
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
"ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"},
"ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
}

View file

@ -1,19 +1,28 @@
defmodule TypeIDTest do
use ExUnit.Case
doctest TypeID, except: [new: 1]
doctest TypeID, except: [new: 2]
describe "new/1" do
test "returns a new TypeID struct" do
tid = TypeID.new("test")
assert is_struct(tid, TypeID)
assert "test" == TypeID.type(tid)
assert "test" == TypeID.prefix(tid)
end
end
describe "type/1" do
test "returns the type (prefix) of the given TypeID" do
describe "new/2" do
test "allows setting the time" do
time = ~U[1950-12-17 00:00:00Z] |> DateTime.to_unix(:millisecond)
tid = TypeID.new("test", time: time)
assert "test" == TypeID.prefix(tid)
assert "7zegbdn300" <> _ = TypeID.suffix(tid)
end
end
describe "prefix/1" do
test "returns the prefix of the given TypeID" do
tid = TypeID.from_string!("test_01h44had5rfswbvpc383ktj0aa")
assert "test" == TypeID.type(tid)
assert "test" == TypeID.prefix(tid)
end
end
@ -60,7 +69,7 @@ defmodule TypeIDTest do
test "verification" do
tid = TypeID.from_string!("test_01h44yssjcf5daefvfr0yb70s8")
assert "test" == TypeID.type(tid)
assert "test" == TypeID.prefix(tid)
assert "018909ec-e64c-795a-a73f-6fc03cb38328" == TypeID.uuid(tid)
end
end