diff --git a/README.md b/README.md index f9243f3..5bf0245 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,39 @@ -# ❄ FlakeID +# ❄ FlakeId > Decentralized, k-ordered ID generation service ## Installation Add `flake_id` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:flake_id, "~> 0.1.0"} ] end ``` ## Usage ```elixir -iex> FlakeID.get() +iex> FlakeId.get() "9n3171dJZpdD77K3DU" ``` See [https://hexdocs.pm/flake_id](https://hexdocs.pm/flake_id) for the complete documentation. ### With Ecto -It is possible to use `FlakeID` with [`Ecto`](https://github.com/elixir-ecto/ecto/). See [`FlakeID.Ecto.Type`](https://hexdocs.pm/flake_id/FlakeID.Ecto.Type.html) documentation for detailed instructions. +It is possible to use `FlakeId` with [`Ecto`](https://github.com/elixir-ecto/ecto/). See [`FlakeId.Ecto.Type`](https://hexdocs.pm/flake_id/FlakeId.Ecto.Type.html) documentation for detailed instructions. ## Prior Art * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License, * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0 ## Copyright and License Copyright © 2017-2019 [Pleroma Authors](https://pleroma.social/) -FlakeID source code is licensed under the GNU LGPLv3 License. +FlakeId source code is licensed under the GNU LGPLv3 License. diff --git a/lib/flake_id.ex b/lib/flake_id.ex index 997fcee..8253ee7 100644 --- a/lib/flake_id.ex +++ b/lib/flake_id.ex @@ -1,100 +1,100 @@ -# FlakeID: Decentralized, k-ordered ID generation service +# FlakeId: Decentralized, k-ordered ID generation service # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: LGPL-3.0-only -defmodule FlakeID do +defmodule FlakeId do @moduledoc """ Decentralized, k-ordered ID generation service. """ import Kernel, except: [to_string: 1] @doc """ Converts a binary Flake to a String ## Examples - iex> FlakeID.to_string(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>) + iex> FlakeId.to_string(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>) "9n2ciuz1wdesFnrGJU" """ def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = binary_flake) do <<integer::integer-size(128)>> = binary_flake Base62.encode(integer) end def to_string(string), do: string @doc """ Converts a String to a binary Flake ## Examples - iex> FlakeID.from_string("9n2ciuz1wdesFnrGJU") + iex> FlakeId.from_string("9n2ciuz1wdesFnrGJU") <<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>> """ @spec from_string(binary()) :: nil | <<_::128>> def from_string(string) def from_string(<<_::integer-size(128)>> = flake), do: flake def from_string(string) when is_binary(string) and byte_size(string) < 18, do: nil def from_string(string), do: string |> Base62.decode!() |> from_integer @doc """ Converts a binary Flake to an integer ## Examples - iex> FlakeID.to_integer(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>) + iex> FlakeId.to_integer(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>) 28939165907792829150732718047232 """ @spec to_integer(<<_::128>>) :: non_neg_integer def to_integer(binary_flake) def to_integer(<<integer::integer-size(128)>>), do: integer @doc """ Converts an integer to a binary Flake ## Examples - iex> FlakeID.from_integer(28939165907792829150732718047232) + iex> FlakeId.from_integer(28939165907792829150732718047232) <<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>> """ @spec from_integer(integer) :: <<_::128>> def from_integer(integer) do <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> = <<integer::integer-size(128)>> end @doc """ Generates a string with Flake """ @spec get :: String.t() - def get, do: FlakeID.Worker.get() |> to_string() + def get, do: FlakeId.Worker.get() |> to_string() @doc """ - Checks that ID is a valid FlakeID + Checks that ID is a valid FlakeId ## Examples - iex> FlakeID.flake_id?("9n2ciuz1wdesFnrGJU") + iex> FlakeId.flake_id?("9n2ciuz1wdesFnrGJU") true - iex> FlakeID.flake_id?("#cofe") + iex> FlakeId.flake_id?("#cofe") false - iex> FlakeID.flake_id?("pleroma.social") + iex> FlakeId.flake_id?("pleroma.social") false """ @spec flake_id?(String.t()) :: boolean def flake_id?(id), do: flake_id?(String.to_charlist(id), true) defp flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: flake_id?(cs, true) defp flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: flake_id?(cs, true) defp flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: flake_id?(cs, true) defp flake_id?([], true), do: true defp flake_id?(_, _), do: false end diff --git a/lib/flake_id/application.ex b/lib/flake_id/application.ex index 1b8e223..c87f562 100644 --- a/lib/flake_id/application.ex +++ b/lib/flake_id/application.ex @@ -1,22 +1,22 @@ -# FlakeID: Decentralized, k-ordered ID generation service +# FlakeId: Decentralized, k-ordered ID generation service # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: LGPL-3.0-only -defmodule FlakeID.Application do +defmodule FlakeId.Application do # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications @moduledoc false use Application def start(_type, _args) do children = [ - FlakeID.Worker + FlakeId.Worker ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options - opts = [strategy: :one_for_one, name: FlakeID.Supervisor] + opts = [strategy: :one_for_one, name: FlakeId.Supervisor] Supervisor.start_link(children, opts) end end diff --git a/lib/flake_id/ecto/compat_type.ex b/lib/flake_id/ecto/compat_type.ex index 1f3fdb1..8e6d8a6 100644 --- a/lib/flake_id/ecto/compat_type.ex +++ b/lib/flake_id/ecto/compat_type.ex @@ -1,68 +1,68 @@ -# FlakeID: Decentralized, k-ordered ID generation service +# FlakeId: Decentralized, k-ordered ID generation service # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: LGPL-3.0-only -defmodule FlakeID.Ecto.CompatType do +defmodule FlakeId.Ecto.CompatType do @moduledoc """ Provides a compatibility Ecto type for someone wishes to migrate from integer ids to flakes. ## Schema Example - @primary_key {:id, FlakeID.Ecto.CompatType, autogenerate: true} + @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} schema "posts" do add :body, :string end """ import Kernel, except: [to_string: 1] @behaviour Ecto.Type def embed_as(_), do: :self def equal?(term1, term2), do: term1 == term2 def type, do: :uuid - defdelegate autogenerate, to: FlakeID, as: :get + defdelegate autogenerate, to: FlakeId, as: :get def cast(value) do {:ok, to_string(value)} end def load(value) do {:ok, to_string(value)} end def dump(value) do {:ok, from_string(value)} end defp to_string(<<0::integer-size(64), id::integer-size(64)>>), do: Kernel.to_string(id) - defp to_string(binary_flake), do: FlakeID.to_string(binary_flake) + defp to_string(binary_flake), do: FlakeId.to_string(binary_flake) # zero or -1 is a null flake for i <- [-1, 0] do defp from_string(unquote(i)), do: <<0::integer-size(128)>> defp from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>> end defp from_string(int) when is_integer(int) do int |> Kernel.to_string() |> from_string() end defp from_string(<<_::integer-size(128)>> = flake), do: flake defp from_string(string) when is_binary(string) and byte_size(string) < 18 do case Integer.parse(string) do {id, ""} -> <<0::integer-size(64), id::integer-size(64)>> _ -> nil end end - defp from_string(string), do: FlakeID.from_string(string) + defp from_string(string), do: FlakeId.from_string(string) end diff --git a/lib/flake_id/ecto/type.ex b/lib/flake_id/ecto/type.ex index 42c3bab..914c864 100644 --- a/lib/flake_id/ecto/type.ex +++ b/lib/flake_id/ecto/type.ex @@ -1,46 +1,46 @@ -# FlakeID: Decentralized, k-ordered ID generation service +# FlakeId: Decentralized, k-ordered ID generation service # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: LGPL-3.0-only -defmodule FlakeID.Ecto.Type do +defmodule FlakeId.Ecto.Type do @moduledoc """ Provides a type for Ecto usage. The underlying data type should be an `:uuid`. ## Migration Example create table(:posts, primary_key: false) do add :id, :uuid, primary_key: true add :body, :text end ## Schema Example - @primary_key {:id, FlakeID.Ecto.Type, autogenerate: true} + @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true} schema "posts" do add :body, :string end """ @behaviour Ecto.Type def embed_as(_), do: :self def equal?(term1, term2), do: term1 == term2 def type, do: :uuid - defdelegate autogenerate, to: FlakeID, as: :get + defdelegate autogenerate, to: FlakeId, as: :get def cast(value) do - {:ok, FlakeID.to_string(value)} + {:ok, FlakeId.to_string(value)} end def load(value) do - {:ok, FlakeID.to_string(value)} + {:ok, FlakeId.to_string(value)} end def dump(value) do - {:ok, FlakeID.from_string(value)} + {:ok, FlakeId.from_string(value)} end end diff --git a/lib/flake_id/worker.ex b/lib/flake_id/worker.ex index b502389..c675255 100644 --- a/lib/flake_id/worker.ex +++ b/lib/flake_id/worker.ex @@ -1,70 +1,70 @@ -# FlakeID: Decentralized, k-ordered ID generation service +# FlakeId: Decentralized, k-ordered ID generation service # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: LGPL-3.0-only -defmodule FlakeID.Worker do +defmodule FlakeId.Worker do @moduledoc false use GenServer defstruct node: nil, time: 0, sq: 0 @type state :: %__MODULE__{ node: non_neg_integer, time: non_neg_integer, sq: non_neg_integer } def start_link(_) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @impl true @spec init([]) :: {:ok, state} def init([]) do {:ok, %__MODULE__{node: worker_id(), time: time()}} end @impl true def handle_call(:get, _from, state) do {flake, new_state} = get(time(), state) {:reply, flake, new_state} end @spec get :: binary def get, do: GenServer.call(__MODULE__, :get) # Matches when the calling time is the same as the state time. Incr. sq @spec get(non_neg_integer, state) :: {<<_::128>>, state} | {:error, :clock_running_backwards} def get(time, %__MODULE__{time: time} = state) do new_state = %__MODULE__{state | sq: state.sq + 1} {gen_flake(new_state), new_state} end # Matches when the times are different, reset sq def get(newtime, %__MODULE__{time: time} = state) when newtime > time do new_state = %__MODULE__{state | time: newtime, sq: 0} {gen_flake(new_state), new_state} end # Error when clock is running backwards def get(newtime, %__MODULE__{time: time}) when newtime < time do {:error, :clock_running_backwards} end @spec gen_flake(state) :: <<_::128>> def gen_flake(%__MODULE__{time: time, node: node, sq: seq}) do <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>> end def time do {mega_seconds, seconds, micro_seconds} = :erlang.timestamp() 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) end def worker_id do <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6) worker end end diff --git a/mix.exs b/mix.exs index d8c9784..3438a20 100644 --- a/mix.exs +++ b/mix.exs @@ -1,51 +1,51 @@ -defmodule FlakeID.MixProject do +defmodule FlakeId.MixProject do use Mix.Project def project do [ app: :flake_id, version: "0.1.0", description: "Decentralized, k-ordered ID generation service", elixir: "~> 1.8", start_permanent: Mix.env() == :prod, deps: deps(), package: package(), # Docs - name: "FlakeID", + name: "FlakeId", source_url: "https://git.pleroma.social/pleroma/flake_id", homepage_url: "https://git.pleroma.social/pleroma/flake_id", docs: [ main: "readme", extras: ["README.md"], source_url_pattern: "https://git.pleroma.social/pleroma/flake_id/blob/master/%{path}#L%{line}" ] ] end # Run "mix help compile.app" to learn about applications. def application do [ extra_applications: [:logger], - mod: {FlakeID.Application, []} + mod: {FlakeId.Application, []} ] end # Run "mix help deps" to learn about dependencies. defp deps do [ {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:ecto, ">= 2.0.0", optional: true}, {:ex_doc, "~> 0.21", only: :dev, runtime: false}, {:base62, "~> 1.2"} ] end defp package do [ licenses: ["LGPLv3"], links: %{"GitLab" => "https://git.pleroma.social/pleroma/flake_id"} ] end end diff --git a/test/flake_id/ecto/compat_type_test.exs b/test/flake_id/ecto/compat_type_test.exs index c5ea4ce..2261c14 100644 --- a/test/flake_id/ecto/compat_type_test.exs +++ b/test/flake_id/ecto/compat_type_test.exs @@ -1,57 +1,57 @@ -defmodule FlakeID.Ecto.CompatTypeTest do +defmodule FlakeId.Ecto.CompatTypeTest do use ExUnit.Case, async: true - alias FlakeID.Ecto.CompatType + alias FlakeId.Ecto.CompatType @flake_string "9n2ciuz1wdesFnrGJU" @flake_binary <<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>> @fake_flake <<0::integer-size(64), 42::integer-size(64)>> test "cast/1" do assert CompatType.cast(@flake_binary) == {:ok, @flake_string} assert CompatType.cast(@flake_string) == {:ok, @flake_string} end test "load/1" do assert CompatType.load(@flake_binary) == {:ok, @flake_string} assert CompatType.load(@flake_string) == {:ok, @flake_string} end test "dump/1" do assert CompatType.dump(@flake_string) == {:ok, @flake_binary} assert CompatType.dump(@flake_binary) == {:ok, @flake_binary} end test "equal?/2" do assert CompatType.equal?(@flake_binary, @flake_binary) == true end test "autogenerate/0" do flake = CompatType.autogenerate() assert String.valid?(flake) - assert FlakeID.flake_id?(flake) + assert FlakeId.flake_id?(flake) end describe "fake flakes (compatibility with older serial integers)" do test "with an integer" do assert CompatType.dump(42) == {:ok, @fake_flake} end test "with an integer as string" do assert CompatType.dump("42") == {:ok, @fake_flake} end test "zero or -1 is a null flake" do null_flake = <<0::integer-size(128)>> assert CompatType.dump(0) == {:ok, null_flake} assert CompatType.dump(-1) == {:ok, null_flake} assert CompatType.dump("0") == {:ok, null_flake} assert CompatType.dump("-1") == {:ok, null_flake} end test "cast" do assert CompatType.cast(@fake_flake) == {:ok, "42"} end end end diff --git a/test/flake_id/ecto/type_test.exs b/test/flake_id/ecto/type_test.exs index d73792c..8ea21a7 100644 --- a/test/flake_id/ecto/type_test.exs +++ b/test/flake_id/ecto/type_test.exs @@ -1,34 +1,34 @@ -defmodule FlakeID.Ecto.TypeTest do +defmodule FlakeId.Ecto.TypeTest do use ExUnit.Case, async: true - alias FlakeID.Ecto.Type + alias FlakeId.Ecto.Type @flake_string "9n2ciuz1wdesFnrGJU" @flake_binary <<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>> test "cast/1" do assert Type.cast(@flake_binary) == {:ok, @flake_string} assert Type.cast(@flake_string) == {:ok, @flake_string} end test "load/1" do assert Type.load(@flake_binary) == {:ok, @flake_string} assert Type.load(@flake_string) == {:ok, @flake_string} end test "dump/1" do assert Type.dump(@flake_string) == {:ok, @flake_binary} assert Type.dump(@flake_binary) == {:ok, @flake_binary} end test "equal?/2" do assert Type.equal?(@flake_binary, @flake_binary) == true end test "autogenerate/0" do flake = Type.autogenerate() assert String.valid?(flake) - assert FlakeID.flake_id?(flake) + assert FlakeId.flake_id?(flake) end end diff --git a/test/flake_id/worker_test.exs b/test/flake_id/worker_test.exs index 046288c..e4d8b6b 100644 --- a/test/flake_id/worker_test.exs +++ b/test/flake_id/worker_test.exs @@ -1,38 +1,38 @@ -defmodule FlakeID.WorkerTest do +defmodule FlakeId.WorkerTest do use ExUnit.Case, async: true - alias FlakeID.Worker + alias FlakeId.Worker test "get/1" do flake = Worker.get() assert is_binary(flake) assert <<_::integer-size(128)>> = flake end describe "get/2" do test "increment `:sq` when the calling time is the same as the state time" do time = Worker.time() node = Worker.worker_id() state = %Worker{time: time, node: node, sq: 0} assert {<<_::integer-size(128)>>, %Worker{node: ^node, time: ^time, sq: 1}} = Worker.get(time, state) end test "reset `:sq` when the times are different" do time = Worker.time() node = Worker.worker_id() state = %Worker{time: time - 1, node: node, sq: 42} assert {<<_::integer-size(128)>>, %Worker{time: ^time, sq: 0}} = Worker.get(time, state) end test "wrror when clock is running backwards" do time = Worker.time() state = %Worker{time: time + 1} assert Worker.get(time, state) == {:error, :clock_running_backwards} end end end diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs index 927088d..c796377 100644 --- a/test/flake_id_test.exs +++ b/test/flake_id_test.exs @@ -1,57 +1,57 @@ -defmodule FlakeIDTest do +defmodule FlakeIdTest do use ExUnit.Case, async: true - doctest FlakeID + doctest FlakeId @flake_string "9n2ciuz1wdesFnrGJU" @flake_binary <<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>> @flake_integer 28_939_165_907_792_829_150_732_718_047_232 test "flake_id?/1" do - assert FlakeID.flake_id?(@flake_string) - refute FlakeID.flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b") - refute FlakeID.flake_id?("#cofe") + assert FlakeId.flake_id?(@flake_string) + refute FlakeId.flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b") + refute FlakeId.flake_id?("#cofe") end test "get/0" do - flake = FlakeID.get() + flake = FlakeId.get() assert String.valid?(flake) - assert FlakeID.flake_id?(flake) + assert FlakeId.flake_id?(flake) end describe "to_string/1" do test "with binary" do - assert FlakeID.to_string(@flake_binary) == @flake_string + assert FlakeId.to_string(@flake_binary) == @flake_string bin = <<1::integer-size(64), 2::integer-size(48), 3::integer-size(16)>> - assert FlakeID.to_string(bin) == "LygHa16ApeN" + assert FlakeId.to_string(bin) == "LygHa16ApeN" end test "does nothing with other types" do - assert FlakeID.to_string("cofe") == "cofe" - assert FlakeID.to_string(42) == 42 + assert FlakeId.to_string("cofe") == "cofe" + assert FlakeId.to_string(42) == 42 end end describe "from_string/1" do test "with a flake string" do - assert FlakeID.from_string(@flake_string) == @flake_binary + assert FlakeId.from_string(@flake_string) == @flake_binary end test "with a flake binary" do - assert FlakeID.from_string(@flake_binary) == @flake_binary + assert FlakeId.from_string(@flake_binary) == @flake_binary end test "with a non flake string" do - assert FlakeID.from_string("cofe") == nil + assert FlakeId.from_string("cofe") == nil end end test "to_integer/1" do - assert FlakeID.to_integer(@flake_binary) == @flake_integer + assert FlakeId.to_integer(@flake_binary) == @flake_integer end test "from_integer/1" do - assert FlakeID.from_integer(@flake_integer) == @flake_binary + assert FlakeId.from_integer(@flake_integer) == @flake_binary end end