Skip to content
Open
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
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