Skip to content
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
9 changes: 6 additions & 3 deletions lib/ecto/types/id.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
@spec cast(t() | raw(), map) :: {:ok, t() | nil} | :error
def cast(data, params) when is_integer(data) do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

Shortcode.to_shortcode(data, prefix)
Shortcode.to_shortcode(data, prefix, prefix_separator: prefix_separator)
end

def cast(data, _) when is_binary(data) and byte_size(data) > 0,
Expand All @@ -33,8 +34,9 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
@spec load(raw() | nil, function, map) :: {:ok, t() | nil} | :error
def load(data, _, params) when is_integer(data) do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

Shortcode.to_shortcode(data, prefix)
Shortcode.to_shortcode(data, prefix, prefix_separator: prefix_separator)
end

def load(nil, _, _), do: {:ok, nil}
Expand All @@ -46,8 +48,9 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do

def dump(data, _, params) when is_binary(data) and byte_size(data) > 0 do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

Shortcode.to_integer(data, prefix)
Shortcode.to_integer(data, prefix, prefix_separator: prefix_separator)
end

def dump(nil, _, _), do: {:ok, nil}
Expand Down
15 changes: 11 additions & 4 deletions lib/ecto/types/uuid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
@spec cast(uuid() | nil, map) :: {:ok, t() | nil} | :error
def cast(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = data, params) do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

Shortcode.to_shortcode(data, prefix)
Shortcode.to_shortcode(data, prefix, prefix_separator: prefix_separator)
end

def cast(data, _) when is_binary(data) and byte_size(data) > 0 do
Expand All @@ -35,8 +36,12 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
@spec load(raw() | nil, function, map) :: {:ok, t() | nil} | :error
def load(<<_::128>> = uuid, _, params) do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

{:ok, uuid |> Ecto.UUID.cast!() |> Shortcode.to_shortcode!(prefix)}
{:ok,
uuid
|> Ecto.UUID.cast!()
|> Shortcode.to_shortcode!(prefix, prefix_separator: prefix_separator)}
end

def load(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = string, _, _) do
Expand All @@ -56,8 +61,9 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do

def dump(data, dumper, params) when is_binary(data) and byte_size(data) > 0 do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

case Shortcode.to_uuid(data, prefix) do
case Shortcode.to_uuid(data, prefix, prefix_separator: prefix_separator) do
{:ok, uuid} -> dump(uuid, dumper, params)
:error -> :error
end
Expand All @@ -69,9 +75,10 @@ if Code.ensure_loaded?(Ecto.ParameterizedType) do
@spec autogenerate(map) :: t()
def autogenerate(params) do
prefix = Map.get(params, :prefix)
prefix_separator = Map.get(params, :separator)

Ecto.UUID.generate()
|> Shortcode.to_shortcode!(prefix)
|> Shortcode.to_shortcode!(prefix, prefix_separator: prefix_separator)
end
end
end
106 changes: 64 additions & 42 deletions lib/shortcode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ defmodule Shortcode do
iex> Shortcode.to_shortcode("14366daa-c0f5-0f52-c9ec-e0a0b1e20006", "prefix")
{:ok, "prefix_C8IF9cqY1HP7GGslHNYLI"}

iex> Shortcode.to_shortcode("14366daa-c0f5-0f52-c9ec-e0a0b1e20006", "prefix", prefix_separator: "-")
{:ok, "prefix-C8IF9cqY1HP7GGslHNYLI"}

iex> Shortcode.to_shortcode(0)
{:ok, "0"}

Expand All @@ -42,9 +45,11 @@ defmodule Shortcode do

"""
@spec to_shortcode(UUID.uuid() | non_neg_integer, nil | binary) :: {:ok, binary} | :error
def to_shortcode(data, prefix \\ nil)
def to_shortcode(data, prefix \\ nil, opts \\ [])

def to_shortcode(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = uuid, prefix, opts) do
prefix_separator = Keyword.get(opts, :prefix_separator) || prefix_separator()

def to_shortcode(<<_::64, ?-, _::32, ?-, _::32, ?-, _::32, ?-, _::96>> = uuid, prefix) do
case Ecto.UUID.cast(uuid) do
{:ok, uuid} ->
shortcode =
Expand All @@ -53,7 +58,7 @@ defmodule Shortcode do
|> String.to_integer(16)
|> Base62.encode()

shortcode = if prefix, do: "#{prefix}#{@prefix_separator}#{shortcode}", else: shortcode
shortcode = if prefix, do: "#{prefix}#{prefix_separator}#{shortcode}", else: shortcode

{:ok, shortcode}

Expand All @@ -62,22 +67,24 @@ defmodule Shortcode do
end
end

def to_shortcode(integer, prefix) when is_integer(integer) and integer >= 0 do
def to_shortcode(integer, prefix, opts) when is_integer(integer) and integer >= 0 do
prefix_separator = Keyword.get(opts, :prefix_separator) || prefix_separator()

shortcode = integer |> Base62.encode()

shortcode = if prefix, do: "#{prefix}#{@prefix_separator}#{shortcode}", else: shortcode
shortcode = if prefix, do: "#{prefix}#{prefix_separator}#{shortcode}", else: shortcode

{:ok, shortcode}
end

def to_shortcode(_, _), do: :error
def to_shortcode(_, _, _), do: :error

@doc """
Same as `to_shortcode/2` but raises `ArgumentError` on invalid arguments.
"""
@spec to_shortcode!(any, nil | binary) :: binary
def to_shortcode!(data, prefix \\ nil) do
case to_shortcode(data, prefix) do
@spec to_shortcode!(any, nil | binary, keyword) :: binary
def to_shortcode!(data, prefix \\ nil, opts \\ []) do
case to_shortcode(data, prefix, opts) do
{:ok, shortcode} -> shortcode
:error -> raise ArgumentError, "cannot convert #{inspect(data)} to shortcode"
end
Expand All @@ -88,35 +95,41 @@ defmodule Shortcode do

## Examples

# iex> Shortcode.to_uuid("0")
# {:ok, "00000000-0000-0000-0000-000000000000"}
iex> Shortcode.to_uuid("0")
{:ok, "00000000-0000-0000-0000-000000000000"}

# iex> Shortcode.to_uuid("C8IF9cqY1HP7GGslHNYLI")
# {:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}
iex> Shortcode.to_uuid("C8IF9cqY1HP7GGslHNYLI")
{:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}

iex> Shortcode.to_uuid("prefix_C8IF9cqY1HP7GGslHNYLI", "prefix")
{:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}

# iex> Shortcode.to_uuid("pre_fix_C8IF9cqY1HP7GGslHNYLI", "pre_fix")
# {:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}
iex> Shortcode.to_uuid("pre_fix_C8IF9cqY1HP7GGslHNYLI", "pre_fix")
{:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}

# iex> Shortcode.to_uuid("foo_C8IF9cqY1HP7GGslHNYLI", "bar")
# :error
iex> Shortcode.to_uuid("prefix-C8IF9cqY1HP7GGslHNYLI", "prefix", prefix_separator: "-")
{:ok, "14366daa-c0f5-0f52-c9ec-e0a0b1e20006"}

# iex> Shortcode.to_uuid("7N42dgm5tFLK9N8MT7fHC8")
# :error
iex> Shortcode.to_uuid("foo_C8IF9cqY1HP7GGslHNYLI", "bar")
:error

# iex> Shortcode.to_uuid(Ecto.UUID.bingenerate())
# :error
iex> Shortcode.to_uuid("foo_C8IF9cqY1HP7GGslHNYLI", "foo", prefix_separator: "-")
:error

# iex> Shortcode.to_uuid("")
# :error
iex> Shortcode.to_uuid("7N42dgm5tFLK9N8MT7fHC8")
:error

iex> Shortcode.to_uuid(Ecto.UUID.bingenerate())
:error

iex> Shortcode.to_uuid("")
:error

"""
@spec to_uuid(binary | any, binary | nil) :: {:ok, UUID.uuid()} | :error
def to_uuid(shortcode, prefix \\ nil)
def to_uuid(shortcode, prefix \\ nil, opts \\ [])

def to_uuid(shortcode, nil) when is_binary(shortcode) and byte_size(shortcode) > 0 do
def to_uuid(shortcode, nil, []) when is_binary(shortcode) and byte_size(shortcode) > 0 do
with {:ok, int_shortcode} <- Base62.decode(shortcode),
hex_shortcode <- Integer.to_string(int_shortcode, 16),
{:valid_length?, true} <- {:valid_length?, String.length(hex_shortcode) <= 32} do
Expand All @@ -138,25 +151,27 @@ defmodule Shortcode do
end
end

def to_uuid(shortcode, prefix) when is_binary(shortcode) do
prefix_with_separator = prefix <> @prefix_separator
def to_uuid(shortcode, prefix, opts) when is_binary(shortcode) do
prefix_separator = Keyword.get(opts, :prefix_separator) || prefix_separator()
prefix_with_separator = if prefix, do: prefix <> prefix_separator, else: ""

shortcode
|> String.split_at(String.length(prefix_with_separator))
|> case do
{^prefix_with_separator, data} -> to_uuid(data, nil)
{^prefix_with_separator, ""} -> :error
{^prefix_with_separator, data} -> to_uuid(data, nil, [])
_ -> :error
end
end

def to_uuid(_, _), do: :error
def to_uuid(_, _, _), do: :error

@doc """
Same as `to_uuid/1` but raises `ArgumentError` on invalid arguments.
"""
@spec to_uuid!(binary, binary | nil) :: UUID.uuid()
def to_uuid!(shortcode, prefix \\ nil) do
case to_uuid(shortcode, prefix) do
@spec to_uuid!(binary, binary | nil, keyword) :: UUID.uuid()
def to_uuid!(shortcode, prefix \\ nil, opts \\ []) do
case to_uuid(shortcode, prefix, opts) do
{:ok, uuid} -> uuid
:error -> raise ArgumentError, "cannot convert shortcode #{inspect(shortcode)} to uuid"
end
Expand All @@ -179,6 +194,9 @@ defmodule Shortcode do
iex> Shortcode.to_integer("prefix_C8IF9cqY1HP7GGslHNYLI", "prefix")
{:ok, 26867168257211004681214735068979920902}

iex> Shortcode.to_integer("prefix-C8IF9cqY1HP7GGslHNYLI", "prefix", prefix_separator: "-")
{:ok, 26867168257211004681214735068979920902}

iex> Shortcode.to_integer("foo_C8IF9cqY1HP7GGslHNYLI", "bar")
:error

Expand All @@ -190,9 +208,9 @@ defmodule Shortcode do

"""
@spec to_integer(binary, binary | nil) :: {:ok, integer} | :error
def to_integer(shortcode, prefix \\ nil)
def to_integer(shortcode, prefix \\ nil, opts \\ [])

def to_integer(shortcode, nil) when is_binary(shortcode) do
def to_integer(shortcode, nil, _) when is_binary(shortcode) do
try do
Base62.decode!(shortcode)
rescue
Expand All @@ -202,29 +220,33 @@ defmodule Shortcode do
end
end

def to_integer(shortcode, prefix) when is_binary(shortcode) do
def to_integer(shortcode, prefix, opts) when is_binary(shortcode) do
prefix_separator = Keyword.get(opts, :prefix_separator) || prefix_separator()

shortcode
|> String.split(@prefix_separator, parts: 2)
|> String.split(prefix_separator, parts: 2)
|> case do
[^prefix, data] -> to_integer(data, nil)
[^prefix, data] -> to_integer(data, nil, opts)
_ -> :error
end
end

def to_integer(_, _), do: :error
def to_integer(_, _, _), do: :error

@doc """
Same as `to_integer/1` but raises `ArgumentError` on invalid arguments.
"""
@spec to_integer!(binary, binary | nil) :: integer
def to_integer!(shortcode, prefix) do
case to_integer(shortcode, prefix) do
@spec to_integer!(binary, binary | nil, keyword) :: integer
def to_integer!(shortcode, prefix, opts) do
case to_integer(shortcode, prefix, opts) do
{:ok, integer} -> integer
:error -> raise ArgumentError, "cannot convert shortcode #{inspect(shortcode)} to integer"
end
end

@doc false
@spec prefix_separator :: binary
def prefix_separator(), do: @prefix_separator
def prefix_separator() do
Application.get_env(:shortcode, :prefix_separator, @prefix_separator)
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Shortcode.MixProject do
use Mix.Project

@source_url "https://github.com/elielhaouzi/shortcode"
@version "0.7.1"
@version "0.8.0"

def project do
[
Expand Down
36 changes: 36 additions & 0 deletions test/ecto/types/id_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ defmodule Shortcode.Ecto.IDTest do
assert {:ok, ^shortcode} = EctoTypeShortcodeID.cast(id, %{prefix: prefix})
end

test "with a valid shortcode with a prefix and a separator" do
id = 1
prefix = "prefix"
separator = "-"
shortcode = Shortcode.to_shortcode!(id, prefix, prefix_separator: separator)

assert {:ok, ^shortcode} =
EctoTypeShortcodeID.cast(id, %{prefix: prefix, separator: separator})
end

test "with a valid integer returns an {:ok, shortcode} tuple" do
assert {:ok, "0"} = EctoTypeShortcodeID.cast(0, %{})
end
Expand Down Expand Up @@ -55,6 +65,19 @@ defmodule Shortcode.Ecto.IDTest do
assert {:ok, ^shortcode} = EctoTypeShortcodeID.load(id, fn -> :noop end, %{prefix: prefix})
end

test "with a valid integer with a prefix and a separator returns an :ok tuple" do
prefix = "prefix"
id = 0
separator = "-"
shortcode = Shortcode.to_shortcode!(id, prefix, prefix_separator: separator)

assert {:ok, ^shortcode} =
EctoTypeShortcodeID.load(id, fn -> :noop end, %{
prefix: prefix,
separator: separator
})
end

test "with nil returns an :ok nil tuple" do
assert {:ok, nil} = EctoTypeShortcodeID.load(nil, fn -> :noop end, %{})
end
Expand Down Expand Up @@ -84,6 +107,19 @@ defmodule Shortcode.Ecto.IDTest do
EctoTypeShortcodeID.dump(shortcode, fn -> :noop end, %{prefix: "prefix"})
end

test "with an valid shortcode with prefix and a separator returns a ok tuple with the raw data" do
id = 0
prefix = "prefix"
separator = "-"
shortcode = Shortcode.to_shortcode!(id, prefix, prefix_separator: separator)

assert {:ok, ^id} =
EctoTypeShortcodeID.dump(shortcode, fn -> :noop end, %{
prefix: prefix,
separator: separator
})
end

test "with nil returns a :ok nil tuple" do
assert {:ok, nil} = EctoTypeShortcodeID.dump(nil, fn -> :noop end, %{})
end
Expand Down
Loading