Skip to content

Commit

Permalink
Merge pull request #1 from jclem/add-ecto-type-impl
Browse files Browse the repository at this point in the history
Add Ecto.Type implementation
  • Loading branch information
jclem committed Dec 13, 2018
2 parents 83fa9f1 + 8168fce commit 3d890cc
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 16 deletions.
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

0 comments on commit 3d890cc

Please sign in to comment.