From 3c0d6e2be7b30bc1f521b43edf6ba6da4a0fe295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 21:52:00 +1100 Subject: [PATCH 01/35] Implement Pika Elixir --- impl/ex/.formatter.exs | 4 ++ impl/ex/.gitignore | 26 +++++++++++++ impl/ex/README.md | 21 +++++++++++ impl/ex/config/config.exs | 3 ++ impl/ex/config/test.exs | 8 ++++ impl/ex/lib/pika.ex | 66 +++++++++++++++++++++++++++++++++ impl/ex/lib/snowflake.ex | 71 ++++++++++++++++++++++++++++++++++++ impl/ex/lib/utils.ex | 19 ++++++++++ impl/ex/mix.exs | 28 ++++++++++++++ impl/ex/mix.lock | 3 ++ impl/ex/test/pika_test.exs | 55 ++++++++++++++++++++++++++++ impl/ex/test/test_helper.exs | 1 + 12 files changed, 305 insertions(+) create mode 100644 impl/ex/.formatter.exs create mode 100644 impl/ex/.gitignore create mode 100644 impl/ex/README.md create mode 100644 impl/ex/config/config.exs create mode 100644 impl/ex/config/test.exs create mode 100644 impl/ex/lib/pika.ex create mode 100644 impl/ex/lib/snowflake.ex create mode 100644 impl/ex/lib/utils.ex create mode 100644 impl/ex/mix.exs create mode 100644 impl/ex/mix.lock create mode 100644 impl/ex/test/pika_test.exs create mode 100644 impl/ex/test/test_helper.exs diff --git a/impl/ex/.formatter.exs b/impl/ex/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/impl/ex/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/impl/ex/.gitignore b/impl/ex/.gitignore new file mode 100644 index 0000000..823a7d9 --- /dev/null +++ b/impl/ex/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +pika-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/impl/ex/README.md b/impl/ex/README.md new file mode 100644 index 0000000..4f03daa --- /dev/null +++ b/impl/ex/README.md @@ -0,0 +1,21 @@ +# Pika + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `pika` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:pika, "~> 0.1.0"} + ] +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 . + diff --git a/impl/ex/config/config.exs b/impl/ex/config/config.exs new file mode 100644 index 0000000..a7cc8c8 --- /dev/null +++ b/impl/ex/config/config.exs @@ -0,0 +1,3 @@ +import Config + +import_config "test.exs" diff --git a/impl/ex/config/test.exs b/impl/ex/config/test.exs new file mode 100644 index 0000000..f30815b --- /dev/null +++ b/impl/ex/config/test.exs @@ -0,0 +1,8 @@ +import Config + +config :pika, + # epoch: 1_650_153_600_000, + prefixes: [ + %{prefix: "user", description: "User IDs"}, + %{prefix: "server", description: "Server IDs", secure: true} + ] diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex new file mode 100644 index 0000000..d204440 --- /dev/null +++ b/impl/ex/lib/pika.ex @@ -0,0 +1,66 @@ +defmodule Pika do + alias Pika.Snowflake + + @spec is_valid_prefix?(binary()) :: boolean() + defp is_valid_prefix?(prefix) do + # Checks if `prefix` is alphanumeric + Regex.match?(~r/^[0-9A-Za-z]+$/, prefix) + end + + @spec gen(binary()) :: {:error, binary()} | {:ok, binary()} + def gen(prefix) do + case is_valid_prefix?(prefix) do + false -> + {:error, "Prefix is invalid (must be Alphanumeric)"} + + true -> + prefixes = Application.get_env(:pika, :prefixes) + + case Enum.filter(prefixes, fn m -> m.prefix == prefix end) do + [] -> + {:error, "Prefix is undefined"} + + [prefix_record] -> + snowflake = Snowflake.generate() |> Integer.to_string() + + unless prefix_record[:secure] do + {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} + else + bytes = :rand.bytes(16) + + tail = + "s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{Base.encode64(snowflake, padding: false)}" + + {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} + end + end + end + end + + def gen!(prefix) do + {:ok, id} = gen(prefix) + + id + end + + def gen do + {:error, "No prefix was specified"} + end + + def deconstruct(id) do + prefixes = Application.get_env(:pika, :prefixes) + + fragments = id |> String.split("_") + [prefix, tail] = fragments + + [prefix_record] = Enum.filter(prefixes, fn m -> m.prefix == prefix end) + IO.puts tail + decoded_tail = Base.decode64!(tail, padding: false) + tail_fragments = decoded_tail |> String.split("_") + snowflake = tail_fragments |> Enum.at(length(tail_fragments) - 1) + + decoded_snowflake = Snowflake.decode(snowflake) + + Map.merge(decoded_snowflake, %{prefix: prefix, tail: tail, snowflake: snowflake, prefix_record: prefix_record}) + end +end diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex new file mode 100644 index 0000000..aba971e --- /dev/null +++ b/impl/ex/lib/snowflake.ex @@ -0,0 +1,71 @@ +defmodule Pika.Snowflake do + import Bitwise + alias Pika.Utils + use GenServer + + def start_link(%{"epoch" => epoch}) when is_integer(epoch) do + GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) + end + + def start_link() do + # State: {node_id, epoch, seq, last_sequence_exhaustion} + GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, + name: __MODULE__ + ) + end + + def init(state) do + {:ok, state} + end + + def generate do + GenServer.call(__MODULE__, :generate) + end + + def decode(snowflake) when is_binary(snowflake) do + snowflake |> Base.decode64!(padding: false)|> String.to_integer() |> decode() + end + + def decode(snowflake) when is_integer(snowflake) do + GenServer.call(__MODULE__, {:decode, snowflake}) + end + + def handle_call( + {:decode, snowflake}, + _from, + state = {_node_id, epoch, _seq, _last_seq_exhaustion} + ) do + timestamp = (snowflake >>> 22) + epoch + node_id = snowflake >>> 12 &&& 0b11_1111_1111 + seq = snowflake &&& 0b1111_1111_1111 + + {:reply, %{timestamp: timestamp, epoch: epoch, node_id: node_id, seq: seq}, state} + end + + def handle_call(:generate, _from, {node_id, epoch, seq, last_seq_exhaustion}) do + now = now_ts() + + if seq >= 4095 and now == last_seq_exhaustion do + :timer.sleep(1) + end + + snowflake = (now - epoch) <<< 22 ||| node_id <<< 12 ||| seq + + seq = + if seq >= 4095 do + 0 + else + seq + 1 + end + + if now === last_seq_exhaustion do + {:reply, snowflake, {node_id, epoch, seq, now}} + else + {:reply, snowflake, {node_id, epoch, seq, now_ts()}} + end + end + + def now_ts do + System.os_time(:millisecond) + end +end diff --git a/impl/ex/lib/utils.ex b/impl/ex/lib/utils.ex new file mode 100644 index 0000000..df2c435 --- /dev/null +++ b/impl/ex/lib/utils.ex @@ -0,0 +1,19 @@ +defmodule Pika.Utils do + def get_mac_address do + {:ok, addresses} = :inet.getifaddrs() + + addresses + |> Enum.filter(fn {name, _opts} -> name != "lo" end) + |> Enum.map(fn {_name, data} -> data[:hwaddr] end) + |> List.first() + |> Enum.map(&Integer.to_string(&1, 16)) + |> Enum.join(":") + end + + @spec compute_node_id() :: integer() + def compute_node_id do + {id, _} = get_mac_address() |> String.replace(":", "") |> Integer.parse(16) + + rem(id, 1024) + end +end diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs new file mode 100644 index 0000000..ce6f9d6 --- /dev/null +++ b/impl/ex/mix.exs @@ -0,0 +1,28 @@ +defmodule Pika.MixProject do + use Mix.Project + + def project do + [ + app: :pika, + version: "0.1.0", + elixir: "~> 1.16", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/impl/ex/mix.lock b/impl/ex/mix.lock new file mode 100644 index 0000000..d5ce558 --- /dev/null +++ b/impl/ex/mix.lock @@ -0,0 +1,3 @@ +%{ + "net_address": {:hex, :net_address, "0.3.0", "0fd8bdccdcb74986b7e808bc1f99a7cf4bbc8232bffd6958e18a963500adb541", [:mix], [], "hexpm", "678886a834e031009eda8a45f3e2cbda94a20a1e5fbc174e88e3f031eeb62c5f"}, +} diff --git a/impl/ex/test/pika_test.exs b/impl/ex/test/pika_test.exs new file mode 100644 index 0000000..ed08f72 --- /dev/null +++ b/impl/ex/test/pika_test.exs @@ -0,0 +1,55 @@ +defmodule PikaTest do + use ExUnit.Case + doctest Pika + + setup do + Pika.Snowflake.start_link() + + :ok + end + + test "Generate an ID" do + {:ok, id} = Pika.gen("user") + + assert String.starts_with?(id, "user") + end + + test "Generate a secure ID" do + {:ok, id} = Pika.gen("server") + + assert String.starts_with?(id, "server") + end + + test "Fail to generate ID" do + {status, _message} = Pika.gen("not_found") + + assert status == :error + end + + test "Fail to validate ID" do + {:error, message} = Pika.gen("!!!") + + assert message == "Prefix is invalid (must be Alphanumeric)" + end + + test "Test 4096+ ids" do + Enum.map(0..4095, fn s -> + snowflake = Pika.Snowflake.generate() + {_timestamp, _node_id, seq} = Pika.Snowflake.decode(snowflake) + + assert seq == s + end) + + last_snowflake = Pika.Snowflake.decode() + {_timestamp, _node_id, last_sequence} = Pika.Snowflake.deconstruct(last_snowflake) + + assert last_sequence == 0 + end + + test "Validate node_id" do + id = Pika.gen("user") + deconstructed = Pika.deconstruct(id) + + deconstructed.node_id == Pika.Utils.compute_node_id() + end +end diff --git a/impl/ex/test/test_helper.exs b/impl/ex/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/impl/ex/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() From 8650e857600db6eb822db14e0a7783d69ce41812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 21:53:33 +1100 Subject: [PATCH 02/35] Fix Elixir tests --- impl/ex/test/pika_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/impl/ex/test/pika_test.exs b/impl/ex/test/pika_test.exs index ed08f72..b79cbfd 100644 --- a/impl/ex/test/pika_test.exs +++ b/impl/ex/test/pika_test.exs @@ -9,13 +9,13 @@ defmodule PikaTest do end test "Generate an ID" do - {:ok, id} = Pika.gen("user") + id = Pika.gen!("user") assert String.starts_with?(id, "user") end test "Generate a secure ID" do - {:ok, id} = Pika.gen("server") + id = Pika.gen!("server") assert String.starts_with?(id, "server") end @@ -35,19 +35,19 @@ defmodule PikaTest do test "Test 4096+ ids" do Enum.map(0..4095, fn s -> snowflake = Pika.Snowflake.generate() - {_timestamp, _node_id, seq} = Pika.Snowflake.decode(snowflake) + %{"seq" => seq} = Pika.Snowflake.decode(snowflake) assert seq == s end) last_snowflake = Pika.Snowflake.decode() - {_timestamp, _node_id, last_sequence} = Pika.Snowflake.deconstruct(last_snowflake) + %{"seq" => seq} = Pika.Snowflake.deconstruct(last_snowflake) assert last_sequence == 0 end test "Validate node_id" do - id = Pika.gen("user") + id = Pika.gen!("user") deconstructed = Pika.deconstruct(id) deconstructed.node_id == Pika.Utils.compute_node_id() From 0345a8bf888645383db28d34e1622393155ddce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:09:28 +1100 Subject: [PATCH 03/35] Fix encoding I wasn't supposed to encode the snowflake in the secure ids, fixed now! --- impl/ex/lib/pika.ex | 3 +-- impl/ex/lib/snowflake.ex | 8 +++----- impl/ex/test/pika_test.exs | 10 +++++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index d204440..e7ff77f 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -29,7 +29,7 @@ defmodule Pika do bytes = :rand.bytes(16) tail = - "s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{Base.encode64(snowflake, padding: false)}" + "s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{snowflake}" {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} end @@ -54,7 +54,6 @@ defmodule Pika do [prefix, tail] = fragments [prefix_record] = Enum.filter(prefixes, fn m -> m.prefix == prefix end) - IO.puts tail decoded_tail = Base.decode64!(tail, padding: false) tail_fragments = decoded_tail |> String.split("_") snowflake = tail_fragments |> Enum.at(length(tail_fragments) - 1) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index aba971e..deba4c2 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -22,11 +22,7 @@ defmodule Pika.Snowflake do GenServer.call(__MODULE__, :generate) end - def decode(snowflake) when is_binary(snowflake) do - snowflake |> Base.decode64!(padding: false)|> String.to_integer() |> decode() - end - - def decode(snowflake) when is_integer(snowflake) do + def decode(snowflake) do GenServer.call(__MODULE__, {:decode, snowflake}) end @@ -35,6 +31,8 @@ defmodule Pika.Snowflake do _from, state = {_node_id, epoch, _seq, _last_seq_exhaustion} ) do + snowflake = snowflake |> String.to_integer() + timestamp = (snowflake >>> 22) + epoch node_id = snowflake >>> 12 &&& 0b11_1111_1111 seq = snowflake &&& 0b1111_1111_1111 diff --git a/impl/ex/test/pika_test.exs b/impl/ex/test/pika_test.exs index b79cbfd..5f83c04 100644 --- a/impl/ex/test/pika_test.exs +++ b/impl/ex/test/pika_test.exs @@ -34,16 +34,16 @@ defmodule PikaTest do test "Test 4096+ ids" do Enum.map(0..4095, fn s -> - snowflake = Pika.Snowflake.generate() - %{"seq" => seq} = Pika.Snowflake.decode(snowflake) + id = Pika.gen!("user") + %{"seq" => seq} = Pika.deconstruct(id) assert seq == s end) - last_snowflake = Pika.Snowflake.decode() - %{"seq" => seq} = Pika.Snowflake.deconstruct(last_snowflake) + last_id = Pika.gen!("user") + %{"seq" => last_seq} = Pika.deconstruct(last_id) - assert last_sequence == 0 + assert last_seq == 0 end test "Validate node_id" do From 54e0cf16eb5d2fcd466efa7a14d76554b13d45d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:20:19 +1100 Subject: [PATCH 04/35] Update Elixir readme --- impl/ex/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/impl/ex/README.md b/impl/ex/README.md index 4f03daa..4275aa1 100644 --- a/impl/ex/README.md +++ b/impl/ex/README.md @@ -1,11 +1,16 @@ # Pika -**TODO: Add description** +Combine Stripe IDs with Snowflakes you get Pika! The last ID system you'll ever need! +Combining pragmatism with functionality + +## Features + +- Written in pure Elixir +- Zero Dependencies ## Installation -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `pika` to your list of dependencies in `mix.exs`: +The package can be installed by adding `pika` to your list of dependencies in `mix.exs`: ```elixir def deps do @@ -14,8 +19,3 @@ 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 . - From 7c5845384ffc0db5bceb1a14b7c121a06649b345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:32:02 +1100 Subject: [PATCH 05/35] Fix tests --- impl/ex/test/pika_test.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/impl/ex/test/pika_test.exs b/impl/ex/test/pika_test.exs index 5f83c04..63b3ea7 100644 --- a/impl/ex/test/pika_test.exs +++ b/impl/ex/test/pika_test.exs @@ -35,21 +35,21 @@ defmodule PikaTest do test "Test 4096+ ids" do Enum.map(0..4095, fn s -> id = Pika.gen!("user") - %{"seq" => seq} = Pika.deconstruct(id) + deconstructed = Pika.deconstruct(id) - assert seq == s + assert deconstructed.seq == s end) last_id = Pika.gen!("user") - %{"seq" => last_seq} = Pika.deconstruct(last_id) + deconstructed = Pika.deconstruct(last_id) - assert last_seq == 0 + assert deconstructed.seq == 0 end test "Validate node_id" do id = Pika.gen!("user") deconstructed = Pika.deconstruct(id) - deconstructed.node_id == Pika.Utils.compute_node_id() + assert deconstructed.node_id == Pika.Utils.compute_node_id() end end From 80ec1328a64c40b710a383398cce6891b598e9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:32:38 +1100 Subject: [PATCH 06/35] Update secure id Didn't notice this in the Rust implementation (yes, I used the Rust impl as my reference) --- impl/ex/lib/pika.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index e7ff77f..bfba5cb 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -29,7 +29,7 @@ defmodule Pika do bytes = :rand.bytes(16) tail = - "s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{snowflake}" + "_s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{snowflake}" {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} end From 2db83d7e7c14a9cf3972e522825cf288faf7a61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:45:04 +1100 Subject: [PATCH 07/35] Update Elixir package details --- impl/ex/mix.exs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index ce6f9d6..cc48719 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -7,6 +7,10 @@ defmodule Pika.MixProject do version: "0.1.0", elixir: "~> 1.16", start_permanent: Mix.env() == :prod, + package: package(), + description: """ + Elixir implementation of hop.io's Pika. Combine Stripe IDs and Snowflakes. + """, deps: deps() ] end @@ -18,6 +22,16 @@ defmodule Pika.MixProject do ] end + def package do + [ + files: ["lib", "mix.exs", "README.md"], + licenses: ["ISC"], + links: %{ + "GitHub" => "https://github.com/hopinc/pika/tree/main/impl/ex" + } + ] + end + # Run "mix help deps" to learn about dependencies. defp deps do [ From 2f73ffcdb6518df2c07c7675ebd0bd73079fe44b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Fri, 16 Feb 2024 22:52:36 +1100 Subject: [PATCH 08/35] Update Elixir package details (again) --- impl/ex/mix.exs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index cc48719..6d2f542 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -1,6 +1,8 @@ defmodule Pika.MixProject do use Mix.Project + @source_url "https://github.com/ArtieFuzzz/pika/tree/ex-impl/impl/ex" + def project do [ app: :pika, @@ -11,14 +13,19 @@ defmodule Pika.MixProject do description: """ Elixir implementation of hop.io's Pika. Combine Stripe IDs and Snowflakes. """, + docs: docs(), deps: deps() ] end # Run "mix help compile.app" to learn about applications. def application do + [] + end + + def docs do [ - extra_applications: [:logger] + source_url: @source_url ] end @@ -27,7 +34,7 @@ defmodule Pika.MixProject do files: ["lib", "mix.exs", "README.md"], licenses: ["ISC"], links: %{ - "GitHub" => "https://github.com/hopinc/pika/tree/main/impl/ex" + "GitHub" => @source_url } ] end From db1ad8d1abdd806033c38d141d2c4c67a828f1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 09:10:18 +1100 Subject: [PATCH 09/35] Update cases in `gen/1` --- impl/ex/lib/pika.ex | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index bfba5cb..effe017 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -10,16 +10,10 @@ defmodule Pika do @spec gen(binary()) :: {:error, binary()} | {:ok, binary()} def gen(prefix) do case is_valid_prefix?(prefix) do - false -> - {:error, "Prefix is invalid (must be Alphanumeric)"} - true -> prefixes = Application.get_env(:pika, :prefixes) case Enum.filter(prefixes, fn m -> m.prefix == prefix end) do - [] -> - {:error, "Prefix is undefined"} - [prefix_record] -> snowflake = Snowflake.generate() |> Integer.to_string() @@ -33,7 +27,13 @@ defmodule Pika do {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} end + + _ -> + {:error, "Prefix is undefined"} end + + _ -> + {:error, "Prefix is invalid (must be Alphanumeric)"} end end @@ -60,6 +60,11 @@ defmodule Pika do decoded_snowflake = Snowflake.decode(snowflake) - Map.merge(decoded_snowflake, %{prefix: prefix, tail: tail, snowflake: snowflake, prefix_record: prefix_record}) + Map.merge(decoded_snowflake, %{ + prefix: prefix, + tail: tail, + snowflake: snowflake, + prefix_record: prefix_record + }) end end From 94cb6045f0d35a8935fca9145d9271a1b618b616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 13:56:14 +1100 Subject: [PATCH 10/35] Add credo Credo is a static analysis tool. https://github.com/rrrene/credo --- impl/ex/.credo.exs | 169 +++++++++++++++++++++++++++++++++++++++++++++ impl/ex/mix.exs | 1 + impl/ex/mix.lock | 5 +- 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 impl/ex/.credo.exs diff --git a/impl/ex/.credo.exs b/impl/ex/.credo.exs new file mode 100644 index 0000000..d10e343 --- /dev/null +++ b/impl/ex/.credo.exs @@ -0,0 +1,169 @@ +%{ + configs: [ + %{ + name: "default", + files: %{ + included: [ + "lib/", + "src/", + "test/", + "web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + plugins: [], + requires: [], + strict: true, + parse_timeout: 5000, + color: false, + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 6d2f542..80eb015 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -42,6 +42,7 @@ defmodule Pika.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/impl/ex/mix.lock b/impl/ex/mix.lock index d5ce558..1190dd5 100644 --- a/impl/ex/mix.lock +++ b/impl/ex/mix.lock @@ -1,3 +1,6 @@ %{ - "net_address": {:hex, :net_address, "0.3.0", "0fd8bdccdcb74986b7e808bc1f99a7cf4bbc8232bffd6958e18a963500adb541", [:mix], [], "hexpm", "678886a834e031009eda8a45f3e2cbda94a20a1e5fbc174e88e3f031eeb62c5f"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.4", "68ca5cf89071511c12fd9919eb84e388d231121988f6932756596195ccf7fd35", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9cf776d062c78bbe0f0de1ecaee183f18f2c3ec591326107989b054b7dddefc2"}, + "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, + "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, } From 0030e362bd7ac1087b1fff6a99ac40b58ab2c47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 14:16:16 +1100 Subject: [PATCH 11/35] Refactor code Credo threw a couple refactor suggestions and I have updated the modules --- impl/ex/lib/pika.ex | 36 +++++++++++++++++++++--------------- impl/ex/lib/snowflake.ex | 4 +++- impl/ex/lib/utils.ex | 9 +++++++-- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index effe017..e5cd70b 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -1,33 +1,39 @@ defmodule Pika do alias Pika.Snowflake + import Pika.Utils, only: [filter_prefixes: 2] - @spec is_valid_prefix?(binary()) :: boolean() - defp is_valid_prefix?(prefix) do + @moduledoc false + + @spec valid_prefix?(binary()) :: boolean() + defp valid_prefix?(prefix) do # Checks if `prefix` is alphanumeric Regex.match?(~r/^[0-9A-Za-z]+$/, prefix) end + defp _gen(prefix, snowflake, secure) do + unless secure do + {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} + end + + bytes = :rand.bytes(16) + + tail = + "_s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{snowflake}" + + {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} + end + @spec gen(binary()) :: {:error, binary()} | {:ok, binary()} def gen(prefix) do - case is_valid_prefix?(prefix) do + case valid_prefix?(prefix) do true -> prefixes = Application.get_env(:pika, :prefixes) - case Enum.filter(prefixes, fn m -> m.prefix == prefix end) do + case filter_prefixes(prefix, prefixes) do [prefix_record] -> snowflake = Snowflake.generate() |> Integer.to_string() - unless prefix_record[:secure] do - {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} - else - bytes = :rand.bytes(16) - - tail = - "_s_#{Base.encode32(bytes, padding: false, case: :lower)}_#{snowflake}" - - {:ok, "#{prefix}_#{Base.encode64(tail, padding: false)}"} - end - + _gen(prefix, snowflake, prefix_record[:secure]) _ -> {:error, "Prefix is undefined"} end diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index deba4c2..7880a44 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -3,11 +3,13 @@ defmodule Pika.Snowflake do alias Pika.Utils use GenServer + @moduledoc false + def start_link(%{"epoch" => epoch}) when is_integer(epoch) do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end - def start_link() do + def start_link do # State: {node_id, epoch, seq, last_sequence_exhaustion} GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__ diff --git a/impl/ex/lib/utils.ex b/impl/ex/lib/utils.ex index df2c435..ce24ec4 100644 --- a/impl/ex/lib/utils.ex +++ b/impl/ex/lib/utils.ex @@ -1,4 +1,6 @@ defmodule Pika.Utils do + @moduledoc false + def get_mac_address do {:ok, addresses} = :inet.getifaddrs() @@ -6,8 +8,7 @@ defmodule Pika.Utils do |> Enum.filter(fn {name, _opts} -> name != "lo" end) |> Enum.map(fn {_name, data} -> data[:hwaddr] end) |> List.first() - |> Enum.map(&Integer.to_string(&1, 16)) - |> Enum.join(":") + |> Enum.map_join(":", &Integer.to_string(&1, 16)) end @spec compute_node_id() :: integer() @@ -16,4 +17,8 @@ defmodule Pika.Utils do rem(id, 1024) end + + def filter_prefixes(prefix, prefixes) do + Enum.filter(prefixes, fn record -> record.prefix == prefix end) + end end From 0934a55df20ad641083a4ae232b2282d0d0afa2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 17:31:37 +1100 Subject: [PATCH 12/35] Add benchmark --- impl/ex/benchmarks/generation.exs | 14 ++++++++++++++ impl/ex/config/test.exs | 5 ++++- impl/ex/mix.exs | 3 ++- impl/ex/mix.lock | 3 +++ 4 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 impl/ex/benchmarks/generation.exs diff --git a/impl/ex/benchmarks/generation.exs b/impl/ex/benchmarks/generation.exs new file mode 100644 index 0000000..d2e60ea --- /dev/null +++ b/impl/ex/benchmarks/generation.exs @@ -0,0 +1,14 @@ +defmodule Generation do + def id(), do: Pika.gen("user") + def snowflake(), do: Pika.Snowflake.generate() +end + +Pika.Snowflake.start_link() + +Benchee.run( + %{ + "Generate IDs" => fn -> Generation.id() end, + "Generate Snowflakes" => fn -> Generation.snowflake() end + }, + time: 5 +) diff --git a/impl/ex/config/test.exs b/impl/ex/config/test.exs index f30815b..1f8aaa3 100644 --- a/impl/ex/config/test.exs +++ b/impl/ex/config/test.exs @@ -1,7 +1,10 @@ import Config + +config :benchee, + fast_warning: false + config :pika, - # epoch: 1_650_153_600_000, prefixes: [ %{prefix: "user", description: "User IDs"}, %{prefix: "server", description: "Server IDs", secure: true} diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 80eb015..b792c9c 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -42,7 +42,8 @@ defmodule Pika.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:benchee, "~> 1.0", only: :dev} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/impl/ex/mix.lock b/impl/ex/mix.lock index 1190dd5..3f98258 100644 --- a/impl/ex/mix.lock +++ b/impl/ex/mix.lock @@ -1,6 +1,9 @@ %{ + "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.4", "68ca5cf89071511c12fd9919eb84e388d231121988f6932756596195ccf7fd35", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9cf776d062c78bbe0f0de1ecaee183f18f2c3ec591326107989b054b7dddefc2"}, + "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, } From 591a1d0cdc792ed974910dbec3ff772477505d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 17:32:23 +1100 Subject: [PATCH 13/35] Add example in README --- impl/ex/README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/impl/ex/README.md b/impl/ex/README.md index 4275aa1..b5ed845 100644 --- a/impl/ex/README.md +++ b/impl/ex/README.md @@ -19,3 +19,31 @@ def deps do ] end ``` + +In your `config.exs`: + +```elixir +config :pika, + prefixes: [ + %{prefix: "user", description: "User IDs"}, + %{prefix: "server", description: "Server IDs", secure: true}, + # ... + ] +``` + +## Example + +`Pika.Snowflake` should be started under a `Supervisor` or `Application` before you start using +`Pika.gen/0` or `Pika.deconstruct/1` + +```elixir +defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [Pika.Snowflake] + + Supervisor.start_link(children, strategy: :one_for_one) + end +end +``` From d8ca6a1729b6c4bd124f36493e6fdc255ed68573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 17:50:07 +1100 Subject: [PATCH 14/35] Pattern match instead of using `unless` --- impl/ex/lib/pika.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index e5cd70b..796be09 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -10,11 +10,15 @@ defmodule Pika do Regex.match?(~r/^[0-9A-Za-z]+$/, prefix) end - defp _gen(prefix, snowflake, secure) do - unless secure do - {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} - end + defp _gen(prefix, snowflake, nil) do + {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} + end + defp _gen(prefix, snowflake, false) do + {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} + end + + defp _gen(prefix, snowflake, true) do bytes = :rand.bytes(16) tail = @@ -34,6 +38,7 @@ defmodule Pika do snowflake = Snowflake.generate() |> Integer.to_string() _gen(prefix, snowflake, prefix_record[:secure]) + _ -> {:error, "Prefix is undefined"} end From 67e49f652c2d8c72c27863453041fc0b68387a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 18:15:54 +1100 Subject: [PATCH 15/35] Add documentation --- impl/ex/config/test.exs | 1 - impl/ex/lib/pika.ex | 25 ++++++++++++++++++++ impl/ex/lib/snowflake.ex | 50 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/impl/ex/config/test.exs b/impl/ex/config/test.exs index 1f8aaa3..3114e42 100644 --- a/impl/ex/config/test.exs +++ b/impl/ex/config/test.exs @@ -1,6 +1,5 @@ import Config - config :benchee, fast_warning: false diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index 796be09..3ae4015 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -10,14 +10,17 @@ defmodule Pika do Regex.match?(~r/^[0-9A-Za-z]+$/, prefix) end + @doc false defp _gen(prefix, snowflake, nil) do {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} end + @doc false defp _gen(prefix, snowflake, false) do {:ok, "#{prefix}_#{Base.encode64(snowflake, padding: false)}"} end + @doc false defp _gen(prefix, snowflake, true) do bytes = :rand.bytes(16) @@ -28,6 +31,14 @@ defmodule Pika do end @spec gen(binary()) :: {:error, binary()} | {:ok, binary()} + @doc """ + Generates an ID given a prefix (which should be configured). + + This function will return an `{:error, binary()}` if one of the follow conditions are met: + + 1. The prefix isn't valid + 2. The prefix isn't configured + """ def gen(prefix) do case valid_prefix?(prefix) do true -> @@ -48,16 +59,30 @@ defmodule Pika do end end + @spec gen!(binary()) :: binary() def gen!(prefix) do {:ok, id} = gen(prefix) id end + @spec gen() :: {:error, binary()} def gen do {:error, "No prefix was specified"} end + @doc """ + Deconstructs a Pika ID and returns it's metadata: + + - prefix + - tail + - snowflake + - timestamp + - prefix_record + - epoch + - node_id + - seq + """ def deconstruct(id) do prefixes = Application.get_env(:pika, :prefixes) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index 7880a44..66bf00a 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -3,9 +3,41 @@ defmodule Pika.Snowflake do alias Pika.Utils use GenServer - @moduledoc false + @moduledoc """ + `Pika.Snowflake` holds the state, generates Snowflakes, and decodes Snowflakes. - def start_link(%{"epoch" => epoch}) when is_integer(epoch) do + `Pika.Snowflake` should be started under a `Supervisor` or `Application` before you start using + `Pika.gen/0` or `Pika.deconstruct/1` + + ```elixir + defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [Pika.Snowflake] + + Supervisor.start_link(children, strategy: :one_for_one) + end + end + ``` + + or manually in `iex` + + ```elixir + iex(1)> Pika.Snowflake.start_link() + {:ok, #PID<0.190.0>} + ``` + + ## Custom epoch + + You can start `Pika.Snowflake` with a custom epoch by passing it: + + ```elixir + Pika.Snowflake.start_link(1_650_153_600_000) + ``` + """ + + def start_link(epoch) when is_integer(epoch) do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end @@ -20,10 +52,23 @@ defmodule Pika.Snowflake do {:ok, state} end + @doc """ + Generates a new Snowflake + """ + @spec generate() :: integer() def generate do GenServer.call(__MODULE__, :generate) end + @doc """ + Decodes a Snowflake and returns: + + - timestamp + - epoch + - node_id + - seq + """ + @spec decode(integer()) :: any() def decode(snowflake) do GenServer.call(__MODULE__, {:decode, snowflake}) end @@ -65,6 +110,7 @@ defmodule Pika.Snowflake do end end + @doc "Returns the current timestamp in milliseconds." def now_ts do System.os_time(:millisecond) end From d1b179768776b1332447a77ea27440b7790cbd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 18:20:35 +1100 Subject: [PATCH 16/35] Update README --- impl/ex/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/impl/ex/README.md b/impl/ex/README.md index b5ed845..033881b 100644 --- a/impl/ex/README.md +++ b/impl/ex/README.md @@ -1,5 +1,7 @@ # Pika +> Elixir implementation of Pika + Combine Stripe IDs with Snowflakes you get Pika! The last ID system you'll ever need! Combining pragmatism with functionality From 7048b234c9dd99067346d41d308383ac5e63fc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 18:40:31 +1100 Subject: [PATCH 17/35] Add CI workflow --- .github/workflows/pr-ex-test.yml | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/pr-ex-test.yml diff --git a/.github/workflows/pr-ex-test.yml b/.github/workflows/pr-ex-test.yml new file mode 100644 index 0000000..0149793 --- /dev/null +++ b/.github/workflows/pr-ex-test.yml @@ -0,0 +1,47 @@ +name: Test Elixir implementation + +on: + pull_request: + branches: + - main + paths: + - "impl/ex/**" + - ".github/workflows/pr-ex-test.yml" + +jobs: + credo: + runs-on: ubuntu + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + elixir-version: "1.16.1" + - name: Run credo + run: | + cd impl/ex + + mix deps.get && mix deps.compile + + mix credo + + test: + runs-on: ubuntu + name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + strategy: + matrix: + otp: ["25", "26"] + elixir: ["1.16.1", "1.15.7", "1.14.0"] + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + - name: Run Tests + run: | + cd impl/ex + + mix deps.get && mix deps.compile + + mix test + From d2728b98a335a5bf588f14c738de751cb4fbe96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 19:24:11 +1100 Subject: [PATCH 18/35] Fix Snowflake blocking --- impl/ex/config/bench.exs | 4 ++++ impl/ex/config/config.exs | 9 ++++++++- impl/ex/config/test.exs | 3 --- impl/ex/lib/snowflake.ex | 32 +++++++++++++++++++++++--------- 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 impl/ex/config/bench.exs diff --git a/impl/ex/config/bench.exs b/impl/ex/config/bench.exs new file mode 100644 index 0000000..a452bfb --- /dev/null +++ b/impl/ex/config/bench.exs @@ -0,0 +1,4 @@ +import Config + +config :benchee, + fast_warning: false diff --git a/impl/ex/config/config.exs b/impl/ex/config/config.exs index a7cc8c8..f8b5d34 100644 --- a/impl/ex/config/config.exs +++ b/impl/ex/config/config.exs @@ -1,3 +1,10 @@ import Config -import_config "test.exs" +case config_env() do + :bench -> + import_config "bench.exs" + :docs -> + :ok + _ -> + import_config "test.exs" +end diff --git a/impl/ex/config/test.exs b/impl/ex/config/test.exs index 3114e42..36805fa 100644 --- a/impl/ex/config/test.exs +++ b/impl/ex/config/test.exs @@ -1,8 +1,5 @@ import Config -config :benchee, - fast_warning: false - config :pika, prefixes: [ %{prefix: "user", description: "User IDs"}, diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index 66bf00a..95bfcf5 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -57,7 +57,15 @@ defmodule Pika.Snowflake do """ @spec generate() :: integer() def generate do - GenServer.call(__MODULE__, :generate) + GenServer.call(__MODULE__, {:generate, now_ts()}) + end + + @doc """ + Generates a new Snowflake with the given `timestamp` + """ + @spec generate(integer()) :: integer() + def generate(timestamp) do + GenServer.call(__MODULE__, {:generate, timestamp}) end @doc """ @@ -87,14 +95,12 @@ defmodule Pika.Snowflake do {:reply, %{timestamp: timestamp, epoch: epoch, node_id: node_id, seq: seq}, state} end - def handle_call(:generate, _from, {node_id, epoch, seq, last_seq_exhaustion}) do - now = now_ts() - - if seq >= 4095 and now == last_seq_exhaustion do - :timer.sleep(1) + def handle_call({:generate, timestamp}, _from, {node_id, epoch, seq, last_seq_exhaustion}) do + if seq >= 4095 and timestamp == last_seq_exhaustion do + block(timestamp) end - snowflake = (now - epoch) <<< 22 ||| node_id <<< 12 ||| seq + snowflake = (timestamp - epoch) <<< 22 ||| node_id <<< 12 ||| seq seq = if seq >= 4095 do @@ -103,13 +109,21 @@ defmodule Pika.Snowflake do seq + 1 end - if now === last_seq_exhaustion do - {:reply, snowflake, {node_id, epoch, seq, now}} + if timestamp === last_seq_exhaustion do + {:reply, snowflake, {node_id, epoch, seq, timestamp}} else {:reply, snowflake, {node_id, epoch, seq, now_ts()}} end end + @doc false + defp block(timestamp) do + if now_ts() - timestamp < 1 do + :timer.sleep(100) + block(timestamp) + end + end + @doc "Returns the current timestamp in milliseconds." def now_ts do System.os_time(:millisecond) From 8a1047ba96d697dd2f8a1b5fbc13f7c2249ce631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Sat, 17 Feb 2024 19:51:47 +1100 Subject: [PATCH 19/35] Fix error when parsing non-integer snowflakes Found out that some generated snowflakes aren't integers, they're instead strings --- impl/ex/benchmarks/generation.exs | 2 ++ impl/ex/config/bench.exs | 4 ---- impl/ex/config/config.exs | 5 ++--- impl/ex/config/dev.exs | 7 +++++++ impl/ex/lib/pika.ex | 2 +- impl/ex/lib/snowflake.ex | 4 +--- impl/ex/test/pika_test.exs | 8 ++++++++ 7 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 impl/ex/config/bench.exs create mode 100644 impl/ex/config/dev.exs diff --git a/impl/ex/benchmarks/generation.exs b/impl/ex/benchmarks/generation.exs index d2e60ea..3143001 100644 --- a/impl/ex/benchmarks/generation.exs +++ b/impl/ex/benchmarks/generation.exs @@ -1,5 +1,6 @@ defmodule Generation do def id(), do: Pika.gen("user") + def id_secure(), do: Pika.gen("server") def snowflake(), do: Pika.Snowflake.generate() end @@ -8,6 +9,7 @@ Pika.Snowflake.start_link() Benchee.run( %{ "Generate IDs" => fn -> Generation.id() end, + "Generate Secure IDs" => fn -> Generation.id_secure() end, "Generate Snowflakes" => fn -> Generation.snowflake() end }, time: 5 diff --git a/impl/ex/config/bench.exs b/impl/ex/config/bench.exs deleted file mode 100644 index a452bfb..0000000 --- a/impl/ex/config/bench.exs +++ /dev/null @@ -1,4 +0,0 @@ -import Config - -config :benchee, - fast_warning: false diff --git a/impl/ex/config/config.exs b/impl/ex/config/config.exs index f8b5d34..20793ab 100644 --- a/impl/ex/config/config.exs +++ b/impl/ex/config/config.exs @@ -1,10 +1,9 @@ import Config case config_env() do - :bench -> - import_config "bench.exs" :docs -> :ok + _ -> - import_config "test.exs" + import_config "#{Mix.env()}.exs" end diff --git a/impl/ex/config/dev.exs b/impl/ex/config/dev.exs new file mode 100644 index 0000000..36805fa --- /dev/null +++ b/impl/ex/config/dev.exs @@ -0,0 +1,7 @@ +import Config + +config :pika, + prefixes: [ + %{prefix: "user", description: "User IDs"}, + %{prefix: "server", description: "Server IDs", secure: true} + ] diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index 3ae4015..0edec48 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -92,7 +92,7 @@ defmodule Pika do [prefix_record] = Enum.filter(prefixes, fn m -> m.prefix == prefix end) decoded_tail = Base.decode64!(tail, padding: false) tail_fragments = decoded_tail |> String.split("_") - snowflake = tail_fragments |> Enum.at(length(tail_fragments) - 1) + snowflake = tail_fragments |> Enum.at(length(tail_fragments) - 1) |> String.to_integer() decoded_snowflake = Snowflake.decode(snowflake) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index 95bfcf5..d5526dd 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -77,7 +77,7 @@ defmodule Pika.Snowflake do - seq """ @spec decode(integer()) :: any() - def decode(snowflake) do + def decode(snowflake) when is_integer(snowflake) do GenServer.call(__MODULE__, {:decode, snowflake}) end @@ -86,8 +86,6 @@ defmodule Pika.Snowflake do _from, state = {_node_id, epoch, _seq, _last_seq_exhaustion} ) do - snowflake = snowflake |> String.to_integer() - timestamp = (snowflake >>> 22) + epoch node_id = snowflake >>> 12 &&& 0b11_1111_1111 seq = snowflake &&& 0b1111_1111_1111 diff --git a/impl/ex/test/pika_test.exs b/impl/ex/test/pika_test.exs index 63b3ea7..a5d7394 100644 --- a/impl/ex/test/pika_test.exs +++ b/impl/ex/test/pika_test.exs @@ -32,6 +32,14 @@ defmodule PikaTest do assert message == "Prefix is invalid (must be Alphanumeric)" end + test "Snowflake custom timestamp" do + timestamp = 1_708_158_291_035 + snowflake = Pika.Snowflake.generate(timestamp) + decoded = Pika.Snowflake.decode(snowflake) + + assert decoded.timestamp == timestamp + end + test "Test 4096+ ids" do Enum.map(0..4095, fn s -> id = Pika.gen!("user") From 03b06696c7552f12ee164a84e80639dd6a88c771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 16:30:06 +1100 Subject: [PATCH 20/35] Include `_` in regex --- impl/ex/lib/pika.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index 0edec48..d6dc798 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -7,7 +7,7 @@ defmodule Pika do @spec valid_prefix?(binary()) :: boolean() defp valid_prefix?(prefix) do # Checks if `prefix` is alphanumeric - Regex.match?(~r/^[0-9A-Za-z]+$/, prefix) + Regex.match?(~r/^[0-9a-z_]+$/, prefix) end @doc false From 674f1906dca0b8e684fee808ee07df1b8ef2b6e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 20:21:50 +1100 Subject: [PATCH 21/35] Lower Elixir version --- impl/ex/mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index b792c9c..5841e10 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -7,7 +7,7 @@ defmodule Pika.MixProject do [ app: :pika, version: "0.1.0", - elixir: "~> 1.16", + elixir: "~> 1.14", start_permanent: Mix.env() == :prod, package: package(), description: """ From c65bea30b87ecb201cf1f663d3e3534817e25ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 20:25:25 +1100 Subject: [PATCH 22/35] Add `ex_doc` --- impl/ex/mix.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 5841e10..f507442 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -43,7 +43,8 @@ defmodule Pika.MixProject do defp deps do [ {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:benchee, "~> 1.0", only: :dev} + {:benchee, "~> 1.0", only: :dev}, + {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] From 34dd6a8b72a7aca1ba2cabf3d0d1be3dbd89c467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 20:47:53 +1100 Subject: [PATCH 23/35] Update documentation --- impl/ex/README.md | 9 +++++++++ impl/ex/lib/pika.ex | 2 +- impl/ex/mix.exs | 4 +++- impl/ex/mix.lock | 6 ++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/impl/ex/README.md b/impl/ex/README.md index 033881b..ebece70 100644 --- a/impl/ex/README.md +++ b/impl/ex/README.md @@ -49,3 +49,12 @@ defmodule MyApp.Application do end end ``` + +Somewhere in your application: + +```elixir +# ... +Pika.gen("user") # or Pika.gen!("user") + +{:ok, "user_MjgyNDQ2NjY1OTk3MjEzNjk3"} +``` diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index d6dc798..f13ad8b 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -2,7 +2,7 @@ defmodule Pika do alias Pika.Snowflake import Pika.Utils, only: [filter_prefixes: 2] - @moduledoc false + @moduledoc nil @spec valid_prefix?(binary()) :: boolean() defp valid_prefix?(prefix) do diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index f507442..21b3e7e 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -25,7 +25,9 @@ defmodule Pika.MixProject do def docs do [ - source_url: @source_url + source_url: @source_url, + main: "readme", + extras: ["README.md"] ] end diff --git a/impl/ex/mix.lock b/impl/ex/mix.lock index 3f98258..29f4686 100644 --- a/impl/ex/mix.lock +++ b/impl/ex/mix.lock @@ -3,7 +3,13 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "credo": {:hex, :credo, "1.7.4", "68ca5cf89071511c12fd9919eb84e388d231121988f6932756596195ccf7fd35", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9cf776d062c78bbe0f0de1ecaee183f18f2c3ec591326107989b054b7dddefc2"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, } From d60c1fc975aa7cbf5b6cec3983de411993bbb65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 20:48:09 +1100 Subject: [PATCH 24/35] Fix Supervision of `Pika.Snowflake` --- impl/ex/lib/snowflake.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index d5526dd..6ffcda9 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -41,7 +41,7 @@ defmodule Pika.Snowflake do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end - def start_link do + def start_link([]) do # State: {node_id, epoch, seq, last_sequence_exhaustion} GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__ From 653c85ee35ca59f1f6976ee8923af0e2aa3700b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Mon, 19 Feb 2024 20:52:43 +1100 Subject: [PATCH 25/35] Fix README in docs? --- impl/ex/mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 21b3e7e..61f7103 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -27,7 +27,7 @@ defmodule Pika.MixProject do [ source_url: @source_url, main: "readme", - extras: ["README.md"] + extras: ["./README.md"] ] end From 109659450d098cfc4af2c7b15c00338db931ba20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 07:33:48 +1100 Subject: [PATCH 26/35] fix(snowflake): Handle `start_link/0` --- impl/ex/lib/snowflake.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index 6ffcda9..d1f7951 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -41,6 +41,10 @@ defmodule Pika.Snowflake do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end + def start_link() do + GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__) + end + def start_link([]) do # State: {node_id, epoch, seq, last_sequence_exhaustion} GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, From ecc24121e9b1be1b84b85ef9a2e88038847084ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 07:35:48 +1100 Subject: [PATCH 27/35] chore: Let compiler catch `gen/0` --- impl/ex/lib/pika.ex | 5 ----- 1 file changed, 5 deletions(-) diff --git a/impl/ex/lib/pika.ex b/impl/ex/lib/pika.ex index f13ad8b..ffe3ecb 100644 --- a/impl/ex/lib/pika.ex +++ b/impl/ex/lib/pika.ex @@ -66,11 +66,6 @@ defmodule Pika do id end - @spec gen() :: {:error, binary()} - def gen do - {:error, "No prefix was specified"} - end - @doc """ Deconstructs a Pika ID and returns it's metadata: From 533ebd9b2e62aec4fa442a602d86c1b7e88b8cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 07:38:15 +1100 Subject: [PATCH 28/35] refactor: Fix `credo` warning --- impl/ex/lib/snowflake.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index d1f7951..c32503d 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -37,16 +37,18 @@ defmodule Pika.Snowflake do ``` """ - def start_link(epoch) when is_integer(epoch) do - GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) + def start_link([]) do + # State: {node_id, epoch, seq, last_sequence_exhaustion} + GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, + name: __MODULE__ + ) end - def start_link() do - GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__) + def start_link(epoch) when is_integer(epoch) do + GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end - def start_link([]) do - # State: {node_id, epoch, seq, last_sequence_exhaustion} + def start_link do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__ ) From 117e7459e89e154e1fb5774d80f3bb2f1f8aab5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 07:38:28 +1100 Subject: [PATCH 29/35] chore(mix): Bump --- impl/ex/mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 61f7103..43b2d54 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -6,7 +6,7 @@ defmodule Pika.MixProject do def project do [ app: :pika, - version: "0.1.0", + version: "0.1.1", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, package: package(), From ee0e190c90d632133825e583595dc6312b8af278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 07:41:07 +1100 Subject: [PATCH 30/35] fix(workflows): Fix Elixir test not being picked up by Runners --- .github/workflows/pr-ex-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-ex-test.yml b/.github/workflows/pr-ex-test.yml index 0149793..65d9ebe 100644 --- a/.github/workflows/pr-ex-test.yml +++ b/.github/workflows/pr-ex-test.yml @@ -10,7 +10,7 @@ on: jobs: credo: - runs-on: ubuntu + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 @@ -25,7 +25,7 @@ jobs: mix credo test: - runs-on: ubuntu + runs-on: ubuntu-latest name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: From b960ec616524ce961f7cdd12c0320c495236375f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 08:18:34 +1100 Subject: [PATCH 31/35] fix(docs): `gen/0` -> `gen/1` How did I not catch this? --- impl/ex/lib/snowflake.ex | 2 +- impl/ex/mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index c32503d..bdcd212 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -7,7 +7,7 @@ defmodule Pika.Snowflake do `Pika.Snowflake` holds the state, generates Snowflakes, and decodes Snowflakes. `Pika.Snowflake` should be started under a `Supervisor` or `Application` before you start using - `Pika.gen/0` or `Pika.deconstruct/1` + `Pika.gen/1` or `Pika.deconstruct/1` ```elixir defmodule MyApp.Application do diff --git a/impl/ex/mix.exs b/impl/ex/mix.exs index 43b2d54..38e5253 100644 --- a/impl/ex/mix.exs +++ b/impl/ex/mix.exs @@ -6,7 +6,7 @@ defmodule Pika.MixProject do def project do [ app: :pika, - version: "0.1.1", + version: "0.1.2", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, package: package(), From c6ffaf293d87e6e3744ffab13209fd9c2f8912df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 08:19:35 +1100 Subject: [PATCH 32/35] fix(README): Fix example mentioning `Pika.gen/0` --- impl/ex/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/impl/ex/README.md b/impl/ex/README.md index ebece70..b1d4fe3 100644 --- a/impl/ex/README.md +++ b/impl/ex/README.md @@ -36,7 +36,7 @@ config :pika, ## Example `Pika.Snowflake` should be started under a `Supervisor` or `Application` before you start using -`Pika.gen/0` or `Pika.deconstruct/1` +`Pika.gen/1` or `Pika.deconstruct/1` ```elixir defmodule MyApp.Application do From 59455fefd8542b5be4f04ba0728bcd9d669c79dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 16:09:47 +1100 Subject: [PATCH 33/35] refactor: Update `Pika.Snowflake.start_link([])` to call `start_link/0` --- impl/ex/lib/snowflake.ex | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/impl/ex/lib/snowflake.ex b/impl/ex/lib/snowflake.ex index bdcd212..15a9372 100644 --- a/impl/ex/lib/snowflake.ex +++ b/impl/ex/lib/snowflake.ex @@ -37,18 +37,14 @@ defmodule Pika.Snowflake do ``` """ - def start_link([]) do - # State: {node_id, epoch, seq, last_sequence_exhaustion} - GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, - name: __MODULE__ - ) - end + def start_link([]), do: start_link() def start_link(epoch) when is_integer(epoch) do GenServer.start_link(__MODULE__, {Utils.compute_node_id(), epoch, 0, 0}, name: __MODULE__) end def start_link do + # State: {node_id, epoch, seq, last_sequence_exhaustion} GenServer.start_link(__MODULE__, {Utils.compute_node_id(), 1_640_995_200_000, 0, 0}, name: __MODULE__ ) From a5e308df798942e002fcb19f49a9feaec0cfaa46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 16:11:00 +1100 Subject: [PATCH 34/35] fix: Attempt to fix empty `:hwaddr` on interfaces --- impl/ex/lib/utils.ex | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/impl/ex/lib/utils.ex b/impl/ex/lib/utils.ex index ce24ec4..4e7bc9e 100644 --- a/impl/ex/lib/utils.ex +++ b/impl/ex/lib/utils.ex @@ -1,14 +1,28 @@ defmodule Pika.Utils do @moduledoc false + defp validate_address(address) do + case address do + nil -> :error + [0, 0, 0, 0, 0, 0] -> :error + [_, _, _, _, _, _] = addr -> {:ok, addr} + _ -> :error + end + end + def get_mac_address do {:ok, addresses} = :inet.getifaddrs() - addresses - |> Enum.filter(fn {name, _opts} -> name != "lo" end) - |> Enum.map(fn {_name, data} -> data[:hwaddr] end) + {_if_name, if_mac} = Enum.reduce(addresses, [], fn ({if_name, if_data}, acc) -> + case Keyword.get(if_data, :hwaddr) |> validate_address do + {:ok, address} -> [{to_string(if_name), address} | acc] + _ -> acc + end + end) |> List.first() - |> Enum.map_join(":", &Integer.to_string(&1, 16)) + + if_mac + |> Enum.map_join(":", fn i -> Integer.to_string(i, 16) |> String.pad_leading(2, "0") end) end @spec compute_node_id() :: integer() From 2d3e43fbe58f174c15871e897b9af61b1daf2a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=99=A5=20ArtieFuzzz=20=E2=99=A5?= Date: Wed, 21 Feb 2024 16:14:56 +1100 Subject: [PATCH 35/35] fix(workflow): Fix OTP compatibility and properly specify OTP version in Credo job --- .github/workflows/pr-ex-test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-ex-test.yml b/.github/workflows/pr-ex-test.yml index 65d9ebe..b2a99d9 100644 --- a/.github/workflows/pr-ex-test.yml +++ b/.github/workflows/pr-ex-test.yml @@ -16,6 +16,7 @@ jobs: - uses: erlef/setup-beam@v1 with: elixir-version: "1.16.1" + otp-version: "26" - name: Run credo run: | cd impl/ex @@ -29,7 +30,7 @@ jobs: name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} strategy: matrix: - otp: ["25", "26"] + otp: ["25"] elixir: ["1.16.1", "1.15.7", "1.14.0"] steps: - uses: actions/checkout@v4