Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ecto.Type implementation #1

Merged
merged 3 commits into from
Dec 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,36 @@ def deps do
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/base62_uuid_field](https://hexdocs.pm/base62_uuid_field).
## Usage

This Ecto type builds on top of the existing `Ecto.UUID` type, except that it ensures that the types at runtime are represented as [Base62-encoded UUIDs](https://github.com/jclem/base62_uuid).

For example, given an `identity_users` table with a `:binary_id` primary key:

```elixir
create table(:identity_users, primary_key: false) do
add :id, :binary_id, primary_key: true
end
```

We can have Base62-encoded primary keys at runtime instead of much longer and less URL-friendly hexadecimal-encoded UUIDs.

```elixir
defmodule App.Identity.User do
use Ecto.Schema

@primary_key {:id, Base62UUIDField, autogenerate: true}

# ...etc.
end
```

```elixir
iex> %App.Identity.User{} |> App.Identity.User.changeset(%{}) |> App.Repo.insert!()
%App.Identity.User{
__meta__: #Ecto.Schema.Metadata<:loaded, "identity_users">,
id: "6UupZ56JriyqxwjYXR9Aiz",
inserted_at: ~N[2018-12-13 18:22:57],
updated_at: ~N[2018-12-13 18:22:57]
}
```
40 changes: 31 additions & 9 deletions lib/base62_uuid_field.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,40 @@
defmodule Base62UUIDField do
@moduledoc """
Documentation for Base62UUIDField.
A field that is stored as a v4 UUID but presented as a Base62-encoded binary string.
"""

@doc """
Hello world.
@behaviour Ecto.Type

## Examples
@spec type() :: :string
def type, do: :string

iex> Base62UUIDField.hello()
:world
@spec cast(any) :: {:ok, String.t()} | :error
def cast(val) when is_binary(val), do: {:ok, val}
def cast(_), do: :error

"""
def hello do
:world
@spec dump(any) :: {:ok, any} | :error
def dump(val) when is_binary(val) do
with {:ok, decoded} <- Base62UUID.decode(val) do
Ecto.UUID.dump(decoded)
else
_ -> :error
end
end

def dump(_), do: :error

@spec load(any) :: {:ok, any} | :error
def load(val) when is_binary(val) do
with {:ok, loaded} <- Ecto.UUID.load(val),
{:ok, decoded} <- Base62UUID.encode(loaded) do
{:ok, decoded}
else
_ -> :error
end
end

def load(_), do: :error

@spec autogenerate() :: String.t()
def autogenerate, do: Base62UUID.generate()
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule Base62UUIDField.MixProject do
defp deps do
[
{:base62_uuid, "~> 2.0.0"},
{:ecto, "~> 3.0.5"},
{:ex_doc, "~> 0.19.1", only: [:dev]},
{:excoveralls, "~> 0.10.3", only: [:test]}
]
Expand Down
2 changes: 2 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"base62_uuid": {:hex, :base62_uuid, "2.0.0", "6901f9692aa8791e95f0305309628056bf79c9fcc2949cbe78c43ae7c19d3990", [:mix], [{:base62, "~> 1.2.0", [hex: :base62, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1.5", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm"},
"decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.0.5", "bf9329b56f781a67fdb19e92e6d9ed79c5c8b31d41653b79dafb7ceddfbe87e0", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.3", "b090a3fbcb3cfa136f0427d038c92a9051f840953ec11b40ee74d9d4eac04d1e", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
Expand Down
55 changes: 51 additions & 4 deletions test/base62_uuid_field_test.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,55 @@
defmodule Base62UUIDFieldTest do
use ExUnit.Case
doctest Base62UUIDField
use ExUnit.Case, async: true

test "greets the world" do
assert Base62UUIDField.hello() == :world
setup do
uuid = Ecto.UUID.generate()
{:ok, ecto_dumped_uuid} = Ecto.UUID.dump(uuid)
{:ok, uuid: uuid, encoded_uuid: Base62UUID.encode!(uuid), ecto_dumped_uuid: ecto_dumped_uuid}
end

test ".autogenerate generates a Base62 UUID" do
assert String.length(Base62UUIDField.autogenerate()) == 22
end

test ".type is a string" do
assert Base62UUIDField.type() == :string
end

test ".cast casts to a string" do
assert Base62UUIDField.cast("1") == {:ok, "1"}
end

test ".cast on a non-binary returns an error" do
assert Base62UUIDField.cast(1) == :error
end

test ".dump decodes the value to a UUID", %{
encoded_uuid: encoded_uuid,
ecto_dumped_uuid: ecto_dumped_uuid
} do
assert Base62UUIDField.dump(encoded_uuid) == {:ok, ecto_dumped_uuid}
end

test ".dump on a non-binary returns an error" do
assert Base62UUIDField.dump(1) == :error
end

test ".dump returns an error for a non-UUID" do
assert Base62UUIDField.dump("!") == :error
end

test ".load encodes the value to a Base62 UUID", %{
ecto_dumped_uuid: ecto_dumped_uuid,
encoded_uuid: encoded_uuid
} do
assert Base62UUIDField.load(ecto_dumped_uuid) == {:ok, encoded_uuid}
end

test ".load on a non-binary returns an error" do
assert Base62UUIDField.load(1) == :error
end

test ".load returns an error for a non-UUID" do
assert Base62UUIDField.load("not-a-uuid") == :error
end
end