Skip to content

Commit

Permalink
Add BitcrowdEcto.FixedWidthInteger
Browse files Browse the repository at this point in the history
  • Loading branch information
maltoe committed Dec 20, 2023
1 parent 8e70c8e commit 6a79f48
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 0 deletions.
66 changes: 66 additions & 0 deletions lib/bitcrowd_ecto/fixed_width_integer.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule BitcrowdEcto.FixedWidthInteger do
@moduledoc """
An Ecto type that automatically validates that the given integer fits the underlying DB type.
This turns the ugly Postgrex errors into neat `validation: :cast` changeset errors without
having to manually `validate_number` all `:integer` fields.
Named widths are based on Postgres' integer types.
https://www.postgresql.org/docs/current/datatype-numeric.html
"""

use Ecto.ParameterizedType

@postgres_type_ranges %{
smallint: -32_768..32_767,
integer: -2_147_483_648..2_147_483_647,
bigint: -9_223_372_036_854_775_808..9_223_372_036_854_775_807,
smallserial: 1..32_767,
serial: 1..2_147_483_647,
bigserial: 1..9_223_372_036_854_775_807
}

@generic_byte_size_ranges %{
2 => -32_768..32_767,
4 => -2_147_483_648..2_147_483_647,
8 => -9_223_372_036_854_775_808..9_223_372_036_854_775_807
}

@impl true
def init(opts) do
opts
|> Keyword.get(:width, 4)
|> width_to_range()
end

defp width_to_range(type) when is_atom(type), do: Map.fetch!(@postgres_type_ranges, type)
defp width_to_range(size) when is_integer(size), do: Map.fetch!(@generic_byte_size_ranges, size)

@impl true
def type(_range), do: :integer

@impl true
def cast(value, range) do
if is_integer(value) and value not in range do
:error
else
Ecto.Type.cast(:integer, value)
end
end

@impl true
def load(value, loader, _range) do
Ecto.Type.load(:integer, value, loader)
end

@impl true
def dump(value, dumper, _range) do
Ecto.Type.dump(:integer, value, dumper)
end

@impl true
def equal?(a, b, _range) do
a == b
end
end
50 changes: 50 additions & 0 deletions test/bitcrowd_ecto/fixed_width_integer_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
defmodule BitcrowdEcto.FixedWidthIntegerTest do
use ExUnit.Case, async: true
import BitcrowdEcto.Assertions
import Ecto.Changeset

defmodule TestSchema do
use Ecto.Schema

embedded_schema do
field(:int_4, BitcrowdEcto.FixedWidthInteger, width: 4)
field(:int_smallint, BitcrowdEcto.FixedWidthInteger, width: :smallint)
field(:int_bigserial, BitcrowdEcto.FixedWidthInteger, width: :bigserial)
end
end

test "casting an out-of-range value results in a changeset error" do
for ok <- [-2, 2, 0, -2_147_483_648, 2_147_483_647] do
cs = cast(%TestSchema{}, %{int_4: ok}, [:int_4])
assert cs.valid?
end

for not_ok <- [-2_147_483_649, 2_147_483_648] do
cs = cast(%TestSchema{}, %{int_4: not_ok}, [:int_4])
refute cs.valid?
assert_error_on(cs, :int_4, :cast)
end

for ok <- [-2, 2, 0, -32_768, 32_767] do
cs = cast(%TestSchema{}, %{int_smallint: ok}, [:int_smallint])
assert cs.valid?
end

for not_ok <- [-32_769, 32_768] do
cs = cast(%TestSchema{}, %{int_smallint: not_ok}, [:int_smallint])
refute cs.valid?
assert_error_on(cs, :int_smallint, :cast)
end

for ok <- [1, 9_223_372_036_854_775_807] do
cs = cast(%TestSchema{}, %{int_bigserial: ok}, [:int_bigserial])
assert cs.valid?
end

for not_ok <- [-1, 0, 9_223_372_036_854_775_808] do
cs = cast(%TestSchema{}, %{int_bigserial: not_ok}, [:int_bigserial])
refute cs.valid?
assert_error_on(cs, :int_bigserial, :cast)
end
end
end

0 comments on commit 6a79f48

Please sign in to comment.