From 6d9c814f790add8e556a88ab00e0ae444ed29b46 Mon Sep 17 00:00:00 2001 From: AJ Foster <2789166+aj-foster@users.noreply.github.com> Date: Fri, 8 Aug 2025 22:00:06 -0400 Subject: [PATCH 1/2] Add IP address type for Ecto --- lib/together/ip.ex | 76 +++++++++++++++++++++++++++++++++++++++ test/together/ip_test.exs | 38 ++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 lib/together/ip.ex create mode 100644 test/together/ip_test.exs diff --git a/lib/together/ip.ex b/lib/together/ip.ex new file mode 100644 index 0000000..6fe1403 --- /dev/null +++ b/lib/together/ip.ex @@ -0,0 +1,76 @@ +defmodule Together.IP do + @moduledoc """ + Ecto-compatible type for storing IP addresses + + Migrations should use the `:inet` type for the database column. + + ## Example + + # Schema + schema "example" do + field :ip_address, Together.IP + end + + # Migration + def change do + create table(:example) do + add :ip_address, :inet + end + end + + """ + if Code.ensure_loaded?(Ecto.Type) do + @behaviour Ecto.Type + + @doc false + @impl Ecto.Type + def type, do: :string + + @doc false + @impl Ecto.Type + def cast(value) when is_binary(value) or is_list(value), do: parse_address(value) + def cast({_, _, _, _} = value), do: {:ok, value} + def cast(_invalid), do: :error + + @doc false + @impl Ecto.Type + def load(value), do: parse_address(value) + + @doc false + @impl Ecto.Type + def dump(value) when is_binary(value) or is_list(value) do + with {:ok, ip_address} <- parse_address(value) do + dump(ip_address) + end + end + + def dump({_, _, _, _} = value) do + case :inet.ntoa(value) do + {:error, :einval} -> :error + ip_address_charlist -> {:ok, to_string(ip_address_charlist)} + end + end + + def dump(_), do: :error + + @doc false + @impl Ecto.Type + def equal?(value1, value2) do + value1 == value2 + end + + @doc false + @impl Ecto.Type + def embed_as(_), do: :self + + @spec parse_address(String.t() | charlist) :: {:ok, :inet.ip_address()} | :error + defp parse_address(ip_address_string_or_charlist) do + ip_address_charlist = to_charlist(ip_address_string_or_charlist) + + case :inet.parse_address(ip_address_charlist) do + {:ok, ip_address} -> {:ok, ip_address} + {:error, :einval} -> :error + end + end + end +end diff --git a/test/together/ip_test.exs b/test/together/ip_test.exs new file mode 100644 index 0000000..3dc186a --- /dev/null +++ b/test/together/ip_test.exs @@ -0,0 +1,38 @@ +defmodule Together.IPTest do + use ExUnit.Case, async: true + + alias Together.IP + + describe "cast/1" do + test "casts a string, charlist, or IP address tuple" do + assert IP.cast("127.0.0.1") == {:ok, {127, 0, 0, 1}} + assert IP.cast(~c'127.0.0.1') == {:ok, {127, 0, 0, 1}} + assert IP.cast({127, 0, 0, 1}) == {:ok, {127, 0, 0, 1}} + end + + test "returns :error for invalid input" do + assert IP.cast("invalid_ip") == :error + assert IP.cast(123) == :error + assert IP.cast([]) == :error + end + end + + describe "load/1" do + test "parses a string into an IP address tuple" do + assert IP.load("127.0.0.1") == {:ok, {127, 0, 0, 1}} + end + end + + describe "dump/1" do + test "dumps a string, charlist, or IP address tuple" do + assert IP.dump("127.0.0.1") == {:ok, "127.0.0.1"} + assert IP.dump(~c'127.0.0.1') == {:ok, "127.0.0.1"} + assert IP.dump({127, 0, 0, 1}) == {:ok, "127.0.0.1"} + end + + test "returns :error for invalid IP address tuples" do + assert IP.dump({256, 0, 0, 1}) == :error + assert IP.dump("invalid_ip") == :error + end + end +end From 9deb47fd26e7553ceffc7624a6820735bde5277b Mon Sep 17 00:00:00 2001 From: AJ Foster <2789166+aj-foster@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:03:36 -0400 Subject: [PATCH 2/2] Install sobelow and add mix check alias --- .sobelow-conf | 13 +++++++++++++ mix.exs | 25 ++++++++++++++++++++++++- mix.lock | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .sobelow-conf diff --git a/.sobelow-conf b/.sobelow-conf new file mode 100644 index 0000000..7cb51c3 --- /dev/null +++ b/.sobelow-conf @@ -0,0 +1,13 @@ +[ + exit: "medium", + format: "txt", + ignore: [], + ignore_files: [], + out: nil, + private: false, + router: nil, + skip: true, + threshold: :low, + verbose: true, + version: false, +] diff --git a/mix.exs b/mix.exs index fbe6e37..c311d96 100644 --- a/mix.exs +++ b/mix.exs @@ -7,6 +7,7 @@ defmodule Together.MixProject do version: "0.1.0", elixir: "~> 1.18", start_permanent: Mix.env() == :prod, + aliases: aliases(), deps: deps(), # Docs @@ -35,7 +36,29 @@ defmodule Together.MixProject do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ecto, "~> 3.5", optional: true}, - {:ex_doc, "~> 0.38", only: [:dev], runtime: false} + {:ex_doc, "~> 0.38", only: [:dev], runtime: false}, + {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false} + ] + end + + defp aliases do + [ + check: [ + "test --warnings-as-errors", + "deps.unlock --check-unused", + "format --check-formatted", + "credo", + "sobelow --config", + "dialyzer --format dialyxir" + ] + ] + end + + def cli do + [ + preferred_envs: [ + check: :test + ] ] end diff --git a/mix.lock b/mix.lock index 1ec3167..12e41d9 100644 --- a/mix.lock +++ b/mix.lock @@ -13,5 +13,6 @@ "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [: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", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, }