diff --git a/README.md b/README.md index ac964f2..5eed06f 100644 --- a/README.md +++ b/README.md @@ -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] +} +``` diff --git a/lib/base62_uuid_field.ex b/lib/base62_uuid_field.ex index 8fbcbc3..c2da823 100644 --- a/lib/base62_uuid_field.ex +++ b/lib/base62_uuid_field.ex @@ -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 diff --git a/mix.exs b/mix.exs index ce11bb5..b711fda 100644 --- a/mix.exs +++ b/mix.exs @@ -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]} ] diff --git a/mix.lock b/mix.lock index ead21fe..25633ab 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, diff --git a/test/base62_uuid_field_test.exs b/test/base62_uuid_field_test.exs index 48d8e49..dd3ef5f 100644 --- a/test/base62_uuid_field_test.exs +++ b/test/base62_uuid_field_test.exs @@ -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