Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .sobelow-conf
Original file line number Diff line number Diff line change
@@ -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,
]
76 changes: 76 additions & 0 deletions lib/together/ip.ex
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never done this, but why are we specifying :string here instead of :inet?

It should also be possible, we might also consider instead using INET from ecto_network


@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
25 changes: 24 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
}
38 changes: 38 additions & 0 deletions test/together/ip_test.exs
Original file line number Diff line number Diff line change
@@ -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